Merge branch 'dev'

This commit is contained in:
benweet 2018-08-19 14:05:01 +01:00
commit 9a6d0bddc8
218 changed files with 15748 additions and 7161 deletions

View File

@ -8,7 +8,7 @@
"env": { "env": {
"test": { "test": {
"presets": ["env", "stage-2"], "presets": ["env", "stage-2"],
"plugins": [ "istanbul" ] "plugins": ["transform-es2015-modules-commonjs", "dynamic-import-node"]
} }
} }
} }

3
.gitignore vendored
View File

@ -3,8 +3,7 @@ node_modules/
dist/ dist/
.history .history
npm-debug.log* npm-debug.log*
yarn-debug.log*
yarn-error.log*
.vscode .vscode
stackedit_v4 stackedit_v4
chrome-app/*.zip chrome-app/*.zip
/test/unit/coverage/

View File

@ -1,46 +0,0 @@
### v5.11
- New file properties modal with extension presets
- Added new Markdown extensions: task lists, image size, mark
### v5.10
- Added temporary folder
- New iframe mode (see [here](https://benweet.github.io/stackedit.js/))
### v5.9
- Added explorer context menu
### v5.8
- New import menu with HTML to Markdown conversion
- HTML to Markdown conversion when pasting rich text in the editor
- Custom scrollbars on webkit
### v5.7
- Support for CouchDB workspaces
- Added FAQ
- Added welcome tour
### v5.6
- Themes support with new dark theme
### v5.5
- Integration with Google Drive
- New landing page
### v5.4
- Multi-workspaces capabilities
### v5.3
- Revision history
### v5.2
- Support for discussions/comments

View File

@ -8,7 +8,9 @@ ENV V4_VERSION 4.3.22
RUN npm pack stackedit@$V4_VERSION \ RUN npm pack stackedit@$V4_VERSION \
&& tar xzf stackedit-*.tgz --strip 1 \ && tar xzf stackedit-*.tgz --strip 1 \
&& yarn \ && yarn \
&& yarn cache clean && yarn cache clean \
&& rm -rf ~/.cache/bower \
&& rm -rf ~/.local/share/bower
WORKDIR /opt/stackedit WORKDIR /opt/stackedit

View File

@ -6,11 +6,12 @@
https://stackedit.io/ https://stackedit.io/
### NEW!!! Embed StackEdit in any website! ### Ecosystem
See https://github.com/benweet/stackedit.js - [Chrome app](https://chrome.google.com/webstore/detail/iiooodelglhkcpgbajoejffhijaclcdg)
- NEW! Embed StackEdit in any website with [stackedit.js](https://github.com/benweet/stackedit.js)
Chrome extension: https://chrome.google.com/webstore/detail/ajehldoplanpchfokmeempkekhnhmoha - NEW! [Chrome extension](https://chrome.google.com/webstore/detail/ajehldoplanpchfokmeempkekhnhmoha) that uses stackedit.js
- [Community](https://community.stackedit.io/)
### Build Setup ### Build Setup

View File

@ -2,6 +2,7 @@ var path = require('path')
var webpack = require('webpack') var webpack = require('webpack')
var utils = require('./utils') var utils = require('./utils')
var config = require('../config') var config = require('../config')
var VueLoaderPlugin = require('vue-loader/lib/plugin')
var vueLoaderConfig = require('./vue-loader.conf') var vueLoaderConfig = require('./vue-loader.conf')
var StylelintPlugin = require('stylelint-webpack-plugin') var StylelintPlugin = require('stylelint-webpack-plugin')
var FaviconsWebpackPlugin = require('favicons-webpack-plugin') var FaviconsWebpackPlugin = require('favicons-webpack-plugin')
@ -81,6 +82,7 @@ module.exports = {
] ]
}, },
plugins: [ plugins: [
new VueLoaderPlugin(),
new StylelintPlugin({ new StylelintPlugin({
files: ['**/*.vue', '**/*.scss'] files: ['**/*.vue', '**/*.scss']
}), }),

View File

@ -98,6 +98,7 @@ var webpackConfig = merge(baseWebpackConfig, {
ServiceWorker: { ServiceWorker: {
events: true events: true
}, },
AppCache: true,
excludes: ['**/.*', '**/*.map', '**/index.html', '**/static/oauth2/callback.html', '**/icons-*/*.png', '**/static/fonts/KaTeX_*'], excludes: ['**/.*', '**/*.map', '**/index.html', '**/static/oauth2/callback.html', '**/icons-*/*.png', '**/static/fonts/KaTeX_*'],
externals: ['/', '/app', '/oauth2/callback'] externals: ['/', '/app', '/oauth2/callback']
}) })

View File

@ -14,7 +14,7 @@ function resolve (dir) {
module.exports = { module.exports = {
entry: { entry: {
style: './src/components/style.scss' style: './src/styles/'
}, },
module: { module: {
rules: [{ rules: [{

8958
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -14,7 +14,9 @@
"build": "node build/build.js && npm run build-style", "build": "node build/build.js && npm run build-style",
"build-style": "webpack --config build/webpack.style.conf.js", "build-style": "webpack --config build/webpack.style.conf.js",
"lint": "eslint --ext .js,.vue src server", "lint": "eslint --ext .js,.vue src server",
"test": "npm run lint", "unit": "jest --config test/unit/jest.conf.js --runInBand",
"unit-with-coverage": "jest --config test/unit/jest.conf.js --runInBand --coverage",
"test": "npm run lint && npm run unit",
"preversion": "npm run test", "preversion": "npm run test",
"postversion": "git push origin master --tags && npm publish", "postversion": "git push origin master --tags && npm publish",
"patch": "npm version patch -m \"Tag v%s\"", "patch": "npm version patch -m \"Tag v%s\"",
@ -22,19 +24,21 @@
"major": "npm version major -m \"Tag v%s\"" "major": "npm version major -m \"Tag v%s\""
}, },
"dependencies": { "dependencies": {
"@vue/test-utils": "^1.0.0-beta.16",
"aws-sdk": "^2.133.0", "aws-sdk": "^2.133.0",
"babel-runtime": "^6.26.0",
"bezier-easing": "^1.1.0", "bezier-easing": "^1.1.0",
"body-parser": "^1.18.2", "body-parser": "^1.18.2",
"clipboard": "^1.7.1", "clipboard": "^1.7.1",
"compression": "^1.7.0", "compression": "^1.7.0",
"diff-match-patch": "^1.0.0", "diff-match-patch": "^1.0.0",
"file-saver": "^1.3.3", "file-saver": "^1.3.8",
"google-id-token-verifier": "^0.2.3", "google-id-token-verifier": "^0.2.3",
"handlebars": "^4.0.10", "handlebars": "^4.0.10",
"indexeddbshim": "^3.0.4", "indexeddbshim": "^3.6.2",
"js-yaml": "^3.9.1", "js-yaml": "^3.11.0",
"katex": "^0.9.0-alpha1", "katex": "^0.9.0",
"markdown-it": "^8.3.1", "markdown-it": "^8.4.1",
"markdown-it-abbr": "^1.0.4", "markdown-it-abbr": "^1.0.4",
"markdown-it-deflist": "^2.0.2", "markdown-it-deflist": "^2.0.2",
"markdown-it-emoji": "^1.3.0", "markdown-it-emoji": "^1.3.0",
@ -46,21 +50,24 @@
"markdown-it-sup": "^1.0.0", "markdown-it-sup": "^1.0.0",
"mermaid": "^7.1.0", "mermaid": "^7.1.0",
"mousetrap": "^1.6.1", "mousetrap": "^1.6.1",
"normalize-scss": "^7.0.0", "normalize-scss": "^7.0.1",
"prismjs": "^1.6.0", "prismjs": "^1.6.0",
"request": "^2.82.0", "request": "^2.85.0",
"serve-static": "^1.12.6", "serve-static": "^1.13.2",
"tmp": "^0.0.33", "tmp": "^0.0.33",
"turndown": "^4.0.1", "turndown": "^4.0.2",
"vue": "^2.3.3", "vue": "^2.5.16",
"vuex": "^2.3.1" "vuex": "^3.0.1"
}, },
"devDependencies": { "devDependencies": {
"autoprefixer": "^6.7.2", "autoprefixer": "^6.7.2",
"babel-core": "^6.22.1", "babel-core": "^6.26.3",
"babel-eslint": "^7.1.1", "babel-eslint": "^8.2.3",
"babel-loader": "^6.2.10", "babel-jest": "^21.0.2",
"babel-plugin-transform-runtime": "^6.22.0", "babel-loader": "^7.1.4",
"babel-plugin-dynamic-import-node": "^1.2.0",
"babel-plugin-transform-es2015-modules-commonjs": "^6.26.0",
"babel-plugin-transform-runtime": "^6.23.0",
"babel-polyfill": "^6.23.0", "babel-polyfill": "^6.23.0",
"babel-preset-env": "^1.3.2", "babel-preset-env": "^1.3.2",
"babel-preset-stage-2": "^6.22.0", "babel-preset-stage-2": "^6.22.0",
@ -68,57 +75,63 @@
"chalk": "^1.1.3", "chalk": "^1.1.3",
"connect-history-api-fallback": "^1.3.0", "connect-history-api-fallback": "^1.3.0",
"copy-webpack-plugin": "^4.5.1", "copy-webpack-plugin": "^4.5.1",
"css-loader": "^0.28.7", "css-loader": "^0.28.11",
"eslint": "^3.19.0", "eslint": "^4.19.1",
"eslint-config-airbnb-base": "^11.1.3", "eslint-config-airbnb-base": "^12.1.0",
"eslint-friendly-formatter": "^2.0.7", "eslint-friendly-formatter": "^4.0.1",
"eslint-import-resolver-webpack": "^0.8.1", "eslint-import-resolver-webpack": "^0.9.0",
"eslint-loader": "^1.7.1", "eslint-loader": "^2.0.0",
"eslint-plugin-html": "^2.0.0", "eslint-plugin-html": "^4.0.3",
"eslint-plugin-import": "^2.2.0", "eslint-plugin-import": "^2.11.0",
"eventsource-polyfill": "^0.9.6", "eventsource-polyfill": "^0.9.6",
"express": "^4.15.5", "express": "^4.16.3",
"extract-text-webpack-plugin": "^2.0.0", "extract-text-webpack-plugin": "^2.0.0",
"favicons-webpack-plugin": "^0.0.7", "favicons-webpack-plugin": "^0.0.9",
"file-loader": "^0.11.1", "file-loader": "^1.1.11",
"friendly-errors-webpack-plugin": "^1.1.3", "friendly-errors-webpack-plugin": "^1.7.0",
"gulp": "^3.9.1", "gulp": "^3.9.1",
"gulp-concat": "^2.6.1", "gulp-concat": "^2.6.1",
"html-webpack-plugin": "^2.28.0", "html-webpack-plugin": "^3.2.0",
"http-proxy-middleware": "^0.17.3", "http-proxy-middleware": "^0.18.0",
"identity-obj-proxy": "^3.0.0",
"ignore-loader": "^0.1.2", "ignore-loader": "^0.1.2",
"node-sass": "^4.5.3", "jest": "^23.0.0",
"jest-raw-loader": "^1.0.1",
"jest-serializer-vue": "^0.3.0",
"node-sass": "^4.9.0",
"npm-bump": "^0.0.23", "npm-bump": "^0.0.23",
"offline-plugin": "^4.8.4", "offline-plugin": "^5.0.3",
"opn": "^4.0.2", "opn": "^4.0.2",
"optimize-css-assets-webpack-plugin": "^1.3.0", "optimize-css-assets-webpack-plugin": "^1.3.2",
"ora": "^1.2.0", "ora": "^1.2.0",
"raw-loader": "^0.5.1", "raw-loader": "^0.5.1",
"rimraf": "^2.6.0", "rimraf": "^2.6.0",
"sass-loader": "^6.0.5", "sass-loader": "^7.0.1",
"semver": "^5.3.0", "semver": "^5.5.0",
"shelljs": "^0.7.6", "shelljs": "^0.8.1",
"stylelint": "^9.2.0",
"stylelint-config-standard": "^16.0.0", "stylelint-config-standard": "^16.0.0",
"stylelint-processor-html": "^1.0.0", "stylelint-processor-html": "^1.0.0",
"stylelint-webpack-plugin": "^0.7.0", "stylelint-webpack-plugin": "^0.10.4",
"url-loader": "^0.5.8", "url-loader": "^1.0.1",
"vue-loader": "^12.1.0", "vue-jest": "^1.0.2",
"vue-style-loader": "^3.0.1", "vue-loader": "^15.0.9",
"vue-template-compiler": "^2.3.3", "vue-style-loader": "^4.1.0",
"vue-template-compiler": "^2.5.16",
"webpack": "^2.6.1", "webpack": "^2.6.1",
"webpack-bundle-analyzer": "^2.2.1", "webpack-bundle-analyzer": "^2.2.1",
"webpack-dev-middleware": "^1.10.0", "webpack-dev-middleware": "^1.10.0",
"webpack-hot-middleware": "^2.18.0", "webpack-hot-middleware": "^2.18.0",
"webpack-merge": "^4.1.0", "webpack-merge": "^4.1.2",
"worker-loader": "^0.8.1" "worker-loader": "^1.1.1"
}, },
"engines": { "engines": {
"node": ">= 4.0.0", "node": ">= 8.0.0",
"npm": ">= 3.0.0" "npm": ">= 5.0.0"
}, },
"browserslist": [ "browserslist": [
"> 1%", "> 1%",
"last 2 versions", "last 2 versions",
"not ie <= 8" "not ie <= 10"
] ]
} }

View File

@ -29,5 +29,8 @@ exports.githubToken = (req, res) => {
githubToken(req.query.clientId, req.query.code) githubToken(req.query.clientId, req.query.code)
.then( .then(
token => res.send(token), token => res.send(token),
err => res.status(400).send(err ? err.message || err.toString() : 'bad_code')); err => res
.status(400)
.send(err ? err.message || err.toString() : 'bad_code'),
);
}; };

View File

@ -1,5 +1,5 @@
/* global window */ /* global window */
const spawn = require('child_process').spawn; const { spawn } = require('child_process');
const fs = require('fs'); const fs = require('fs');
const tmp = require('tmp'); const tmp = require('tmp');
const user = require('./user'); const user = require('./user');
@ -76,7 +76,7 @@ exports.generate = (req, res) => {
params.push('--toc'); params.push('--toc');
} }
options.tocDepth = parseInt(options.tocDepth, 10); options.tocDepth = parseInt(options.tocDepth, 10);
if (!isNaN(options.tocDepth)) { if (!Number.isNaN(options.tocDepth)) {
params.push('--toc-depth', options.tocDepth); params.push('--toc-depth', options.tocDepth);
} }
options.highlightStyle = highlightStyles.indexOf(options.highlightStyle) !== -1 ? options.highlightStyle : 'kate'; options.highlightStyle = highlightStyles.indexOf(options.highlightStyle) !== -1 ? options.highlightStyle : 'kate';

View File

@ -1,5 +1,5 @@
/* global window,MathJax */ /* global window,MathJax */
const spawn = require('child_process').spawn; const { spawn } = require('child_process');
const fs = require('fs'); const fs = require('fs');
const tmp = require('tmp'); const tmp = require('tmp');
const user = require('./user'); const user = require('./user');
@ -84,13 +84,13 @@ exports.generate = (req, res) => {
// Margins // Margins
const marginTop = parseInt(`${options.marginTop}`, 10); const marginTop = parseInt(`${options.marginTop}`, 10);
params.push('-T', isNaN(marginTop) ? 25 : marginTop); params.push('-T', Number.isNaN(marginTop) ? 25 : marginTop);
const marginRight = parseInt(`${options.marginRight}`, 10); const marginRight = parseInt(`${options.marginRight}`, 10);
params.push('-R', isNaN(marginRight) ? 25 : marginRight); params.push('-R', Number.isNaN(marginRight) ? 25 : marginRight);
const marginBottom = parseInt(`${options.marginBottom}`, 10); const marginBottom = parseInt(`${options.marginBottom}`, 10);
params.push('-B', isNaN(marginBottom) ? 25 : marginBottom); params.push('-B', Number.isNaN(marginBottom) ? 25 : marginBottom);
const marginLeft = parseInt(`${options.marginLeft}`, 10); const marginLeft = parseInt(`${options.marginLeft}`, 10);
params.push('-L', isNaN(marginLeft) ? 25 : marginLeft); params.push('-L', Number.isNaN(marginLeft) ? 25 : marginLeft);
// Header // Header
if (options.headerCenter) { if (options.headerCenter) {

View File

@ -2,10 +2,12 @@ const request = require('request');
const AWS = require('aws-sdk'); const AWS = require('aws-sdk');
const verifier = require('google-id-token-verifier'); const verifier = require('google-id-token-verifier');
const BUCKET_NAME = process.env.USER_BUCKET_NAME || 'stackedit-users'; const {
const PAYPAL_URI = process.env.PAYPAL_URI || 'https://www.paypal.com/cgi-bin/webscr'; USER_BUCKET_NAME = 'stackedit-users',
const PAYPAL_RECEIVER_EMAIL = process.env.PAYPAL_RECEIVER_EMAIL || 'stackedit.project@gmail.com'; PAYPAL_URI = 'https://www.paypal.com/cgi-bin/webscr',
const GOOGLE_CLIENT_ID = process.env.GOOGLE_CLIENT_ID; PAYPAL_RECEIVER_EMAIL = 'stackedit.project@gmail.com',
GOOGLE_CLIENT_ID,
} = process.env;
const s3Client = new AWS.S3(); const s3Client = new AWS.S3();
const cb = (resolve, reject) => (err, res) => { const cb = (resolve, reject) => (err, res) => {
@ -18,21 +20,22 @@ const cb = (resolve, reject) => (err, res) => {
exports.getUser = id => new Promise((resolve, reject) => { exports.getUser = id => new Promise((resolve, reject) => {
s3Client.getObject({ s3Client.getObject({
Bucket: BUCKET_NAME, Bucket: USER_BUCKET_NAME,
Key: id, Key: id,
}, cb(resolve, reject)); }, cb(resolve, reject));
}) })
.then( .then(
res => JSON.parse(`${res.Body}`), res => JSON.parse(`${res.Body}`),
(err) => { (err) => {
if (err.code !== 'NoSuchKey') { if (err.code !== 'NoSuchKey') {
throw err; throw err;
} }
}); },
);
exports.putUser = (id, user) => new Promise((resolve, reject) => { exports.putUser = (id, user) => new Promise((resolve, reject) => {
s3Client.putObject({ s3Client.putObject({
Bucket: BUCKET_NAME, Bucket: USER_BUCKET_NAME,
Key: id, Key: id,
Body: JSON.stringify(user), Body: JSON.stringify(user),
}, cb(resolve, reject)); }, cb(resolve, reject));
@ -40,20 +43,24 @@ exports.putUser = (id, user) => new Promise((resolve, reject) => {
exports.removeUser = id => new Promise((resolve, reject) => { exports.removeUser = id => new Promise((resolve, reject) => {
s3Client.deleteObject({ s3Client.deleteObject({
Bucket: BUCKET_NAME, Bucket: USER_BUCKET_NAME,
Key: id, Key: id,
}, cb(resolve, reject)); }, cb(resolve, reject));
}); });
exports.getUserFromToken = idToken => new Promise( exports.getUserFromToken = idToken => new Promise((resolve, reject) => verifier
(resolve, reject) => verifier.verify(idToken, GOOGLE_CLIENT_ID, cb(resolve, reject))) .verify(idToken, GOOGLE_CLIENT_ID, cb(resolve, reject)))
.then(tokenInfo => exports.getUser(tokenInfo.sub)); .then(tokenInfo => exports.getUser(tokenInfo.sub));
exports.userInfo = (req, res) => exports.getUserFromToken(req.query.idToken) exports.userInfo = (req, res) => exports.getUserFromToken(req.query.idToken)
.then(user => res.send(Object.assign({ .then(
sponsorUntil: 0, user => res.send(Object.assign({
}, user)), sponsorUntil: 0,
err => res.status(400).send(err ? err.message || err.toString() : 'invalid_token')); }, user)),
err => res
.status(400)
.send(err ? err.message || err.toString() : 'invalid_token'),
);
exports.paypalIpn = (req, res, next) => Promise.resolve() exports.paypalIpn = (req, res, next) => Promise.resolve()
.then(() => { .then(() => {

View File

@ -2,14 +2,16 @@
<div class="app" :class="classes"> <div class="app" :class="classes">
<splash-screen v-if="!ready"></splash-screen> <splash-screen v-if="!ready"></splash-screen>
<layout v-else></layout> <layout v-else></layout>
<modal v-if="showModal"></modal> <modal></modal>
<notification></notification> <notification></notification>
<context-menu></context-menu> <context-menu></context-menu>
</div> </div>
</template> </template>
<script> <script>
import Vue from 'vue'; import '../styles';
import '../styles/markdownHighlighting.scss';
import '../styles/app.scss';
import Layout from './Layout'; import Layout from './Layout';
import Modal from './Modal'; import Modal from './Modal';
import Notification from './Notification'; import Notification from './Notification';
@ -19,50 +21,7 @@ import syncSvc from '../services/syncSvc';
import networkSvc from '../services/networkSvc'; import networkSvc from '../services/networkSvc';
import sponsorSvc from '../services/sponsorSvc'; import sponsorSvc from '../services/sponsorSvc';
import tempFileSvc from '../services/tempFileSvc'; import tempFileSvc from '../services/tempFileSvc';
import timeSvc from '../services/timeSvc'; import './common/vueGlobals';
import store from '../store';
// Global directives
Vue.directive('focus', {
inserted(el) {
el.focus();
const value = el.value;
if (value && el.setSelectionRange) {
el.setSelectionRange(0, value.length);
}
},
});
const setVisible = (el, value) => {
el.style.display = value ? '' : 'none';
if (value) {
el.removeAttribute('aria-hidden');
} else {
el.setAttribute('aria-hidden', 'true');
}
};
Vue.directive('show', {
bind(el, { value }) {
setVisible(el, value);
},
update(el, { value, oldValue }) {
if (value !== oldValue) {
setVisible(el, value);
}
},
});
Vue.directive('title', {
bind(el, { value }) {
el.title = value;
el.setAttribute('aria-label', value);
},
});
// Global filters
Vue.filter('formatTime', time =>
// Access the minute counter for reactive refresh
timeSvc.format(time, store.state.minuteCounter));
const themeClasses = { const themeClasses = {
light: ['app--light'], light: ['app--light'],
@ -85,28 +44,22 @@ export default {
const result = themeClasses[this.$store.getters['data/computedSettings'].colorTheme]; const result = themeClasses[this.$store.getters['data/computedSettings'].colorTheme];
return Array.isArray(result) ? result : themeClasses.light; return Array.isArray(result) ? result : themeClasses.light;
}, },
showModal() {
return !!this.$store.getters['modal/config'];
},
}, },
created() { async created() {
syncSvc.init() try {
.then(() => { await syncSvc.init();
networkSvc.init(); await networkSvc.init();
sponsorSvc.init(); await sponsorSvc.init();
this.ready = true; this.ready = true;
tempFileSvc.setReady(); tempFileSvc.setReady();
}) } catch (err) {
.catch((err) => { if (err && err.message === 'RELOAD') {
if (err && err.message !== 'reload') { window.location.reload();
console.error(err); // eslint-disable-line no-console } else if (err && err.message !== 'RELOAD') {
this.$store.dispatch('notification/error', err); console.error(err); // eslint-disable-line no-console
} this.$store.dispatch('notification/error', err);
}); }
}
}, },
}; };
</script> </script>
<style lang="scss">
@import 'common/app';
</style>

View File

@ -1,24 +1,24 @@
<template> <template>
<div class="button-bar"> <div class="button-bar">
<div class="button-bar__inner button-bar__inner--top"> <div class="button-bar__inner button-bar__inner--top">
<button class="button-bar__button button" :class="{ 'button-bar__button--on': layoutSettings.showNavigationBar }" v-if="!light" @click="toggleNavigationBar()" v-title="'Toggle navigation bar'"> <button class="button-bar__button button-bar__button--navigation-bar-toggler button" :class="{ 'button-bar__button--on': layoutSettings.showNavigationBar }" v-if="!light" @click="toggleNavigationBar()" v-title="'Toggle navigation bar'">
<icon-navigation-bar></icon-navigation-bar> <icon-navigation-bar></icon-navigation-bar>
</button> </button>
<button class="button-bar__button button" :class="{ 'button-bar__button--on': layoutSettings.showSidePreview }" tour-step-anchor="editor" @click="toggleSidePreview()" v-title="'Toggle side preview'"> <button class="button-bar__button button-bar__button--side-preview-toggler button" :class="{ 'button-bar__button--on': layoutSettings.showSidePreview }" tour-step-anchor="editor" @click="toggleSidePreview()" v-title="'Toggle side preview'">
<icon-side-preview></icon-side-preview> <icon-side-preview></icon-side-preview>
</button> </button>
<button class="button-bar__button button" @click="toggleEditor(false)" v-title="'Reader mode'"> <button class="button-bar__button button-bar__button--editor-toggler button" @click="toggleEditor(false)" v-title="'Reader mode'">
<icon-eye></icon-eye> <icon-eye></icon-eye>
</button> </button>
</div> </div>
<div class="button-bar__inner button-bar__inner--bottom"> <div class="button-bar__inner button-bar__inner--bottom">
<button class="button-bar__button button" :class="{ 'button-bar__button--on': layoutSettings.focusMode }" @click="toggleFocusMode()" v-title="'Toggle focus mode'"> <button class="button-bar__button button-bar__button--focus-mode-toggler button" :class="{ 'button-bar__button--on': layoutSettings.focusMode }" @click="toggleFocusMode()" v-title="'Toggle focus mode'">
<icon-target></icon-target> <icon-target></icon-target>
</button> </button>
<button class="button-bar__button button" :class="{ 'button-bar__button--on': layoutSettings.scrollSync }" @click="toggleScrollSync()" v-title="'Toggle scroll sync'"> <button class="button-bar__button button-bar__button--scroll-sync-toggler button" :class="{ 'button-bar__button--on': layoutSettings.scrollSync }" @click="toggleScrollSync()" v-title="'Toggle scroll sync'">
<icon-scroll-sync></icon-scroll-sync> <icon-scroll-sync></icon-scroll-sync>
</button> </button>
<button class="button-bar__button button" :class="{ 'button-bar__button--on': layoutSettings.showStatusBar }" @click="toggleStatusBar()" v-title="'Toggle status bar'"> <button class="button-bar__button button-bar__button--status-bar-toggler button" :class="{ 'button-bar__button--on': layoutSettings.showStatusBar }" @click="toggleStatusBar()" v-title="'Toggle status bar'">
<icon-status-bar></icon-status-bar> <icon-status-bar></icon-status-bar>
</button> </button>
</div> </div>
@ -49,7 +49,7 @@ export default {
</script> </script>
<style lang="scss"> <style lang="scss">
@import 'common/variables.scss'; @import '../styles/variables.scss';
.button-bar { .button-bar {
position: absolute; position: absolute;

View File

@ -4,7 +4,7 @@
<script> <script>
import Prism from 'prismjs'; import Prism from 'prismjs';
import cledit from '../services/cledit'; import cledit from '../services/editor/cledit';
export default { export default {
props: ['value', 'lang', 'disabled'], props: ['value', 'lang', 'disabled'],
@ -28,7 +28,7 @@ export default {
</script> </script>
<style lang="scss"> <style lang="scss">
@import 'common/variables.scss'; @import '../styles/variables.scss';
.code-editor { .code-editor {
margin: 0; margin: 0;

View File

@ -4,7 +4,7 @@
<div v-for="(item, idx) in items" :key="idx"> <div v-for="(item, idx) in items" :key="idx">
<div class="context-menu__separator" v-if="item.type === 'separator'"></div> <div class="context-menu__separator" v-if="item.type === 'separator'"></div>
<div class="context-menu__item context-menu__item--disabled" v-else-if="item.disabled">{{item.name}}</div> <div class="context-menu__item context-menu__item--disabled" v-else-if="item.disabled">{{item.name}}</div>
<a class="context-menu__item" href="javascript:void(0)" v-else @click.stop="close(item)">{{item.name}}</a> <a class="context-menu__item" href="javascript:void(0)" v-else @click="close(item)">{{item.name}}</a>
</div> </div>
</div> </div>
</div> </div>
@ -22,10 +22,8 @@ export default {
]), ]),
}, },
methods: { methods: {
close(item) { close(item = null) {
if (item) { this.resolve(item);
this.resolve(item);
}
this.$store.dispatch('contextMenu/close'); this.$store.dispatch('contextMenu/close');
}, },
}, },

View File

@ -66,13 +66,14 @@ export default {
editorElt.querySelectorAll(`.discussion-editor-highlighting--${discussionId}`) editorElt.querySelectorAll(`.discussion-editor-highlighting--${discussionId}`)
.cl_each(elt => elt.classList.add('discussion-editor-highlighting--selected')); .cl_each(elt => elt.classList.add('discussion-editor-highlighting--selected'));
} }
}); },
);
}, },
}; };
</script> </script>
<style lang="scss"> <style lang="scss">
@import 'common/variables.scss'; @import '../styles/variables.scss';
.editor { .editor {
position: absolute; position: absolute;

View File

@ -2,20 +2,20 @@
<div class="explorer flex flex--column"> <div class="explorer flex flex--column">
<div class="side-title flex flex--row flex--space-between"> <div class="side-title flex flex--row flex--space-between">
<div class="flex flex--row"> <div class="flex flex--row">
<button class="side-title__button button" @click="newItem()" v-title="'New file'"> <button class="side-title__button side-title__button--new-file button" @click="newItem()" v-title="'New file'">
<icon-file-plus></icon-file-plus> <icon-file-plus></icon-file-plus>
</button> </button>
<button class="side-title__button button" @click="newItem(true)" v-title="'New folder'"> <button class="side-title__button side-title__button--new-folder button" @click="newItem(true)" v-title="'New folder'">
<icon-folder-plus></icon-folder-plus> <icon-folder-plus></icon-folder-plus>
</button> </button>
<button class="side-title__button button" @click="deleteItem()" v-title="'Delete'"> <button class="side-title__button side-title__button--delete button" @click="deleteItem()" v-title="'Delete'">
<icon-delete></icon-delete> <icon-delete></icon-delete>
</button> </button>
<button class="side-title__button button" @click="editItem()" v-title="'Rename'"> <button class="side-title__button side-title__button--rename button" @click="editItem()" v-title="'Rename'">
<icon-pen></icon-pen> <icon-pen></icon-pen>
</button> </button>
</div> </div>
<button class="side-title__button button" @click="toggleExplorer(false)" v-title="'Close explorer'"> <button class="side-title__button side-title__button--close button" @click="toggleExplorer(false)" v-title="'Close explorer'">
<icon-close></icon-close> <icon-close></icon-close>
</button> </button>
</div> </div>
@ -28,6 +28,7 @@
<script> <script>
import { mapState, mapGetters, mapActions } from 'vuex'; import { mapState, mapGetters, mapActions } from 'vuex';
import ExplorerNode from './ExplorerNode'; import ExplorerNode from './ExplorerNode';
import explorerSvc from '../services/explorerSvc';
export default { export default {
components: { components: {
@ -49,10 +50,8 @@ export default {
...mapActions('data', [ ...mapActions('data', [
'toggleExplorer', 'toggleExplorer',
]), ]),
...mapActions('explorer', [ newItem: isFolder => explorerSvc.newItem(isFolder),
'newItem', deleteItem: () => explorerSvc.deleteItem(),
'deleteItem',
]),
editItem() { editItem() {
const node = this.selectedNode; const node = this.selectedNode;
if (!node.isTrash && !node.isTemp) { if (!node.isTrash && !node.isTemp) {
@ -68,7 +67,8 @@ export default {
this.$store.dispatch('explorer/openNode', currentFileId); this.$store.dispatch('explorer/openNode', currentFileId);
}, { }, {
immediate: true, immediate: true,
}); },
);
}, },
}; };
</script> </script>

View File

@ -1,15 +1,15 @@
<template> <template>
<div class="explorer-node" :class="{'explorer-node--selected': isSelected, 'explorer-node--open': isOpen, 'explorer-node--drag-target': isDragTargetFolder}" @dragover.prevent @dragenter.stop="node.noDrop || setDragTarget(node.item.id)" @dragleave.stop="isDragTarget && setDragTargetId()" @drop.prevent.stop="onDrop" @contextmenu="onContextMenu"> <div class="explorer-node" :class="{'explorer-node--selected': isSelected, 'explorer-node--folder': node.isFolder, 'explorer-node--open': isOpen, 'explorer-node--trash': node.isTrash, 'explorer-node--temp': node.isTemp, 'explorer-node--drag-target': isDragTargetFolder}" @dragover.prevent @dragenter.stop="node.noDrop || setDragTarget(node)" @dragleave.stop="isDragTarget && setDragTarget()" @drop.prevent.stop="onDrop" @contextmenu="onContextMenu">
<div class="explorer-node__item-editor" v-if="isEditing" :class="['explorer-node__item-editor--' + node.item.type]" :style="{paddingLeft: leftPadding}" draggable="true" @dragstart.stop.prevent> <div class="explorer-node__item-editor" v-if="isEditing" :style="{paddingLeft: leftPadding}" draggable="true" @dragstart.stop.prevent>
<input type="text" class="text-input" v-focus @blur="submitEdit()" @keydown.stop @keydown.enter="submitEdit()" @keydown.esc="submitEdit(true)" v-model="editingNodeName"> <input type="text" class="text-input" v-focus @blur="submitEdit()" @keydown.stop @keydown.enter="submitEdit()" @keydown.esc="submitEdit(true)" v-model="editingNodeName">
</div> </div>
<div class="explorer-node__item" v-else :class="['explorer-node__item--' + node.item.type]" :style="{paddingLeft: leftPadding}" @click="select()" draggable="true" @dragstart.stop="setDragSourceId" @dragend.stop="setDragTargetId()"> <div class="explorer-node__item" v-else :style="{paddingLeft: leftPadding}" @click="select()" draggable="true" @dragstart.stop="setDragSourceId" @dragend.stop="setDragTarget()">
{{node.item.name}} {{node.item.name}}
<icon-provider class="explorer-node__location" v-for="location in node.locations" :key="location.id" :provider-id="location.providerId"></icon-provider> <icon-provider class="explorer-node__location" v-for="location in node.locations" :key="location.id" :provider-id="location.providerId"></icon-provider>
</div> </div>
<div class="explorer-node__children" v-if="node.isFolder && isOpen"> <div class="explorer-node__children" v-if="node.isFolder && isOpen">
<explorer-node v-for="node in node.folders" :key="node.item.id" :node="node" :depth="depth + 1"></explorer-node> <explorer-node v-for="node in node.folders" :key="node.item.id" :node="node" :depth="depth + 1"></explorer-node>
<div v-if="newChild" class="explorer-node__new-child" :class="['explorer-node__new-child--' + newChild.item.type]" :style="{paddingLeft: childLeftPadding}"> <div v-if="newChild" class="explorer-node__new-child" :class="{'explorer-node__new-child--folder': newChild.isFolder}" :style="{paddingLeft: childLeftPadding}">
<input type="text" class="text-input" v-focus @blur="submitNewChild()" @keydown.stop @keydown.enter="submitNewChild()" @keydown.esc="submitNewChild(true)" v-model.trim="newChildName"> <input type="text" class="text-input" v-focus @blur="submitNewChild()" @keydown.stop @keydown.enter="submitNewChild()" @keydown.esc="submitNewChild(true)" v-model.trim="newChildName">
</div> </div>
<explorer-node v-for="node in node.files" :key="node.item.id" :node="node" :depth="depth + 1"></explorer-node> <explorer-node v-for="node in node.files" :key="node.item.id" :node="node" :depth="depth + 1"></explorer-node>
@ -19,7 +19,8 @@
<script> <script>
import { mapMutations, mapActions } from 'vuex'; import { mapMutations, mapActions } from 'vuex';
import utils from '../services/utils'; import workspaceSvc from '../services/workspaceSvc';
import explorerSvc from '../services/explorerSvc';
export default { export default {
name: 'explorer-node', // Required for recursivity name: 'explorer-node', // Required for recursivity
@ -72,13 +73,10 @@ export default {
}, },
methods: { methods: {
...mapMutations('explorer', [ ...mapMutations('explorer', [
'setDragTargetId',
'setEditingId', 'setEditingId',
]), ]),
...mapActions('explorer', [ ...mapActions('explorer', [
'setDragTarget', 'setDragTarget',
'newItem',
'deleteItem',
]), ]),
select(id = this.node.item.id, doOpen = true) { select(id = this.node.item.id, doOpen = true) {
const node = this.$store.getters['explorer/nodeMap'][id]; const node = this.$store.getters['explorer/nodeMap'][id];
@ -98,35 +96,37 @@ export default {
} }
return true; return true;
}, },
submitNewChild(cancel) { async submitNewChild(cancel) {
const newChildNode = this.$store.state.explorer.newChildNode; const { newChildNode } = this.$store.state.explorer;
if (!cancel && !newChildNode.isNil && newChildNode.item.name) { if (!cancel && !newChildNode.isNil && newChildNode.item.name) {
if (newChildNode.isFolder) { try {
const id = utils.uid(); if (newChildNode.isFolder) {
this.$store.commit('folder/setItem', { const item = await workspaceSvc.storeItem(newChildNode.item);
...newChildNode.item, this.select(item.id);
id, } else {
name: utils.sanitizeName(newChildNode.item.name), const item = await workspaceSvc.createFile(newChildNode.item);
}); this.select(item.id);
this.select(id); }
} else { } catch (e) {
this.$store.dispatch('createFile', newChildNode.item) // Cancel
.then(file => this.select(file.id));
} }
} }
this.$store.commit('explorer/setNewItem', null); this.$store.commit('explorer/setNewItem', null);
}, },
submitEdit(cancel) { async submitEdit(cancel) {
const editingNode = this.$store.getters['explorer/editingNode']; const { item } = this.$store.getters['explorer/editingNode'];
const id = editingNode.item.id;
const value = this.editingValue; 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); this.setEditingId(null);
if (!cancel && item.id && value) {
try {
await workspaceSvc.storeItem({
...item,
name: value,
});
} catch (e) {
// Cancel
}
}
}, },
setDragSourceId(evt) { setDragSourceId(evt) {
if (this.node.noDrag) { if (this.node.noDrag) {
@ -141,27 +141,22 @@ export default {
onDrop() { onDrop() {
const sourceNode = this.$store.getters['explorer/dragSourceNode']; const sourceNode = this.$store.getters['explorer/dragSourceNode'];
const targetNode = this.$store.getters['explorer/dragTargetNodeFolder']; const targetNode = this.$store.getters['explorer/dragTargetNodeFolder'];
this.setDragTargetId(); this.setDragTarget();
if (!sourceNode.isNil if (!sourceNode.isNil
&& !targetNode.isNil && !targetNode.isNil
&& sourceNode.item.id !== targetNode.item.id && sourceNode.item.id !== targetNode.item.id
) { ) {
const patch = { workspaceSvc.storeItem({
id: sourceNode.item.id, ...sourceNode.item,
parentId: targetNode.item.id, parentId: targetNode.item.id,
}; });
if (sourceNode.isFolder) {
this.$store.commit('folder/patchItem', patch);
} else {
this.$store.commit('file/patchItem', patch);
}
} }
}, },
onContextMenu(evt) { async onContextMenu(evt) {
if (this.select(undefined, false)) { if (this.select(undefined, false)) {
evt.preventDefault(); evt.preventDefault();
evt.stopPropagation(); evt.stopPropagation();
this.$store.dispatch('contextMenu/open', { const item = await this.$store.dispatch('contextMenu/open', {
coordinates: { coordinates: {
left: evt.clientX, left: evt.clientX,
top: evt.clientY, top: evt.clientY,
@ -169,11 +164,11 @@ export default {
items: [{ items: [{
name: 'New file', name: 'New file',
disabled: !this.node.isFolder || this.node.isTrash, disabled: !this.node.isFolder || this.node.isTrash,
perform: () => this.newItem(false), perform: () => explorerSvc.newItem(false),
}, { }, {
name: 'New folder', name: 'New folder',
disabled: !this.node.isFolder || this.node.isTrash || this.node.isTemp, disabled: !this.node.isFolder || this.node.isTrash || this.node.isTemp,
perform: () => this.newItem(true), perform: () => explorerSvc.newItem(true),
}, { }, {
type: 'separator', type: 'separator',
}, { }, {
@ -182,10 +177,12 @@ export default {
perform: () => this.setEditingId(this.node.item.id), perform: () => this.setEditingId(this.node.item.id),
}, { }, {
name: 'Delete', name: 'Delete',
perform: () => this.deleteItem(), perform: () => explorerSvc.deleteItem(),
}], }],
}) });
.then(item => item.perform()); if (item) {
item.perform();
}
} }
}, },
}, },
@ -229,17 +226,25 @@ $item-font-size: 14px;
} }
} }
.explorer-node__item--folder, .explorer-node--trash,
.explorer-node__item-editor--folder, .explorer-node--temp {
color: rgba(0, 0, 0, 0.5);
}
.explorer-node--folder > .explorer-node__item,
.explorer-node--folder > .explorer-node__item-editor,
.explorer-node__new-child--folder { .explorer-node__new-child--folder {
&::before { &::before {
content: '▹'; content: '▹';
position: absolute; position: absolute;
margin-left: -13px; margin-left: -13px;
}
}
.explorer-node--open > & { .explorer-node--folder.explorer-node--open > .explorer-node__item,
content: '▾'; .explorer-node--folder.explorer-node--open > .explorer-node__item-editor {
} &::before {
content: '▾';
} }
} }

View File

@ -34,7 +34,7 @@
<script> <script>
import { mapState } from 'vuex'; import { mapState } from 'vuex';
import editorSvc from '../services/editorSvc'; import editorSvc from '../services/editorSvc';
import cledit from '../services/cledit'; import cledit from '../services/editor/cledit';
import store from '../store'; import store from '../store';
import EditorClassApplier from './common/EditorClassApplier'; import EditorClassApplier from './common/EditorClassApplier';
@ -70,7 +70,8 @@ class DynamicClassApplier {
() => ({ () => ({
start: this.startMarker.offset, start: this.startMarker.offset,
end: this.endMarker.offset, end: this.endMarker.offset,
})); }),
);
} }
} }
@ -126,7 +127,10 @@ export default {
offsetList.forEach((offset, i) => { offsetList.forEach((offset, i) => {
const key = `${offset.start}:${offset.end}`; const key = `${offset.start}:${offset.end}`;
this.classAppliers[key] = oldClassAppliers[key] || new DynamicClassApplier( this.classAppliers[key] = oldClassAppliers[key] || new DynamicClassApplier(
'find-replace-highlighting', offset, i > 200); 'find-replace-highlighting',
offset,
i > 200,
);
}); });
} catch (e) { } catch (e) {
// Ignore // Ignore
@ -156,9 +160,9 @@ export default {
this.findPosition = 0; this.findPosition = 0;
}, },
find(mode = 'forward') { find(mode = 'forward') {
const selectedClassApplier = this.selectedClassApplier; const { selectedClassApplier } = this;
this.unselectClassApplier(); this.unselectClassApplier();
const selectionMgr = editorSvc.clEditor.selectionMgr; const { selectionMgr } = editorSvc.clEditor;
const startOffset = Math.min(selectionMgr.selectionStart, selectionMgr.selectionEnd); const startOffset = Math.min(selectionMgr.selectionStart, selectionMgr.selectionEnd);
const endOffset = Math.max(selectionMgr.selectionStart, selectionMgr.selectionEnd); const endOffset = Math.max(selectionMgr.selectionStart, selectionMgr.selectionEnd);
const keys = Object.keys(this.classAppliers); const keys = Object.keys(this.classAppliers);
@ -206,7 +210,10 @@ export default {
return; return;
} }
editorSvc.clEditor.replaceAll( editorSvc.clEditor.replaceAll(
this.replaceRegex, this.replaceText, this.selectedClassApplier.startMarker.offset); this.replaceRegex,
this.replaceText,
this.selectedClassApplier.startMarker.offset,
);
this.$nextTick(() => this.find()); this.$nextTick(() => this.find());
} }
}, },
@ -227,7 +234,9 @@ export default {
// Highlight occurences // Highlight occurences
this.debouncedHighlightOccurrences = cledit.Utils.debounce( this.debouncedHighlightOccurrences = cledit.Utils.debounce(
() => this.highlightOccurrences(), 25); () => this.highlightOccurrences(),
25,
);
// Refresh highlighting when find text changes or changing options // Refresh highlighting when find text changes or changing options
this.$watch(() => this.findText, this.debouncedHighlightOccurrences); this.$watch(() => this.findText, this.debouncedHighlightOccurrences);
this.$watch(() => this.findCaseSensitive, this.debouncedHighlightOccurrences); this.$watch(() => this.findCaseSensitive, this.debouncedHighlightOccurrences);
@ -273,7 +282,7 @@ export default {
</script> </script>
<style lang="scss"> <style lang="scss">
@import 'common/variables.scss'; @import '../styles/variables.scss';
.find-replace { .find-replace {
padding: 0 35px 0 25px; padding: 0 35px 0 25px;
@ -344,7 +353,7 @@ export default {
.find-replace__find-stats { .find-replace__find-stats {
text-align: right; text-align: right;
font-size: 0.75em; font-size: 0.75em;
opacity: 0.5; opacity: 0.6;
} }
.find-replace-highlighting { .find-replace-highlighting {

View File

@ -140,7 +140,7 @@ export default {
</script> </script>
<style lang="scss"> <style lang="scss">
@import 'common/variables.scss'; @import '../styles/variables.scss';
.layout { .layout {
position: absolute; position: absolute;

View File

@ -1,11 +1,11 @@
<template> <template>
<div class="modal" @keydown.esc="onEscape" @keydown.tab="onTab"> <div class="modal" v-if="config" @keydown.esc="onEscape" @keydown.tab="onTab" @focusin="onFocusInOut" @focusout="onFocusInOut">
<component v-if="currentModalComponent" :is="currentModalComponent"></component> <component v-if="currentModalComponent" :is="currentModalComponent"></component>
<modal-inner v-else aria-label="Dialog"> <modal-inner v-else aria-label="Dialog">
<div class="modal__content" v-html="config.content"></div> <div class="modal__content" v-html="simpleModal.contentHtml(config)"></div>
<div class="modal__button-bar"> <div class="modal__button-bar">
<button class="button" v-if="config.rejectText" @click="config.reject()">{{config.rejectText}}</button> <button class="button" v-if="simpleModal.rejectText" @click="config.reject()">{{simpleModal.rejectText}}</button>
<button class="button" v-if="config.resolveText" @click="config.resolve()">{{config.resolveText}}</button> <button class="button button--resolve" v-if="simpleModal.resolveText" @click="config.resolve()">{{simpleModal.resolveText}}</button>
</div> </div>
</modal-inner> </modal-inner>
</div> </div>
@ -13,6 +13,7 @@
<script> <script>
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import simpleModals from '../data/simpleModals';
import editorSvc from '../services/editorSvc'; import editorSvc from '../services/editorSvc';
import ModalInner from './modals/common/ModalInner'; import ModalInner from './modals/common/ModalInner';
import FilePropertiesModal from './modals/FilePropertiesModal'; import FilePropertiesModal from './modals/FilePropertiesModal';
@ -41,6 +42,7 @@ import DropboxPublishModal from './modals/providers/DropboxPublishModal';
import GithubAccountModal from './modals/providers/GithubAccountModal'; import GithubAccountModal from './modals/providers/GithubAccountModal';
import GithubOpenModal from './modals/providers/GithubOpenModal'; import GithubOpenModal from './modals/providers/GithubOpenModal';
import GithubSaveModal from './modals/providers/GithubSaveModal'; import GithubSaveModal from './modals/providers/GithubSaveModal';
import GithubWorkspaceModal from './modals/providers/GithubWorkspaceModal';
import GithubPublishModal from './modals/providers/GithubPublishModal'; import GithubPublishModal from './modals/providers/GithubPublishModal';
import GistSyncModal from './modals/providers/GistSyncModal'; import GistSyncModal from './modals/providers/GistSyncModal';
import GistPublishModal from './modals/providers/GistPublishModal'; import GistPublishModal from './modals/providers/GistPublishModal';
@ -84,6 +86,7 @@ export default {
GithubAccountModal, GithubAccountModal,
GithubOpenModal, GithubOpenModal,
GithubSaveModal, GithubSaveModal,
GithubWorkspaceModal,
GithubPublishModal, GithubPublishModal,
GistSyncModal, GistSyncModal,
GistPublishModal, GistPublishModal,
@ -110,6 +113,9 @@ export default {
} }
return null; return null;
}, },
simpleModal() {
return simpleModals[this.config.type] || {};
},
}, },
methods: { methods: {
onEscape() { onEscape() {
@ -132,14 +138,14 @@ export default {
const isFocusIn = evt.type === 'focusin'; const isFocusIn = evt.type === 'focusin';
if (evt.target.parentNode && evt.target.parentNode.parentNode) { if (evt.target.parentNode && evt.target.parentNode.parentNode) {
// Focus effect // Focus effect
if (evt.target.parentNode.classList.contains('form-entry__field') && if (evt.target.parentNode.classList.contains('form-entry__field')
evt.target.parentNode.parentNode.classList.contains('form-entry')) { && evt.target.parentNode.parentNode.classList.contains('form-entry')) {
evt.target.parentNode.parentNode.classList.toggle('form-entry--focused', isFocusIn); evt.target.parentNode.parentNode.classList.toggle('form-entry--focused', isFocusIn);
} }
} }
if (isFocusIn && this.config) { if (isFocusIn && this.config) {
const modalInner = this.$el.querySelector('.modal__inner-2'); const modalInner = this.$el.querySelector('.modal__inner-2');
let target = evt.target; let { target } = evt;
while (target) { while (target) {
if (target === modalInner) { if (target === modalInner) {
return; return;
@ -151,20 +157,24 @@ export default {
}, },
}, },
mounted() { mounted() {
window.addEventListener('focusin', this.onFocusInOut); this.$watch(
window.addEventListener('focusout', this.onFocusInOut); () => this.config,
const tabbables = getTabbables(this.$el); (isOpen) => {
tabbables[0].focus(); if (isOpen) {
}, const tabbables = getTabbables(this.$el);
destroyed() { if (tabbables[0]) {
window.removeEventListener('focusin', this.onFocusInOut); tabbables[0].focus();
window.removeEventListener('focusout', this.onFocusInOut); }
}
},
{ immediate: true },
);
}, },
}; };
</script> </script>
<style lang="scss"> <style lang="scss">
@import 'common/variables.scss'; @import '../styles/variables.scss';
.modal { .modal {
position: absolute; position: absolute;
@ -173,8 +183,8 @@ export default {
background-color: rgba(160, 160, 160, 0.5); background-color: rgba(160, 160, 160, 0.5);
overflow: auto; overflow: auto;
hr { p {
margin: 0.5em 0; line-height: 1.5;
} }
} }
@ -188,7 +198,7 @@ export default {
.modal__inner-2 { .modal__inner-2 {
margin: 40px 10px 100px; margin: 40px 10px 100px;
background-color: #f8f8f8; background-color: #f8f8f8;
padding: 40px 50px 30px; padding: 50px 50px 40px;
border-radius: $border-radius-base; border-radius: $border-radius-base;
position: relative; position: relative;
overflow: hidden; overflow: hidden;
@ -221,9 +231,9 @@ export default {
.modal__image { .modal__image {
float: left; float: left;
width: 64px; width: 60px;
height: 64px; height: 60px;
margin: 1.5em 1.5em 0.5em 0; margin: 1.5em 1.2em 0.5em 0;
& + *::after { & + *::after {
content: ''; content: '';
@ -240,7 +250,7 @@ export default {
} }
.modal__sub-title { .modal__sub-title {
opacity: 0.5; opacity: 0.6;
font-size: 0.75rem; font-size: 0.75rem;
margin-bottom: 1.5rem; margin-bottom: 1.5rem;
} }
@ -262,9 +272,16 @@ export default {
} }
} }
.modal__info--multiline {
padding-top: 0.1em;
padding-bottom: 0.1em;
}
.modal__button-bar { .modal__button-bar {
margin-top: 1.75rem; margin-top: 2rem;
text-align: right; display: flex;
flex-direction: row;
justify-content: flex-end;
} }
.form-entry { .form-entry {

View File

@ -3,7 +3,7 @@
<!-- Explorer --> <!-- Explorer -->
<div class="navigation-bar__inner navigation-bar__inner--left navigation-bar__inner--button"> <div class="navigation-bar__inner navigation-bar__inner--left navigation-bar__inner--button">
<button class="navigation-bar__button button" v-if="light" @click="close()" v-title="'Close StackEdit'"><icon-close-circle></icon-close-circle></button> <button class="navigation-bar__button button" v-if="light" @click="close()" v-title="'Close StackEdit'"><icon-close-circle></icon-close-circle></button>
<button class="navigation-bar__button button" v-else tour-step-anchor="explorer" @click="toggleExplorer()" v-title="'Toggle explorer'"><icon-folder></icon-folder></button> <button class="navigation-bar__button navigation-bar__button--explorer-toggler button" v-else tour-step-anchor="explorer" @click="toggleExplorer()" v-title="'Toggle explorer'"><icon-folder></icon-folder></button>
</div> </div>
<!-- Side bar --> <!-- Side bar -->
<div class="navigation-bar__inner navigation-bar__inner--right navigation-bar__inner--button"> <div class="navigation-bar__inner navigation-bar__inner--right navigation-bar__inner--button">
@ -19,7 +19,7 @@
<!-- Title --> <!-- Title -->
<div class="navigation-bar__title navigation-bar__title--fake text-input"></div> <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> <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 --> <!-- Sync/Publish -->
<div class="flex flex--row" :class="{'navigation-bar__hidden': styles.hideLocations}"> <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> <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 utils from '../services/utils';
import pagedownButtons from '../data/pagedownButtons'; import pagedownButtons from '../data/pagedownButtons';
import store from '../store'; import store from '../store';
import workspaceSvc from '../services/workspaceSvc';
// According to mousetrap // According to mousetrap
const mod = /Mac|iPod|iPhone|iPad/.test(navigator.platform) ? 'Meta' : 'Ctrl'; const mod = /Mac|iPod|iPhone|iPad/.test(navigator.platform) ? 'Meta' : 'Ctrl';
@ -63,17 +64,16 @@ const mod = /Mac|iPod|iPhone|iPad/.test(navigator.platform) ? 'Meta' : 'Ctrl';
const getShortcut = (method) => { const getShortcut = (method) => {
let result = ''; let result = '';
Object.entries(store.getters['data/computedSettings'].shortcuts).some(([keys, shortcut]) => { Object.entries(store.getters['data/computedSettings'].shortcuts).some(([keys, shortcut]) => {
if (`${shortcut.method || shortcut}` !== method) { if (`${shortcut.method || shortcut}` === method) {
return false; result = keys.split('+').map(key => key.toLowerCase()).map((key) => {
if (key === 'mod') {
return mod;
}
// Capitalize
return key && `${key[0].toUpperCase()}${key.slice(1)}`;
}).join('+');
} }
result = keys.split('+').map(key => key.toLowerCase()).map((key) => { return result;
if (key === 'mod') {
return mod;
}
// Capitalize
return key && `${key[0].toUpperCase()}${key.slice(1)}`;
}).join('+');
return true;
}); });
return result && ` ${result}`; return result && ` ${result}`;
}; };
@ -151,6 +151,13 @@ export default {
} }
return result; return result;
}, },
editCancelTrigger() {
const current = this.$store.getters['file/current'];
return utils.serializeObject([
current.id,
current.name,
]);
},
}, },
methods: { methods: {
...mapMutations('content', [ ...mapMutations('content', [
@ -184,16 +191,22 @@ export default {
editorSvc.pagedownEditor.uiManager.doClick(name); editorSvc.pagedownEditor.uiManager.doClick(name);
} }
}, },
editTitle(toggle) { async editTitle(toggle) {
this.titleFocus = toggle; this.titleFocus = toggle;
if (toggle) { if (toggle) {
this.titleInputElt.setSelectionRange(0, this.titleInputElt.value.length); this.titleInputElt.setSelectionRange(0, this.titleInputElt.value.length);
} else { } else {
const title = this.title.trim(); const title = this.title.trim();
this.title = this.$store.getters['file/current'].name;
if (title) { if (title) {
this.$store.dispatch('file/patchCurrent', { name: utils.sanitizeName(title) }); try {
} else { await workspaceSvc.storeItem({
this.title = this.$store.getters['file/current'].name; ...this.$store.getters['file/current'],
name: title,
});
} catch (e) {
// Cancel
}
} }
} }
}, },
@ -209,10 +222,13 @@ export default {
}, },
created() { created() {
this.$watch( this.$watch(
() => this.$store.getters['file/current'].name, () => this.editCancelTrigger,
(name) => { () => {
this.title = name; this.title = '';
}, { immediate: true }); this.editTitle(false);
},
{ immediate: true },
);
}, },
mounted() { mounted() {
this.titleFakeElt = this.$el.querySelector('.navigation-bar__title--fake'); this.titleFakeElt = this.$el.querySelector('.navigation-bar__title--fake');
@ -223,7 +239,7 @@ export default {
</script> </script>
<style lang="scss"> <style lang="scss">
@import 'common/variables.scss'; @import '../styles/variables.scss';
.navigation-bar { .navigation-bar {
position: absolute; position: absolute;

View File

@ -23,7 +23,7 @@ export default {
</script> </script>
<style lang="scss"> <style lang="scss">
@import 'common/variables.scss'; @import '../styles/variables.scss';
.notification { .notification {
position: absolute; position: absolute;

View File

@ -22,7 +22,7 @@ import { mapGetters, mapActions } from 'vuex';
import CommentList from './gutters/CommentList'; import CommentList from './gutters/CommentList';
import PreviewNewDiscussionButton from './gutters/PreviewNewDiscussionButton'; import PreviewNewDiscussionButton from './gutters/PreviewNewDiscussionButton';
const appUri = `${location.protocol}//${location.host}`; const appUri = `${window.location.protocol}//${window.location.host}`;
export default { export default {
components: { components: {
@ -98,13 +98,14 @@ export default {
previewElt.querySelectorAll(`.discussion-preview-highlighting--${discussionId}`) previewElt.querySelectorAll(`.discussion-preview-highlighting--${discussionId}`)
.cl_each(elt => elt.classList.add('discussion-preview-highlighting--selected')); .cl_each(elt => elt.classList.add('discussion-preview-highlighting--selected'));
} }
}); },
);
}, },
}; };
</script> </script>
<style lang="scss"> <style lang="scss">
@import 'common/variables.scss'; @import '../styles/variables.scss';
.preview, .preview,
.preview__inner-1 { .preview__inner-1 {

View File

@ -1,22 +0,0 @@
<template>
<span class="provider-name">{{name}}</span>
</template>
<script>
import userSvc from '../services/userSvc';
export default {
props: ['providerId'],
computed: {
name() {
switch (this.userId) {
default:
return 'Google Drive';
}
},
},
created() {
userSvc.getInfo(this.userId);
},
};
</script>

View File

@ -13,7 +13,7 @@
</div> </div>
<div class="side-bar__inner"> <div class="side-bar__inner">
<main-menu v-if="panel === 'menu'"></main-menu> <main-menu v-if="panel === 'menu'"></main-menu>
<workspaces-menu v-if="panel === 'workspaces'"></workspaces-menu> <workspaces-menu v-else-if="panel === 'workspaces'"></workspaces-menu>
<sync-menu v-else-if="panel === 'sync'"></sync-menu> <sync-menu v-else-if="panel === 'sync'"></sync-menu>
<publish-menu v-else-if="panel === 'publish'"></publish-menu> <publish-menu v-else-if="panel === 'publish'"></publish-menu>
<history-menu v-else-if="panel === 'history'"></history-menu> <history-menu v-else-if="panel === 'history'"></history-menu>
@ -75,7 +75,11 @@ export default {
}), }),
computed: { computed: {
panel() { panel() {
return !this.$store.state.light && this.$store.getters['data/layoutSettings'].sideBarPanel; if (this.$store.state.light) {
return null; // No menu in light mode
}
const result = this.$store.getters['data/layoutSettings'].sideBarPanel;
return panelNames[result] ? result : 'menu';
}, },
panelName() { panelName() {
return panelNames[this.panel]; return panelNames[this.panel];
@ -93,7 +97,7 @@ export default {
</script> </script>
<style lang="scss"> <style lang="scss">
@import 'common/variables.scss'; @import '../styles/variables.scss';
.side-bar { .side-bar {
overflow: hidden; overflow: hidden;
@ -112,6 +116,11 @@ export default {
hr + hr { hr + hr {
display: none; display: none;
} }
.textfield {
font-size: 14px;
height: 26px;
}
} }
.side-bar__inner { .side-bar__inner {
@ -164,7 +173,7 @@ export default {
padding: 10px; padding: 10px;
margin: -10px -10px 10px; margin: -10px -10px 10px;
background-color: $info-bg; background-color: $info-bg;
font-size: 0.95em; font-size: 0.9em;
p { p {
margin: 10px; margin: 10px;

View File

@ -92,7 +92,7 @@ export default {
this.htmlSelection = true; this.htmlSelection = true;
if (!text) { if (!text) {
this.htmlSelection = false; this.htmlSelection = false;
text = editorSvc.previewCtx.text; ({ text } = editorSvc.previewCtx);
} }
if (text != null) { if (text != null) {
this.htmlStats.forEach((stat) => { this.htmlStats.forEach((stat) => {

View File

@ -64,7 +64,7 @@ export default {
const updateMaskY = () => { const updateMaskY = () => {
const scrollPosition = editorSvc.getScrollPosition(); const scrollPosition = editorSvc.getScrollPosition();
if (scrollPosition) { if (scrollPosition) {
const sectionDesc = editorSvc.previewCtx.sectionDescList[scrollPosition.sectionIdx]; const sectionDesc = editorSvc.previewCtxMeasured.sectionDescList[scrollPosition.sectionIdx];
this.maskY = sectionDesc.tocDimension.startOffset + this.maskY = sectionDesc.tocDimension.startOffset +
(scrollPosition.posInSection * sectionDesc.tocDimension.height); (scrollPosition.posInSection * sectionDesc.tocDimension.height);
} }

View File

@ -2,21 +2,21 @@
<div class="tour" @keydown.esc="skip"> <div class="tour" @keydown.esc="skip">
<div class="tour-step" :class="'tour-step--' + step" :style="stepStyle"> <div class="tour-step" :class="'tour-step--' + step" :style="stepStyle">
<div class="tour-step__inner" v-if="step === 'welcome'"> <div class="tour-step__inner" v-if="step === 'welcome'">
<h2>Welcome to StackEdit!</h2> <h2>Welcome back!</h2>
<p>Greater, lighter, faster... <b>StackEdit 5</b> is here!</p> <p>The new <b>StackEdit 5</b> is here!</p>
<p>Please click <b>Next</b> to take a quick tour.</p> <p>Please click <b>Next</b> to take a quick tour.</p>
<div class="tour-step__button-bar"> <div class="tour-step__button-bar">
<button class="button" @click="finish">Skip</button> <button class="button" @click="finish">Skip</button>
<button class="button" @click="next">Next</button> <button class="button button--resolve" @click="next">Next</button>
</div> </div>
</div> </div>
<div class="tour-step__inner" v-else-if="step === 'editor'"> <div class="tour-step__inner" v-else-if="step === 'editor'">
<h2>Your Markdown editor</h2> <h2>Your Markdown editor</h2>
<p>StackEdit renders your Markdown into HTML in real-time.</p> <p>StackEdit converts your Markdown to HTML in real-time.</p>
<p>Click <icon-side-preview></icon-side-preview> to toggle the side preview.</p> <p>Click <icon-side-preview></icon-side-preview> to toggle the side preview.</p>
<div class="tour-step__button-bar"> <div class="tour-step__button-bar">
<button class="button" @click="finish">Skip</button> <button class="button" @click="finish">Skip</button>
<button class="button" @click="next">Next</button> <button class="button button--resolve" @click="next">Next</button>
</div> </div>
</div> </div>
<div class="tour-step__inner" v-else-if="step === 'explorer'"> <div class="tour-step__inner" v-else-if="step === 'explorer'">
@ -25,7 +25,7 @@
<p>Click <icon-folder></icon-folder> to open the file explorer.</p> <p>Click <icon-folder></icon-folder> to open the file explorer.</p>
<div class="tour-step__button-bar"> <div class="tour-step__button-bar">
<button class="button" @click="finish">Skip</button> <button class="button" @click="finish">Skip</button>
<button class="button" @click="next">Next</button> <button class="button button--resolve" @click="next">Next</button>
</div> </div>
</div> </div>
<div class="tour-step__inner" v-else-if="step === 'menu'"> <div class="tour-step__inner" v-else-if="step === 'menu'">
@ -34,7 +34,7 @@
<p>Click <icon-provider provider-id="stackedit"></icon-provider> to explore the menu.</p> <p>Click <icon-provider provider-id="stackedit"></icon-provider> to explore the menu.</p>
<div class="tour-step__button-bar"> <div class="tour-step__button-bar">
<button class="button" @click="finish">Skip</button> <button class="button" @click="finish">Skip</button>
<button class="button" @click="next">Next</button> <button class="button button--resolve" @click="next">Next</button>
</div> </div>
</div> </div>
<div class="tour-step__inner" v-else-if="step === 'end'"> <div class="tour-step__inner" v-else-if="step === 'end'">
@ -42,7 +42,7 @@
<p>If you like StackEdit, please rate 5 stars on the <a target="_blank" href="https://chrome.google.com/webstore/detail/iiooodelglhkcpgbajoejffhijaclcdg/reviews">Chrome Web Store</a>.</p> <p>If you like StackEdit, please rate 5 stars on the <a target="_blank" href="https://chrome.google.com/webstore/detail/iiooodelglhkcpgbajoejffhijaclcdg/reviews">Chrome Web Store</a>.</p>
<p>You can also star the project on <a target="_blank" href="https://github.com/benweet/stackedit">GitHub</a> and join the <a target="_blank" href="https://community.stackedit.io/">community</a>.</p> <p>You can also star the project on <a target="_blank" href="https://github.com/benweet/stackedit">GitHub</a> and join the <a target="_blank" href="https://community.stackedit.io/">community</a>.</p>
<div class="tour-step__button-bar"> <div class="tour-step__button-bar">
<button class="button" @click="finish">Ok</button> <button class="button button--resolve" @click="finish">Ok</button>
</div> </div>
</div> </div>
</div> </div>
@ -126,7 +126,7 @@ export default {
<style lang="scss"> <style lang="scss">
@import 'common/variables.scss'; @import '../styles/variables.scss';
.tour { .tour {
position: absolute; position: absolute;
@ -139,12 +139,12 @@ export default {
} }
$tour-step-background: mix(#f3f3f3, $selection-highlighting-color, 75%); $tour-step-background: mix(#f3f3f3, $selection-highlighting-color, 75%);
$tour-step-width: 220px; $tour-step-width: 240px;
.tour-step__inner { .tour-step__inner {
position: absolute; position: absolute;
background-color: $tour-step-background; background-color: $tour-step-background;
padding: 1.5em 1em 1em; padding: 1.5em;
font-size: 0.9em; font-size: 0.9em;
line-height: 1.33; line-height: 1.33;
width: $tour-step-width; width: $tour-step-width;
@ -213,6 +213,13 @@ $tour-step-width: 220px;
} }
.tour-step__button-bar { .tour-step__button-bar {
text-align: right; margin-top: 1.5em;
display: flex;
flex-direction: row;
justify-content: flex-end;
.button {
font-size: 1.1em;
}
} }
</style> </style>

View File

@ -10,13 +10,11 @@ export default {
props: ['userId'], props: ['userId'],
computed: { computed: {
url() { url() {
const userInfo = this.$store.state.userInfo.itemMap[this.userId]; userSvc.getInfo(this.userId);
const userInfo = this.$store.state.userInfo.itemsById[this.userId];
return userInfo && userInfo.imageUrl && `url('${userInfo.imageUrl}')`; return userInfo && userInfo.imageUrl && `url('${userInfo.imageUrl}')`;
}, },
}, },
created() {
userSvc.getInfo(this.userId);
},
}; };
</script> </script>

View File

@ -9,12 +9,10 @@ export default {
props: ['userId'], props: ['userId'],
computed: { computed: {
name() { name() {
const userInfo = this.$store.state.userInfo.itemMap[this.userId]; userSvc.getInfo(this.userId);
const userInfo = this.$store.state.userInfo.itemsById[this.userId];
return userInfo ? userInfo.name : 'Someone'; return userInfo ? userInfo.name : 'Someone';
}, },
}, },
created() {
userSvc.getInfo(this.userId);
},
}; };
</script> </script>

View File

@ -1,4 +1,4 @@
import cledit from '../../services/cledit'; import cledit from '../../services/editor/cledit';
import editorSvc from '../../services/editorSvc'; import editorSvc from '../../services/editorSvc';
import utils from '../../services/utils'; import utils from '../../services/utils';
@ -10,7 +10,9 @@ const nextTickExecCbs = cledit.Utils.debounce(() => {
} }
if (savedSelection) { if (savedSelection) {
editorSvc.clEditor.selectionMgr.setSelectionStartEnd( editorSvc.clEditor.selectionMgr.setSelectionStartEnd(
savedSelection.start, savedSelection.end); savedSelection.start,
savedSelection.end,
);
} }
savedSelection = null; savedSelection = null;
}); });

View File

@ -1,4 +1,4 @@
import cledit from '../../services/cledit'; import cledit from '../../services/editor/cledit';
import editorSvc from '../../services/editorSvc'; import editorSvc from '../../services/editorSvc';
import utils from '../../services/utils'; import utils from '../../services/utils';
@ -40,14 +40,22 @@ export default class PreviewClassApplier {
const offset = this.offsetGetter(); const offset = this.offsetGetter();
if (offset) { if (offset) {
const offsetStart = editorSvc.getPreviewOffset( const offsetStart = editorSvc.getPreviewOffset(
offset.start, editorSvc.previewCtx.sectionDescList); offset.start,
editorSvc.previewCtx.sectionDescList,
);
const offsetEnd = editorSvc.getPreviewOffset( const offsetEnd = editorSvc.getPreviewOffset(
offset.end, editorSvc.previewCtx.sectionDescList); offset.end,
editorSvc.previewCtx.sectionDescList,
);
if (offsetStart != null && offsetEnd != null && offsetStart !== offsetEnd) { if (offsetStart != null && offsetEnd != null && offsetStart !== offsetEnd) {
const start = cledit.Utils.findContainer( const start = cledit.Utils.findContainer(
editorSvc.previewElt, Math.min(offsetStart, offsetEnd)); editorSvc.previewElt,
Math.min(offsetStart, offsetEnd),
);
const end = cledit.Utils.findContainer( const end = cledit.Utils.findContainer(
editorSvc.previewElt, Math.max(offsetStart, offsetEnd)); editorSvc.previewElt,
Math.max(offsetStart, offsetEnd),
);
const range = document.createRange(); const range = document.createRange();
range.setStart(start.container, start.offsetInContainer); range.setStart(start.container, start.offsetInContainer);
range.setEnd(end.container, end.offsetInContainer); range.setEnd(end.container, end.offsetInContainer);

View File

@ -0,0 +1,80 @@
import Vue from 'vue';
import Clipboard from 'clipboard';
import timeSvc from '../../services/timeSvc';
import store from '../../store';
// Global directives
Vue.directive('focus', {
inserted(el) {
el.focus();
const { value } = el;
if (value && el.setSelectionRange) {
el.setSelectionRange(0, value.length);
}
},
});
const setVisible = (el, value) => {
el.style.display = value ? '' : 'none';
if (value) {
el.removeAttribute('aria-hidden');
} else {
el.setAttribute('aria-hidden', 'true');
}
};
Vue.directive('show', {
bind(el, { value }) {
setVisible(el, value);
},
update(el, { value, oldValue }) {
if (value !== oldValue) {
setVisible(el, value);
}
},
});
const setElTitle = (el, title) => {
el.title = title;
el.setAttribute('aria-label', title);
};
Vue.directive('title', {
bind(el, { 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);
},
});
// Global filters
Vue.filter('formatTime', time =>
// Access the minute counter for reactive refresh
timeSvc.format(time, store.state.minuteCounter));

View File

@ -47,11 +47,13 @@ export default {
...mapMutations('discussion', [ ...mapMutations('discussion', [
'setIsCommenting', 'setIsCommenting',
]), ]),
removeComment() { async removeComment() {
this.$store.dispatch('modal/commentDeletion') try {
.then( await this.$store.dispatch('modal/open', 'commentDeletion');
() => this.$store.dispatch('discussion/cleanCurrentFile', { filterComment: this.comment }), this.$store.dispatch('discussion/cleanCurrentFile', { filterComment: this.comment });
() => {}); // Cancel } catch (e) {
// Cancel
}
}, },
}, },
mounted() { mounted() {
@ -63,8 +65,7 @@ export default {
let scrollerMirrorElt; let scrollerMirrorElt;
const getScrollerMirrorElt = () => { const getScrollerMirrorElt = () => {
if (!scrollerMirrorElt) { if (!scrollerMirrorElt) {
scrollerMirrorElt = document.querySelector( scrollerMirrorElt = document.querySelector(`.comment-list .comment--${commentId} .comment__text-inner`);
`.comment-list .comment--${commentId} .comment__text-inner`);
} }
return scrollerMirrorElt || { scrollTop: 0 }; return scrollerMirrorElt || { scrollTop: 0 };
}; };

View File

@ -107,10 +107,13 @@ export default {
this.currentDiscussionLastCommentId this.currentDiscussionLastCommentId
&& this.$el.querySelector(`.comment--${this.currentDiscussionLastCommentId}`), && this.$el.querySelector(`.comment--${this.currentDiscussionLastCommentId}`),
this.$el.querySelector('.comment--new'), this.$el.querySelector('.comment--new'),
true); true,
);
} else { } else {
tops[discussionId] = getTop(discussion, tops[discussionId] = getTop(
this.$el.querySelector(`.comment--discussion-${discussionId}`)); discussion,
this.$el.querySelector(`.comment--discussion-${discussionId}`),
);
} }
}); });
this.tops = tops; this.tops = tops;
@ -120,7 +123,8 @@ export default {
this.$watch( this.$watch(
() => this.updateTopsTrigger, () => this.updateTopsTrigger,
() => this.updateTops(), () => this.updateTops(),
{ immediate: true }); { immediate: true },
);
const layoutSettings = this.$store.getters['data/layoutSettings']; const layoutSettings = this.$store.getters['data/layoutSettings'];
this.scrollerElt = layoutSettings.showEditor this.scrollerElt = layoutSettings.showEditor
@ -161,7 +165,8 @@ export default {
this.$watch( this.$watch(
() => this.updateStickyTrigger, () => this.updateStickyTrigger,
() => this.updateSticky(), () => this.updateSticky(),
{ immediate: true }); { immediate: true },
);
// Move preview discussions once previewCtxWithDiffs has been calculated // Move preview discussions once previewCtxWithDiffs has been calculated
if (!editorSvc.previewCtxWithDiffs) { if (!editorSvc.previewCtxWithDiffs) {
@ -178,7 +183,7 @@ export default {
</script> </script>
<style lang="scss"> <style lang="scss">
@import '../common/variables.scss'; @import '../../styles/variables.scss';
.comment-list { .comment-list {
position: absolute; position: absolute;

View File

@ -28,7 +28,7 @@
</template> </template>
<script> <script>
import { mapState, mapGetters, mapMutations } from 'vuex'; import { mapState, mapGetters, mapMutations, mapActions } from 'vuex';
import editorSvc from '../../services/editorSvc'; import editorSvc from '../../services/editorSvc';
import animationSvc from '../../services/animationSvc'; import animationSvc from '../../services/animationSvc';
import markdownConversionSvc from '../../services/markdownConversionSvc'; import markdownConversionSvc from '../../services/markdownConversionSvc';
@ -67,6 +67,9 @@ export default {
...mapMutations('discussion', [ ...mapMutations('discussion', [
'setCurrentDiscussionId', 'setCurrentDiscussionId',
]), ]),
...mapActions('notification', [
'info',
]),
goToDiscussion(discussionId = this.currentDiscussionId) { goToDiscussion(discussionId = this.currentDiscussionId) {
this.setCurrentDiscussionId(discussionId); this.setCurrentDiscussionId(discussionId);
const layoutSettings = this.$store.getters['data/layoutSettings']; const layoutSettings = this.$store.getters['data/layoutSettings'];
@ -75,7 +78,7 @@ export default {
? editorSvc.clEditor.selectionMgr.getCoordinates(discussion.end) ? editorSvc.clEditor.selectionMgr.getCoordinates(discussion.end)
: editorSvc.getPreviewOffsetCoordinates(editorSvc.getPreviewOffset(discussion.end)); : editorSvc.getPreviewOffsetCoordinates(editorSvc.getPreviewOffset(discussion.end));
if (!coordinates) { if (!coordinates) {
this.$store.dispatch('notification/info', "Discussion can't be located in the file."); this.info("Discussion can't be located in the file.");
} else { } else {
const scrollerElt = layoutSettings.showEditor const scrollerElt = layoutSettings.showEditor
? editorSvc.editorElt.parentNode ? editorSvc.editorElt.parentNode
@ -93,20 +96,22 @@ export default {
.start(); .start();
} }
}, },
removeDiscussion() { async removeDiscussion() {
this.$store.dispatch('modal/discussionDeletion') try {
.then( await this.$store.dispatch('modal/open', 'discussionDeletion');
() => this.$store.dispatch('discussion/cleanCurrentFile', { this.$store.dispatch('discussion/cleanCurrentFile', {
filterDiscussion: this.currentDiscussion, filterDiscussion: this.currentDiscussion,
}), });
() => {}); // Cancel } catch (e) {
// Cancel
}
}, },
}, },
}; };
</script> </script>
<style lang="scss"> <style lang="scss">
@import '../common/variables.scss'; @import '../../styles/variables.scss';
.current-discussion { .current-discussion {
position: absolute; position: absolute;

View File

@ -3,7 +3,7 @@
<div class="comment__header flex flex--row flex--space-between flex--align-center"> <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 flex flex--row flex--align-center">
<div class="comment__user-image"> <div class="comment__user-image">
<user-image :user-id="loginToken.sub"></user-image> <user-image :user-id="userId"></user-image>
</div> </div>
<span class="user-name">{{loginToken.name}}</span> <span class="user-name">{{loginToken.name}}</span>
</div> </div>
@ -24,7 +24,7 @@
import { mapGetters, mapMutations, mapActions } from 'vuex'; import { mapGetters, mapMutations, mapActions } from 'vuex';
import Prism from 'prismjs'; import Prism from 'prismjs';
import UserImage from '../UserImage'; import UserImage from '../UserImage';
import cledit from '../../services/cledit'; import cledit from '../../services/editor/cledit';
import editorSvc from '../../services/editorSvc'; import editorSvc from '../../services/editorSvc';
import markdownConversionSvc from '../../services/markdownConversionSvc'; import markdownConversionSvc from '../../services/markdownConversionSvc';
import utils from '../../services/utils'; import utils from '../../services/utils';
@ -33,9 +33,12 @@ export default {
components: { components: {
UserImage, UserImage,
}, },
computed: mapGetters('workspace', [ computed: {
'loginToken', ...mapGetters('workspace', [
]), 'loginToken',
'userId',
]),
},
methods: { methods: {
...mapMutations('discussion', [ ...mapMutations('discussion', [
'setNewCommentFocus', 'setNewCommentFocus',
@ -53,7 +56,7 @@ export default {
const discussionId = this.$store.state.discussion.currentDiscussionId; const discussionId = this.$store.state.discussion.currentDiscussionId;
const comment = { const comment = {
discussionId, discussionId,
sub: this.loginToken.sub, sub: this.userId,
text, text,
created: Date.now(), created: Date.now(),
}; };
@ -83,9 +86,11 @@ export default {
const clEditor = cledit(preElt, scrollerElt, true); const clEditor = cledit(preElt, scrollerElt, true);
clEditor.init({ clEditor.init({
sectionHighlighter: section => Prism.highlight( sectionHighlighter: section => Prism.highlight(
section.text, editorSvc.prismGrammars[section.data]), section.text,
sectionParser: text => markdownConversionSvc.parseSections( editorSvc.prismGrammars[section.data],
editorSvc.converter, text).sections, ),
sectionParser: text => markdownConversionSvc
.parseSections(editorSvc.converter, text).sections,
content: this.$store.state.discussion.newCommentText, content: this.$store.state.discussion.newCommentText,
selectionStart: this.$store.state.discussion.newCommentSelection.start, selectionStart: this.$store.state.discussion.newCommentSelection.start,
selectionEnd: this.$store.state.discussion.newCommentSelection.end, selectionEnd: this.$store.state.discussion.newCommentSelection.end,
@ -111,14 +116,14 @@ export default {
clEditor.focus(); clEditor.focus();
} }
}), }),
{ immediate: true }); { immediate: true },
);
if (isSticky) { if (isSticky) {
let scrollerMirrorElt; let scrollerMirrorElt;
const getScrollerMirrorElt = () => { const getScrollerMirrorElt = () => {
if (!scrollerMirrorElt) { if (!scrollerMirrorElt) {
scrollerMirrorElt = document.querySelector( scrollerMirrorElt = document.querySelector('.comment-list .comment--new .comment__text-inner');
'.comment-list .comment--new .comment__text-inner');
} }
return scrollerMirrorElt || { scrollTop: 0 }; return scrollerMirrorElt || { scrollTop: 0 };
}; };
@ -147,7 +152,7 @@ export default {
); );
this.$watch( this.$watch(
() => this.$store.state.discussion.newCommentText, () => this.$store.state.discussion.newCommentText,
newCommentText => clEditor.setContent(newCommentText), newCommentText => clEditor.setContent(newCommentText),
); );
} }
}, },

View File

@ -28,7 +28,7 @@ export default {
) { ) {
this.selection = editorSvc.getTrimmedSelection(); this.selection = editorSvc.getTrimmedSelection();
if (this.selection) { if (this.selection) {
const text = editorSvc.previewCtxWithDiffs.text; const { text } = editorSvc.previewCtxWithDiffs;
offset = editorSvc.getPreviewOffset(this.selection.end); offset = editorSvc.getPreviewOffset(this.selection.end);
while (offset && text[offset - 1] === '\n') { while (offset && text[offset - 1] === '\n') {
offset -= 1; offset -= 1;
@ -46,7 +46,8 @@ export default {
editorSvc.$on('previewSelectionRange', () => this.checkSelection()); editorSvc.$on('previewSelectionRange', () => this.checkSelection());
this.$watch( this.$watch(
() => this.$store.getters['layout/styles'].previewWidth, () => this.$store.getters['layout/styles'].previewWidth,
() => this.checkSelection()); () => this.checkSelection(),
);
this.checkSelection(); this.checkSelection();
}); });
}, },

View File

@ -33,7 +33,7 @@ export default {
</script> </script>
<style lang="scss"> <style lang="scss">
@import '../common/variables.scss'; @import '../../styles/variables.scss';
.sticky-comment { .sticky-comment {
position: absolute; position: absolute;

View File

@ -12,18 +12,19 @@
</menu-entry> </menu-entry>
<menu-entry @click.native="exportPdf"> <menu-entry @click.native="exportPdf">
<icon-download slot="icon"></icon-download> <icon-download slot="icon"></icon-download>
<div><div class="menu-entry__label">sponsor</div> Export as PDF</div> <div><div class="menu-entry__label" :class="{'menu-entry__label--warning': !isSponsor}">sponsor</div> Export as PDF</div>
<span>Produce a PDF from an HTML template.</span> <span>Produce a PDF from an HTML template.</span>
</menu-entry> </menu-entry>
<menu-entry @click.native="exportPandoc"> <menu-entry @click.native="exportPandoc">
<icon-download slot="icon"></icon-download> <icon-download slot="icon"></icon-download>
<div><div class="menu-entry__label">sponsor</div> Export with Pandoc</div> <div><div class="menu-entry__label" :class="{'menu-entry__label--warning': !isSponsor}">sponsor</div> Export with Pandoc</div>
<span>Convert to PDF, Word, EPUB...</span> <span>Convert to PDF, Word, EPUB...</span>
</menu-entry> </menu-entry>
</div> </div>
</template> </template>
<script> <script>
import { mapGetters } from 'vuex';
import MenuEntry from './common/MenuEntry'; import MenuEntry from './common/MenuEntry';
import exportSvc from '../../services/exportSvc'; import exportSvc from '../../services/exportSvc';
@ -31,23 +32,24 @@ export default {
components: { components: {
MenuEntry, MenuEntry,
}, },
computed: mapGetters(['isSponsor']),
methods: { methods: {
exportMarkdown() { exportMarkdown() {
const currentFile = this.$store.getters['file/current']; const currentFile = this.$store.getters['file/current'];
return exportSvc.exportToDisk(currentFile.id, 'md') return exportSvc.exportToDisk(currentFile.id, 'md')
.catch(() => {}); // Cancel .catch(() => { /* Cancel */ });
}, },
exportHtml() { exportHtml() {
return this.$store.dispatch('modal/open', 'htmlExport') return this.$store.dispatch('modal/open', 'htmlExport')
.catch(() => {}); // Cancel .catch(() => { /* Cancel */ });
}, },
exportPdf() { exportPdf() {
return this.$store.dispatch('modal/open', 'pdfExport') return this.$store.dispatch('modal/open', 'pdfExport')
.catch(() => {}); // Cancel .catch(() => { /* Cancel */ });
}, },
exportPandoc() { exportPandoc() {
return this.$store.dispatch('modal/open', 'pandocExport') return this.$store.dispatch('modal/open', 'pandocExport')
.catch(() => {}); // Cancel .catch(() => { /* Cancel */ });
}, },
}, },
}; };

View File

@ -1,26 +1,41 @@
<template> <template>
<div class="history side-bar__panel"> <div class="history side-bar__panel side-bar__panel--menu">
<div class="side-bar__info" v-if="!syncToken"> <div class="side-bar__info">
<p>You have to <a href="javascript:void(0)" @click="signin">sign in with Google</a> to enable revision history.</p> <p v-if="syncLocations.length > 1">
<p><b>Note:</b> This will sync your main workspace.</p> <select slot="field" class="textfield" v-model="syncLocationId" @keydown.enter="resolve()">
</div> <option v-for="location in syncLocations" :key="location.id" :value="location.id">
<div class="side-bar__info" v-if="loading"> {{ location.description }}
<p>Loading history</p> </option>
</div> </select>
<div class="side-bar__info" v-else-if="!revisionsWithSpacer.length"> </p>
<p><b>{{currentFileName}}</b> has no history.</p> <p v-if="!historyContext">Synchronize <b>{{currentFileName}}</b> to enable revision history or <a href="javascript:void(0)" @click="signin">sign in with Google</a> to synchronize your main workspace.</p>
</div> <p v-else-if="loading">Loading history</p>
<div class="revision" v-for="revision in revisionsWithSpacer" :key="revision.id"> <p v-else-if="!revisionsWithSpacer.length"><b>{{currentFileName}}</b> has no history.</p>
<div class="history__spacer" v-if="revision.spacer"></div> <div class="menu-entry menu-entry--info flex flex--row flex--align-center" v-else>
<a class="revision__button button flex flex--row" href="javascript:void(0)" @click="open(revision)"> <div class="menu-entry__icon menu-entry__icon--image">
<div class="revision__icon"> <icon-provider :provider-id="syncLocation.providerId"></icon-provider>
<user-image :user-id="revision.sub"></user-image>
</div> </div>
<div class="revision__header flex flex--column"> <span v-if="syncLocation.url">
<user-name :user-id="revision.sub"></user-name> The following revisions are stored in <a :href="syncLocation.url" target="_blank">{{ syncLocationProviderName }}</a>.
<div class="revision__created">{{revision.created | formatTime}}</div> </span>
</div> <span v-else>
</a> The following revisions are stored in {{ syncLocationProviderName }}.
</span>
</div>
</div>
<div>
<div class="revision" v-for="revision in revisionsWithSpacer" :key="revision.id">
<div class="history__spacer" v-if="revision.spacer"></div>
<a class="revision__button button flex flex--row" href="javascript:void(0)" @click="open(revision)">
<div class="revision__icon">
<user-image :user-id="revision.sub"></user-image>
</div>
<div class="revision__header flex flex--column">
<user-name :user-id="revision.sub"></user-name>
<div class="revision__created">{{revision.created | formatTime}}</div>
</div>
</a>
</div>
</div> </div>
<div class="history__spacer history__spacer--last" v-if="revisions.length"></div> <div class="history__spacer history__spacer--last" v-if="revisions.length"></div>
<div class="flex flex--row flex--end" v-if="showMoreButton"> <div class="flex flex--row flex--end" v-if="showMoreButton">
@ -30,8 +45,8 @@
</template> </template>
<script> <script>
import { mapMutations, mapGetters } from 'vuex'; import { mapState, mapMutations, mapGetters } from 'vuex';
import providerRegistry from '../../services/providers/providerRegistry'; import providerRegistry from '../../services/providers/common/providerRegistry';
import MenuEntry from './common/MenuEntry'; import MenuEntry from './common/MenuEntry';
import UserImage from '../UserImage'; import UserImage from '../UserImage';
import UserName from '../UserName'; import UserName from '../UserName';
@ -44,11 +59,11 @@ import syncSvc from '../../services/syncSvc';
let editorClassAppliers = []; let editorClassAppliers = [];
let previewClassAppliers = []; let previewClassAppliers = [];
let cachedFileId; let cachedHistoryContextHash;
let revisionsPromise; let revisionsPromise;
let revisionContentPromises; let revisionContentPromises;
const pageSize = 30; const pageSize = 30;
const spacerThreshold = 12 * 60 * 60 * 1000; // 12h const spacerThreshold = 60 * 60 * 1000; // 1h
export default { export default {
components: { components: {
@ -60,16 +75,73 @@ export default {
allRevisions: [], allRevisions: [],
loading: false, loading: false,
showCount: pageSize, showCount: pageSize,
syncLocationId: null,
}), }),
computed: { computed: {
...mapGetters('workspace', [ ...mapGetters('data', [
'syncToken', 'syncDataByItemId',
]), ]),
...mapGetters('syncLocation', {
syncLocations: 'currentWithWorkspaceSyncLocation',
}),
...mapState('content', [
'revisionContent',
]),
syncLocation() {
return utils.someResult(this.syncLocations, (syncLocation) => {
if (syncLocation.id === this.syncLocationId) {
return syncLocation;
}
return null;
});
},
syncLocationProviderName() {
if (!this.syncLocation) {
return null;
}
return providerRegistry.providersById[this.syncLocation.providerId].name;
},
currentFileName() { currentFileName() {
return this.$store.getters['file/current'].name; return this.$store.getters['file/current'].name;
}, },
historyContext() {
const { syncLocation } = this;
if (syncLocation) {
const provider = providerRegistry.providersById[syncLocation.providerId];
const token = provider.getToken(syncLocation);
const fileId = this.$store.getters['file/current'].id;
const contentId = `${fileId}/content`;
const historyContext = {
token,
fileId,
contentId,
syncLocation: this.syncLocation,
};
if (syncLocation.id !== 'main') {
return historyContext;
}
// Add syncData for workspace sync location
const { syncDataByItemId } = this;
const fileSyncData = syncDataByItemId[fileId];
const contentSyncData = syncDataByItemId[contentId];
if (fileSyncData && contentSyncData) {
return {
...historyContext,
fileSyncData,
contentSyncData,
};
}
}
return null;
},
historyContextHash() {
return utils.serializeObject(this.historyContext);
},
revisions() { revisions() {
return this.allRevisions.slice(0, this.showCount); return this.allRevisions.slice()
.sort((revision1, revision2) => revision2.created - revision1.created)
.slice(0, this.showCount);
}, },
revisionsWithSpacer() { revisionsWithSpacer() {
let previousCreated = 0; let previousCreated = 0;
@ -85,23 +157,18 @@ export default {
showMoreButton() { showMoreButton() {
return this.showCount < this.allRevisions.length; return this.showCount < this.allRevisions.length;
}, },
refreshTrigger() {
return utils.serializeObject([
this.$store.getters['file/current'].id,
this.syncToken,
]);
},
}, },
methods: { methods: {
...mapMutations('content', [ ...mapMutations('content', [
'setRevisionContent', 'setRevisionContent',
]), ]),
signin() { async signin() {
return googleHelper.signin() try {
.then( await googleHelper.signin();
() => syncSvc.requestSync(), syncSvc.requestSync();
() => {}, // Cancel } catch (e) {
); // Cancel
}
}, },
close() { close() {
this.$store.dispatch('data/setSideBarPanel', 'menu'); this.$store.dispatch('data/setSideBarPanel', 'menu');
@ -112,25 +179,31 @@ export default {
open(revision) { open(revision) {
let revisionContentPromise = revisionContentPromises[revision.id]; let revisionContentPromise = revisionContentPromises[revision.id];
if (!revisionContentPromise) { if (!revisionContentPromise) {
revisionContentPromise = new Promise((resolve, reject) => { const historyContext = utils.deepCopy(this.historyContext);
const syncToken = this.syncToken; if (historyContext) {
const currentFile = this.$store.getters['file/current']; const provider = providerRegistry.providersById[this.syncLocation.providerId];
this.$store.dispatch('queue/enqueue', revisionContentPromise = new Promise((resolve, reject) => this.$store.dispatch(
() => Promise.resolve() 'queue/enqueue',
.then(() => this.workspaceProvider.getRevisionContent( () => provider.getFileRevisionContent({
syncToken, currentFile.id, revision.id)) ...historyContext,
.then(resolve, reject)); revisionId: revision.id,
}); })
revisionContentPromises[revision.id] = revisionContentPromise; .then(resolve, reject),
revisionContentPromise.catch(() => { ));
revisionContentPromises[revision.id] = null; revisionContentPromises[revision.id] = revisionContentPromise;
}); revisionContentPromise.catch((err) => {
this.$store.dispatch('notification/error', err);
revisionContentPromises[revision.id] = null;
});
}
}
if (revisionContentPromise) {
revisionContentPromise.then(revisionContent =>
this.$store.dispatch('content/setRevisionContent', revisionContent));
} }
revisionContentPromise.then(revisionContent =>
this.$store.dispatch('content/setRevisionContent', revisionContent));
}, },
refreshHighlighters() { refreshHighlighters() {
const revisionContent = this.$store.state.content.revisionContent; const { revisionContent } = this;
editorClassAppliers.forEach(editorClassApplier => editorClassApplier.stop()); editorClassAppliers.forEach(editorClassApplier => editorClassApplier.stop());
editorClassAppliers = []; editorClassAppliers = [];
previewClassAppliers.forEach(previewClassApplier => previewClassApplier.stop()); previewClassAppliers.forEach(previewClassApplier => previewClassApplier.stop());
@ -145,41 +218,53 @@ export default {
end: offset + text.length, end: offset + text.length,
}; };
editorClassAppliers.push(new EditorClassApplier( editorClassAppliers.push(new EditorClassApplier(
[`revision-diff--${utils.uid()}`, ...classes], offsets)); [`revision-diff--${utils.uid()}`, ...classes],
offsets,
));
previewClassAppliers.push(new PreviewClassApplier( previewClassAppliers.push(new PreviewClassApplier(
[`revision-diff--${utils.uid()}`, ...classes], offsets)); [`revision-diff--${utils.uid()}`, ...classes],
offsets,
));
} }
offset += text.length; offset += text.length;
}); });
} }
}, },
}, },
created() { watch: {
// Find the workspace provider // Fix syncLocationId
const workspace = this.$store.getters['workspace/currentWorkspace']; syncLocation: {
this.workspaceProvider = providerRegistry.providers[workspace.providerId]; immediate: true,
handler(value) {
// Watch file changes if (!value) {
this.$watch( const firstSyncLocation = this.syncLocations[0];
() => this.refreshTrigger, if (firstSyncLocation) {
() => { this.syncLocationId = firstSyncLocation.id;
}
}
},
},
// Load revision list on context changes
historyContextHash: {
immediate: true,
handler() {
this.allRevisions = []; this.allRevisions = [];
const id = this.$store.getters['file/current'].id; const historyContext = utils.deepCopy(this.historyContext);
const syncToken = this.syncToken; if (historyContext) {
if (id && syncToken) { if (this.historyContextHash !== cachedHistoryContextHash) {
if (id !== cachedFileId) {
this.setRevisionContent(); this.setRevisionContent();
cachedFileId = id; cachedHistoryContextHash = this.historyContextHash;
revisionContentPromises = {}; revisionContentPromises = {};
const currentFile = this.$store.getters['file/current']; const provider = providerRegistry.providersById[this.syncLocation.providerId];
revisionsPromise = new Promise((resolve, reject) => { revisionsPromise = new Promise((resolve, reject) => this.$store.dispatch(
this.$store.dispatch('queue/enqueue', 'queue/enqueue',
() => Promise.resolve() () => provider
.then(() => this.workspaceProvider.listRevisions(syncToken, currentFile.id)) .listFileRevisions(historyContext)
.then(resolve, reject)); .then(resolve, reject),
}) ))
.catch(() => { .catch((err) => {
cachedFileId = null; this.$store.dispatch('notification/error', err);
cachedHistoryContextHash = null;
return []; return [];
}); });
} }
@ -191,39 +276,40 @@ export default {
}); });
} }
} }
}, { immediate: true }); },
},
const loadOne = () => { // Load each revision on revision list changes
if (!this.destroyed) { revisions(revisions) {
this.$store.dispatch('queue/enqueue', const { historyContext } = this;
() => { if (historyContext) {
let loadPromise; this.$store.dispatch(
this.revisions.some((revision) => { 'queue/enqueue',
if (!revision.created) { () => utils.awaitSequence(revisions, async (revision) => {
const syncToken = this.syncToken; // Make sure revisions and historyContext haven't changed
const currentFile = this.$store.getters['file/current']; if (!this.destroyed
loadPromise = this.workspaceProvider && this.revisions === revisions
.loadRevision(syncToken, currentFile.id, revision) && this.historyContext === historyContext
.then(() => loadOne()); ) {
} const provider = providerRegistry.providersById[this.syncLocation.providerId];
return loadPromise; await provider.loadFileRevision({
}); ...historyContext,
return loadPromise; revision,
}); });
}
}),
);
} }
}; },
// Refresh highlighters on open/close revision
this.$watch( revisionContent: {
() => this.revisions, immediate: true,
() => loadOne(), handler() {
{ immediate: true }); this.refreshHighlighters();
},
// Watch diffs changes },
this.$watch( },
() => this.$store.state.content.revisionContent, created() {
() => this.refreshHighlighters()); // Close revision on escape
// Close revision
this.onKeyup = (evt) => { this.onKeyup = (evt) => {
if (evt.which === 27) { if (evt.which === 27) {
// Esc key // Esc key
@ -246,11 +332,7 @@ export default {
</script> </script>
<style lang="scss"> <style lang="scss">
@import '../common/variables.scss'; @import '../../styles/variables.scss';
.history {
padding: 5px 5px 50px;
}
.history__button { .history__button {
font-size: 14px; font-size: 14px;
@ -266,7 +348,7 @@ export default {
position: absolute; position: absolute;
height: 100%; height: 100%;
top: 0; top: 0;
left: 24px; left: 19px;
border-left: 2px dotted $hr-color; border-left: 2px dotted $hr-color;
} }
} }
@ -277,7 +359,7 @@ export default {
.revision__button { .revision__button {
text-align: left; text-align: left;
padding: 15px; padding: 10px;
height: auto; height: auto;
text-transform: none; text-transform: none;
position: relative; position: relative;
@ -287,7 +369,7 @@ export default {
position: absolute; position: absolute;
height: 100%; height: 100%;
top: 0; top: 0;
left: 24px; left: 19px;
border-left: 2px solid $hr-color; border-left: 2px solid $hr-color;
} }
@ -318,20 +400,21 @@ export default {
.revision__header { .revision__header {
font-size: 15px; font-size: 15px;
width: 100%; width: 100%;
line-height: 1.33;
} }
.revision__created { .revision__created {
font-size: 0.75em; font-size: 0.75em;
opacity: 0.5; opacity: 0.6;
} }
.layout--revision { .layout--revision {
.cledit-section *, .cledit-section *,
.cl-preview-section * { .cl-preview-section * {
color: transparentize($editor-color-light, 0.67) !important; color: transparentize($editor-color-light, 0.5) !important;
.app--dark & { .app--dark & {
color: transparentize($editor-color-dark, 0.67) !important; color: transparentize($editor-color-dark, 0.5) !important;
} }
} }

View File

@ -7,7 +7,7 @@
</div> </div>
<div class="flex flex--column"> <div class="flex flex--column">
<div>Import Markdown</div> <div>Import Markdown</div>
<span>Open a plain text file.</span> <span>Import a plain text file.</span>
</div> </div>
</label> </label>
<input class="hidden-file" id="import-html-file-input" type="file" @change="onImportHtml"> <input class="hidden-file" id="import-html-file-input" type="file" @change="onImportHtml">
@ -27,8 +27,9 @@
import TurndownService from 'turndown/lib/turndown.browser.umd'; import TurndownService from 'turndown/lib/turndown.browser.umd';
import htmlSanitizer from '../../libs/htmlSanitizer'; import htmlSanitizer from '../../libs/htmlSanitizer';
import MenuEntry from './common/MenuEntry'; import MenuEntry from './common/MenuEntry';
import providerUtils from '../../services/providers/providerUtils'; import Provider from '../../services/providers/common/Provider';
import store from '../../store'; import store from '../../store';
import workspaceSvc from '../../services/workspaceSvc';
const turndownService = new TurndownService(store.getters['data/computedSettings'].turndown); const turndownService = new TurndownService(store.getters['data/computedSettings'].turndown);
@ -52,27 +53,25 @@ export default {
MenuEntry, MenuEntry,
}, },
methods: { methods: {
onImportMarkdown(evt) { async onImportMarkdown(evt) {
const file = evt.target.files[0]; const file = evt.target.files[0];
readFile(file) const content = await readFile(file);
.then(content => this.$store.dispatch('createFile', { const item = await workspaceSvc.createFile({
...providerUtils.parseContent(content), ...Provider.parseContent(content),
name: file.name, name: file.name,
}) });
.then(item => this.$store.commit('file/setCurrentId', item.id))); this.$store.commit('file/setCurrentId', item.id);
}, },
onImportHtml(evt) { async onImportHtml(evt) {
const file = evt.target.files[0]; const file = evt.target.files[0];
readFile(file) const content = await readFile(file);
.then(content => this.$store.dispatch('createFile', { const sanitizedContent = htmlSanitizer.sanitizeHtml(content)
...providerUtils.parseContent( .replace(/&#160;/g, ' '); // Replace non-breaking spaces with classic spaces
turndownService.turndown( const item = await workspaceSvc.createFile({
htmlSanitizer.sanitizeHtml(content) ...Provider.parseContent(turndownService.turndown(sanitizedContent)),
.replace(/&#160;/g, ' '), // Replace non-breaking spaces with classic spaces name: file.name,
)), });
name: file.name, this.$store.commit('file/setCurrentId', item.id);
}))
.then(item => this.$store.commit('file/setCurrentId', item.id));
}, },
}, },
}; };

View File

@ -1,9 +1,9 @@
<template> <template>
<div class="side-bar__panel side-bar__panel--menu"> <div class="side-bar__panel side-bar__panel--menu">
<div class="menu-info-entries"> <div class="side-bar__info">
<div class="menu-entry menu-entry--info flex flex--row flex--align-center" v-if="loginToken"> <div class="menu-entry menu-entry--info flex flex--row flex--align-center" v-if="loginToken">
<div class="menu-entry__icon menu-entry__icon--image"> <div class="menu-entry__icon menu-entry__icon--image">
<user-image :user-id="loginToken.sub"></user-image> <user-image :user-id="userId"></user-image>
</div> </div>
<span>Signed in as <b>{{loginToken.name}}</b>.</span> <span>Signed in as <b>{{loginToken.name}}</b>.</span>
</div> </div>
@ -11,7 +11,18 @@
<div class="menu-entry__icon menu-entry__icon--image"> <div class="menu-entry__icon menu-entry__icon--image">
<icon-provider :provider-id="currentWorkspace.providerId"></icon-provider> <icon-provider :provider-id="currentWorkspace.providerId"></icon-provider>
</div> </div>
<span><b>{{currentWorkspace.name}}</b> synced.</span> <span v-if="currentWorkspace.providerId === 'googleDriveAppData'">
<b>{{currentWorkspace.name}}</b> synced with your Google Drive app data folder.
</span>
<span v-else-if="currentWorkspace.providerId === 'googleDriveWorkspace'">
<b>{{currentWorkspace.name}}</b> synced with a <a :href="workspaceLocationUrl" target="_blank">Google Drive folder</a>.
</span>
<span v-else-if="currentWorkspace.providerId === 'couchdbWorkspace'">
<b>{{currentWorkspace.name}}</b> synced with a <a :href="workspaceLocationUrl" target="_blank">CouchDB database</a>.
</span>
<span v-else-if="currentWorkspace.providerId === 'githubWorkspace'">
<b>{{currentWorkspace.name}}</b> synced with a <a :href="workspaceLocationUrl" target="_blank">GitHub repo</a>.
</span>
</div> </div>
<div class="menu-entry menu-entry--info flex flex--row flex--align-center" v-else> <div class="menu-entry menu-entry--info flex flex--row flex--align-center" v-else>
<div class="menu-entry__icon menu-entry__icon--disabled"> <div class="menu-entry__icon menu-entry__icon--disabled">
@ -27,7 +38,7 @@
</menu-entry> </menu-entry>
<menu-entry @click.native="setPanel('workspaces')"> <menu-entry @click.native="setPanel('workspaces')">
<icon-database slot="icon"></icon-database> <icon-database slot="icon"></icon-database>
<div>Workspaces</div> <div><div class="menu-entry__label menu-entry__label--warning">new</div> Workspaces</div>
<span>Switch to another workspace.</span> <span>Switch to another workspace.</span>
</menu-entry> </menu-entry>
<hr> <hr>
@ -83,6 +94,7 @@
<script> <script>
import { mapGetters, mapActions } from 'vuex'; import { mapGetters, mapActions } from 'vuex';
import MenuEntry from './common/MenuEntry'; import MenuEntry from './common/MenuEntry';
import providerRegistry from '../../services/providers/common/providerRegistry';
import UserImage from '../UserImage'; import UserImage from '../UserImage';
import googleHelper from '../../services/providers/helpers/googleHelper'; import googleHelper from '../../services/providers/helpers/googleHelper';
import syncSvc from '../../services/syncSvc'; import syncSvc from '../../services/syncSvc';
@ -97,25 +109,34 @@ export default {
'currentWorkspace', 'currentWorkspace',
'syncToken', 'syncToken',
'loginToken', 'loginToken',
'userId',
]), ]),
workspaceLocationUrl() {
const provider = providerRegistry.providersById[this.currentWorkspace.providerId];
return provider.getWorkspaceLocationUrl(this.currentWorkspace);
},
}, },
methods: { methods: {
...mapActions('data', { ...mapActions('data', {
setPanel: 'setSideBarPanel', setPanel: 'setSideBarPanel',
}), }),
signin() { async signin() {
return googleHelper.signin() try {
.then( await googleHelper.signin();
() => syncSvc.requestSync(), syncSvc.requestSync();
() => {}, // Cancel } catch (e) {
); // Cancel
}
}, },
fileProperties() { async fileProperties() {
return this.$store.dispatch('modal/open', 'fileProperties') try {
.catch(() => {}); // Cancel await this.$store.dispatch('modal/open', 'fileProperties');
} catch (e) {
// Cancel
}
}, },
print() { print() {
print(); window.print();
}, },
}, },
}; };

View File

@ -7,7 +7,7 @@
</menu-entry> </menu-entry>
<menu-entry @click.native="templates"> <menu-entry @click.native="templates">
<icon-code-braces slot="icon"></icon-code-braces> <icon-code-braces slot="icon"></icon-code-braces>
<div>Templates</div> <div><div class="menu-entry__label menu-entry__label--count">{{templateCount}}</div> Templates</div>
<span>Configure Handlebars templates for your exports.</span> <span>Configure Handlebars templates for your exports.</span>
</menu-entry> </menu-entry>
<menu-entry @click.native="reset"> <menu-entry @click.native="reset">
@ -50,6 +50,11 @@ export default {
components: { components: {
MenuEntry, MenuEntry,
}, },
computed: {
templateCount() {
return Object.keys(this.$store.getters['data/allTemplatesById']).length;
},
},
methods: { methods: {
onImportBackup(evt) { onImportBackup(evt) {
const file = evt.target.files[0]; const file = evt.target.files[0];
@ -72,35 +77,36 @@ export default {
...utils.queryParams, ...utils.queryParams,
exportWorkspace: true, exportWorkspace: true,
}, true); }, true);
const iframeElt = utils.createHiddenIframe(url); window.location.href = url;
document.body.appendChild(iframeElt); window.location.reload(true);
setTimeout(() => {
document.body.removeChild(iframeElt);
}, 60000);
}, },
settings() { async settings() {
return this.$store.dispatch('modal/open', 'settings') try {
.then( const settings = await this.$store.dispatch('modal/open', 'settings');
settings => this.$store.dispatch('data/setSettings', settings), this.$store.dispatch('data/setSettings', settings);
() => {}, // Cancel } catch (e) {
); // Cancel
}
}, },
templates() { async templates() {
return this.$store.dispatch('modal/open', 'templates') try {
.then( const { templates } = await this.$store.dispatch('modal/open', 'templates');
({ templates }) => this.$store.dispatch('data/setTemplates', templates), this.$store.dispatch('data/setTemplatesById', templates);
() => {}, // Cancel } catch (e) {
); // Cancel
}
}, },
reset() { async reset() {
return this.$store.dispatch('modal/reset') try {
.then(() => { await this.$store.dispatch('modal/open', 'reset');
location.href = '#reset=true'; window.location.href = '#reset=true';
location.reload(); window.location.reload();
}); } catch (e) {
// Cancel
}
}, },
about() { about() {
return this.$store.dispatch('modal/open', 'about'); this.$store.dispatch('modal/open', 'about');
}, },
}, },
}; };

View File

@ -16,7 +16,7 @@
</menu-entry> </menu-entry>
<menu-entry @click.native="managePublish"> <menu-entry @click.native="managePublish">
<icon-view-list slot="icon"></icon-view-list> <icon-view-list slot="icon"></icon-view-list>
<div>File publication</div> <div><div class="menu-entry__label menu-entry__label--count">{{locationCount}}</div> File publication</div>
<span>Manage current file publication locations.</span> <span>Manage current file publication locations.</span>
</menu-entry> </menu-entry>
</div> </div>
@ -113,15 +113,19 @@ import zendeskHelper from '../../services/providers/helpers/zendeskHelper';
import publishSvc from '../../services/publishSvc'; import publishSvc from '../../services/publishSvc';
import store from '../../store'; import store from '../../store';
const tokensToArray = (tokens, filter = () => true) => Object.keys(tokens) const tokensToArray = (tokens, filter = () => true) => Object.values(tokens)
.map(sub => tokens[sub])
.filter(token => filter(token)) .filter(token => filter(token))
.sort((token1, token2) => token1.name.localeCompare(token2.name)); .sort((token1, token2) => token1.name.localeCompare(token2.name));
const openPublishModal = (token, type) => store.dispatch('modal/open', { const publishModalOpener = type => async (token) => {
type, try {
token, const publishLocation = await store.dispatch('modal/open', {
}).then(publishLocation => publishSvc.createPublishLocation(publishLocation)); type,
token,
});
publishSvc.createPublishLocation(publishLocation);
} catch (e) { /* cancel */ }
};
export default { export default {
components: { components: {
@ -137,26 +141,29 @@ export default {
...mapGetters('publishLocation', { ...mapGetters('publishLocation', {
publishLocations: 'current', publishLocations: 'current',
}), }),
locationCount() {
return Object.keys(this.publishLocations).length;
},
currentFileName() { currentFileName() {
return this.$store.getters['file/current'].name; return this.$store.getters['file/current'].name;
}, },
googleDriveTokens() { googleDriveTokens() {
return tokensToArray(this.$store.getters['data/googleTokens'], token => token.isDrive); return tokensToArray(this.$store.getters['data/googleTokensBySub'], token => token.isDrive);
}, },
dropboxTokens() { dropboxTokens() {
return tokensToArray(this.$store.getters['data/dropboxTokens']); return tokensToArray(this.$store.getters['data/dropboxTokensBySub']);
}, },
githubTokens() { githubTokens() {
return tokensToArray(this.$store.getters['data/githubTokens']); return tokensToArray(this.$store.getters['data/githubTokensBySub']);
}, },
wordpressTokens() { wordpressTokens() {
return tokensToArray(this.$store.getters['data/wordpressTokens']); return tokensToArray(this.$store.getters['data/wordpressTokensBySub']);
}, },
bloggerTokens() { bloggerTokens() {
return tokensToArray(this.$store.getters['data/googleTokens'], token => token.isBlogger); return tokensToArray(this.$store.getters['data/googleTokensBySub'], token => token.isBlogger);
}, },
zendeskTokens() { zendeskTokens() {
return tokensToArray(this.$store.getters['data/zendeskTokens']); return tokensToArray(this.$store.getters['data/zendeskTokensBySub']);
}, },
noToken() { noToken() {
return !this.googleDriveTokens.length return !this.googleDriveTokens.length
@ -173,77 +180,53 @@ export default {
publishSvc.requestPublish(); publishSvc.requestPublish();
} }
}, },
managePublish() { async managePublish() {
return this.$store.dispatch('modal/open', 'publishManagement'); try {
await this.$store.dispatch('modal/open', 'publishManagement');
} catch (e) { /* cancel */ }
}, },
addGoogleDriveAccount() { async addGoogleDriveAccount() {
return this.$store.dispatch('modal/open', { try {
type: 'googleDriveAccount', await this.$store.dispatch('modal/open', { type: 'googleDriveAccount' });
onResolve: () => googleHelper.addDriveAccount(!store.getters['data/localSettings'].googleDriveRestrictedAccess), await googleHelper.addDriveAccount(!store.getters['data/localSettings'].googleDriveRestrictedAccess);
}) } catch (e) { /* cancel */ }
.catch(() => {}); // Cancel
}, },
addDropboxAccount() { async addDropboxAccount() {
return this.$store.dispatch('modal/open', { try {
type: 'dropboxAccount', await this.$store.dispatch('modal/open', { type: 'dropboxAccount' });
onResolve: () => dropboxHelper.addAccount(!store.getters['data/localSettings'].dropboxRestrictedAccess), await dropboxHelper.addAccount(!store.getters['data/localSettings'].dropboxRestrictedAccess);
}) } catch (e) { /* cancel */ }
.catch(() => {}); // Cancel
}, },
addGithubAccount() { async addGithubAccount() {
return this.$store.dispatch('modal/open', { try {
type: 'githubAccount', await this.$store.dispatch('modal/open', { type: 'githubAccount' });
onResolve: () => githubHelper.addAccount(store.getters['data/localSettings'].githubRepoFullAccess), await githubHelper.addAccount(store.getters['data/localSettings'].githubRepoFullAccess);
}) } catch (e) { /* cancel */ }
.catch(() => {}); // Cancel
}, },
addWordpressAccount() { async addWordpressAccount() {
return wordpressHelper.addAccount() try {
.catch(() => {}); // Cancel await wordpressHelper.addAccount();
} catch (e) { /* cancel */ }
}, },
addBloggerAccount() { async addBloggerAccount() {
return googleHelper.addBloggerAccount() try {
.catch(() => {}); // Cancel await googleHelper.addBloggerAccount();
} catch (e) { /* cancel */ }
}, },
addZendeskAccount() { async addZendeskAccount() {
return this.$store.dispatch('modal/open', { try {
type: 'zendeskAccount', const { subdomain, clientId } = await this.$store.dispatch('modal/open', { type: 'zendeskAccount' });
onResolve: ({ subdomain, clientId }) => zendeskHelper.addAccount(subdomain, clientId), await zendeskHelper.addAccount(subdomain, clientId);
}) } catch (e) { /* cancel */ }
.catch(() => {}); // Cancel
},
publishGoogleDrive(token) {
return openPublishModal(token, 'googleDrivePublish')
.catch(() => {}); // Cancel
},
publishDropbox(token) {
return openPublishModal(token, 'dropboxPublish')
.catch(() => {}); // Cancel
},
publishGithub(token) {
return openPublishModal(token, 'githubPublish')
.catch(() => {}); // Cancel
},
publishGist(token) {
return openPublishModal(token, 'gistPublish')
.catch(() => {}); // Cancel
},
publishWordpress(token) {
return openPublishModal(token, 'wordpressPublish')
.catch(() => {}); // Cancel
},
publishBlogger(token) {
return openPublishModal(token, 'bloggerPublish')
.catch(() => {}); // Cancel
},
publishBloggerPage(token) {
return openPublishModal(token, 'bloggerPagePublish')
.catch(() => {}); // Cancel
},
publishZendesk(token) {
return openPublishModal(token, 'zendeskPublish')
.catch(() => {}); // Cancel
}, },
publishGoogleDrive: publishModalOpener('googleDrivePublish'),
publishDropbox: publishModalOpener('dropboxPublish'),
publishGithub: publishModalOpener('githubPublish'),
publishGist: publishModalOpener('gistPublish'),
publishWordpress: publishModalOpener('wordpressPublish'),
publishBlogger: publishModalOpener('bloggerPublish'),
publishBloggerPage: publishModalOpener('bloggerPagePublish'),
publishZendesk: publishModalOpener('zendeskPublish'),
}, },
}; };
</script> </script>

View File

@ -1,7 +1,7 @@
<template> <template>
<div class="side-bar__panel side-bar__panel--menu"> <div class="side-bar__panel side-bar__panel--menu">
<div class="side-bar__info" v-if="isCurrentTemp"> <div class="side-bar__info" v-if="isCurrentTemp">
<p><b>{{currentFileName}}</b> can not be synchronized as it's a temporary file.</p> <p><b>{{currentFileName}}</b> can not be synced as it's a temporary file.</p>
</div> </div>
<div v-else> <div v-else>
<div class="side-bar__info" v-if="noToken"> <div class="side-bar__info" v-if="noToken">
@ -16,7 +16,7 @@
</menu-entry> </menu-entry>
<menu-entry @click.native="manageSync"> <menu-entry @click.native="manageSync">
<icon-view-list slot="icon"></icon-view-list> <icon-view-list slot="icon"></icon-view-list>
<div>File synchronization</div> <div><div class="menu-entry__label menu-entry__label--count">{{locationCount}}</div> File synchronization</div>
<span>Manage current file synchronized locations.</span> <span>Manage current file synchronized locations.</span>
</menu-entry> </menu-entry>
</div> </div>
@ -91,8 +91,7 @@ import githubProvider from '../../services/providers/githubProvider';
import syncSvc from '../../services/syncSvc'; import syncSvc from '../../services/syncSvc';
import store from '../../store'; import store from '../../store';
const tokensToArray = (tokens, filter = () => true) => Object.keys(tokens) const tokensToArray = (tokens, filter = () => true) => Object.values(tokens)
.map(sub => tokens[sub])
.filter(token => filter(token)) .filter(token => filter(token))
.sort((token1, token2) => token1.name.localeCompare(token2.name)); .sort((token1, token2) => token1.name.localeCompare(token2.name));
@ -116,19 +115,22 @@ export default {
'isCurrentTemp', 'isCurrentTemp',
]), ]),
...mapGetters('syncLocation', { ...mapGetters('syncLocation', {
syncLocations: 'current', syncLocations: 'currentWithWorkspaceSyncLocation',
}), }),
locationCount() {
return Object.keys(this.syncLocations).length;
},
currentFileName() { currentFileName() {
return this.$store.getters['file/current'].name; return this.$store.getters['file/current'].name;
}, },
googleDriveTokens() { googleDriveTokens() {
return tokensToArray(this.$store.getters['data/googleTokens'], token => token.isDrive); return tokensToArray(this.$store.getters['data/googleTokensBySub'], token => token.isDrive);
}, },
dropboxTokens() { dropboxTokens() {
return tokensToArray(this.$store.getters['data/dropboxTokens']); return tokensToArray(this.$store.getters['data/dropboxTokensBySub']);
}, },
githubTokens() { githubTokens() {
return tokensToArray(this.$store.getters['data/githubTokens']); return tokensToArray(this.$store.getters['data/githubTokensBySub']);
}, },
noToken() { noToken() {
return !this.googleDriveTokens.length return !this.googleDriveTokens.length
@ -142,63 +144,74 @@ export default {
syncSvc.requestSync(); syncSvc.requestSync();
} }
}, },
manageSync() { async manageSync() {
return this.$store.dispatch('modal/open', 'syncManagement'); try {
await this.$store.dispatch('modal/open', 'syncManagement');
} catch (e) { /* cancel */ }
}, },
addGoogleDriveAccount() { async addGoogleDriveAccount() {
return this.$store.dispatch('modal/open', { try {
type: 'googleDriveAccount', await this.$store.dispatch('modal/open', { type: 'googleDriveAccount' });
onResolve: () => googleHelper.addDriveAccount(!store.getters['data/localSettings'].googleDriveRestrictedAccess), await googleHelper.addDriveAccount(!store.getters['data/localSettings'].googleDriveRestrictedAccess);
}) } catch (e) { /* cancel */ }
.catch(() => {}); // Cancel
}, },
addDropboxAccount() { async addDropboxAccount() {
return this.$store.dispatch('modal/open', { try {
type: 'dropboxAccount', await this.$store.dispatch('modal/open', { type: 'dropboxAccount' });
onResolve: () => dropboxHelper.addAccount(!store.getters['data/localSettings'].dropboxRestrictedAccess), await dropboxHelper.addAccount(!store.getters['data/localSettings'].dropboxRestrictedAccess);
}) } catch (e) { /* cancel */ }
.catch(() => {}); // Cancel
}, },
addGithubAccount() { async addGithubAccount() {
return this.$store.dispatch('modal/open', { try {
type: 'githubAccount', await this.$store.dispatch('modal/open', { type: 'githubAccount' });
onResolve: () => githubHelper.addAccount(store.getters['data/localSettings'].githubRepoFullAccess), await githubHelper.addAccount(store.getters['data/localSettings'].githubRepoFullAccess);
}) } catch (e) { /* cancel */ }
.catch(() => {}); // Cancel
}, },
openGoogleDrive(token) { async openGoogleDrive(token) {
return googleHelper.openPicker(token, 'doc') const files = await googleHelper.openPicker(token, 'doc');
.then(files => this.$store.dispatch('queue/enqueue', this.$store.dispatch(
() => googleDriveProvider.openFiles(token, files))); 'queue/enqueue',
() => googleDriveProvider.openFiles(token, files),
);
}, },
openDropbox(token) { async openDropbox(token) {
return dropboxHelper.openChooser(token) const paths = await dropboxHelper.openChooser(token);
.then(paths => this.$store.dispatch('queue/enqueue', this.$store.dispatch(
() => dropboxProvider.openFiles(token, paths))); 'queue/enqueue',
() => dropboxProvider.openFiles(token, paths),
);
}, },
saveGoogleDrive(token) { async saveGoogleDrive(token) {
return openSyncModal(token, 'googleDriveSave') try {
.catch(() => {}); // Cancel await openSyncModal(token, 'googleDriveSave');
} catch (e) { /* cancel */ }
}, },
saveDropbox(token) { async saveDropbox(token) {
return openSyncModal(token, 'dropboxSave') try {
.catch(() => {}); // Cancel await openSyncModal(token, 'dropboxSave');
} catch (e) { /* cancel */ }
}, },
openGithub(token) { async openGithub(token) {
return store.dispatch('modal/open', { try {
type: 'githubOpen', const syncLocation = await store.dispatch('modal/open', {
token, type: 'githubOpen',
}) token,
.then(syncLocation => this.$store.dispatch('queue/enqueue', });
() => githubProvider.openFile(token, syncLocation))); this.$store.dispatch(
'queue/enqueue',
() => githubProvider.openFile(token, syncLocation),
);
} catch (e) { /* cancel */ }
}, },
saveGithub(token) { async saveGithub(token) {
return openSyncModal(token, 'githubSave') try {
.catch(() => {}); // Cancel await openSyncModal(token, 'githubSave');
} catch (e) { /* cancel */ }
}, },
saveGist(token) { async saveGist(token) {
return openSyncModal(token, 'gistSync') try {
.catch(() => {}); // Cancel await openSyncModal(token, 'gistSync');
} catch (e) { /* cancel */ }
}, },
}, },
}; };

View File

@ -1,23 +1,30 @@
<template> <template>
<div class="side-bar__panel side-bar__panel--menu"> <div class="side-bar__panel side-bar__panel--menu">
<div class="workspace" v-for="(workspace, id) in sanitizedWorkspaces" :key="id"> <div class="workspace" v-for="(workspace, id) in workspacesById" :key="id">
<menu-entry :href="workspace.url" target="_blank"> <menu-entry :href="workspace.url" target="_blank">
<icon-provider slot="icon" :provider-id="workspace.providerId"></icon-provider> <icon-provider slot="icon" :provider-id="workspace.providerId"></icon-provider>
<div class="workspace__name"><div class="menu-entry__label" v-if="currentWorkspace === workspace">current</div>{{workspace.name}}</div> <div class="workspace__name"><div class="menu-entry__label" v-if="currentWorkspace === workspace">current</div>{{workspace.name}}</div>
</menu-entry> </menu-entry>
</div> </div>
<hr> <hr>
<menu-entry @click.native="addGoogleDriveWorkspace">
<icon-provider slot="icon" provider-id="googleDriveWorkspace"></icon-provider>
<span>Add Google Drive workspace</span>
</menu-entry>
<menu-entry @click.native="addCouchdbWorkspace"> <menu-entry @click.native="addCouchdbWorkspace">
<icon-provider slot="icon" provider-id="couchdbWorkspace"></icon-provider> <icon-provider slot="icon" provider-id="couchdbWorkspace"></icon-provider>
<span>Add CouchDB workspace</span> <div>CouchDB workspace</div>
<span>Add a workspace synced with a CouchDB database.</span>
</menu-entry>
<menu-entry @click.native="addGithubWorkspace">
<icon-provider slot="icon" provider-id="githubWorkspace"></icon-provider>
<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>
<div>Google Drive workspace</div>
<span>Add a workspace synced with a Google Drive folder.</span>
</menu-entry> </menu-entry>
<menu-entry @click.native="manageWorkspaces"> <menu-entry @click.native="manageWorkspaces">
<icon-database slot="icon"></icon-database> <icon-database slot="icon"></icon-database>
<span>Manage workspaces</span> <span><div class="menu-entry__label menu-entry__label--count">{{workspaceCount}}</div> Manage workspaces</span>
</menu-entry> </menu-entry>
</div> </div>
</template> </template>
@ -32,37 +39,53 @@ export default {
MenuEntry, MenuEntry,
}, },
computed: { computed: {
...mapGetters('data', [
'sanitizedWorkspaces',
]),
...mapGetters('workspace', [ ...mapGetters('workspace', [
'workspacesById',
'currentWorkspace', 'currentWorkspace',
]), ]),
workspaceCount() {
return Object.keys(this.workspacesById).length;
},
}, },
methods: { methods: {
addGoogleDriveWorkspace() { async addCouchdbWorkspace() {
return googleHelper.addDriveAccount(true) try {
.then(token => this.$store.dispatch('modal/open', { this.$store.dispatch('modal/open', {
type: 'couchdbWorkspace',
});
} catch (e) {
// Cancel
}
},
async addGithubWorkspace() {
try {
this.$store.dispatch('modal/open', {
type: 'githubWorkspace',
});
} catch (e) {
// Cancel
}
},
async addGoogleDriveWorkspace() {
try {
const token = await googleHelper.addDriveAccount(true);
this.$store.dispatch('modal/open', {
type: 'googleDriveWorkspace', type: 'googleDriveWorkspace',
token, token,
})) });
.catch(() => {}); // Cancel } catch (e) {
}, // Cancel
addCouchdbWorkspace() { }
return this.$store.dispatch('modal/open', {
type: 'couchdbWorkspace',
})
.catch(() => {}); // Cancel
}, },
manageWorkspaces() { manageWorkspaces() {
return this.$store.dispatch('modal/open', 'workspaceManagement'); this.$store.dispatch('modal/open', 'workspaceManagement');
}, },
}, },
}; };
</script> </script>
<style lang="scss"> <style lang="scss">
@import '../common/variables.scss'; @import '../../styles/variables.scss';
.workspace .menu-entry { .workspace .menu-entry {
padding-top: 12px; padding-top: 12px;

View File

@ -10,7 +10,7 @@
</template> </template>
<style lang="scss"> <style lang="scss">
@import '../../common/variables.scss'; @import '../../../styles/variables.scss';
.menu-entry { .menu-entry {
text-align: left; text-align: left;
@ -24,9 +24,13 @@
span { span {
display: inline-block; display: inline-block;
font-size: 0.75rem; font-size: 0.75rem;
opacity: 0.5; opacity: 0.67;
line-height: 1.3; line-height: 1.3;
.menu-entry__label {
opacity: 1;
}
span { span {
display: inline; display: inline;
opacity: 1; opacity: 1;
@ -34,12 +38,6 @@
} }
} }
.menu-info-entries {
padding: 10px;
margin: -10px -10px 10px;
background-color: rgba(255, 255, 255, 0.2);
}
.menu-entry--info { .menu-entry--info {
padding-top: 3px; padding-top: 3px;
padding-bottom: 3px; padding-bottom: 3px;
@ -70,10 +68,22 @@
float: right; float: right;
font-size: 0.6rem; font-size: 0.6rem;
font-weight: 600; font-weight: 600;
padding: 0.05em 0.25em; line-height: 1;
background-color: darken($error-color, 10); padding: 0.15em 0.25em;
background-color: #fff;
border-radius: 3px; border-radius: 3px;
opacity: 0.6;
}
.menu-entry__label--warning {
color: #fff; color: #fff;
background-color: darken($error-color, 10);
opacity: 1;
}
.menu-entry__label--count {
font-size: 0.75rem;
font-weight: 400;
} }
.menu-entry__text { .menu-entry__text {

View File

@ -2,17 +2,17 @@
<modal-inner class="modal__inner-1--about-modal" aria-label="About"> <modal-inner class="modal__inner-1--about-modal" aria-label="About">
<div class="modal__content"> <div class="modal__content">
<div class="logo-background"></div> <div class="logo-background"></div>
<small>v{{version}} © 2018 Dock5 Software</small> <small>© 2013-2018 Dock5 Software Ltd.<br>v{{version}}</small>
<hr> <hr>
StackEdit on <a target="_blank" href="https://github.com/benweet/stackedit/">GitHub</a> StackEdit on <a target="_blank" href="https://github.com/benweet/stackedit/">GitHub</a>
<br> <br>
<a target="_blank" href="https://github.com/benweet/stackedit/issues">Issue tracker</a> <a target="_blank" href="https://github.com/benweet/stackedit/blob/master/CHANGELOG.md">Changelog</a> <a target="_blank" href="https://github.com/benweet/stackedit/issues">Issue tracker</a> <a target="_blank" href="https://github.com/benweet/stackedit/releases">Changelog</a>
<br> <br>
<a target="_blank" href="https://chrome.google.com/webstore/detail/iiooodelglhkcpgbajoejffhijaclcdg">Chrome app</a> <a target="_blank" href="https://chrome.google.com/webstore/detail/ajehldoplanpchfokmeempkekhnhmoha">Chrome extension</a> <a target="_blank" href="https://chrome.google.com/webstore/detail/iiooodelglhkcpgbajoejffhijaclcdg">Chrome app</a> <a target="_blank" href="https://chrome.google.com/webstore/detail/ajehldoplanpchfokmeempkekhnhmoha">Chrome extension</a>
<br> <br>
StackEdit on <a target="_blank" href="https://twitter.com/stackedit/">Twitter</a> <a target="_blank" href="https://community.stackedit.io/">Community</a> <a target="_blank" href="https://community.stackedit.io/c/how-to">Tutos and How To</a>
<br> <br>
<a target="_blank" href="https://community.stackedit.io/">Community</a> StackEdit on <a target="_blank" href="https://twitter.com/stackedit/">Twitter</a>
<div class="modal__info"> <div class="modal__info">
For commercial support or custom development, please <a href="mailto:stackedit.project@gmail.com">send us an email</a>. For commercial support or custom development, please <a href="mailto:stackedit.project@gmail.com">send us an email</a>.
</div> </div>
@ -24,7 +24,7 @@
<a target="_blank" href="privacy_policy.html">Privacy Policy</a> <a target="_blank" href="privacy_policy.html">Privacy Policy</a>
</div> </div>
<div class="modal__button-bar"> <div class="modal__button-bar">
<button class="button" @click="config.resolve()">Close</button> <button class="button button--resolve" @click="config.resolve()">Close</button>
</div> </div>
</modal-inner> </modal-inner>
</template> </template>
@ -59,7 +59,7 @@ export default {
.logo-background { .logo-background {
height: 75px; height: 75px;
margin: 0.5rem 0; margin: 0.5em 0;
} }
small { small {

View File

@ -41,6 +41,9 @@
</form-entry> </form-entry>
<form-entry label="Status"> <form-entry label="Status">
<input slot="field" class="textfield" type="text" v-model.trim="status" @keydown.enter="resolve()"> <input slot="field" class="textfield" type="text" v-model.trim="status" @keydown.enter="resolve()">
<div class="form-entry__info">
<b>Example:</b> draft
</div>
</form-entry> </form-entry>
<form-entry label="Date" info="YYYY-MM-DD"> <form-entry label="Date" info="YYYY-MM-DD">
<input slot="field" class="textfield" type="text" v-model.trim="date" @keydown.enter="resolve()"> <input slot="field" class="textfield" type="text" v-model.trim="date" @keydown.enter="resolve()">
@ -54,7 +57,7 @@
</div> </div>
</div> </div>
<div class="modal__error modal__error--file-properties">{{error}}</div> <div class="modal__error modal__error--file-properties">{{error}}</div>
<div class="modal__info"> <div class="modal__info modal__info--multiline">
<p><strong>ProTip:</strong> You can manually toggle extensions:</p> <p><strong>ProTip:</strong> You can manually toggle extensions:</p>
<pre class=" language-yaml"><code class="prism language-yaml"><span class="token key atrule">extensions</span><span class="token punctuation">:</span> <pre class=" language-yaml"><code class="prism language-yaml"><span class="token key atrule">extensions</span><span class="token punctuation">:</span>
<span class="token key atrule">emoji</span><span class="token punctuation">:</span> <span class="token key atrule">emoji</span><span class="token punctuation">:</span>
@ -75,7 +78,7 @@
</div> </div>
<div class="modal__button-bar"> <div class="modal__button-bar">
<button class="button" @click="config.reject()">Cancel</button> <button class="button" @click="config.reject()">Cancel</button>
<button class="button" @click="resolve()">Ok</button> <button class="button button--resolve" @click="resolve()">Ok</button>
</div> </div>
</modal-inner> </modal-inner>
</template> </template>
@ -223,10 +226,10 @@ export default {
</script> </script>
<style lang="scss"> <style lang="scss">
@import '../common/variables.scss'; @import '../../styles/variables.scss';
.modal__inner-1--file-properties { .modal__inner-1.modal__inner-1--file-properties {
max-width: 540px; max-width: 520px;
} }
.modal__error--file-properties { .modal__error--file-properties {

View File

@ -4,7 +4,7 @@
<p>Please choose a template for your <b>HTML export</b>.</p> <p>Please choose a template for your <b>HTML export</b>.</p>
<form-entry label="Template"> <form-entry label="Template">
<select class="textfield" slot="field" v-model="selectedTemplate" @keydown.enter="resolve()"> <select class="textfield" slot="field" v-model="selectedTemplate" @keydown.enter="resolve()">
<option v-for="(template, id) in allTemplates" :key="id" :value="id"> <option v-for="(template, id) in allTemplatesById" :key="id" :value="id">
{{ template.name }} {{ template.name }}
</option> </option>
</select> </select>
@ -14,15 +14,15 @@
</form-entry> </form-entry>
</div> </div>
<div class="modal__button-bar"> <div class="modal__button-bar">
<button class="button button--copy">Copy to clipboard</button> <button class="button button--copy" v-clipboard="result" @click="info('HTML copied to clipboard!')">Copy</button>
<button class="button" @click="config.reject()">Cancel</button> <button class="button" @click="config.reject()">Cancel</button>
<button class="button" @click="resolve()">Ok</button> <button class="button button--resolve" @click="resolve()">Ok</button>
</div> </div>
</modal-inner> </modal-inner>
</template> </template>
<script> <script>
import Clipboard from 'clipboard'; import { mapActions } from 'vuex';
import exportSvc from '../../services/exportSvc'; import exportSvc from '../../services/exportSvc';
import modalTemplate from './common/modalTemplate'; import modalTemplate from './common/modalTemplate';
@ -37,29 +37,27 @@ export default modalTemplate({
let timeoutId; let timeoutId;
this.$watch('selectedTemplate', (selectedTemplate) => { this.$watch('selectedTemplate', (selectedTemplate) => {
clearTimeout(timeoutId); clearTimeout(timeoutId);
timeoutId = setTimeout(() => { timeoutId = setTimeout(async () => {
const currentFile = this.$store.getters['file/current']; const currentFile = this.$store.getters['file/current'];
exportSvc.applyTemplate(currentFile.id, this.allTemplates[selectedTemplate]) const html = await exportSvc.applyTemplate(
.then((html) => { currentFile.id,
this.result = html; this.allTemplatesById[selectedTemplate],
}); );
this.result = html;
}, 10); }, 10);
}, { }, {
immediate: true, immediate: true,
}); });
this.clipboard = new Clipboard(this.$el.querySelector('.button--copy'), {
text: () => this.result,
});
},
destroyed() {
this.clipboard.destroy();
}, },
methods: { methods: {
...mapActions('notification', [
'info',
]),
resolve() { resolve() {
const config = this.config; const { config } = this;
const currentFile = this.$store.getters['file/current']; const currentFile = this.$store.getters['file/current'];
config.resolve(); config.resolve();
exportSvc.exportToDisk(currentFile.id, 'html', this.allTemplates[this.selectedTemplate]); exportSvc.exportToDisk(currentFile.id, 'html', this.allTemplatesById[this.selectedTemplate]);
}, },
}, },
}); });

View File

@ -17,7 +17,7 @@
</div> </div>
<div class="modal__button-bar"> <div class="modal__button-bar">
<button class="button" @click="reject()">Cancel</button> <button class="button" @click="reject()">Cancel</button>
<button class="button" @click="resolve()">Ok</button> <button class="button button--resolve" @click="resolve()">Ok</button>
</div> </div>
</modal-inner> </modal-inner>
</template> </template>
@ -36,9 +36,8 @@ export default modalTemplate({
}), }),
computed: { computed: {
googlePhotosTokens() { googlePhotosTokens() {
const googleTokens = this.$store.getters['data/googleTokens']; const googleTokensBySub = this.$store.getters['data/googleTokensBySub'];
return Object.entries(googleTokens) return Object.values(googleTokensBySub)
.map(([, token]) => token)
.filter(token => token.isPhotos) .filter(token => token.isPhotos)
.sort((token1, token2) => token1.name.localeCompare(token2.name)); .sort((token1, token2) => token1.name.localeCompare(token2.name));
}, },
@ -48,28 +47,30 @@ export default modalTemplate({
if (!this.url) { if (!this.url) {
this.setError('url'); this.setError('url');
} else { } else {
const callback = this.config.callback; const { callback } = this.config;
this.config.resolve(); this.config.resolve();
callback(this.url); callback(this.url);
} }
}, },
reject() { reject() {
const callback = this.config.callback; const { callback } = this.config;
this.config.reject(); this.config.reject();
callback(null); callback(null);
}, },
addGooglePhotosAccount() { addGooglePhotosAccount() {
return googleHelper.addPhotosAccount(); return googleHelper.addPhotosAccount();
}, },
openGooglePhotos(token) { async openGooglePhotos(token) {
const callback = this.config.callback; const { callback } = this.config;
this.config.reject(); this.config.reject();
googleHelper.openPicker(token, 'img') const res = await googleHelper.openPicker(token, 'img');
.then(res => res[0] && this.$store.dispatch('modal/open', { if (res[0]) {
this.$store.dispatch('modal/open', {
type: 'googlePhoto', type: 'googlePhoto',
url: res[0].url, url: res[0].url,
callback, callback,
})); });
}
}, },
}, },
}); });

View File

@ -8,7 +8,7 @@
</div> </div>
<div class="modal__button-bar"> <div class="modal__button-bar">
<button class="button" @click="reject()">Cancel</button> <button class="button" @click="reject()">Cancel</button>
<button class="button" @click="resolve()">Ok</button> <button class="button button--resolve" @click="resolve()">Ok</button>
</div> </div>
</modal-inner> </modal-inner>
</template> </template>
@ -25,13 +25,13 @@ export default modalTemplate({
if (!this.url) { if (!this.url) {
this.setError('url'); this.setError('url');
} else { } else {
const callback = this.config.callback; const { callback } = this.config;
this.config.resolve(); this.config.resolve();
callback(this.url); callback(this.url);
} }
}, },
reject() { reject() {
const callback = this.config.callback; const { callback } = this.config;
this.config.reject(); this.config.reject();
callback(null); callback(null);
}, },

View File

@ -20,7 +20,7 @@
</div> </div>
<div class="modal__button-bar"> <div class="modal__button-bar">
<button class="button" @click="config.reject()">Cancel</button> <button class="button" @click="config.reject()">Cancel</button>
<button class="button" @click="resolve()">Ok</button> <button class="button button--resolve" @click="resolve()">Ok</button>
</div> </div>
</modal-inner> </modal-inner>
</template> </template>
@ -38,19 +38,20 @@ export default modalTemplate({
selectedFormat: 'pandocExportFormat', selectedFormat: 'pandocExportFormat',
}, },
methods: { methods: {
resolve() { async resolve() {
this.config.resolve(); this.config.resolve();
const currentFile = this.$store.getters['file/current']; const currentFile = this.$store.getters['file/current'];
const currentContent = this.$store.getters['content/current']; const currentContent = this.$store.getters['content/current'];
const selectedFormat = this.selectedFormat; const { selectedFormat } = this;
this.$store.dispatch('queue/enqueue', () => Promise.all([ const [sponsorToken, token] = await this.$store.dispatch('queue/enqueue', () => Promise.all([
Promise.resolve().then(() => { Promise.resolve().then(() => {
const sponsorToken = this.$store.getters['workspace/sponsorToken']; const tokenToRefresh = this.$store.getters['workspace/sponsorToken'];
return sponsorToken && googleHelper.refreshToken(sponsorToken); return tokenToRefresh && googleHelper.refreshToken(tokenToRefresh);
}), }),
sponsorSvc.getToken(), sponsorSvc.getToken(),
]) ]));
.then(([sponsorToken, token]) => networkSvc.request({ try {
const { body } = await networkSvc.request({
method: 'POST', method: 'POST',
url: 'pandocExport', url: 'pandocExport',
params: { params: {
@ -63,19 +64,16 @@ export default modalTemplate({
body: JSON.stringify(editorSvc.getPandocAst()), body: JSON.stringify(editorSvc.getPandocAst()),
blob: true, blob: true,
timeout: 60000, timeout: 60000,
}) });
.then((res) => { FileSaver.saveAs(body, `${currentFile.name}.${selectedFormat}`);
FileSaver.saveAs(res.body, `${currentFile.name}.${selectedFormat}`); } catch (err) {
}, (err) => { if (err.status === 401) {
if (err.status !== 401) { this.$store.dispatch('modal/open', 'sponsorOnly');
throw err; } else {
}
this.$store.dispatch('modal/sponsorOnly');
}))
.catch((err) => {
console.error(err); // eslint-disable-line no-console console.error(err); // eslint-disable-line no-console
this.$store.dispatch('notification/error', err); this.$store.dispatch('notification/error', err);
})); }
}
}, },
}, },
}); });

View File

@ -4,7 +4,7 @@
<p>Please choose a template for your <b>PDF export</b>.</p> <p>Please choose a template for your <b>PDF export</b>.</p>
<form-entry label="Template"> <form-entry label="Template">
<select class="textfield" slot="field" v-model="selectedTemplate" @keydown.enter="resolve()"> <select class="textfield" slot="field" v-model="selectedTemplate" @keydown.enter="resolve()">
<option v-for="(template, id) in allTemplates" :key="id" :value="id"> <option v-for="(template, id) in allTemplatesById" :key="id" :value="id">
{{ template.name }} {{ template.name }}
</option> </option>
</select> </select>
@ -15,7 +15,7 @@
</div> </div>
<div class="modal__button-bar"> <div class="modal__button-bar">
<button class="button" @click="config.reject()">Cancel</button> <button class="button" @click="config.reject()">Cancel</button>
<button class="button" @click="resolve()">Ok</button> <button class="button button--resolve" @click="resolve()">Ok</button>
</div> </div>
</modal-inner> </modal-inner>
</template> </template>
@ -33,19 +33,24 @@ export default modalTemplate({
selectedTemplate: 'pdfExportTemplate', selectedTemplate: 'pdfExportTemplate',
}, },
methods: { methods: {
resolve() { async resolve() {
this.config.resolve(); this.config.resolve();
const currentFile = this.$store.getters['file/current']; const currentFile = this.$store.getters['file/current'];
this.$store.dispatch('queue/enqueue', () => Promise.all([ const [sponsorToken, token, html] = await this.$store
Promise.resolve().then(() => { .dispatch('queue/enqueue', () => Promise.all([
const sponsorToken = this.$store.getters['workspace/sponsorToken']; Promise.resolve().then(() => {
return sponsorToken && googleHelper.refreshToken(sponsorToken); const tokenToRefresh = this.$store.getters['workspace/sponsorToken'];
}), return tokenToRefresh && googleHelper.refreshToken(tokenToRefresh);
sponsorSvc.getToken(), }),
exportSvc.applyTemplate( sponsorSvc.getToken(),
currentFile.id, this.allTemplates[this.selectedTemplate], true), exportSvc.applyTemplate(
]) currentFile.id,
.then(([sponsorToken, token, html]) => networkSvc.request({ this.allTemplatesById[this.selectedTemplate],
true,
),
]));
try {
const { body } = await networkSvc.request({
method: 'POST', method: 'POST',
url: 'pdfExport', url: 'pdfExport',
params: { params: {
@ -56,19 +61,16 @@ export default modalTemplate({
body: html, body: html,
blob: true, blob: true,
timeout: 60000, timeout: 60000,
}) });
.then((res) => { FileSaver.saveAs(body, `${currentFile.name}.pdf`);
FileSaver.saveAs(res.body, `${currentFile.name}.pdf`); } catch (err) {
}, (err) => { if (err.status === 401) {
if (err.status !== 401) { this.$store.dispatch('modal/open', 'sponsorOnly');
throw err; } else {
}
this.$store.dispatch('modal/sponsorOnly');
}))
.catch((err) => {
console.error(err); // eslint-disable-line no-console console.error(err); // eslint-disable-line no-console
this.$store.dispatch('notification/error', err); this.$store.dispatch('notification/error', err);
})); }
}
}, },
}, },
}); });

View File

@ -1,38 +1,53 @@
<template> <template>
<modal-inner class="modal__inner-1--publish-management" aria-label="Manage publication locations"> <modal-inner class="modal__inner-1--publish-management" aria-label="Manage publication locations">
<div class="modal__content"> <div class="modal__content">
<div class="modal__image">
<icon-upload></icon-upload>
</div>
<p v-if="publishLocations.length"><b>{{currentFileName}}</b> is published to the following location(s):</p> <p v-if="publishLocations.length"><b>{{currentFileName}}</b> is published to the following location(s):</p>
<p v-else><b>{{currentFileName}}</b> is not published yet.</p> <p v-else><b>{{currentFileName}}</b> is not published yet.</p>
<div> <div>
<div class="publish-entry flex flex--row flex--align-center" v-for="location in publishLocations" :key="location.id"> <div class="publish-entry flex flex--column" v-for="location in publishLocations" :key="location.id">
<div class="publish-entry__icon flex flex--column flex--center"> <div class="publish-entry__header flex flex--row flex--align-center">
<icon-provider :provider-id="location.providerId"></icon-provider> <div class="publish-entry__icon flex flex--column flex--center">
<icon-provider :provider-id="location.providerId"></icon-provider>
</div>
<div class="publish-entry__description">
{{location.description}}
</div>
<div class="publish-entry__buttons flex flex--row flex--center">
<button class="publish-entry__button button" @click="remove(location)" v-title="'Remove location'">
<icon-delete></icon-delete>
</button>
</div>
</div> </div>
<div class="publish-entry__description"> <div class="publish-entry__row flex flex--row flex--align-center">
{{location.description}} <div class="publish-entry__url">
</div> {{location.url}}
<div class="publish-entry__buttons flex flex--row flex--center"> </div>
<a class="publish-entry__button button" :href="location.url" target="_blank"> <div class="publish-entry__buttons flex flex--row flex--center" v-if="location.url">
<icon-open-in-new></icon-open-in-new> <button class="publish-entry__button button" v-clipboard="location.url" @click="info('Location URL copied to clipboard!')" v-title="'Copy URL'">
</a> <icon-content-copy></icon-content-copy>
<button class="publish-entry__button button" @click="remove(location)"> </button>
<icon-delete></icon-delete> <a class="publish-entry__button button" v-if="location.url" :href="location.url" target="_blank" v-title="'Open location'">
</button> <icon-open-in-new></icon-open-in-new>
</a>
</div>
</div> </div>
</div> </div>
</div> </div>
<div class="modal__info" v-if="publishLocations.length"> <div class="modal__info" v-if="publishLocations.length">
<b>Note:</b> Removing a synchronized location won't delete any file. <b>Tip:</b> Removing a location won't delete any file.
</div> </div>
</div> </div>
<div class="modal__button-bar"> <div class="modal__button-bar">
<button class="button" @click="config.resolve()">Close</button> <button class="button button--resolve" @click="config.resolve()">Close</button>
</div> </div>
</modal-inner> </modal-inner>
</template> </template>
<script> <script>
import { mapGetters } from 'vuex'; import { mapGetters, mapActions } from 'vuex';
import ModalInner from './common/ModalInner'; import ModalInner from './common/ModalInner';
export default { export default {
@ -51,6 +66,9 @@ export default {
}, },
}, },
methods: { methods: {
...mapActions('notification', [
'info',
]),
remove(location) { remove(location) {
this.$store.commit('publishLocation/deleteItem', location.id); this.$store.commit('publishLocation/deleteItem', location.id);
}, },
@ -59,47 +77,73 @@ export default {
</script> </script>
<style lang="scss"> <style lang="scss">
@import '../common/variables.scss'; @import '../../styles/variables.scss';
.modal__inner-1--publish-management {
max-width: 560px;
}
.publish-entry { .publish-entry {
padding: 0.5rem 0.25rem; margin: 1.5em 0;
border-bottom: 1px solid $hr-color; height: auto;
font-size: 17px;
line-height: 1.5;
}
&:last-child { $button-size: 30px;
border-bottom: none; $small-button-size: 22px;
}
.publish-entry__header {
line-height: $button-size;
}
.publish-entry__row {
margin-top: 1px;
padding-top: 1px;
border-top: 1px solid rgba(128, 128, 128, 0.15);
line-height: $small-button-size;
} }
.publish-entry__icon { .publish-entry__icon {
height: 30px; height: 22px;
width: 30px; width: 22px;
margin-right: 0.75rem; margin-right: 0.75rem;
flex: none; flex: none;
} }
.publish-entry__description { .publish-entry__description {
opacity: 0.5;
line-height: 1.4;
font-size: 0.9em;
width: 100%; width: 100%;
overflow: hidden; overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.publish-entry__url {
width: 100%;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
opacity: 0.5;
font-size: 0.67em;
} }
.publish-entry__buttons { .publish-entry__buttons {
margin-left: 0.75rem; margin-left: 0.75rem;
.publish-entry__row & {
margin-left: 0.5rem;
}
} }
.publish-entry__button { .publish-entry__button {
width: 38px; width: $button-size;
height: 38px; height: $button-size;
padding: 6px; padding: 4px;
background-color: transparent; background-color: transparent;
opacity: 0.75; opacity: 0.75;
.publish-entry__row & {
width: $small-button-size;
height: $small-button-size;
padding: 4px;
}
&:active, &:active,
&:focus, &:focus,
&:hover { &:hover {

View File

@ -25,7 +25,7 @@
</div> </div>
<div class="modal__button-bar"> <div class="modal__button-bar">
<button class="button" @click="config.reject()">Cancel</button> <button class="button" @click="config.reject()">Cancel</button>
<button class="button" @click="!error && config.resolve(strippedCustomSettings)">Ok</button> <button class="button button--resolve" @click="!error && config.resolve(strippedCustomSettings)">Ok</button>
</div> </div>
</modal-inner> </modal-inner>
</template> </template>
@ -36,7 +36,7 @@ import { mapGetters } from 'vuex';
import ModalInner from './common/ModalInner'; import ModalInner from './common/ModalInner';
import Tab from './common/Tab'; import Tab from './common/Tab';
import CodeEditor from '../CodeEditor'; import CodeEditor from '../CodeEditor';
import defaultSettings from '../../data/defaultSettings.yml'; import defaultSettings from '../../data/defaults/defaultSettings.yml';
const emptySettings = `# Add your custom settings here to override the const emptySettings = `# Add your custom settings here to override the
# default settings. # default settings.
@ -81,10 +81,10 @@ export default {
</script> </script>
<style lang="scss"> <style lang="scss">
@import '../common/variables.scss'; @import '../../styles/variables.scss';
.modal__inner-1--settings { .modal__inner-1.modal__inner-1--settings {
max-width: 600px; max-width: 560px;
} }
.modal__error--settings { .modal__error--settings {

View File

@ -1,7 +1,7 @@
<template> <template>
<modal-inner class="modal__inner-1--sponsor" aria-label="Sponsor"> <modal-inner class="modal__inner-1--sponsor" aria-label="Sponsor">
<div class="modal__content"> <div class="modal__content">
<p>Please choose a <b>PayPal</b> option.</p> <p>Please choose a <b>PayPal</b> option:</p>
<a class="paypal-option button flex flex--row flex--center" v-for="button in buttons" :key="button.id" :href="button.link"> <a class="paypal-option button flex flex--row flex--center" v-for="button in buttons" :key="button.id" :href="button.link">
<div class="flex flex--column"> <div class="flex flex--column">
<div>{{button.price}}<div class="paypal-option__offer" v-if="button.offer">{{button.offer}}</div></div> <div>{{button.price}}<div class="paypal-option__offer" v-if="button.offer">{{button.offer}}</div></div>
@ -63,10 +63,10 @@ export default {
</script> </script>
<style lang="scss"> <style lang="scss">
@import '../common/variables.scss'; @import '../../styles/variables.scss';
.modal__inner-1--sponsor { .modal__inner-1.modal__inner-1--sponsor {
max-width: 380px; max-width: 400px;
} }
.paypal-option { .paypal-option {
@ -81,7 +81,7 @@ export default {
span { span {
display: inline-block; display: inline-block;
font-size: 0.75rem; font-size: 0.75rem;
opacity: 0.5; opacity: 0.6;
white-space: normal; white-space: normal;
line-height: 1.5; line-height: 1.5;
} }

View File

@ -1,38 +1,53 @@
<template> <template>
<modal-inner class="modal__inner-1--sync-management" aria-label="Manage synchronized locations"> <modal-inner class="modal__inner-1--sync-management" aria-label="Manage synchronized locations">
<div class="modal__content"> <div class="modal__content">
<div class="modal__image">
<icon-sync></icon-sync>
</div>
<p v-if="syncLocations.length"><b>{{currentFileName}}</b> is synchronized with the following location(s):</p> <p v-if="syncLocations.length"><b>{{currentFileName}}</b> is synchronized with the following location(s):</p>
<p v-else><b>{{currentFileName}}</b> is not synchronized yet.</p> <p v-else><b>{{currentFileName}}</b> is not synchronized yet.</p>
<div> <div>
<div class="sync-entry flex flex--row flex--align-center" v-for="location in syncLocations" :key="location.id"> <div class="sync-entry flex flex--column" v-for="location in syncLocations" :key="location.id">
<div class="sync-entry__icon flex flex--column flex--center"> <div class="sync-entry__header flex flex--row flex--align-center">
<icon-provider :provider-id="location.providerId"></icon-provider> <div class="sync-entry__icon flex flex--column flex--center">
<icon-provider :provider-id="location.providerId"></icon-provider>
</div>
<div class="sync-entry__description">
{{location.description}}
</div>
<div class="sync-entry__buttons flex flex--row flex--center">
<button class="sync-entry__button button" @click="remove(location)" v-title="'Remove location'">
<icon-delete></icon-delete>
</button>
</div>
</div> </div>
<div class="sync-entry__description"> <div class="sync-entry__row flex flex--row flex--align-center">
{{location.description}} <div class="sync-entry__url">
</div> {{location.url || 'Google Drive app data'}}
<div class="sync-entry__buttons flex flex--row flex--center"> </div>
<a class="sync-entry__button button" :href="location.url" target="_blank"> <div class="sync-entry__buttons flex flex--row flex--center" v-if="location.url">
<icon-open-in-new></icon-open-in-new> <button class="sync-entry__button button" v-clipboard="location.url" @click="info('Location URL copied to clipboard!')" v-title="'Copy URL'">
</a> <icon-content-copy></icon-content-copy>
<button class="sync-entry__button button" @click="remove(location)"> </button>
<icon-delete></icon-delete> <a class="sync-entry__button button" v-if="location.url" :href="location.url" target="_blank" v-title="'Open location'">
</button> <icon-open-in-new></icon-open-in-new>
</a>
</div>
</div> </div>
</div> </div>
</div> </div>
<div class="modal__info" v-if="syncLocations.length"> <div class="modal__info" v-if="syncLocations.length">
<b>Note:</b> Removing a synchronized location won't delete any file. <b>Tip:</b> Removing a location won't delete any file.
</div> </div>
</div> </div>
<div class="modal__button-bar"> <div class="modal__button-bar">
<button class="button" @click="config.resolve()">Close</button> <button class="button button--resolve" @click="config.resolve()">Close</button>
</div> </div>
</modal-inner> </modal-inner>
</template> </template>
<script> <script>
import { mapGetters } from 'vuex'; import { mapGetters, mapActions } from 'vuex';
import ModalInner from './common/ModalInner'; import ModalInner from './common/ModalInner';
export default { export default {
@ -44,66 +59,100 @@ export default {
'config', 'config',
]), ]),
...mapGetters('syncLocation', { ...mapGetters('syncLocation', {
syncLocations: 'current', syncLocations: 'currentWithWorkspaceSyncLocation',
}), }),
currentFileName() { currentFileName() {
return this.$store.getters['file/current'].name; return this.$store.getters['file/current'].name;
}, },
}, },
methods: { methods: {
...mapActions('notification', [
'info',
]),
remove(location) { remove(location) {
this.$store.commit('syncLocation/deleteItem', location.id); if (location.id === 'main') {
this.info('This location can not be removed.');
} else {
this.$store.commit('syncLocation/deleteItem', location.id);
}
}, },
}, },
}; };
</script> </script>
<style lang="scss"> <style lang="scss">
@import '../common/variables.scss'; @import '../../styles/variables.scss';
.modal__inner-1--sync-management {
max-width: 560px;
}
.sync-entry { .sync-entry {
padding: 0.5rem 0.25rem; margin: 1.5em 0;
border-bottom: 1px solid $hr-color; height: auto;
font-size: 17px;
line-height: 1.5;
}
&:last-child { $button-size: 30px;
border-bottom: none; $small-button-size: 22px;
}
.sync-entry__header {
line-height: $button-size;
}
.sync-entry__row {
margin-top: 1px;
padding-top: 1px;
border-top: 1px solid rgba(128, 128, 128, 0.15);
line-height: $small-button-size;
} }
.sync-entry__icon { .sync-entry__icon {
height: 30px; height: 22px;
width: 30px; width: 22px;
margin-right: 0.75rem; margin-right: 0.75rem;
flex: none; flex: none;
} }
.sync-entry__description { .sync-entry__description {
opacity: 0.5;
line-height: 1.4;
font-size: 0.9em;
width: 100%; width: 100%;
overflow: hidden; overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.sync-entry__url {
width: 100%;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
opacity: 0.5;
font-size: 0.67em;
} }
.sync-entry__buttons { .sync-entry__buttons {
margin-left: 0.75rem; margin-left: 0.75rem;
.sync-entry__row & {
margin-left: 0.5rem;
}
} }
.sync-entry__button { .sync-entry__button {
width: 38px; width: $button-size;
height: 38px; height: $button-size;
padding: 6px; padding: 4px;
background-color: transparent; background-color: transparent;
opacity: 0.75; opacity: 0.75;
.sync-entry__row & {
width: $small-button-size;
height: $small-button-size;
padding: 4px;
}
&:active, &:active,
&:focus, &:focus,
&:hover { &:hover {
opacity: 1; opacity: 1;
background-color: rgba(0, 0, 0, 0.1);
} }
} }
</style> </style>

View File

@ -45,7 +45,7 @@
</div> </div>
<div class="modal__button-bar"> <div class="modal__button-bar">
<button class="button" @click="config.reject()">Cancel</button> <button class="button" @click="config.reject()">Cancel</button>
<button class="button" @click="resolve()">Ok</button> <button class="button button--resolve" @click="resolve()">Ok</button>
</div> </div>
</modal-inner> </modal-inner>
</template> </template>
@ -55,8 +55,8 @@ import { mapGetters } from 'vuex';
import utils from '../../services/utils'; import utils from '../../services/utils';
import ModalInner from './common/ModalInner'; import ModalInner from './common/ModalInner';
import CodeEditor from '../CodeEditor'; import CodeEditor from '../CodeEditor';
import emptyTemplateValue from '../../data/emptyTemplateValue.html'; import emptyTemplateValue from '../../data/empties/emptyTemplateValue.html';
import emptyTemplateHelpers from '!raw-loader!../../data/emptyTemplateHelpers.js'; // eslint-disable-line import emptyTemplateHelpers from '!raw-loader!../../data/empties/emptyTemplateHelpers.js'; // eslint-disable-line
const collator = new Intl.Collator(undefined, { sensitivity: 'base' }); const collator = new Intl.Collator(undefined, { sensitivity: 'base' });
@ -91,11 +91,11 @@ export default {
}, },
created() { created() {
this.$watch( this.$watch(
() => this.$store.getters['data/allTemplates'], () => this.$store.getters['data/allTemplatesById'],
(allTemplates) => { (allTemplatesById) => {
const templates = {}; const templates = {};
// Sort templates by name // Sort templates by name
Object.entries(allTemplates) Object.entries(allTemplatesById)
.sort(([, template1], [, template2]) => collator.compare(template1.name, template2.name)) .sort(([, template1], [, template2]) => collator.compare(template1.name, template2.name))
.forEach(([id, template]) => { .forEach(([id, template]) => {
const templateClone = utils.deepCopy(template); const templateClone = utils.deepCopy(template);
@ -105,10 +105,12 @@ export default {
this.templates = templates; this.templates = templates;
this.selectedId = this.config.selectedId; this.selectedId = this.config.selectedId;
if (!templates[this.selectedId]) { if (!templates[this.selectedId]) {
this.selectedId = Object.keys(templates)[0]; [this.selectedId] = Object.keys(templates);
} }
this.isEditing = false; this.isEditing = false;
}, { immediate: true }); },
{ immediate: true },
);
this.$watch('selectedId', (selectedId) => { this.$watch('selectedId', (selectedId) => {
const template = this.templates[selectedId]; const template = this.templates[selectedId];
this.showHelpers = template.helpers !== emptyTemplateHelpers; this.showHelpers = template.helpers !== emptyTemplateHelpers;
@ -137,7 +139,7 @@ export default {
}, },
remove() { remove() {
delete this.templates[this.selectedId]; delete this.templates[this.selectedId];
this.selectedId = Object.keys(this.templates)[0]; [this.selectedId] = Object.keys(this.templates);
}, },
submitEdit(cancel) { submitEdit(cancel) {
const template = this.templates[this.selectedId]; const template = this.templates[this.selectedId];
@ -161,7 +163,7 @@ export default {
</script> </script>
<style lang="scss"> <style lang="scss">
.modal__inner-1--templates { .modal__inner-1.modal__inner-1--templates {
max-width: 680px; max-width: 600px;
} }
</style> </style>

View File

@ -1,39 +1,69 @@
<template> <template>
<modal-inner class="modal__inner-1--workspace-management" aria-label="Manage workspaces"> <modal-inner class="modal__inner-1--workspace-management" aria-label="Manage workspaces">
<div class="modal__content"> <div class="modal__content">
<div class="workspace-entry flex flex--row flex--align-center" v-for="(workspace, id) in sanitizedWorkspaces" :key="id"> <div class="modal__image">
<div class="workspace-entry__icon flex flex--column flex--center"> <icon-database></icon-database>
<icon-provider :provider-id="workspace.providerId"></icon-provider> </div>
</div> <p>The following workspaces are locally available:</p>
<div class="workspace-entry__description flex flex--column"> <div class="workspace-entry flex flex--column" v-for="(workspace, id) in workspacesById" :key="id">
<input class="text-input" type="text" v-if="editedId === id" v-focus @blur="submitEdit()" @keydown.enter="submitEdit()" @keydown.esc.stop="submitEdit(true)" v-model="editingName"> <div class="flex flex--column">
<div class="workspace-entry__name" v-else> <div class="workspace-entry__header flex flex--row flex--align-center">
{{workspace.name}} <div class="workspace-entry__icon">
<icon-provider :provider-id="workspace.providerId"></icon-provider>
</div>
<input class="text-input" type="text" v-if="editedId === id" v-focus @blur="submitEdit()" @keydown.enter="submitEdit()" @keydown.esc.stop="submitEdit(true)" v-model="editingName">
<div class="workspace-entry__name" v-else>{{workspace.name}}</div>
<div class="workspace-entry__buttons flex flex--row">
<button class="workspace-entry__button button" @click="edit(id)" v-title="'Edit name'">
<icon-pen></icon-pen>
</button>
<button class="workspace-entry__button button" @click="remove(id)" v-title="'Remove'">
<icon-delete></icon-delete>
</button>
</div>
</div> </div>
<div class="workspace-entry__url"> <div class="workspace-entry__row flex flex--row flex--align-center">
{{workspace.url}} <div class="workspace-entry__url">
{{workspace.url}}
</div>
<div class="workspace-entry__buttons flex flex--row">
<button class="workspace-entry__button button" v-clipboard="workspace.url" @click="info('Workspace URL copied to clipboard!')" v-title="'Copy URL'">
<icon-content-copy></icon-content-copy>
</button>
<a class="workspace-entry__button button" :href="workspace.url" target="_blank" v-title="'Open workspace'">
<icon-open-in-new></icon-open-in-new>
</a>
</div>
</div>
<div class="workspace-entry__row flex flex--row flex--align-center" v-if="workspace.locationUrl">
<div class="workspace-entry__url">
{{workspace.locationUrl}}
</div>
<div class="workspace-entry__buttons flex flex--row">
<button class="workspace-entry__button button" v-clipboard="workspace.locationUrl" @click="info('Workspace URL copied to clipboard!')" v-title="'Copy URL'">
<icon-content-copy></icon-content-copy>
</button>
<a class="workspace-entry__button button" :href="workspace.locationUrl" target="_blank" v-title="'Open workspace location'">
<icon-open-in-new></icon-open-in-new>
</a>
</div>
</div> </div>
</div> </div>
<div class="workspace-entry__buttons flex flex--row flex--center"> </div>
<button class="workspace-entry__button button" @click="edit(id)"> <div class="modal__info">
<icon-pen></icon-pen> <b>ProTip:</b> A workspace is accessible <b>offline</b> once it has been opened for the first time.
</button>
<button class="workspace-entry__button button" v-if="id !== currentWorkspace.id && id !== mainWorkspace.id" @click="remove(id)">
<icon-delete></icon-delete>
</button>
</div>
</div> </div>
</div> </div>
<div class="modal__button-bar"> <div class="modal__button-bar">
<button class="button" @click="config.resolve()">Close</button> <button class="button button--resolve" @click="config.resolve()">Close</button>
</div> </div>
</modal-inner> </modal-inner>
</template> </template>
<script> <script>
import { mapGetters } from 'vuex'; import { mapGetters, mapActions } from 'vuex';
import ModalInner from './common/ModalInner'; import ModalInner from './common/ModalInner';
import localDbSvc from '../../services/localDbSvc'; import workspaceSvc from '../../services/workspaceSvc';
export default { export default {
components: { components: {
@ -47,105 +77,128 @@ export default {
...mapGetters('modal', [ ...mapGetters('modal', [
'config', 'config',
]), ]),
...mapGetters('data', [
'workspaces',
'sanitizedWorkspaces',
]),
...mapGetters('workspace', [ ...mapGetters('workspace', [
'workspacesById',
'mainWorkspace', 'mainWorkspace',
'currentWorkspace', 'currentWorkspace',
]), ]),
}, },
methods: { methods: {
...mapActions('notification', [
'info',
]),
edit(id) { edit(id) {
this.editedId = id; this.editedId = id;
this.editingName = this.workspaces[id].name; this.editingName = this.workspacesById[id].name;
}, },
submitEdit(cancel) { submitEdit(cancel) {
const workspace = this.workspaces[this.editedId]; const workspace = this.workspacesById[this.editedId];
if (workspace && !cancel && this.editingName) { if (workspace) {
this.$store.dispatch('data/patchWorkspaces', { if (!cancel && this.editingName) {
[this.editedId]: { this.$store.dispatch('workspace/patchWorkspacesById', {
...workspace, [this.editedId]: {
name: this.editingName, ...workspace,
}, name: this.editingName,
}); },
} else { });
this.editingName = workspace.name; } else {
this.editingName = workspace.name;
}
} }
this.editedId = null; this.editedId = null;
}, },
remove(id) { async remove(id) {
return this.$store.dispatch('modal/removeWorkspace') if (id === this.mainWorkspace.id) {
.then( this.info('Your main workspace can not be removed.');
() => localDbSvc.removeWorkspace(id), } else if (id === this.currentWorkspace.id) {
() => {}, // Cancel this.info('Please close the workspace before removing it.');
); } else {
try {
await this.$store.dispatch('modal/open', 'removeWorkspace');
workspaceSvc.removeWorkspace(id);
} catch (e) { /* Cancel */ }
}
}, },
}, },
}; };
</script> </script>
<style lang="scss"> <style lang="scss">
@import '../common/variables.scss'; @import '../../styles/variables.scss';
.modal__inner-1--workspace-management {
max-width: 560px;
}
.workspace-entry { .workspace-entry {
text-align: left; margin: 1.75em 0;
padding-left: 10px;
margin: 15px 0;
height: auto; height: auto;
font-size: 17px; font-size: 17px;
line-height: 1.5; line-height: 1.5;
text-transform: none; }
&:last-child { $button-size: 30px;
border-bottom: none; $small-button-size: 22px;
}
span { .workspace-entry__header {
text-overflow: ellipsis; line-height: $button-size;
overflow: hidden;
.text-input {
border: 1px solid $link-color;
padding: 0 5px;
line-height: $button-size;
height: $button-size;
} }
} }
.workspace-entry__row {
margin-top: 1px;
padding-top: 1px;
border-top: 1px solid rgba(128, 128, 128, 0.15);
line-height: $small-button-size;
}
.workspace-entry__icon { .workspace-entry__icon {
height: 20px; height: 22px;
width: 20px; width: 22px;
margin-right: 12px; margin-right: 0.75rem;
flex: none; flex: none;
} }
.workspace-entry__description {
width: 100%;
word-wrap: break-word;
overflow: hidden;
}
.workspace-entry__name { .workspace-entry__name {
width: 100%;
overflow: hidden; overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
font-weight: bold; font-weight: bold;
} }
.workspace-entry__url { .workspace-entry__url {
width: 100%;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
opacity: 0.5; opacity: 0.5;
font-size: 0.75em; font-size: 0.67em;
} }
.workspace-entry__buttons { .workspace-entry__buttons {
margin-left: 0.75rem; margin-left: 0.75rem;
.workspace-entry__row & {
margin-left: 0.5rem;
}
} }
.workspace-entry__button { .workspace-entry__button {
width: 36px; width: $button-size;
height: 36px; height: $button-size;
padding: 6px; padding: 4px;
background-color: transparent; background-color: transparent;
opacity: 0.75; opacity: 0.75;
.workspace-entry__row & {
width: $small-button-size;
height: $small-button-size;
padding: 4px;
}
&:active, &:active,
&:focus, &:focus,
&:hover { &:hover {

View File

@ -5,7 +5,7 @@
<icon-close></icon-close> <icon-close></icon-close>
</button> </button>
<div class="modal__sponsor-button" v-if="showSponsorButton"> <div class="modal__sponsor-button" v-if="showSponsorButton">
StackEdit is <a class="not-tabbable" target="_blank" href="https://github.com/benweet/stackedit/">open source</a>. Please consider StackEdit is <a class="not-tabbable" target="_blank" href="https://github.com/benweet/stackedit/">open source</a>, please consider
<a class="not-tabbable" href="javascript:void(0)" @click="sponsor">sponsoring</a> for just $5. <a class="not-tabbable" href="javascript:void(0)" @click="sponsor">sponsoring</a> for just $5.
</div> </div>
<slot></slot> <slot></slot>
@ -24,40 +24,38 @@ export default {
'config', 'config',
]), ]),
showSponsorButton() { showSponsorButton() {
const type = this.$store.getters['modal/config'].type; const { type } = this.$store.getters['modal/config'];
return !this.$store.getters.isSponsor && type !== 'sponsor' && type !== 'signInForSponsorship'; return !this.$store.getters.isSponsor && type !== 'sponsor' && type !== 'signInForSponsorship';
}, },
}, },
methods: { methods: {
sponsor() { async sponsor() {
Promise.resolve() try {
.then(() => !this.$store.getters['workspace/sponsorToken'] && if (!this.$store.getters['workspace/sponsorToken']) {
// If user has to sign in // User has to sign in
this.$store.dispatch('modal/signInForSponsorship', { await this.$store.dispatch('modal/open', 'signInForSponsorship');
onResolve: () => googleHelper.signin() await googleHelper.signin();
.then(() => syncSvc.requestSync()), syncSvc.requestSync();
})) }
.then(() => { if (!this.$store.getters.isSponsor) {
if (!this.$store.getters.isSponsor) { await this.$store.dispatch('modal/open', 'sponsor');
this.$store.dispatch('modal/open', 'sponsor'); }
} } catch (e) { /* cancel */ }
})
.catch(() => {}); // Cancel
}, },
}, },
}; };
</script> </script>
<style lang="scss"> <style lang="scss">
@import '../../common/variables.scss'; @import '../../../styles/variables.scss';
.modal__close-button { .modal__close-button {
position: absolute; position: absolute;
top: 8px; top: 8px;
right: 8px; right: 8px;
color: rgba(0, 0, 0, 0.5); color: rgba(0, 0, 0, 0.5);
width: 30px; width: 32px;
height: 30px; height: 32px;
padding: 2px; padding: 2px;
&:active, &:active,

View File

@ -52,28 +52,26 @@ export default (desc) => {
}, },
}; };
if (key === 'selectedTemplate') { if (key === 'selectedTemplate') {
component.computed.allTemplates = () => { component.computed.allTemplatesById = () => {
const allTemplates = store.getters['data/allTemplates']; const allTemplatesById = store.getters['data/allTemplatesById'];
const sortedTemplates = {}; const sortedTemplatesById = {};
Object.entries(allTemplates) Object.entries(allTemplatesById)
.sort(([, template1], [, template2]) => collator.compare(template1.name, template2.name)) .sort(([, template1], [, template2]) => collator.compare(template1.name, template2.name))
.forEach(([templateId, template]) => { .forEach(([templateId, template]) => {
sortedTemplates[templateId] = template; sortedTemplatesById[templateId] = template;
}); });
return sortedTemplates; return sortedTemplatesById;
}; };
// Make use of `function` to have `this` bound to the component // Make use of `function` to have `this` bound to the component
component.methods.configureTemplates = function () { // eslint-disable-line func-names component.methods.configureTemplates = async function () { // eslint-disable-line func-names
store.dispatch('modal/open', { const { templates, selectedId } = await store.dispatch('modal/open', {
type: 'templates', type: 'templates',
selectedId: this.selectedTemplate, selectedId: this.selectedTemplate,
}) });
.then(({ templates, selectedId }) => { store.dispatch('data/setTemplatesById', templates);
store.dispatch('data/setTemplates', templates); store.dispatch('data/patchLocalSettings', {
store.dispatch('data/patchLocalSettings', { [id]: selectedId,
[id]: selectedId, });
});
});
}; };
} }
}); });

View File

@ -4,7 +4,7 @@
<div class="modal__image"> <div class="modal__image">
<icon-provider provider-id="bloggerPage"></icon-provider> <icon-provider provider-id="bloggerPage"></icon-provider>
</div> </div>
<p>This will publish <b>{{currentFileName}}</b> to your <b>Blogger Page</b>.</p> <p>Publish <b>{{currentFileName}}</b> to your <b>Blogger Page</b>.</p>
<form-entry label="Blog URL" error="blogUrl"> <form-entry label="Blog URL" error="blogUrl">
<input slot="field" class="textfield" type="text" v-model.trim="blogUrl" @keydown.enter="resolve()"> <input slot="field" class="textfield" type="text" v-model.trim="blogUrl" @keydown.enter="resolve()">
<div class="form-entry__info"> <div class="form-entry__info">
@ -16,7 +16,7 @@
</form-entry> </form-entry>
<form-entry label="Template"> <form-entry label="Template">
<select slot="field" class="textfield" v-model="selectedTemplate" @keydown.enter="resolve()"> <select slot="field" class="textfield" v-model="selectedTemplate" @keydown.enter="resolve()">
<option v-for="(template, id) in allTemplates" :key="id" :value="id"> <option v-for="(template, id) in allTemplatesById" :key="id" :value="id">
{{ template.name }} {{ template.name }}
</option> </option>
</select> </select>
@ -30,7 +30,7 @@
</div> </div>
<div class="modal__button-bar"> <div class="modal__button-bar">
<button class="button" @click="config.reject()">Cancel</button> <button class="button" @click="config.reject()">Cancel</button>
<button class="button" @click="resolve()">Ok</button> <button class="button button--resolve" @click="resolve()">Ok</button>
</div> </div>
</modal-inner> </modal-inner>
</template> </template>
@ -54,7 +54,10 @@ export default modalTemplate({
} else { } else {
// Return new location // Return new location
const location = bloggerPageProvider.makeLocation( const location = bloggerPageProvider.makeLocation(
this.config.token, this.blogUrl, this.pageId); this.config.token,
this.blogUrl,
this.pageId,
);
location.templateId = this.selectedTemplate; location.templateId = this.selectedTemplate;
this.config.resolve(location); this.config.resolve(location);
} }

View File

@ -4,7 +4,7 @@
<div class="modal__image"> <div class="modal__image">
<icon-provider provider-id="blogger"></icon-provider> <icon-provider provider-id="blogger"></icon-provider>
</div> </div>
<p>This will publish <b>{{currentFileName}}</b> to your <b>Blogger</b> site.</p> <p>Publish <b>{{currentFileName}}</b> to your <b>Blogger</b> site.</p>
<form-entry label="Blog URL" error="blogUrl"> <form-entry label="Blog URL" error="blogUrl">
<input slot="field" class="textfield" type="text" v-model.trim="blogUrl" @keydown.enter="resolve()"> <input slot="field" class="textfield" type="text" v-model.trim="blogUrl" @keydown.enter="resolve()">
<div class="form-entry__info"> <div class="form-entry__info">
@ -16,7 +16,7 @@
</form-entry> </form-entry>
<form-entry label="Template"> <form-entry label="Template">
<select slot="field" class="textfield" v-model="selectedTemplate" @keydown.enter="resolve()"> <select slot="field" class="textfield" v-model="selectedTemplate" @keydown.enter="resolve()">
<option v-for="(template, id) in allTemplates" :key="id" :value="id"> <option v-for="(template, id) in allTemplatesById" :key="id" :value="id">
{{ template.name }} {{ template.name }}
</option> </option>
</select> </select>
@ -31,7 +31,7 @@
</div> </div>
<div class="modal__button-bar"> <div class="modal__button-bar">
<button class="button" @click="config.reject()">Cancel</button> <button class="button" @click="config.reject()">Cancel</button>
<button class="button" @click="resolve()">Ok</button> <button class="button button--resolve" @click="resolve()">Ok</button>
</div> </div>
</modal-inner> </modal-inner>
</template> </template>
@ -55,7 +55,10 @@ export default modalTemplate({
} else { } else {
// Return new location // Return new location
const location = bloggerProvider.makeLocation( const location = bloggerProvider.makeLocation(
this.config.token, this.blogUrl, this.postId); this.config.token,
this.blogUrl,
this.postId,
);
location.templateId = this.selectedTemplate; location.templateId = this.selectedTemplate;
this.config.resolve(location); this.config.resolve(location);
} }

View File

@ -14,7 +14,7 @@
</div> </div>
<div class="modal__button-bar"> <div class="modal__button-bar">
<button class="button" @click="config.reject()">Cancel</button> <button class="button" @click="config.reject()">Cancel</button>
<button class="button" @click="resolve()">Ok</button> <button class="button button--resolve" @click="resolve()">Ok</button>
</div> </div>
</modal-inner> </modal-inner>
</template> </template>
@ -45,7 +45,7 @@ export default modalTemplate({
name: this.name, name: this.name,
password: this.password, password: this.password,
}; };
this.$store.dispatch('data/setCouchdbToken', token); this.$store.dispatch('data/addCouchdbToken', token);
this.config.resolve(); this.config.resolve();
} }
}, },

View File

@ -4,20 +4,20 @@
<div class="modal__image"> <div class="modal__image">
<icon-provider provider-id="couchdb"></icon-provider> <icon-provider provider-id="couchdb"></icon-provider>
</div> </div>
<p>This will create a workspace synchronized with a <b>CouchDB</b> database.</p> <p>Create a workspace synced with a <b>CouchDB</b> database.</p>
<form-entry label="Database URL" error="dbUrl"> <form-entry label="Database URL" error="dbUrl">
<input slot="field" class="textfield" type="text" v-model.trim="dbUrl" @keydown.enter="resolve()"> <input slot="field" class="textfield" type="text" v-model.trim="dbUrl" @keydown.enter="resolve()">
<div class="form-entry__info"> <div class="form-entry__info">
<b>Example:</b> https://instance.smileupps.com/stackedit-workspace <b>Example:</b> https://instance.smileupps.com/stackedit-workspace
</div> </div>
<div class="form-entry__actions"> <div class="form-entry__actions">
<a href="https://community.stackedit.io/t/couchdb-workspace-setup/" target="_blank">More info</a> <a href="https://community.stackedit.io/t/couchdb-workspace-setup/" target="_blank">How to setup?</a>
</div> </div>
</form-entry> </form-entry>
</div> </div>
<div class="modal__button-bar"> <div class="modal__button-bar">
<button class="button" @click="config.reject()">Cancel</button> <button class="button" @click="config.reject()">Cancel</button>
<button class="button" @click="resolve()">Ok</button> <button class="button button--resolve" @click="resolve()">Ok</button>
</div> </div>
</modal-inner> </modal-inner>
</template> </template>

View File

@ -4,7 +4,7 @@
<div class="modal__image"> <div class="modal__image">
<icon-provider provider-id="dropbox"></icon-provider> <icon-provider provider-id="dropbox"></icon-provider>
</div> </div>
<p>This will link your <b>Dropbox</b> account to <b>StackEdit</b>.</p> <p>Link your <b>Dropbox</b> account to <b>StackEdit</b>.</p>
<div class="form-entry"> <div class="form-entry">
<div class="form-entry__checkbox"> <div class="form-entry__checkbox">
<label> <label>
@ -18,7 +18,7 @@
</div> </div>
<div class="modal__button-bar"> <div class="modal__button-bar">
<button class="button" @click="config.reject()">Cancel</button> <button class="button" @click="config.reject()">Cancel</button>
<button class="button" @click="config.resolve()">Ok</button> <button class="button button--resolve" @click="config.resolve()">Ok</button>
</div> </div>
</modal-inner> </modal-inner>
</template> </template>

View File

@ -4,17 +4,17 @@
<div class="modal__image"> <div class="modal__image">
<icon-provider provider-id="dropbox"></icon-provider> <icon-provider provider-id="dropbox"></icon-provider>
</div> </div>
<p>This will publish <b>{{currentFileName}}</b> to your <b>Dropbox</b>.</p> <p>Publish <b>{{currentFileName}}</b> to your <b>Dropbox</b>.</p>
<form-entry label="File path" error="path"> <form-entry label="File path" error="path">
<input slot="field" class="textfield" type="text" v-model.trim="path" @keydown.enter="resolve()"> <input slot="field" class="textfield" type="text" v-model.trim="path" @keydown.enter="resolve()">
<div class="form-entry__info"> <div class="form-entry__info">
<b>Example:</b> {{config.token.fullAccess ? '' : '/Applications/StackEdit (restricted)'}}/path/to/My Document.html<br> <b>Example:</b> {{config.token.fullAccess ? '' : '/Applications/StackEdit (restricted)'}}/path/to/My Document.html<br>
If the file exists, it will be replaced. If the file exists, it will be overwritten.
</div> </div>
</form-entry> </form-entry>
<form-entry label="Template"> <form-entry label="Template">
<select slot="field" class="textfield" v-model="selectedTemplate" @keydown.enter="resolve()"> <select slot="field" class="textfield" v-model="selectedTemplate" @keydown.enter="resolve()">
<option v-for="(template, id) in allTemplates" :key="id" :value="id"> <option v-for="(template, id) in allTemplatesById" :key="id" :value="id">
{{ template.name }} {{ template.name }}
</option> </option>
</select> </select>
@ -25,7 +25,7 @@
</div> </div>
<div class="modal__button-bar"> <div class="modal__button-bar">
<button class="button" @click="config.reject()">Cancel</button> <button class="button" @click="config.reject()">Cancel</button>
<button class="button" @click="resolve()">Ok</button> <button class="button button--resolve" @click="resolve()">Ok</button>
</div> </div>
</modal-inner> </modal-inner>
</template> </template>

View File

@ -4,18 +4,18 @@
<div class="modal__image"> <div class="modal__image">
<icon-provider provider-id="dropbox"></icon-provider> <icon-provider provider-id="dropbox"></icon-provider>
</div> </div>
<p>This will save <b>{{currentFileName}}</b> to your <b>Dropbox</b> and keep it synchronized.</p> <p>Save <b>{{currentFileName}}</b> to your <b>Dropbox</b> and keep it synced.</p>
<form-entry label="File path" error="path"> <form-entry label="File path" error="path">
<input slot="field" class="textfield" type="text" v-model.trim="path" @keydown.enter="resolve()"> <input slot="field" class="textfield" type="text" v-model.trim="path" @keydown.enter="resolve()">
<div class="form-entry__info"> <div class="form-entry__info">
<b>Example:</b> {{config.token.fullAccess ? '' : '/Applications/StackEdit (restricted)'}}/path/to/My Document.md<br> <b>Example:</b> {{config.token.fullAccess ? '' : '/Applications/StackEdit (restricted)'}}/path/to/My Document.md<br>
If the file exists, it will be replaced. If the file exists, it will be overwritten.
</div> </div>
</form-entry> </form-entry>
</div> </div>
<div class="modal__button-bar"> <div class="modal__button-bar">
<button class="button" @click="config.reject()">Cancel</button> <button class="button" @click="config.reject()">Cancel</button>
<button class="button" @click="resolve()">Ok</button> <button class="button button--resolve" @click="resolve()">Ok</button>
</div> </div>
</modal-inner> </modal-inner>
</template> </template>

View File

@ -4,7 +4,7 @@
<div class="modal__image"> <div class="modal__image">
<icon-provider provider-id="gist"></icon-provider> <icon-provider provider-id="gist"></icon-provider>
</div> </div>
<p>This will publish <b>{{currentFileName}}</b> to a <b>Gist</b>.</p> <p>Publish <b>{{currentFileName}}</b> to a <b>Gist</b>.</p>
<form-entry label="Filename" error="filename"> <form-entry label="Filename" error="filename">
<input slot="field" class="textfield" type="text" v-model.trim="filename" @keydown.enter="resolve()"> <input slot="field" class="textfield" type="text" v-model.trim="filename" @keydown.enter="resolve()">
</form-entry> </form-entry>
@ -18,12 +18,12 @@
<form-entry label="Existing Gist ID" info="optional"> <form-entry label="Existing Gist ID" info="optional">
<input slot="field" class="textfield" type="text" v-model.trim="gistId" @keydown.enter="resolve()"> <input slot="field" class="textfield" type="text" v-model.trim="gistId" @keydown.enter="resolve()">
<div class="form-entry__info"> <div class="form-entry__info">
If the file exists in the Gist, it will be replaced. If the file exists in the Gist, it will be overwritten.
</div> </div>
</form-entry> </form-entry>
<form-entry label="Template"> <form-entry label="Template">
<select slot="field" class="textfield" v-model="selectedTemplate" @keydown.enter="resolve()"> <select slot="field" class="textfield" v-model="selectedTemplate" @keydown.enter="resolve()">
<option v-for="(template, id) in allTemplates" :key="id" :value="id"> <option v-for="(template, id) in allTemplatesById" :key="id" :value="id">
{{ template.name }} {{ template.name }}
</option> </option>
</select> </select>
@ -37,7 +37,7 @@
</div> </div>
<div class="modal__button-bar"> <div class="modal__button-bar">
<button class="button" @click="config.reject()">Cancel</button> <button class="button" @click="config.reject()">Cancel</button>
<button class="button" @click="resolve()">Ok</button> <button class="button button--resolve" @click="resolve()">Ok</button>
</div> </div>
</modal-inner> </modal-inner>
</template> </template>
@ -65,7 +65,11 @@ export default modalTemplate({
} else { } else {
// Return new location // Return new location
const location = gistProvider.makeLocation( const location = gistProvider.makeLocation(
this.config.token, this.filename, this.isPublic, this.gistId); this.config.token,
this.filename,
this.isPublic,
this.gistId,
);
location.templateId = this.selectedTemplate; location.templateId = this.selectedTemplate;
this.config.resolve(location); this.config.resolve(location);
} }

View File

@ -4,7 +4,7 @@
<div class="modal__image"> <div class="modal__image">
<icon-provider provider-id="gist"></icon-provider> <icon-provider provider-id="gist"></icon-provider>
</div> </div>
<p>This will save <b>{{currentFileName}}</b> to a <b>Gist</b> and keep it synchronized.</p> <p>Save <b>{{currentFileName}}</b> to a <b>Gist</b> and keep it synced.</p>
<form-entry label="Filename" error="filename"> <form-entry label="Filename" error="filename">
<input slot="field" class="textfield" type="text" v-model.trim="filename" @keydown.enter="resolve()"> <input slot="field" class="textfield" type="text" v-model.trim="filename" @keydown.enter="resolve()">
</form-entry> </form-entry>
@ -18,13 +18,13 @@
<form-entry label="Existing Gist ID" info="optional"> <form-entry label="Existing Gist ID" info="optional">
<input slot="field" class="textfield" type="text" v-model.trim="gistId" @keydown.enter="resolve()"> <input slot="field" class="textfield" type="text" v-model.trim="gistId" @keydown.enter="resolve()">
<div class="form-entry__info"> <div class="form-entry__info">
If the file exists in the Gist, it will be replaced. If the file exists in the Gist, it will be overwritten.
</div> </div>
</form-entry> </form-entry>
</div> </div>
<div class="modal__button-bar"> <div class="modal__button-bar">
<button class="button" @click="config.reject()">Cancel</button> <button class="button" @click="config.reject()">Cancel</button>
<button class="button" @click="resolve()">Ok</button> <button class="button button--resolve" @click="resolve()">Ok</button>
</div> </div>
</modal-inner> </modal-inner>
</template> </template>
@ -51,7 +51,11 @@ export default modalTemplate({
} else { } else {
// Return new location // Return new location
const location = gistProvider.makeLocation( const location = gistProvider.makeLocation(
this.config.token, this.filename, this.isPublic, this.gistId); this.config.token,
this.filename,
this.isPublic,
this.gistId,
);
this.config.resolve(location); this.config.resolve(location);
} }
}, },

View File

@ -4,18 +4,18 @@
<div class="modal__image"> <div class="modal__image">
<icon-provider provider-id="github"></icon-provider> <icon-provider provider-id="github"></icon-provider>
</div> </div>
<p>This will link your <b>GitHub</b> account to <b>StackEdit</b>.</p> <p>Link your <b>GitHub</b> account to <b>StackEdit</b>.</p>
<div class="form-entry"> <div class="form-entry">
<div class="form-entry__checkbox"> <div class="form-entry__checkbox">
<label> <label>
<input type="checkbox" v-model="repoFullAccess"> Grant access to my <b>private repositories</b> <input type="checkbox" v-model="repoFullAccess"> Grant access to your private repositories
</label> </label>
</div> </div>
</div> </div>
</div> </div>
<div class="modal__button-bar"> <div class="modal__button-bar">
<button class="button" @click="config.reject()">Cancel</button> <button class="button" @click="config.reject()">Cancel</button>
<button class="button" @click="config.resolve()">Ok</button> <button class="button button--resolve" @click="config.resolve()">Ok</button>
</div> </div>
</modal-inner> </modal-inner>
</template> </template>

View File

@ -4,29 +4,29 @@
<div class="modal__image"> <div class="modal__image">
<icon-provider provider-id="github"></icon-provider> <icon-provider provider-id="github"></icon-provider>
</div> </div>
<p>This will open a file from your <b>GitHub</b> repository and keep it synchronized.</p> <p>Open a file from your <b>GitHub</b> repository and keep it synced.</p>
<form-entry label="Repository URL" error="repoUrl"> <form-entry label="Repository URL" error="repoUrl">
<input slot="field" class="textfield" type="text" v-model.trim="repoUrl" @keydown.enter="resolve()"> <input slot="field" class="textfield" type="text" v-model.trim="repoUrl" @keydown.enter="resolve()">
<div class="form-entry__info"> <div class="form-entry__info">
<b>Example:</b> https://github.com/benweet/stackedit <b>Example:</b> https://github.com/benweet/stackedit
</div> </div>
</form-entry> </form-entry>
<form-entry label="Branch" info="optional">
<input slot="field" class="textfield" type="text" v-model.trim="branch" @keydown.enter="resolve()">
<div class="form-entry__info">
If not provided, the <code>master</code> branch will be used.
</div>
</form-entry>
<form-entry label="File path" error="path"> <form-entry label="File path" error="path">
<input slot="field" class="textfield" type="text" v-model.trim="path" @keydown.enter="resolve()"> <input slot="field" class="textfield" type="text" v-model.trim="path" @keydown.enter="resolve()">
<div class="form-entry__info"> <div class="form-entry__info">
<b>Example:</b> docs/README.md <b>Example:</b> path/to/README.md
</div>
</form-entry>
<form-entry label="Branch" info="optional">
<input slot="field" class="textfield" type="text" v-model.trim="branch" @keydown.enter="resolve()">
<div class="form-entry__info">
If not supplied, the <code>master</code> branch will be used.
</div> </div>
</form-entry> </form-entry>
</div> </div>
<div class="modal__button-bar"> <div class="modal__button-bar">
<button class="button" @click="config.reject()">Cancel</button> <button class="button" @click="config.reject()">Cancel</button>
<button class="button" @click="resolve()">Ok</button> <button class="button button--resolve" @click="resolve()">Ok</button>
</div> </div>
</modal-inner> </modal-inner>
</template> </template>
@ -34,6 +34,7 @@
<script> <script>
import githubProvider from '../../../services/providers/githubProvider'; import githubProvider from '../../../services/providers/githubProvider';
import modalTemplate from '../common/modalTemplate'; import modalTemplate from '../common/modalTemplate';
import utils from '../../../services/utils';
export default modalTemplate({ export default modalTemplate({
data: () => ({ data: () => ({
@ -52,13 +53,18 @@ export default modalTemplate({
this.setError('path'); this.setError('path');
} }
if (this.repoUrl && this.path) { if (this.repoUrl && this.path) {
const parsedRepo = githubProvider.parseRepoUrl(this.repoUrl); const parsedRepo = utils.parseGithubRepoUrl(this.repoUrl);
if (!parsedRepo) { if (!parsedRepo) {
this.setError('repoUrl'); this.setError('repoUrl');
} else { } else {
// Return new location // Return new location
const location = githubProvider.makeLocation( const location = githubProvider.makeLocation(
this.config.token, parsedRepo.owner, parsedRepo.repo, this.branch || 'master', this.path); this.config.token,
parsedRepo.owner,
parsedRepo.repo,
this.branch || 'master',
this.path,
);
this.config.resolve(location); this.config.resolve(location);
} }
} }

View File

@ -4,29 +4,29 @@
<div class="modal__image"> <div class="modal__image">
<icon-provider provider-id="github"></icon-provider> <icon-provider provider-id="github"></icon-provider>
</div> </div>
<p>This will publish <b>{{currentFileName}}</b> to your <b>GitHub</b> repository.</p> <p>Publish <b>{{currentFileName}}</b> to your <b>GitHub</b> repository.</p>
<form-entry label="Repository URL" error="repoUrl"> <form-entry label="Repository URL" error="repoUrl">
<input slot="field" class="textfield" type="text" v-model.trim="repoUrl" @keydown.enter="resolve()"> <input slot="field" class="textfield" type="text" v-model.trim="repoUrl" @keydown.enter="resolve()">
<div class="form-entry__info"> <div class="form-entry__info">
<b>Example:</b> https://github.com/benweet/stackedit <b>Example:</b> https://github.com/benweet/stackedit
</div> </div>
</form-entry> </form-entry>
<form-entry label="Branch" info="optional">
<input slot="field" class="textfield" type="text" v-model.trim="branch" @keydown.enter="resolve()">
<div class="form-entry__info">
If not provided, the master branch will be used.
</div>
</form-entry>
<form-entry label="File path" error="path"> <form-entry label="File path" error="path">
<input slot="field" class="textfield" type="text" v-model.trim="path" @keydown.enter="resolve()"> <input slot="field" class="textfield" type="text" v-model.trim="path" @keydown.enter="resolve()">
<div class="form-entry__info"> <div class="form-entry__info">
<b>Example:</b> docs/README.md<br> <b>Example:</b> path/to/README.md<br>
If the file exists, it will be replaced. If the file exists, it will be overwritten.
</div>
</form-entry>
<form-entry label="Branch" info="optional">
<input slot="field" class="textfield" type="text" v-model.trim="branch" @keydown.enter="resolve()">
<div class="form-entry__info">
If not supplied, the <code>master</code> branch will be used.
</div> </div>
</form-entry> </form-entry>
<form-entry label="Template"> <form-entry label="Template">
<select slot="field" class="textfield" v-model="selectedTemplate" @keydown.enter="resolve()"> <select slot="field" class="textfield" v-model="selectedTemplate" @keydown.enter="resolve()">
<option v-for="(template, id) in allTemplates" :key="id" :value="id"> <option v-for="(template, id) in allTemplatesById" :key="id" :value="id">
{{ template.name }} {{ template.name }}
</option> </option>
</select> </select>
@ -37,7 +37,7 @@
</div> </div>
<div class="modal__button-bar"> <div class="modal__button-bar">
<button class="button" @click="config.reject()">Cancel</button> <button class="button" @click="config.reject()">Cancel</button>
<button class="button" @click="resolve()">Ok</button> <button class="button button--resolve" @click="resolve()">Ok</button>
</div> </div>
</modal-inner> </modal-inner>
</template> </template>
@ -73,7 +73,12 @@ export default modalTemplate({
} else { } else {
// Return new location // Return new location
const location = githubProvider.makeLocation( const location = githubProvider.makeLocation(
this.config.token, parsedRepo[1], parsedRepo[2], this.branch || 'master', this.path); this.config.token,
parsedRepo[1],
parsedRepo[2],
this.branch || 'master',
this.path,
);
location.templateId = this.selectedTemplate; location.templateId = this.selectedTemplate;
this.config.resolve(location); this.config.resolve(location);
} }

View File

@ -4,7 +4,7 @@
<div class="modal__image"> <div class="modal__image">
<icon-provider provider-id="github"></icon-provider> <icon-provider provider-id="github"></icon-provider>
</div> </div>
<p>This will save <b>{{currentFileName}}</b> to your <b>GitHub</b> repository and keep it synchronized.</p> <p>Save <b>{{currentFileName}}</b> to your <b>GitHub</b> repository and keep it synced.</p>
<form-entry label="Repository URL" error="repoUrl"> <form-entry label="Repository URL" error="repoUrl">
<input slot="field" class="textfield" type="text" v-model.trim="repoUrl" @keydown.enter="resolve()"> <input slot="field" class="textfield" type="text" v-model.trim="repoUrl" @keydown.enter="resolve()">
<div class="form-entry__info"> <div class="form-entry__info">
@ -14,20 +14,20 @@
<form-entry label="Branch" info="optional"> <form-entry label="Branch" info="optional">
<input slot="field" class="textfield" type="text" v-model.trim="branch" @keydown.enter="resolve()"> <input slot="field" class="textfield" type="text" v-model.trim="branch" @keydown.enter="resolve()">
<div class="form-entry__info"> <div class="form-entry__info">
If not provided, the <code>master</code> branch will be used. If not supplied, the <code>master</code> branch will be used.
</div> </div>
</form-entry> </form-entry>
<form-entry label="File path" error="path"> <form-entry label="File path" error="path">
<input slot="field" class="textfield" type="text" v-model.trim="path" @keydown.enter="resolve()"> <input slot="field" class="textfield" type="text" v-model.trim="path" @keydown.enter="resolve()">
<div class="form-entry__info"> <div class="form-entry__info">
<b>Example:</b> docs/README.md<br> <b>Example:</b> path/to/README.md<br>
If the file exists, it will be replaced. If the file exists, it will be overwritten.
</div> </div>
</form-entry> </form-entry>
</div> </div>
<div class="modal__button-bar"> <div class="modal__button-bar">
<button class="button" @click="config.reject()">Cancel</button> <button class="button" @click="config.reject()">Cancel</button>
<button class="button" @click="resolve()">Ok</button> <button class="button button--resolve" @click="resolve()">Ok</button>
</div> </div>
</modal-inner> </modal-inner>
</template> </template>
@ -35,6 +35,7 @@
<script> <script>
import githubProvider from '../../../services/providers/githubProvider'; import githubProvider from '../../../services/providers/githubProvider';
import modalTemplate from '../common/modalTemplate'; import modalTemplate from '../common/modalTemplate';
import utils from '../../../services/utils';
export default modalTemplate({ export default modalTemplate({
data: () => ({ data: () => ({
@ -49,22 +50,22 @@ export default modalTemplate({
}, },
methods: { methods: {
resolve() { resolve() {
if (!this.repoUrl) { const parsedRepo = utils.parseGithubRepoUrl(this.repoUrl);
if (!parsedRepo) {
this.setError('repoUrl'); this.setError('repoUrl');
} }
if (!this.path) { if (!this.path) {
this.setError('path'); this.setError('path');
} }
if (this.repoUrl && this.path) { if (parsedRepo && this.path) {
const parsedRepo = githubProvider.parseRepoUrl(this.repoUrl); const location = githubProvider.makeLocation(
if (!parsedRepo) { this.config.token,
this.setError('repoUrl'); parsedRepo.owner,
} else { parsedRepo.repo,
// Return new location this.branch || 'master',
const location = githubProvider.makeLocation( this.path,
this.config.token, parsedRepo.owner, parsedRepo.repo, this.branch || 'master', this.path); );
this.config.resolve(location); this.config.resolve(location);
}
} }
}, },
}, },

View File

@ -0,0 +1,65 @@
<template>
<modal-inner aria-label="Synchronize with GitHub">
<div class="modal__content">
<div class="modal__image">
<icon-provider provider-id="github"></icon-provider>
</div>
<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">
<b>Example:</b> https://github.com/benweet/stackedit
</div>
</form-entry>
<form-entry label="Branch" info="optional">
<input slot="field" class="textfield" type="text" v-model.trim="branch" @keydown.enter="resolve()">
<div class="form-entry__info">
If not supplied, the <code>master</code> branch will be used.
</div>
</form-entry>
<form-entry label="Folder path" info="optional">
<input slot="field" class="textfield" type="text" v-model.trim="path" @keydown.enter="resolve()">
<div class="form-entry__info">
If not supplied, the root folder will be used.
</div>
</form-entry>
</div>
<div class="modal__button-bar">
<button class="button" @click="config.reject()">Cancel</button>
<button class="button button--resolve" @click="resolve()">Ok</button>
</div>
</modal-inner>
</template>
<script>
import utils from '../../../services/utils';
import modalTemplate from '../common/modalTemplate';
export default modalTemplate({
data: () => ({
branch: '',
path: '',
}),
computedLocalSettings: {
repoUrl: 'githubWorkspaceRepoUrl',
},
methods: {
resolve() {
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',
branch: this.branch || 'master',
path: path || undefined,
}, true);
this.config.resolve();
window.open(url);
}
},
},
});
</script>

View File

@ -4,7 +4,7 @@
<div class="modal__image"> <div class="modal__image">
<icon-provider provider-id="googleDrive"></icon-provider> <icon-provider provider-id="googleDrive"></icon-provider>
</div> </div>
<p>This will link your <b>Google Drive</b> account to <b>StackEdit</b>.</p> <p>Link your <b>Google Drive</b> account to <b>StackEdit</b>.</p>
<div class="form-entry"> <div class="form-entry">
<div class="form-entry__checkbox"> <div class="form-entry__checkbox">
<label> <label>
@ -18,7 +18,7 @@
</div> </div>
<div class="modal__button-bar"> <div class="modal__button-bar">
<button class="button" @click="config.reject()">Cancel</button> <button class="button" @click="config.reject()">Cancel</button>
<button class="button" @click="config.resolve()">Ok</button> <button class="button button--resolve" @click="config.resolve()">Ok</button>
</div> </div>
</modal-inner> </modal-inner>
</template> </template>

View File

@ -4,7 +4,7 @@
<div class="modal__image"> <div class="modal__image">
<icon-provider provider-id="googleDrive"></icon-provider> <icon-provider provider-id="googleDrive"></icon-provider>
</div> </div>
<p>This will publish <b>{{currentFileName}}</b> to your <b>Google Drive</b> account.</p> <p>Publish <b>{{currentFileName}}</b> to your <b>Google Drive</b> account.</p>
<form-entry label="Folder ID" info="optional"> <form-entry label="Folder ID" info="optional">
<input slot="field" class="textfield" type="text" v-model.trim="folderId" @keydown.enter="resolve()"> <input slot="field" class="textfield" type="text" v-model.trim="folderId" @keydown.enter="resolve()">
<div class="form-entry__info"> <div class="form-entry__info">
@ -32,9 +32,9 @@
</label> </label>
</div> </div>
</div> </div>
<form-entry label="Template"> <form-entry label="Template" v-if="format === 'html'">
<select slot="field" class="textfield" v-model="selectedTemplate" @keydown.enter="resolve()"> <select slot="field" class="textfield" v-model="selectedTemplate" @keydown.enter="resolve()">
<option v-for="(template, id) in allTemplates" :key="id" :value="id"> <option v-for="(template, id) in allTemplatesById" :key="id" :value="id">
{{ template.name }} {{ template.name }}
</option> </option>
</select> </select>
@ -48,7 +48,7 @@
</div> </div>
<div class="modal__button-bar"> <div class="modal__button-bar">
<button class="button" @click="config.reject()">Cancel</button> <button class="button" @click="config.reject()">Cancel</button>
<button class="button" @click="resolve()">Ok</button> <button class="button button--resolve" @click="resolve()">Ok</button>
</div> </div>
</modal-inner> </modal-inner>
</template> </template>
@ -73,16 +73,18 @@ export default modalTemplate({
'modal/hideUntil', 'modal/hideUntil',
googleHelper.openPicker(this.config.token, 'folder') googleHelper.openPicker(this.config.token, 'folder')
.then((folders) => { .then((folders) => {
this.$store.dispatch('data/patchLocalSettings', { if (folders[0]) {
googleDriveFolderId: folders[0].id, this.$store.dispatch('data/patchLocalSettings', {
}); googleDriveFolderId: folders[0].id,
})); });
}
}),
);
}, },
resolve() { resolve() {
// Return new location // Return new location
const location = googleDriveProvider.makeLocation( const location = googleDriveProvider.makeLocation(this.config.token, this.fileId);
this.config.token, this.fileId); if (this.format === 'html') {
if (this.format) {
location.templateId = this.selectedTemplate; location.templateId = this.selectedTemplate;
} }
this.config.resolve(location); this.config.resolve(location);

View File

@ -4,7 +4,7 @@
<div class="modal__image"> <div class="modal__image">
<icon-provider provider-id="googleDrive"></icon-provider> <icon-provider provider-id="googleDrive"></icon-provider>
</div> </div>
<p>This will save <b>{{currentFileName}}</b> to your <b>Google Drive</b> account and keep it synchronized.</p> <p>Save <b>{{currentFileName}}</b> to your <b>Google Drive</b> account and keep it synced.</p>
<form-entry label="Folder ID" info="optional"> <form-entry label="Folder ID" info="optional">
<input slot="field" class="textfield" type="text" v-model.trim="folderId" @keydown.enter="resolve()"> <input slot="field" class="textfield" type="text" v-model.trim="folderId" @keydown.enter="resolve()">
<div class="form-entry__info"> <div class="form-entry__info">
@ -23,7 +23,7 @@
</div> </div>
<div class="modal__button-bar"> <div class="modal__button-bar">
<button class="button" @click="config.reject()">Cancel</button> <button class="button" @click="config.reject()">Cancel</button>
<button class="button" @click="resolve()">Ok</button> <button class="button button--resolve" @click="resolve()">Ok</button>
</div> </div>
</modal-inner> </modal-inner>
</template> </template>
@ -46,15 +46,21 @@ export default modalTemplate({
'modal/hideUntil', 'modal/hideUntil',
googleHelper.openPicker(this.config.token, 'folder') googleHelper.openPicker(this.config.token, 'folder')
.then((folders) => { .then((folders) => {
this.$store.dispatch('data/patchLocalSettings', { if (folders[0]) {
googleDriveFolderId: folders[0].id, this.$store.dispatch('data/patchLocalSettings', {
}); googleDriveFolderId: folders[0].id,
})); });
}
}),
);
}, },
resolve() { resolve() {
// Return new location // Return new location
const location = googleDriveProvider.makeLocation( const location = googleDriveProvider.makeLocation(
this.config.token, this.fileId, this.folderId); this.config.token,
this.fileId,
this.folderId,
);
this.config.resolve(location); this.config.resolve(location);
}, },
}, },

View File

@ -4,7 +4,7 @@
<div class="modal__image"> <div class="modal__image">
<icon-provider provider-id="googleDrive"></icon-provider> <icon-provider provider-id="googleDrive"></icon-provider>
</div> </div>
<p>This will create a workspace synchronized with a <b>Google Drive</b> folder.</p> <p>Create a workspace synced with a <b>Google Drive</b> folder.</p>
<form-entry label="Folder ID" info="optional"> <form-entry label="Folder ID" info="optional">
<input slot="field" class="textfield" type="text" v-model.trim="folderId" @keydown.enter="resolve()"> <input slot="field" class="textfield" type="text" v-model.trim="folderId" @keydown.enter="resolve()">
<div class="form-entry__info"> <div class="form-entry__info">
@ -17,7 +17,7 @@
</div> </div>
<div class="modal__button-bar"> <div class="modal__button-bar">
<button class="button" @click="config.reject()">Cancel</button> <button class="button" @click="config.reject()">Cancel</button>
<button class="button" @click="resolve()">Ok</button> <button class="button button--resolve" @click="resolve()">Ok</button>
</div> </div>
</modal-inner> </modal-inner>
</template> </template>
@ -37,10 +37,13 @@ export default modalTemplate({
'modal/hideUntil', 'modal/hideUntil',
googleHelper.openPicker(this.config.token, 'folder') googleHelper.openPicker(this.config.token, 'folder')
.then((folders) => { .then((folders) => {
this.$store.dispatch('data/patchLocalSettings', { if (folders[0]) {
googleDriveWorkspaceFolderId: folders[0].id, this.$store.dispatch('data/patchLocalSettings', {
}); googleDriveWorkspaceFolderId: folders[0].id,
})); });
}
}),
);
}, },
resolve() { resolve() {
const url = utils.addQueryParams('app', { const url = utils.addQueryParams('app', {

View File

@ -11,7 +11,7 @@
</div> </div>
<div class="modal__button-bar"> <div class="modal__button-bar">
<button class="button" @click="reject()">Cancel</button> <button class="button" @click="reject()">Cancel</button>
<button class="button" @click="resolve()">Ok</button> <button class="button button--resolve" @click="resolve()">Ok</button>
</div> </div>
</modal-inner> </modal-inner>
</template> </template>
@ -42,20 +42,20 @@ export default {
}, },
methods: { methods: {
resolve() { resolve() {
let url = this.config.url; let { url } = this.config;
const size = parseInt(this.size, 10); const size = parseInt(this.size, 10);
if (!isNaN(size)) { if (!Number.isNaN(size)) {
url = makeThumbnail(url, size); url = makeThumbnail(url, size);
} }
if (this.title) { if (this.title) {
url += ` "${this.title}"`; url += ` "${this.title}"`;
} }
const callback = this.config.callback; const { callback } = this.config;
this.config.resolve(); this.config.resolve();
callback(url); callback(url);
}, },
reject() { reject() {
const callback = this.config.callback; const { callback } = this.config;
this.config.reject(); this.config.reject();
callback(null); callback(null);
}, },

View File

@ -4,12 +4,12 @@
<div class="modal__image"> <div class="modal__image">
<icon-provider provider-id="wordpress"></icon-provider> <icon-provider provider-id="wordpress"></icon-provider>
</div> </div>
<p>This will publish <b>{{currentFileName}}</b> to your <b>WordPress</b> site.</p> <p>Publish <b>{{currentFileName}}</b> to your <b>WordPress</b> site.</p>
<form-entry label="Site domain" error="domain"> <form-entry label="Site domain" error="domain">
<input slot="field" class="textfield" type="text" v-model.trim="domain" @keydown.enter="resolve()"> <input slot="field" class="textfield" type="text" v-model.trim="domain" @keydown.enter="resolve()">
<div class="form-entry__info"> <div class="form-entry__info">
<b>Example:</b> example.wordpress.com<br> <b>Example:</b> example.wordpress.com<br>
<b>Jetpack plugin</b> is required for self-hosted sites. <b>Note:</b> Jetpack is required for self-hosted sites.
</div> </div>
</form-entry> </form-entry>
<form-entry label="Existing post ID" info="optional"> <form-entry label="Existing post ID" info="optional">
@ -17,7 +17,7 @@
</form-entry> </form-entry>
<form-entry label="Template"> <form-entry label="Template">
<select slot="field" class="textfield" v-model="selectedTemplate" @keydown.enter="resolve()"> <select slot="field" class="textfield" v-model="selectedTemplate" @keydown.enter="resolve()">
<option v-for="(template, id) in allTemplates" :key="id" :value="id"> <option v-for="(template, id) in allTemplatesById" :key="id" :value="id">
{{ template.name }} {{ template.name }}
</option> </option>
</select> </select>
@ -33,7 +33,7 @@
</div> </div>
<div class="modal__button-bar"> <div class="modal__button-bar">
<button class="button" @click="config.reject()">Cancel</button> <button class="button" @click="config.reject()">Cancel</button>
<button class="button" @click="resolve()">Ok</button> <button class="button button--resolve" @click="resolve()">Ok</button>
</div> </div>
</modal-inner> </modal-inner>
</template> </template>
@ -57,7 +57,10 @@ export default modalTemplate({
} else { } else {
// Return new location // Return new location
const location = wordpressProvider.makeLocation( const location = wordpressProvider.makeLocation(
this.config.token, this.domain, this.postId); this.config.token,
this.domain,
this.postId,
);
location.templateId = this.selectedTemplate; location.templateId = this.selectedTemplate;
this.config.resolve(location); this.config.resolve(location);
} }

View File

@ -4,7 +4,7 @@
<div class="modal__image"> <div class="modal__image">
<icon-provider provider-id="zendesk"></icon-provider> <icon-provider provider-id="zendesk"></icon-provider>
</div> </div>
<p>This will link your <b>Zendesk</b> account to <b>StackEdit</b>.</p> <p>Link your <b>Zendesk</b> account to <b>StackEdit</b>.</p>
<form-entry label="Site URL" error="siteUrl"> <form-entry label="Site URL" error="siteUrl">
<input slot="field" class="textfield" type="text" v-model.trim="siteUrl" @keydown.enter="resolve()"> <input slot="field" class="textfield" type="text" v-model.trim="siteUrl" @keydown.enter="resolve()">
<div class="form-entry__info"> <div class="form-entry__info">
@ -21,18 +21,18 @@
</div> </div>
<div class="modal__button-bar"> <div class="modal__button-bar">
<button class="button" @click="config.reject()">Cancel</button> <button class="button" @click="config.reject()">Cancel</button>
<button class="button" @click="resolve()">Ok</button> <button class="button button--resolve" @click="resolve()">Ok</button>
</div> </div>
</modal-inner> </modal-inner>
</template> </template>
<script> <script>
import utils from '../../../services/utils';
import modalTemplate from '../common/modalTemplate'; import modalTemplate from '../common/modalTemplate';
import constants from '../../../data/constants';
export default modalTemplate({ export default modalTemplate({
data: () => ({ data: () => ({
redirectUrl: utils.oauth2RedirectUri, redirectUrl: constants.oauth2RedirectUri,
}), }),
computedLocalSettings: { computedLocalSettings: {
siteUrl: 'zendeskSiteUrl', siteUrl: 'zendeskSiteUrl',

View File

@ -4,7 +4,7 @@
<div class="modal__image"> <div class="modal__image">
<icon-provider provider-id="zendesk"></icon-provider> <icon-provider provider-id="zendesk"></icon-provider>
</div> </div>
<p>This will publish <b>{{currentFileName}}</b> to your <b>Zendesk Help Center</b>.</p> <p>Publish <b>{{currentFileName}}</b> to your <b>Zendesk Help Center</b>.</p>
<form-entry label="Section ID" error="sectionId"> <form-entry label="Section ID" error="sectionId">
<input slot="field" class="textfield" type="text" v-model.trim="sectionId" @keydown.enter="resolve()"> <input slot="field" class="textfield" type="text" v-model.trim="sectionId" @keydown.enter="resolve()">
<div class="form-entry__info"> <div class="form-entry__info">
@ -22,7 +22,7 @@
</form-entry> </form-entry>
<form-entry label="Template"> <form-entry label="Template">
<select slot="field" class="textfield" v-model="selectedTemplate" @keydown.enter="resolve()"> <select slot="field" class="textfield" v-model="selectedTemplate" @keydown.enter="resolve()">
<option v-for="(template, id) in allTemplates" :key="id" :value="id"> <option v-for="(template, id) in allTemplatesById" :key="id" :value="id">
{{ template.name }} {{ template.name }}
</option> </option>
</select> </select>
@ -37,7 +37,7 @@
</div> </div>
<div class="modal__button-bar"> <div class="modal__button-bar">
<button class="button" @click="config.reject()">Cancel</button> <button class="button" @click="config.reject()">Cancel</button>
<button class="button" @click="resolve()">Ok</button> <button class="button button--resolve" @click="resolve()">Ok</button>
</div> </div>
</modal-inner> </modal-inner>
</template> </template>
@ -62,7 +62,11 @@ export default modalTemplate({
} else { } else {
// Return new location // Return new location
const location = zendeskProvider.makeLocation( const location = zendeskProvider.makeLocation(
this.config.token, this.sectionId, this.locale || 'en-us', this.articleId); this.config.token,
this.sectionId,
this.locale || 'en-us',
this.articleId,
);
location.templateId = this.selectedTemplate; location.templateId = this.selectedTemplate;
this.config.resolve(location); this.config.resolve(location);
} }

View File

@ -1 +0,0 @@
@import './common/base';

30
src/data/constants.js Normal file
View File

@ -0,0 +1,30 @@
const origin = `${window.location.protocol}//${window.location.host}`;
export default {
cleanTrashAfter: 0 * 24 * 60 * 60 * 1000, // 7 days
origin,
oauth2RedirectUri: `${origin}/oauth2/callback`,
types: [
'contentState',
'syncedContent',
'content',
'file',
'folder',
'syncLocation',
'publishLocation',
'data',
],
localStorageDataIds: [
'workspaces',
'settings',
'layoutSettings',
'tokens',
],
userIdPrefixes: {
db: 'dropbox',
gh: 'github',
go: 'google',
},
textMaxLength: 250000,
defaultName: 'Untitled',
};

View File

@ -15,6 +15,7 @@ export default () => ({
dropboxPublishTemplate: 'styledHtml', dropboxPublishTemplate: 'styledHtml',
githubRepoFullAccess: false, githubRepoFullAccess: false,
githubRepoUrl: '', githubRepoUrl: '',
githubWorkspaceRepoUrl: '',
githubPublishTemplate: 'jekyllSite', githubPublishTemplate: 'jekyllSite',
gistIsPublic: false, gistIsPublic: false,
gistPublishTemplate: 'plainText', gistPublishTemplate: 'plainText',

View File

@ -1,11 +1,11 @@
# light or dark # light or dark
colorTheme: light colorTheme: light
# Auto-sync frequency (in ms). Minimum is 60000.
autoSyncEvery: 60000
# Adjust font size in editor and preview # Adjust font size in editor and preview
fontSizeFactor: 1 fontSizeFactor: 1
# Adjust maximum text width in editor and preview # Adjust maximum text width in editor and preview
maxWidthFactor: 1 maxWidthFactor: 1
# Auto-sync frequency (in ms). Minimum is 60000.
autoSyncEvery: 90000
# Editor settings # Editor settings
editor: editor:
@ -54,7 +54,7 @@ wkhtmltopdf:
marginRight: 25 marginRight: 25
marginBottom: 25 marginBottom: 25
marginLeft: 25 marginLeft: 25
# `A3`, `A4`, `Legal` or `Letter` # A3, A4, Legal or Letter
pageSize: A4 pageSize: A4
# Options passed to pandoc # Options passed to pandoc
@ -77,6 +77,12 @@ turndown:
linkStyle: inlined linkStyle: inlined
linkReferenceStyle: full linkReferenceStyle: full
# GitHub commit messages
github:
createFileMessage: '{{path}} created from https://stackedit.io/'
updateFileMessage: '{{path}} updated from https://stackedit.io/'
deleteFileMessage: '{{path}} deleted from https://stackedit.io/'
# Default content for new files # Default content for new files
newFileContent: | newFileContent: |

View File

@ -1,6 +1,6 @@
export default () => ({ export default () => ({
main: { main: {
name: 'Main workspace', name: 'Main workspace',
// The rest will be filled by the data/sanitizedWorkspaces getter // The rest will be filled by the workspace/workspacesById getter
}, },
}); });

Some files were not shown because too many files have changed in this diff Show More