Modal refactoring

This commit is contained in:
Benoit Schweblin 2017-09-26 23:54:26 +01:00
parent 872f557d03
commit ac66a77741
85 changed files with 2021 additions and 954 deletions

View File

@ -1,3 +1,4 @@
build/*.js
config/*.js
src/libs/*.js
./index.js

View File

@ -24,7 +24,7 @@ var app = express()
var compiler = webpack(webpackConfig)
// StackEdit custom middlewares
require('./server')(app);
require('../server')(app);
var devMiddleware = require('webpack-dev-middleware')(compiler, {
publicPath: webpackConfig.output.publicPath,
@ -62,8 +62,8 @@ app.use(devMiddleware)
app.use(hotMiddleware)
// serve pure static assets
// var staticPath = path.posix.join(config.dev.assetsPublicPath, config.dev.assetsSubDirectory)
app.use(express.static('./static'))
var staticPath = path.posix.join(config.dev.assetsPublicPath, config.dev.assetsSubDirectory)
app.use(staticPath, express.static('./static'))
var uri = 'http://localhost:' + port

View File

@ -55,7 +55,10 @@ module.exports = {
}
},
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
test: /\.(ttf|eot|otf|woff2)$/, loader: 'ignore-loader'
},
{
test: /\.woff(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,

View File

@ -8,6 +8,7 @@ var CopyWebpackPlugin = require('copy-webpack-plugin')
var HtmlWebpackPlugin = require('html-webpack-plugin')
var ExtractTextPlugin = require('extract-text-webpack-plugin')
var OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin')
var OfflinePlugin = require('offline-plugin');
var env = config.build.env
@ -90,7 +91,11 @@ var webpackConfig = merge(baseWebpackConfig, {
to: config.build.assetsSubDirectory,
ignore: ['.*']
}
])
]),
new OfflinePlugin({
excludes: ['**/.*', '**/*.map', '**/index.html', '**/static/oauth2/callback.html'],
externals: ['/app', '/oauth2/callback']
})
]
})

27
index.js Normal file
View File

@ -0,0 +1,27 @@
var cluster = require('cluster');
var http = require('http');
var https = require('https');
var path = require('path');
var express = require('express');
var app = express();
require('./server')(app);
var port = process.env.PORT || 8080;
if(port === 443) {
var fs = require('fs');
var credentials = {
key: fs.readFileSync(path.join(__dirname, '/../../shared/config/ssl.key'), 'utf8'),
cert: fs.readFileSync(path.join(__dirname, '/../../shared/config/ssl.crt'), 'utf8'),
ca: fs.readFileSync(path.join(__dirname, '/../../shared/config/ssl.ca'), 'utf8').split('\n\n')
};
var httpsServer = https.createServer(credentials, app);
httpsServer.listen(port, null, function() {
console.log('HTTPS server started: https://localhost');
});
port = 80;
}
var httpServer = http.createServer(app);
httpServer.listen(port, null, function() {
console.log('HTTP server started: http://localhost:' + port);
});

View File

@ -1,9 +1,13 @@
{
"name": "StackEdit",
"version": "1.0.0",
"description": "A Vue.js project",
"author": "",
"private": true,
"name": "stackedit",
"version": "5.0.0",
"description": "Free, open-source, full-featured Markdown editor",
"author": "Benoit Schweblin",
"license": "Apache-2.0",
"bugs": {
"url": "https://github.com/benweet/stackedit/issues"
},
"main": "index.js",
"scripts": {
"postinstall": "gulp --cwd build build-prism",
"start": "node build/dev-server.js",
@ -14,6 +18,7 @@
"bezier-easing": "^1.1.0",
"clipboard": "^1.7.1",
"clunderscore": "^1.0.3",
"compression": "^1.7.0",
"diff-match-patch": "^1.0.0",
"file-saver": "^1.3.3",
"handlebars": "^4.0.10",
@ -33,6 +38,8 @@
"prismjs": "^1.6.0",
"raw-loader": "^0.5.1",
"request": "^2.82.0",
"serve-static": "^1.12.6",
"stackedit": "^4.3.15",
"vue": "^2.3.3",
"vuex": "^2.3.1"
},
@ -58,7 +65,7 @@
"eslint-plugin-html": "^2.0.0",
"eslint-plugin-import": "^2.2.0",
"eventsource-polyfill": "^0.9.6",
"express": "^4.14.1",
"express": "^4.15.5",
"extract-text-webpack-plugin": "^2.0.0",
"file-loader": "^0.11.1",
"friendly-errors-webpack-plugin": "^1.1.3",
@ -66,7 +73,9 @@
"gulp-concat": "^2.6.1",
"html-webpack-plugin": "^2.28.0",
"http-proxy-middleware": "^0.17.3",
"ignore-loader": "^0.1.2",
"node-sass": "^4.5.3",
"offline-plugin": "^4.8.4",
"opn": "^4.0.2",
"optimize-css-assets-webpack-plugin": "^1.3.0",
"ora": "^1.2.0",

View File

@ -15,7 +15,6 @@ function githubToken(clientId, code) {
if (err) {
reject(err);
}
console.log(body)
var token = qs.parse(body).access_token;
if (token) {
resolve(token);
@ -26,13 +25,11 @@ function githubToken(clientId, code) {
});
}
module.exports = function (app) {
app.get('/oauth2/githubToken', function (req, res) {
githubToken(req.query.clientId, req.query.code)
.then(function (token) {
res.send(token);
}, function (err) {
res.status(400).send(err ? err.message || err.toString() : 'bad_code');
});
});
exports.githubToken = function (req, res) {
githubToken(req.query.clientId, req.query.code)
.then(function (token) {
res.send(token);
}, function (err) {
res.status(400).send(err ? err.message || err.toString() : 'bad_code');
});
};

50
server/index.js Normal file
View File

@ -0,0 +1,50 @@
var compression = require('compression');
var serveStatic = require('serve-static');
var path = require('path');
module.exports = function (app) {
// Force HTTPS on stackedit.io
app.all('*', function(req, res, next) {
if (req.headers.host === 'stackedit.io' && !req.secure && req.headers['x-forwarded-proto'] !== 'https') {
return res.redirect('https://stackedit.io' + req.url);
}
/\.(eot|ttf|woff|svg)$/.test(req.url) && res.header('Access-Control-Allow-Origin', '*');
next();
});
// Use gzip compression
app.use(compression());
app.post('/pdfExport', require('./pdf').export);
app.get('/oauth2/githubToken', require('./github').githubToken);
// Serve landing.html in /
app.get('/', function(req, res) {
res.sendFile(require.resolve('stackedit/views/landing.html'));
});
// Serve editor.html in /viewer
app.get('/editor', function(req, res) {
res.sendFile(require.resolve('stackedit/views/editor.html'));
});
// Serve viewer.html in /viewer
app.get('/viewer', function(req, res) {
res.sendFile(require.resolve('stackedit/views/viewer.html'));
});
// Serve index.html in /app
app.get('/app', function(req, res) {
res.sendFile(path.join(__dirname, '../dist/index.html'));
});
// Serve callback.html in /app
app.get('/oauth2/callback', function(req, res) {
res.sendFile(path.join(__dirname, '../dist/static/oauth2/callback.html'));
});
// Serve static resources
app.use(serveStatic(path.join(__dirname, '../dist'))); // v5
app.use(serveStatic(path.dirname(require.resolve('stackedit/public/cache.manifest')))); // v4
// Error 404
app.use(function(req, res) {
res.status(404).sendFile(require.resolve('stackedit/views/error_404.html'));
});
};

145
server/pdf.js Normal file
View File

@ -0,0 +1,145 @@
/* global window,MathJax */
var spawn = require('child_process').spawn;
var fs = require('fs');
var path = require('path');
var os = require('os');
var request = require('request');
function waitForJavaScript() {
if(window.MathJax) {
// Amazon EC2: fix TeX font detection
MathJax.Hub.Register.StartupHook("HTML-CSS Jax Startup",function () {
var HTMLCSS = MathJax.OutputJax["HTML-CSS"];
HTMLCSS.Font.checkWebFont = function (check,font,callback) {
if (check.time(callback)) {
return;
}
if (check.total === 0) {
HTMLCSS.Font.testFont(font);
setTimeout(check,200);
} else {
callback(check.STATUS.OK);
}
};
});
MathJax.Hub.Queue(function () {
window.status = 'done';
});
}
else {
setTimeout(function() {
window.status = 'done';
}, 2000);
}
}
var authorizedPageSizes = [
'A3',
'A4',
'Legal',
'Letter'
];
exports.export = function(req, res, next) {
function onError(err) {
next(err);
}
function onUnknownError() {
res.statusCode = 400;
res.end('Unknown error');
}
function onUnauthorizedError() {
res.statusCode = 401;
res.end('Unauthorized');
}
function onTimeout() {
res.statusCode = 408;
res.end('Request timeout');
}
request({
uri: 'https://monetizejs.com/api/payments',
qs: {
access_token: req.query.token
},
json: true
}, function (err, paymentsRes, payments) {
var authorized = payments && payments.app == 'ESTHdCYOi18iLhhO' && (
(payments.chargeOption && payments.chargeOption.alias == 'once') ||
(payments.subscriptionOption && payments.subscriptionOption.alias == 'yearly'));
if(err || paymentsRes.statusCode != 200 || !authorized) {
return onUnauthorizedError();
}
var options, params = [];
try {
options = JSON.parse(req.query.options);
}
catch(e) {
options = {};
}
// Margins
var marginTop = parseInt(options.marginTop);
params.push('-T', isNaN(marginTop) ? 25 : marginTop);
var marginRight = parseInt(options.marginRight);
params.push('-R', isNaN(marginRight) ? 25 : marginRight);
var marginBottom = parseInt(options.marginBottom);
params.push('-B', isNaN(marginBottom) ? 25 : marginBottom);
var marginLeft = parseInt(options.marginLeft);
params.push('-L', isNaN(marginLeft) ? 25 : marginLeft);
// Header
options.headerCenter && params.push('--header-center', options.headerCenter);
options.headerLeft && params.push('--header-left', options.headerLeft);
options.headerRight && params.push('--header-right', options.headerRight);
options.headerFontName && params.push('--header-font-name', options.headerFontName);
options.headerFontSize && params.push('--header-font-size', options.headerFontSize);
// Footer
options.footerCenter && params.push('--footer-center', options.footerCenter);
options.footerLeft && params.push('--footer-left', options.footerLeft);
options.footerRight && params.push('--footer-right', options.footerRight);
options.footerFontName && params.push('--footer-font-name', options.footerFontName);
options.footerFontSize && params.push('--footer-font-size', options.footerFontSize);
// Page size
params.push('--page-size', authorizedPageSizes.indexOf(options.pageSize) === -1 ? 'A4' : options.pageSize);
// Use a temp file as wkhtmltopdf can't access /dev/stdout on Amazon EC2 for some reason
var filePath = path.join(os.tmpDir(), Date.now() + '.pdf');
var binPath = process.env.WKHTMLTOPDF_PATH || 'wkhtmltopdf';
params.push('--run-script', waitForJavaScript.toString() + 'waitForJavaScript()');
params.push('--window-status', 'done');
var wkhtmltopdf = spawn(binPath, params.concat('-', filePath), {
stdio: [
'pipe',
'ignore',
'ignore'
]
});
var timeoutId = setTimeout(function() {
timeoutId = undefined;
wkhtmltopdf.kill();
}, 30000);
wkhtmltopdf.on('error', onError);
wkhtmltopdf.stdin.on('error', onError);
wkhtmltopdf.on('close', function(code) {
if(!timeoutId) {
return onTimeout();
}
clearTimeout(timeoutId);
if(code) {
return onUnknownError();
}
var readStream = fs.createReadStream(filePath);
readStream.on('open', function() {
readStream.pipe(res);
});
readStream.on('close', function() {
fs.unlink(filePath, function() {
});
});
readStream.on('error', onUnknownError);
});
req.pipe(wkhtmltopdf.stdin);
});
};

View File

@ -0,0 +1,10 @@
<svg
xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128">
<g id="_x32__stroke">
<g id="Wordpress_1_">
<rect clip-rule="evenodd" fill="none" fill-rule="evenodd" height="128" width="128"/>
<path clip-rule="evenodd" d="M65.123,69.595l-19.205,55.797 c5.736,1.688,11.8,2.608,18.081,2.608c7.452,0,14.6-1.288,21.253-3.628c-0.168-0.276-0.328-0.564-0.456-0.88L65.123,69.595z M120.16,33.294c0.276,2.04,0.432,4.224,0.432,6.58c0,6.492-1.216,13.792-4.868,22.924l-19.549,56.517 C115.204,108.223,128,87.606,128,63.998C128,52.87,125.156,42.41,120.16,33.294z M107.204,60.769 c0-7.912-2.844-13.388-5.276-17.648c-3.244-5.276-6.288-9.74-6.288-15.012c0-5.884,4.46-11.36,10.748-11.36 c0.284,0,0.552,0.036,0.828,0.052C95.832,6.368,80.659,0,63.999,0C41.638,0,21.969,11.472,10.525,28.844 c1.504,0.048,2.92,0.076,4.12,0.076c6.692,0,17.057-0.812,17.057-0.812c3.448-0.204,3.856,4.868,0.408,5.272 c0,0-3.468,0.408-7.324,0.612l23.305,69.321l14.008-42.005L52.13,33.992c-3.448-0.204-6.716-0.612-6.716-0.612 c-3.448-0.204-3.044-5.476,0.408-5.272c0,0,10.568,0.812,16.857,0.812c6.692,0,17.057-0.812,17.057-0.812 c3.452-0.204,3.856,4.868,0.408,5.272c0,0-3.472,0.408-7.324,0.612l23.129,68.793l6.388-21.328 C105.096,72.601,107.204,66.245,107.204,60.769z M0,63.997c0,25.332,14.72,47.225,36.069,57.597L5.54,37.952 C1.992,45.909,0,54.717,0,63.997z" fill="#00759D" fill-rule="evenodd" id="Wordpress"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,10 @@
<svg
xmlns="http://www.w3.org/2000/svg" viewBox="0 0 152 116">
<g>
<path d="M70.125,30.375l0,84.675l-70.125,0l70.125,-84.675Z" style="fill:#03363d;fill-rule:nonzero;"/>
<path d="M70.125,0c0,19.35 -15.675,35.025 -35.025,35.025c-19.35,0 -35.1,-15.675 -35.1,-35.025l70.125,0Z" style="fill:#03363d;fill-rule:nonzero;"/>
<path d="M81.675,115.05c0,-19.35 15.675,-35.025 35.025,-35.025c19.35,0 35.025,15.675 35.025,35.025l-70.05,0Z" style="fill:#03363d;fill-rule:nonzero;"/>
<path d="M81.675,84.675l0,-84.675l70.125,0l-70.125,84.675Z" style="fill:#03363d;fill-rule:nonzero;"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 605 B

View File

@ -20,7 +20,6 @@
<script>
import { mapMutations, mapActions } from 'vuex';
import utils from '../services/utils';
import defaultContent from '../data/defaultContent.md';
export default {
name: 'explorer-node',
@ -99,9 +98,14 @@ export default {
id,
});
} else {
// Add empty line at the end if needed
const ensureFinalNewLine = text => `${text}\n`.replace(/\n\n$/, '\n');
const text = ensureFinalNewLine(this.$store.getters['data/computedSettings'].newFileContent);
const properties = ensureFinalNewLine(this.$store.getters['data/computedSettings'].newFileProperties);
this.$store.commit('content/setItem', {
id: `${id}/content`,
text: defaultContent,
text,
properties,
});
this.$store.commit('file/setItem', {
...newChildNode.item,

View File

@ -11,14 +11,19 @@
<publish-management-modal v-else-if="config.type === 'publishManagement'"></publish-management-modal>
<google-drive-sync-modal v-else-if="config.type === 'googleDriveSync'"></google-drive-sync-modal>
<google-drive-publish-modal v-else-if="config.type === 'googleDrivePublish'"></google-drive-publish-modal>
<dropbox-account-modal v-else-if="config.type === 'dropboxAccount'"></dropbox-account-modal>
<dropbox-sync-modal v-else-if="config.type === 'dropboxSync'"></dropbox-sync-modal>
<dropbox-publish-modal v-else-if="config.type === 'dropboxPublish'"></dropbox-publish-modal>
<github-account-modal v-else-if="config.type === 'githubAccount'"></github-account-modal>
<github-sync-modal v-else-if="config.type === 'githubSync'"></github-sync-modal>
<github-publish-modal v-else-if="config.type === 'githubPublish'"></github-publish-modal>
<gist-sync-modal v-else-if="config.type === 'gistSync'"></gist-sync-modal>
<gist-publish-modal v-else-if="config.type === 'gistPublish'"></gist-publish-modal>
<wordpress-publish-modal v-else-if="config.type === 'wordpressPublish'"></wordpress-publish-modal>
<blogger-publish-modal v-else-if="config.type === 'bloggerPublish'"></blogger-publish-modal>
<blogger-page-publish-modal v-else-if="config.type === 'bloggerPagePublish'"></blogger-page-publish-modal>
<zendesk-account-modal v-else-if="config.type === 'zendeskAccount'"></zendesk-account-modal>
<zendesk-publish-modal v-else-if="config.type === 'zendeskPublish'"></zendesk-publish-modal>
<div v-else class="modal__inner-1">
<div class="modal__inner-2">
<div class="modal__content" v-html="config.content"></div>
@ -45,14 +50,19 @@ import SyncManagementModal from './modals/SyncManagementModal';
import PublishManagementModal from './modals/PublishManagementModal';
import GoogleDriveSyncModal from './modals/GoogleDriveSyncModal';
import GoogleDrivePublishModal from './modals/GoogleDrivePublishModal';
import DropboxAccountModal from './modals/DropboxAccountModal';
import DropboxSyncModal from './modals/DropboxSyncModal';
import DropboxPublishModal from './modals/DropboxPublishModal';
import GithubAccountModal from './modals/GithubAccountModal';
import GithubSyncModal from './modals/GithubSyncModal';
import GithubPublishModal from './modals/GithubPublishModal';
import GistSyncModal from './modals/GistSyncModal';
import GistPublishModal from './modals/GistPublishModal';
import WordpressPublishModal from './modals/WordpressPublishModal';
import BloggerPublishModal from './modals/BloggerPublishModal';
import BloggerPagePublishModal from './modals/BloggerPagePublishModal';
import ZendeskAccountModal from './modals/ZendeskAccountModal';
import ZendeskPublishModal from './modals/ZendeskPublishModal';
export default {
components: {
@ -67,14 +77,19 @@ export default {
PublishManagementModal,
GoogleDriveSyncModal,
GoogleDrivePublishModal,
DropboxAccountModal,
DropboxSyncModal,
DropboxPublishModal,
GithubAccountModal,
GithubSyncModal,
GithubPublishModal,
GistSyncModal,
GistPublishModal,
WordpressPublishModal,
BloggerPublishModal,
BloggerPagePublishModal,
ZendeskAccountModal,
ZendeskPublishModal,
},
computed: mapGetters('modal', [
'config',

View File

@ -12,7 +12,8 @@
</div>
<div class="navigation-bar__inner navigation-bar__inner--right navigation-bar__inner--title flex flex--row">
<div class="navigation-bar__spinner">
<div v-show="showSpinner" class="spinner"></div>
<div v-if="!offline && showSpinner" class="spinner"></div>
<icon-sync-off v-if="offline"></icon-sync-off>
</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>
@ -21,12 +22,9 @@
<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">
<icon-provider :provider-id="location.providerId"></icon-provider>
</a>
<button class="navigation-bar__button navigation-bar__button--sync button" v-if="!offline && isSyncPossible" :disabled="isSyncRequested" @click="requestSync">
<button class="navigation-bar__button navigation-bar__button--sync button" v-if="isSyncPossible" :disabled="isSyncRequested || offline" @click="requestSync">
<icon-sync></icon-sync>
</button>
<button class="navigation-bar__button navigation-bar__button--sync-off button" v-if="offline && isSyncPossible" disabled="disabled">
<icon-sync-off></icon-sync-off>
</button>
<a class="navigation-bar__button navigation-bar__button--location button" :class="{'navigation-bar__button--blink': location.id === currentLocation.id}" v-for="location in publishLocations" :key="location.id" :href="location.url" target="_blank">
<icon-provider :provider-id="location.providerId"></icon-provider>
</a>
@ -253,7 +251,7 @@ export default {
width: 38px;
&.navigation-bar__button--stackedit {
opacity: 0.8;
opacity: 0.85;
&:active,
&:focus,
@ -277,7 +275,6 @@ export default {
}
.navigation-bar__button--sync,
.navigation-bar__button--sync-off,
.navigation-bar__button--publish {
padding: 0 6px;
margin: 0 5px;
@ -292,15 +289,6 @@ export default {
}
}
.navigation-bar__button--sync-off[disabled] {
&,
&:active,
&:focus,
&:hover {
color: $error-color;
}
}
.navigation-bar__title--input,
.navigation-bar__button {
&:active,
@ -313,14 +301,18 @@ export default {
.navigation-bar__button--location {
width: 20px;
padding: 0 2px 12px;
height: 20px;
border-radius: 10px;
padding: 2px;
margin-top: 8px;
opacity: 0.5;
background-color: rgba(255, 255, 255, 0.2);
&:active,
&:focus,
&:hover {
background-color: transparent;
opacity: 1;
background-color: rgba(255, 255, 255, 0.2);
}
}
@ -328,13 +320,6 @@ export default {
animation: blink 1s linear infinite;
}
@keyframes blink {
50% {
opacity: 1;
filter: contrast(0.8) brightness(1.25);
}
}
.navigation-bar__title--fake {
position: absolute;
left: -9999px;
@ -382,12 +367,18 @@ export default {
$r: 9px;
$d: $r * 2;
$b: $d/10;
$t: 1500ms;
$t: 3000ms;
.navigation-bar__spinner {
width: $d;
margin: 10px 5px 0 10px;
width: 22px;
margin: 8px 0 0 8px;
color: rgba(255, 255, 255, 0.67);
.icon {
width: 22px;
height: 22px;
color: transparentize($error-color, 0.5);
}
}
.spinner {
@ -397,6 +388,7 @@ $t: 1500ms;
position: relative;
border: $b solid currentColor;
border-radius: 50%;
margin: 2px;
&::before,
&::after {
@ -410,11 +402,18 @@ $t: 1500ms;
}
&::before {
height: $r * 0.5;
height: $r * 0.35;
left: $r - $b * 1.5;
top: 50%;
animation: spin $t linear infinite;
}
&::after {
height: $r * 0.5;
left: $r - $b * 1.5;
top: 50%;
animation: spin $t/4 linear infinite;
}
}
@keyframes spin {
@ -422,4 +421,10 @@ $t: 1500ms;
transform: rotate(360deg);
}
}
@keyframes blink {
50% {
opacity: 1;
}
}
</style>

View File

@ -85,7 +85,7 @@ export default {
computeHtml() {
let text;
if (editorSvc.previewSelectionRange) {
text = editorSvc.previewSelectionRange.toString();
text = `${editorSvc.previewSelectionRange}`;
}
this.htmlSelection = true;
if (!text) {

View File

@ -12,7 +12,7 @@ import editorSvc from '../services/editorSvc';
export default {
data: () => ({
maskY: -999,
maskY: 0,
}),
computed: {
...mapGetters('layout', [
@ -64,9 +64,11 @@ export default {
// Change mask postion on scroll
const updateMaskY = () => {
const scrollPosition = editorSvc.getScrollPosition();
const sectionDesc = editorSvc.sectionDescList[scrollPosition.sectionIdx];
this.maskY = sectionDesc.tocDimension.startOffset +
(scrollPosition.posInSection * sectionDesc.tocDimension.height);
if (scrollPosition) {
const sectionDesc = editorSvc.sectionDescList[scrollPosition.sectionIdx];
this.maskY = sectionDesc.tocDimension.startOffset +
(scrollPosition.posInSection * sectionDesc.tocDimension.height);
}
};
Vue.nextTick(() => {

View File

@ -2,15 +2,18 @@
<div class="side-bar__panel side-bar__panel--menu">
<menu-entry @click.native="exportMarkdown">
<icon-download slot="icon"></icon-download>
Export as Markdown
<div>Export as Markdown</div>
<span>Save file as plain text.</span>
</menu-entry>
<menu-entry @click.native="exportHtml">
<icon-download slot="icon"></icon-download>
Export as HTML
<div>Export as HTML</div>
<span>Generate an HTML page from a template.</span>
</menu-entry>
<menu-entry @click.native="exportPdf">
<icon-download slot="icon"></icon-download>
Export as PDF
<div>Export as PDF</div>
<span>Produce a PDF from an HTML template.</span>
</menu-entry>
</div>
</template>

View File

@ -73,8 +73,7 @@ export default {
return this.$store.dispatch('modal/notImplemented');
},
fileProperties() {
return this.$store.dispatch('modal/open', 'fileProperties')
.then(properties => this.$store.dispatch('content/patchCurrent', { properties }));
return this.$store.dispatch('modal/open', 'fileProperties');
},
},
};

View File

@ -40,6 +40,13 @@
<span>{{token.name}}</span>
</menu-entry>
</div>
<div v-for="token in wordpressTokens" :key="token.sub">
<menu-entry @click.native="publishWordpress(token)">
<icon-provider slot="icon" provider-id="wordpress"></icon-provider>
<div>Publish to WordPress</div>
<span>{{token.name}}</span>
</menu-entry>
</div>
<div v-for="token in bloggerTokens" :key="token.sub">
<menu-entry @click.native="publishBlogger(token)">
<icon-provider slot="icon" provider-id="blogger"></icon-provider>
@ -52,6 +59,13 @@
<span>{{token.name}}</span>
</menu-entry>
</div>
<div v-for="token in zendeskTokens" :key="token.sub">
<menu-entry @click.native="publishZendesk(token)">
<icon-provider slot="icon" provider-id="zendesk"></icon-provider>
<div>Publish to Zendesk Help Center</div>
<span>{{token.name}} {{token.subdomain}}</span>
</menu-entry>
</div>
<hr>
<menu-entry @click.native="addGoogleDriveAccount">
<icon-provider slot="icon" provider-id="googleDrive"></icon-provider>
@ -65,10 +79,18 @@
<icon-provider slot="icon" provider-id="github"></icon-provider>
<span>Add GitHub account</span>
</menu-entry>
<menu-entry @click.native="addWordpressAccount">
<icon-provider slot="icon" provider-id="wordpress"></icon-provider>
<span>Add WordPress account</span>
</menu-entry>
<menu-entry @click.native="addBloggerAccount">
<icon-provider slot="icon" provider-id="blogger"></icon-provider>
<span>Add Blogger account</span>
</menu-entry>
<menu-entry @click.native="addZendeskAccount">
<icon-provider slot="icon" provider-id="zendesk"></icon-provider>
<span>Add Zendesk account</span>
</menu-entry>
</div>
</template>
@ -78,6 +100,8 @@ import MenuEntry from './MenuEntry';
import googleHelper from '../../services/providers/helpers/googleHelper';
import dropboxHelper from '../../services/providers/helpers/dropboxHelper';
import githubHelper from '../../services/providers/helpers/githubHelper';
import wordpressHelper from '../../services/providers/helpers/wordpressHelper';
import zendeskHelper from '../../services/providers/helpers/zendeskHelper';
import publishSvc from '../../services/publishSvc';
import store from '../../store';
@ -117,9 +141,15 @@ export default {
githubTokens() {
return tokensToArray(this.$store.getters['data/githubTokens']);
},
wordpressTokens() {
return tokensToArray(this.$store.getters['data/wordpressTokens']);
},
bloggerTokens() {
return tokensToArray(this.$store.getters['data/googleTokens'], token => token.isBlogger);
},
zendeskTokens() {
return tokensToArray(this.$store.getters['data/zendeskTokens']);
},
},
methods: {
requestPublish() {
@ -134,14 +164,29 @@ export default {
return googleHelper.addDriveAccount();
},
addDropboxAccount() {
return dropboxHelper.addAccount();
return this.$store.dispatch('modal/open', {
type: 'dropboxAccount',
onResolve: () => dropboxHelper.addAccount(!store.getters['data/localSettings'].dropboxRestrictedAccess),
});
},
addGithubAccount() {
return githubHelper.addAccount();
return this.$store.dispatch('modal/open', {
type: 'githubAccount',
onResolve: () => githubHelper.addAccount(store.getters['data/localSettings'].githubRepoFullAccess),
});
},
addWordpressAccount() {
return wordpressHelper.addAccount();
},
addBloggerAccount() {
return googleHelper.addBloggerAccount();
},
addZendeskAccount() {
return this.$store.dispatch('modal/open', {
type: 'zendeskAccount',
onResolve: ({ subdomain, clientId }) => zendeskHelper.addAccount(subdomain, clientId),
});
},
publishGoogleDrive(token) {
return openPublishModal(token, 'googleDrivePublish');
},
@ -154,12 +199,18 @@ export default {
publishGist(token) {
return openPublishModal(token, 'gistPublish');
},
publishWordpress(token) {
return openPublishModal(token, 'wordpressPublish');
},
publishBlogger(token) {
return openPublishModal(token, 'bloggerPublish');
},
publishBloggerPage(token) {
return openPublishModal(token, 'bloggerPagePublish');
},
publishZendesk(token) {
return openPublishModal(token, 'zendeskPublish');
},
},
};
</script>

View File

@ -81,7 +81,6 @@ import dropboxHelper from '../../services/providers/helpers/dropboxHelper';
import githubHelper from '../../services/providers/helpers/githubHelper';
import googleDriveProvider from '../../services/providers/googleDriveProvider';
import dropboxProvider from '../../services/providers/dropboxProvider';
import dropboxRestrictedProvider from '../../services/providers/dropboxRestrictedProvider';
import syncSvc from '../../services/syncSvc';
import store from '../../store';
@ -142,10 +141,16 @@ export default {
return googleHelper.addDriveAccount();
},
addDropboxAccount() {
return dropboxHelper.addAccount();
return this.$store.dispatch('modal/open', {
type: 'dropboxAccount',
onResolve: () => dropboxHelper.addAccount(!store.getters['data/localSettings'].dropboxRestrictedAccess),
});
},
addGithubAccount() {
return githubHelper.addAccount();
return this.$store.dispatch('modal/open', {
type: 'githubAccount',
onResolve: () => githubHelper.addAccount(store.getters['data/localSettings'].githubRepoFullAccess),
});
},
openGoogleDrive(token) {
return googleHelper.openPicker(token, 'doc')
@ -155,12 +160,7 @@ export default {
openDropbox(token) {
return dropboxHelper.openChooser(token)
.then(paths => this.$store.dispatch('queue/enqueue',
() => {
if (token.fullAccess) {
return dropboxProvider.openFiles(token, paths);
}
return dropboxRestrictedProvider.openFiles(token, paths);
}));
() => dropboxProvider.openFiles(token, paths)));
},
saveGoogleDrive(token) {
return openSyncModal(token, 'googleDriveSync');

View File

@ -1,40 +1,31 @@
<template>
<div class="modal__inner-1 modal__inner-1--google-drive-sync">
<div class="modal__inner-1">
<div class="modal__inner-2">
<div class="modal__image">
<icon-provider provider-id="bloggerPage"></icon-provider>
</div>
<p>This will publish <b>{{currentFileName}}</b> to your <b>Blogger Page</b>.</p>
<div class="form-entry">
<label class="form-entry__label" for="blogUrl">Blog URL</label>
<div class="form-entry__field">
<input id="blogUrl" type="text" class="textfield" v-model="blogUrl" @keyup.enter="resolve()">
</div>
<form-entry label="Blog URL">
<input slot="field" class="textfield" type="text" v-model.trim="blogUrl" @keyup.enter="resolve()">
<div class="form-entry__info">
<b>Example:</b> http://example.blogger.com/
</div>
</div>
<div class="form-entry">
<label class="form-entry__label" for="fileId">Existing page ID (optional)</label>
<div class="form-entry__field">
<input id="fileId" type="text" class="textfield" v-model="pageId" @keyup.enter="resolve()">
</div>
</div>
<div class="form-entry">
<label class="form-entry__label" for="template">Template</label>
<div class="form-entry__field">
<select class="textfield" id="template" v-model="selectedTemplate" @keyup.enter="resolve()">
<option v-for="(template, id) in allTemplates" :key="id" v-bind:value="id">
{{ template.name }}
</option>
</select>
</div>
</form-entry>
<form-entry label="Existing page ID (optional)">
<input slot="field" class="textfield" type="text" v-model.trim="pageId" @keyup.enter="resolve()">
</form-entry>
<form-entry label="Template">
<select slot="field" class="textfield" v-model="selectedTemplate" @keyup.enter="resolve()">
<option v-for="(template, id) in allTemplates" :key="id" :value="id">
{{ template.name }}
</option>
</select>
<div class="form-entry__actions">
<a href="javascript:void(0)" @click="configureTemplates">Configure templates</a>
</div>
</div>
</form-entry>
<div class="modal__tip">
<b>Tip:</b> You can provide a value for <code>title</code> in the <b>file properties</b>.
<b>ProTip:</b> You can provide a value for <code>title</code> in the <a href="javascript:void(0)" @click="openFileProperties">file properties</a>.
</div>
<div class="modal__button-bar">
<button class="button" @click="config.reject()">Cancel</button>
@ -45,51 +36,18 @@
</template>
<script>
import { mapGetters } from 'vuex';
import bloggerPageProvider from '../../services/providers/bloggerPageProvider';
import store from '../../store';
import modalTemplate from './modalTemplate';
const computedLocalSetting = id => ({
get() {
return store.getters['data/localSettings'][id];
},
set(value) {
store.dispatch('data/patchLocalSettings', {
[id]: value,
});
},
});
export default {
export default modalTemplate({
data: () => ({
pageId: '',
}),
computed: {
...mapGetters('modal', [
'config',
]),
currentFileName() {
return this.$store.getters['file/current'].name;
},
...mapGetters('data', [
'allTemplates',
]),
blogUrl: computedLocalSetting('bloggerBlogUrl'),
selectedTemplate: computedLocalSetting('bloggerPublishTemplate'),
computedLocalSettings: {
blogUrl: 'bloggerBlogUrl',
selectedTemplate: 'bloggerPublishTemplate',
},
methods: {
configureTemplates() {
this.$store.dispatch('modal/open', {
type: 'templates',
selectedId: this.selectedTemplate,
})
.then(({ templates, selectedId }) => {
this.$store.dispatch('data/setTemplates', templates);
this.$store.dispatch('data/patchLocalSettings', {
bloggerPublishTemplate: selectedId,
});
});
},
resolve() {
if (this.blogUrl) {
// Return new location
@ -100,5 +58,5 @@ export default {
}
},
},
};
});
</script>

View File

@ -1,41 +1,32 @@
<template>
<div class="modal__inner-1 modal__inner-1--google-drive-sync">
<div class="modal__inner-1">
<div class="modal__inner-2">
<div class="modal__image">
<icon-provider provider-id="blogger"></icon-provider>
</div>
<p>This will publish <b>{{currentFileName}}</b> to your <b>Blogger</b> site.</p>
<div class="form-entry">
<label class="form-entry__label" for="blogUrl">Blog URL</label>
<div class="form-entry__field">
<input id="blogUrl" type="text" class="textfield" v-model="blogUrl" @keyup.enter="resolve()">
</div>
<form-entry label="Blog URL">
<input slot="field" class="textfield" type="text" v-model.trim="blogUrl" @keyup.enter="resolve()">
<div class="form-entry__info">
<b>Example:</b> http://example.blogger.com/
</div>
</div>
<div class="form-entry">
<label class="form-entry__label" for="fileId">Existing post ID (optional)</label>
<div class="form-entry__field">
<input id="fileId" type="text" class="textfield" v-model="postId" @keyup.enter="resolve()">
</div>
</div>
<div class="form-entry">
<label class="form-entry__label" for="template">Template</label>
<div class="form-entry__field">
<select class="textfield" id="template" v-model="selectedTemplate" @keyup.enter="resolve()">
<option v-for="(template, id) in allTemplates" :key="id" v-bind:value="id">
{{ template.name }}
</option>
</select>
</div>
</form-entry>
<form-entry label="Existing post ID (optional)">
<input slot="field" class="textfield" type="text" v-model.trim="postId" @keyup.enter="resolve()">
</form-entry>
<form-entry label="Template">
<select slot="field" class="textfield" v-model="selectedTemplate" @keyup.enter="resolve()">
<option v-for="(template, id) in allTemplates" :key="id" :value="id">
{{ template.name }}
</option>
</select>
<div class="form-entry__actions">
<a href="javascript:void(0)" @click="configureTemplates">Configure templates</a>
</div>
</div>
</form-entry>
<div class="modal__tip">
<b>Tip:</b> You can provide values for <code>title</code>, <code>tags</code>,
<code>status</code> and <code>date</code> in the <b>file properties</b>.
<b>ProTip:</b> You can provide values for <code>title</code>, <code>tags</code>,
<code>status</code> and <code>date</code> in the <a href="javascript:void(0)" @click="openFileProperties">file properties</a>.
</div>
<div class="modal__button-bar">
<button class="button" @click="config.reject()">Cancel</button>
@ -46,51 +37,18 @@
</template>
<script>
import { mapGetters } from 'vuex';
import bloggerProvider from '../../services/providers/bloggerProvider';
import store from '../../store';
import modalTemplate from './modalTemplate';
const computedLocalSetting = id => ({
get() {
return store.getters['data/localSettings'][id];
},
set(value) {
store.dispatch('data/patchLocalSettings', {
[id]: value,
});
},
});
export default {
export default modalTemplate({
data: () => ({
postId: '',
}),
computed: {
...mapGetters('modal', [
'config',
]),
currentFileName() {
return this.$store.getters['file/current'].name;
},
...mapGetters('data', [
'allTemplates',
]),
blogUrl: computedLocalSetting('bloggerBlogUrl'),
selectedTemplate: computedLocalSetting('bloggerPublishTemplate'),
computedLocalSettings: {
blogUrl: 'bloggerBlogUrl',
selectedTemplate: 'bloggerPublishTemplate',
},
methods: {
configureTemplates() {
this.$store.dispatch('modal/open', {
type: 'templates',
selectedId: this.selectedTemplate,
})
.then(({ templates, selectedId }) => {
this.$store.dispatch('data/setTemplates', templates);
this.$store.dispatch('data/patchLocalSettings', {
bloggerPublishTemplate: selectedId,
});
});
},
resolve() {
if (this.blogUrl) {
// Return new location
@ -101,5 +59,5 @@ export default {
}
},
},
};
});
</script>

View File

@ -0,0 +1,34 @@
<template>
<div class="modal__inner-1">
<div class="modal__inner-2">
<div class="modal__image">
<icon-provider provider-id="dropbox"></icon-provider>
</div>
<p>This will link your <b>Dropbox</b> account to your <b>StackEdit</b> workspace.</p>
<div class="form-entry">
<div class="form-entry__checkbox">
<label>
<input type="checkbox" v-model="restrictedAccess"> Restrict access
</label>
<div class="form-entry__info">
If checked, access will be restricted to the <b>/Applications/StackEdit (restricted)</b> folder.
</div>
</div>
</div>
<div class="modal__button-bar">
<button class="button" @click="config.reject()">Cancel</button>
<button class="button" @click="config.resolve()">Ok</button>
</div>
</div>
</div>
</template>
<script>
import modalTemplate from './modalTemplate';
export default modalTemplate({
computedLocalSettings: {
restrictedAccess: 'dropboxRestrictedAccess',
},
});
</script>

View File

@ -1,33 +1,27 @@
<template>
<div class="modal__inner-1 modal__inner-1--google-drive-sync">
<div class="modal__inner-1">
<div class="modal__inner-2">
<div class="modal__image">
<icon-provider provider-id="dropbox"></icon-provider>
</div>
<p>This will publish <b>{{currentFileName}}</b> to your <b>Dropbox</b>.</p>
<div class="form-entry">
<label class="form-entry__label" for="path">File path</label>
<div class="form-entry__field">
<input id="path" type="text" class="textfield" v-model.trim="path" @keyup.enter="resolve()">
</div>
<form-entry label="File path">
<input slot="field" class="textfield" type="text" v-model.trim="path" @keyup.enter="resolve()">
<div class="form-entry__info">
<b>Example:</b> /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.
</div>
</div>
<div class="form-entry">
<label class="form-entry__label" for="template">Template</label>
<div class="form-entry__field">
<select class="textfield" id="template" v-model="selectedTemplate" @keyup.enter="resolve()">
<option v-for="(template, id) in allTemplates" :key="id" v-bind:value="id">
{{ template.name }}
</option>
</select>
</div>
</form-entry>
<form-entry label="Template">
<select slot="field" class="textfield" v-model="selectedTemplate" @keyup.enter="resolve()">
<option v-for="(template, id) in allTemplates" :key="id" :value="id">
{{ template.name }}
</option>
</select>
<div class="form-entry__actions">
<a href="javascript:void(0)" @click="configureTemplates">Configure templates</a>
</div>
</div>
</form-entry>
<div class="modal__button-bar">
<button class="button" @click="config.reject()">Cancel</button>
<button class="button" @click="resolve()">Ok</button>
@ -37,64 +31,28 @@
</template>
<script>
import { mapGetters } from 'vuex';
import dropboxProvider from '../../services/providers/dropboxProvider';
import dropboxRestrictedProvider from '../../services/providers/dropboxRestrictedProvider';
import store from '../../store';
import modalTemplate from './modalTemplate';
const computedLocalSetting = id => ({
get() {
return store.getters['data/localSettings'][id];
},
set(value) {
store.dispatch('data/patchLocalSettings', {
[id]: value,
});
},
});
export default {
export default modalTemplate({
data: () => ({
path: '',
}),
computed: {
...mapGetters('modal', [
'config',
]),
currentFileName() {
return this.$store.getters['file/current'].name;
},
...mapGetters('data', [
'allTemplates',
]),
selectedTemplate: computedLocalSetting('dropboxPublishTemplate'),
computedLocalSettings: {
selectedTemplate: 'dropboxPublishTemplate',
},
created() {
this.path = `/${this.currentFileName}.html`;
},
methods: {
configureTemplates() {
this.$store.dispatch('modal/open', {
type: 'templates',
selectedId: this.selectedTemplate,
})
.then(({ templates, selectedId }) => {
this.$store.dispatch('data/setTemplates', templates);
this.$store.dispatch('data/patchLocalSettings', {
dropboxPublishTemplate: selectedId,
});
});
},
resolve() {
if (dropboxProvider.checkPath(this.path)) {
// Return new location
const location = this.config.token.fullAccess
? dropboxProvider.makeLocation(this.config.token, this.path)
: dropboxRestrictedProvider.makeLocation(this.config.token, this.path);
const location = dropboxProvider.makeLocation(this.config.token, this.path);
location.templateId = this.selectedTemplate;
this.config.resolve(location);
}
},
},
};
});
</script>

View File

@ -1,20 +1,17 @@
<template>
<div class="modal__inner-1 modal__inner-1--google-drive-sync">
<div class="modal__inner-1">
<div class="modal__inner-2">
<div class="modal__image">
<icon-provider provider-id="dropbox"></icon-provider>
</div>
<p>This will save <b>{{currentFileName}}</b> to your <b>Dropbox</b> and keep it synchronized.</p>
<div class="form-entry">
<label class="form-entry__label" for="path">File path</label>
<div class="form-entry__field">
<input id="path" type="text" class="textfield" v-model.trim="path" @keyup.enter="resolve()">
</div>
<form-entry label="File path">
<input slot="field" class="textfield" type="text" v-model.trim="path" @keyup.enter="resolve()">
<div class="form-entry__info">
<b>Example:</b> /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.
</div>
</div>
</form-entry>
<div class="modal__button-bar">
<button class="button" @click="config.reject()">Cancel</button>
<button class="button" @click="resolve()">Ok</button>
@ -24,22 +21,13 @@
</template>
<script>
import { mapGetters } from 'vuex';
import dropboxProvider from '../../services/providers/dropboxProvider';
import dropboxRestrictedProvider from '../../services/providers/dropboxRestrictedProvider';
import modalTemplate from './modalTemplate';
export default {
export default modalTemplate({
data: () => ({
path: '',
}),
computed: {
...mapGetters('modal', [
'config',
]),
currentFileName() {
return this.$store.getters['file/current'].name;
},
},
created() {
this.path = `/${this.currentFileName}.md`;
},
@ -47,12 +35,10 @@ export default {
resolve() {
if (dropboxProvider.checkPath(this.path)) {
// Return new location
const location = this.config.token.fullAccess
? dropboxProvider.makeLocation(this.config.token, this.path)
: dropboxRestrictedProvider.makeLocation(this.config.token, this.path);
const location = dropboxProvider.makeLocation(this.config.token, this.path);
this.config.resolve(location);
}
},
},
};
});
</script>

View File

@ -19,7 +19,7 @@
<div class="modal__error modal__error--file-properties">{{error}}</div>
<div class="modal__button-bar">
<button class="button" @click="config.reject()">Cancel</button>
<button class="button" @click="!error && config.resolve(strippedCustomProperties)">Ok</button>
<button class="button" @click="resolve()">Ok</button>
</div>
</div>
</div>
@ -38,6 +38,7 @@ export default {
CodeEditor,
},
data: () => ({
contentId: null,
tab: 'custom',
defaultProperties,
customProperties: null,
@ -52,7 +53,9 @@ export default {
},
},
created() {
const properties = this.$store.getters['content/current'].properties;
const content = this.$store.getters['content/current'];
this.contentId = content.id;
const properties = content.properties;
this.setCustomProperties(properties === '\n' ? emptyProperties : properties);
},
methods: {
@ -65,6 +68,15 @@ export default {
this.error = e.message;
}
},
resolve() {
if (!this.error) {
this.$store.commit('content/patchItem', {
id: this.contentId,
properties: this.strippedCustomProperties,
});
this.config.resolve();
}
},
},
};
</script>

View File

@ -0,0 +1,26 @@
<template>
<div class="form-entry">
<label class="form-entry__label" :for="uid">{{label}}</label>
<div class="form-entry__field">
<slot name="field"></slot>
</div>
<slot></slot>
</div>
</template>
<script>
import utils from '../../services/utils';
export default {
props: ['label'],
data: () => ({
uid: utils.uid(),
}),
mounted() {
this.$el.querySelector('input,select').id = this.uid;
},
};
</script>
<style lang="scss">
</style>

View File

@ -1,16 +1,13 @@
<template>
<div class="modal__inner-1 modal__inner-1--google-drive-sync">
<div class="modal__inner-1">
<div class="modal__inner-2">
<div class="modal__image">
<icon-provider provider-id="gist"></icon-provider>
</div>
<p>This will publish <b>{{currentFileName}}</b> to a <b>Gist</b>.</p>
<div class="form-entry">
<label class="form-entry__label" for="filename">Filename</label>
<div class="form-entry__field">
<input id="filename" type="text" class="textfield" v-model.trim="filename" @keyup.enter="resolve()">
</div>
</div>
<form-entry label="Filename">
<input slot="field" class="textfield" type="text" v-model.trim="filename" @keyup.enter="resolve()">
</form-entry>
<div class="form-entry">
<div class="form-entry__checkbox">
<label>
@ -18,30 +15,24 @@
</label>
</div>
</div>
<div class="form-entry">
<label class="form-entry__label" for="gistId">Gist ID (optional)</label>
<div class="form-entry__field">
<input id="gistId" type="text" class="textfield" v-model.trim="gistId" @keyup.enter="resolve()">
</div>
<form-entry label="Existing Gist ID (optional)">
<input slot="field" class="textfield" type="text" v-model.trim="gistId" @keyup.enter="resolve()">
<div class="form-entry__info">
If the file exists in the provided Gist, it will be replaced.
</div>
</div>
<div class="form-entry">
<label class="form-entry__label" for="template">Template</label>
<div class="form-entry__field">
<select class="textfield" id="template" v-model="selectedTemplate" @keyup.enter="resolve()">
<option v-for="(template, id) in allTemplates" :key="id" v-bind:value="id">
{{ template.name }}
</option>
</select>
If the file exists in the Gist, it will be replaced.
</div>
</form-entry>
<form-entry label="Template">
<select slot="field" class="textfield" v-model="selectedTemplate" @keyup.enter="resolve()">
<option v-for="(template, id) in allTemplates" :key="id" :value="id">
{{ template.name }}
</option>
</select>
<div class="form-entry__actions">
<a href="javascript:void(0)" @click="configureTemplates">Configure templates</a>
</div>
</div>
</form-entry>
<div class="modal__tip">
<b>Tip:</b> You can provide a value for <code>title</code> in the <b>file properties</b>.
<b>ProTip:</b> You can provide a value for <code>title</code> in the <a href="javascript:void(0)" @click="openFileProperties">file properties</a>.
</div>
<div class="modal__button-bar">
<button class="button" @click="config.reject()">Cancel</button>
@ -52,55 +43,22 @@
</template>
<script>
import { mapGetters } from 'vuex';
import gistProvider from '../../services/providers/gistProvider';
import store from '../../store';
import modalTemplate from './modalTemplate';
const computedLocalSetting = id => ({
get() {
return store.getters['data/localSettings'][id];
},
set(value) {
store.dispatch('data/patchLocalSettings', {
[id]: value,
});
},
});
export default {
export default modalTemplate({
data: () => ({
filename: '',
gistId: '',
}),
computed: {
...mapGetters('modal', [
'config',
]),
currentFileName() {
return this.$store.getters['file/current'].name;
},
...mapGetters('data', [
'allTemplates',
]),
isPublic: computedLocalSetting('gistIsPublic'),
selectedTemplate: computedLocalSetting('gistPublishTemplate'),
computedLocalSettings: {
isPublic: 'gistIsPublic',
selectedTemplate: 'gistPublishTemplate',
},
created() {
this.filename = `${this.currentFileName}.md`;
},
methods: {
configureTemplates() {
this.$store.dispatch('modal/open', {
type: 'templates',
selectedId: this.selectedTemplate,
})
.then(({ templates, selectedId }) => {
this.$store.dispatch('data/setTemplates', templates);
this.$store.dispatch('data/patchLocalSettings', {
gistPublishTemplate: selectedId,
});
});
},
resolve() {
if (this.filename) {
// Return new location
@ -111,5 +69,5 @@ export default {
}
},
},
};
});
</script>

View File

@ -1,16 +1,13 @@
<template>
<div class="modal__inner-1 modal__inner-1--google-drive-sync">
<div class="modal__inner-1">
<div class="modal__inner-2">
<div class="modal__image">
<icon-provider provider-id="gist"></icon-provider>
</div>
<p>This will save <b>{{currentFileName}}</b> to a <b>Gist</b> repository and keep it synchronized.</p>
<div class="form-entry">
<label class="form-entry__label" for="filename">Filename</label>
<div class="form-entry__field">
<input id="filename" type="text" class="textfield" v-model.trim="filename" @keyup.enter="resolve()">
</div>
</div>
<p>This will save <b>{{currentFileName}}</b> to a <b>Gist</b> and keep it synchronized.</p>
<form-entry label="Filename">
<input slot="field" class="textfield" type="text" v-model.trim="filename" @keyup.enter="resolve()">
</form-entry>
<div class="form-entry">
<div class="form-entry__checkbox">
<label>
@ -18,15 +15,12 @@
</label>
</div>
</div>
<div class="form-entry">
<label class="form-entry__label" for="gistId">Gist ID (optional)</label>
<div class="form-entry__field">
<input id="gistId" type="text" class="textfield" v-model.trim="gistId" @keyup.enter="resolve()">
</div>
<form-entry label="Existing Gist ID (optional)">
<input slot="field" class="textfield" type="text" v-model.trim="gistId" @keyup.enter="resolve()">
<div class="form-entry__info">
If the file exists in the provided Gist, it will be replaced.
If the file exists in the Gist, it will be replaced.
</div>
</div>
</form-entry>
<div class="modal__button-bar">
<button class="button" @click="config.reject()">Cancel</button>
<button class="button" @click="resolve()">Ok</button>
@ -36,34 +30,16 @@
</template>
<script>
import { mapGetters } from 'vuex';
import gistProvider from '../../services/providers/gistProvider';
import store from '../../store';
import modalTemplate from './modalTemplate';
const computedLocalSetting = id => ({
get() {
return store.getters['data/localSettings'][id];
},
set(value) {
store.dispatch('data/patchLocalSettings', {
[id]: value,
});
},
});
export default {
export default modalTemplate({
data: () => ({
filename: '',
gistId: '',
}),
computed: {
...mapGetters('modal', [
'config',
]),
currentFileName() {
return this.$store.getters['file/current'].name;
},
isPublic: computedLocalSetting('gistIsPublic'),
computedLocalSettings: {
isPublic: 'gistIsPublic',
},
created() {
this.filename = `${this.currentFileName}.md`;
@ -78,5 +54,5 @@ export default {
}
},
},
};
});
</script>

View File

@ -0,0 +1,31 @@
<template>
<div class="modal__inner-1">
<div class="modal__inner-2">
<div class="modal__image">
<icon-provider provider-id="github"></icon-provider>
</div>
<p>This will link your <b>Github</b> account to your <b>StackEdit</b> workspace.</p>
<div class="form-entry">
<div class="form-entry__checkbox">
<label>
<input type="checkbox" v-model="repoFullAccess"> Request access to private repositories
</label>
</div>
</div>
<div class="modal__button-bar">
<button class="button" @click="config.reject()">Cancel</button>
<button class="button" @click="config.resolve()">Ok</button>
</div>
</div>
</div>
</template>
<script>
import modalTemplate from './modalTemplate';
export default modalTemplate({
computedLocalSettings: {
repoFullAccess: 'githubRepoFullAccess',
},
});
</script>

View File

@ -1,51 +1,39 @@
<template>
<div class="modal__inner-1 modal__inner-1--google-drive-sync">
<div class="modal__inner-1">
<div class="modal__inner-2">
<div class="modal__image">
<icon-provider provider-id="github"></icon-provider>
</div>
<p>This will publish <b>{{currentFileName}}</b> to your <b>GitHub</b> repository.</p>
<div class="form-entry">
<label class="form-entry__label" for="repo">Repository URL</label>
<div class="form-entry__field">
<input id="repo" type="text" class="textfield" v-model.trim="repoUrl" @keyup.enter="resolve()">
</div>
<form-entry label="Repository URL">
<input slot="field" class="textfield" type="text" v-model.trim="repoUrl" @keyup.enter="resolve()">
<div class="form-entry__info">
<b>Example:</b> https://github.com/benweet/stackedit
</div>
</div>
<div class="form-entry">
<label class="form-entry__label" for="branch">Branch (optional)</label>
<div class="form-entry__field">
<input id="branch" type="text" class="textfield" v-model.trim="branch" @keyup.enter="resolve()">
</div>
</form-entry>
<form-entry label="Branch (optional)">
<input slot="field" class="textfield" type="text" v-model.trim="branch" @keyup.enter="resolve()">
<div class="form-entry__info">
If not provided, the master branch will be used.
</div>
</div>
<div class="form-entry">
<label class="form-entry__label" for="path">File path</label>
<div class="form-entry__field">
<input id="path" type="text" class="textfield" v-model.trim="path" @keyup.enter="resolve()">
</div>
</form-entry>
<form-entry label="File path">
<input slot="field" class="textfield" type="text" v-model.trim="path" @keyup.enter="resolve()">
<div class="form-entry__info">
<b>Example:</b> docs/README.md<br>
If the file exists, it will be replaced.
</div>
</div>
<div class="form-entry">
<label class="form-entry__label" for="template">Template</label>
<div class="form-entry__field">
<select class="textfield" id="template" v-model="selectedTemplate" @keyup.enter="resolve()">
<option v-for="(template, id) in allTemplates" :key="id" v-bind:value="id">
{{ template.name }}
</option>
</select>
</div>
</form-entry>
<form-entry label="Template">
<select slot="field" class="textfield" v-model="selectedTemplate" @keyup.enter="resolve()">
<option v-for="(template, id) in allTemplates" :key="id" :value="id">
{{ template.name }}
</option>
</select>
<div class="form-entry__actions">
<a href="javascript:void(0)" @click="configureTemplates">Configure templates</a>
</div>
</div>
</form-entry>
<div class="modal__button-bar">
<button class="button" @click="config.reject()">Cancel</button>
<button class="button" @click="resolve()">Ok</button>
@ -55,55 +43,22 @@
</template>
<script>
import { mapGetters } from 'vuex';
import githubProvider from '../../services/providers/githubProvider';
import store from '../../store';
import modalTemplate from './modalTemplate';
const computedLocalSetting = id => ({
get() {
return store.getters['data/localSettings'][id];
},
set(value) {
store.dispatch('data/patchLocalSettings', {
[id]: value,
});
},
});
export default {
export default modalTemplate({
data: () => ({
branch: '',
path: '',
}),
computed: {
...mapGetters('modal', [
'config',
]),
currentFileName() {
return this.$store.getters['file/current'].name;
},
...mapGetters('data', [
'allTemplates',
]),
repoUrl: computedLocalSetting('githubRepoUrl'),
selectedTemplate: computedLocalSetting('githubPublishTemplate'),
computedLocalSettings: {
repoUrl: 'githubRepoUrl',
selectedTemplate: 'githubPublishTemplate',
},
created() {
this.path = `${this.currentFileName}.md`;
},
methods: {
configureTemplates() {
this.$store.dispatch('modal/open', {
type: 'templates',
selectedId: this.selectedTemplate,
})
.then(({ templates, selectedId }) => {
this.$store.dispatch('data/setTemplates', templates);
this.$store.dispatch('data/patchLocalSettings', {
githubPublishTemplate: selectedId,
});
});
},
resolve() {
if (this.repoUrl && this.path) {
const parsedRepo = this.repoUrl.match(/[/:]?([^/:]+)\/([^/]+?)(?:\.git)?$/);
@ -117,5 +72,5 @@ export default {
}
},
},
};
});
</script>

View File

@ -1,38 +1,29 @@
<template>
<div class="modal__inner-1 modal__inner-1--google-drive-sync">
<div class="modal__inner-1">
<div class="modal__inner-2">
<div class="modal__image">
<icon-provider provider-id="github"></icon-provider>
</div>
<p>This will save <b>{{currentFileName}}</b> to your <b>GitHub</b> repository and keep it synchronized.</p>
<div class="form-entry">
<label class="form-entry__label" for="repo">Repository URL</label>
<div class="form-entry__field">
<input id="repo" type="text" class="textfield" v-model.trim="repoUrl" @keyup.enter="resolve()">
</div>
<form-entry label="Repository URL">
<input slot="field" class="textfield" type="text" v-model.trim="repoUrl" @keyup.enter="resolve()">
<div class="form-entry__info">
<b>Example:</b> https://github.com/benweet/stackedit
</div>
</div>
<div class="form-entry">
<label class="form-entry__label" for="branch">Branch (optional)</label>
<div class="form-entry__field">
<input id="branch" type="text" class="textfield" v-model.trim="branch" @keyup.enter="resolve()">
</div>
</form-entry>
<form-entry label="Branch (optional)">
<input slot="field" class="textfield" type="text" v-model.trim="branch" @keyup.enter="resolve()">
<div class="form-entry__info">
If not provided, the master branch will be used.
</div>
</div>
<div class="form-entry">
<label class="form-entry__label" for="path">File path</label>
<div class="form-entry__field">
<input id="path" type="text" class="textfield" v-model.trim="path" @keyup.enter="resolve()">
</div>
</form-entry>
<form-entry label="File path">
<input slot="field" class="textfield" type="text" v-model.trim="path" @keyup.enter="resolve()">
<div class="form-entry__info">
<b>Example:</b> docs/README.md<br>
If the file exists, it will be replaced.
</div>
</div>
</form-entry>
<div class="modal__button-bar">
<button class="button" @click="config.reject()">Cancel</button>
<button class="button" @click="resolve()">Ok</button>
@ -42,31 +33,16 @@
</template>
<script>
import { mapGetters } from 'vuex';
import githubProvider from '../../services/providers/githubProvider';
import modalTemplate from './modalTemplate';
export default {
export default modalTemplate({
data: () => ({
branch: '',
path: '',
}),
computed: {
...mapGetters('modal', [
'config',
]),
currentFileName() {
return this.$store.getters['file/current'].name;
},
repoUrl: {
get() {
return this.$store.getters['data/localSettings'].githubRepoUrl;
},
set(value) {
this.$store.dispatch('data/patchLocalSettings', {
githubRepoUrl: value,
});
},
},
computedLocalSettings: {
repoUrl: 'githubRepoUrl',
},
created() {
this.path = `${this.currentFileName}.md`;
@ -84,5 +60,5 @@ export default {
}
},
},
};
});
</script>

View File

@ -1,19 +1,25 @@
<template>
<div class="modal__inner-1 modal__inner-1--google-drive-sync">
<div class="modal__inner-1">
<div class="modal__inner-2">
<div class="modal__image">
<icon-provider provider-id="googleDrive"></icon-provider>
</div>
<p>This will publish <b>{{currentFileName}}</b> to your <b>Google Drive</b> account.</p>
<div class="form-entry">
<label class="form-entry__label" for="fileId">File ID (optional)</label>
<div class="form-entry__field">
<input id="fileId" type="text" class="textfield" v-model="fileId" @keyup.enter="resolve()">
</div>
<form-entry label="Folder ID (optional)">
<input slot="field" class="textfield" type="text" v-model.trim="folderId" @keyup.enter="resolve()">
<div class="form-entry__info">
If no file ID is supplied, a new file will be created in your Google Drive root folder.
If no folder ID is supplied, the file will be created in your root folder.
</div>
</div>
<div class="form-entry__actions">
<a href="javascript:void(0)" @click="openFolder">Choose folder</a>
</div>
</form-entry>
<form-entry label="Existing file ID (optional)">
<input slot="field" class="textfield" type="text" v-model.trim="fileId" @keyup.enter="resolve()">
<div class="form-entry__info">
This will overwrite the file on the server.
</div>
</form-entry>
<div class="form-entry">
<div class="form-entry__radio">
<label>
@ -26,21 +32,18 @@
</label>
</div>
</div>
<div class="form-entry">
<label class="form-entry__label" for="template">Template</label>
<div class="form-entry__field">
<select class="textfield" id="template" v-model="selectedTemplate" :disabled="format === 'markdown'" @keyup.enter="resolve()">
<option v-for="(template, id) in allTemplates" :key="id" v-bind:value="id">
{{ template.name }}
</option>
</select>
</div>
<form-entry label="Template">
<select slot="field" class="textfield" v-model="selectedTemplate" @keyup.enter="resolve()">
<option v-for="(template, id) in allTemplates" :key="id" :value="id">
{{ template.name }}
</option>
</select>
<div class="form-entry__actions">
<a href="javascript:void(0)" @click="configureTemplates">Configure templates</a>
</div>
</div>
</form-entry>
<div class="modal__tip">
<b>Tip:</b> You can provide a value for <code>title</code> in the <b>file properties</b>.
<b>ProTip:</b> You can provide a value for <code>title</code> in the <a href="javascript:void(0)" @click="openFileProperties">file properties</a>.
</div>
<div class="modal__button-bar">
<button class="button" @click="config.reject()">Cancel</button>
@ -51,52 +54,20 @@
</template>
<script>
import { mapGetters } from 'vuex';
import googleHelper from '../../services/providers/helpers/googleHelper';
import googleDriveProvider from '../../services/providers/googleDriveProvider';
import store from '../../store';
import modalTemplate from './modalTemplate';
const computedLocalSetting = id => ({
get() {
return store.getters['data/localSettings'][id];
},
set(value) {
store.dispatch('data/patchLocalSettings', {
[id]: value,
});
},
});
export default {
export default modalTemplate({
data: () => ({
fileId: '',
}),
computed: {
...mapGetters('modal', [
'config',
]),
currentFileName() {
return this.$store.getters['file/current'].name;
},
...mapGetters('data', [
'allTemplates',
]),
selectedTemplate: computedLocalSetting('googleDrivePublishTemplate'),
format: computedLocalSetting('googleDrivePublishFormat'),
computedLocalSettings: {
folderId: 'googleDriveFolderId',
selectedTemplate: 'googleDrivePublishTemplate',
format: 'googleDrivePublishFormat',
},
methods: {
configureTemplates() {
this.$store.dispatch('modal/open', {
type: 'templates',
selectedId: this.selectedTemplate,
})
.then(({ templates, selectedId }) => {
this.$store.dispatch('data/setTemplates', templates);
this.$store.dispatch('data/patchLocalSettings', {
googleDrivePublishTemplate: selectedId,
});
});
},
openFolder() {
return this.$store.dispatch(
'modal/hideUntil',
@ -117,5 +88,5 @@ export default {
this.config.resolve(location);
},
},
};
});
</script>

View File

@ -1,34 +1,25 @@
<template>
<div class="modal__inner-1 modal__inner-1--google-drive-sync">
<div class="modal__inner-1">
<div class="modal__inner-2">
<div class="modal__image">
<icon-provider provider-id="googleDrive"></icon-provider>
</div>
<p>This will save <b>{{currentFileName}}</b> to your <b>Google Drive</b> account and keep it synchronized.</p>
<a href="javascript:void(0)" v-if="!showOptions" @click="showOptions = true">See options </a>
<div v-else>
<div class="form-entry">
<label class="form-entry__label" for="folderId">Folder ID (optional)</label>
<div class="form-entry__field">
<input id="folderId" type="text" class="textfield" v-model.trim="folderId" @keyup.enter="resolve()">
</div>
<div class="form-entry__info">
If no folder ID is supplied, the file will be created in your root folder.
</div>
<div class="form-entry__actions">
<a href="javascript:void(0)" @click="openFolder">Choose folder</a>
</div>
<form-entry label="Folder ID (optional)">
<input slot="field" class="textfield" type="text" v-model.trim="folderId" @keyup.enter="resolve()">
<div class="form-entry__info">
If no folder ID is supplied, the file will be created in your root folder.
</div>
<div class="form-entry">
<label class="form-entry__label" for="fileId">File ID (optional)</label>
<div class="form-entry__field">
<input id="fileId" type="text" class="textfield" v-model="fileId" @keyup.enter="resolve()">
</div>
<div class="form-entry__info">
This will overwrite the existing file on the server.
</div>
<div class="form-entry__actions">
<a href="javascript:void(0)" @click="openFolder">Choose folder</a>
</div>
</div>
</form-entry>
<form-entry label="Existing file ID (optional)">
<input slot="field" class="textfield" type="text" v-model.trim="fileId" @keyup.enter="resolve()">
<div class="form-entry__info">
This will overwrite the file on the server.
</div>
</form-entry>
<div class="modal__button-bar">
<button class="button" @click="config.reject()">Cancel</button>
<button class="button" @click="resolve()">Ok</button>
@ -38,35 +29,16 @@
</template>
<script>
import { mapGetters } from 'vuex';
import googleHelper from '../../services/providers/helpers/googleHelper';
import googleDriveProvider from '../../services/providers/googleDriveProvider';
import modalTemplate from './modalTemplate';
export default {
export default modalTemplate({
data: () => ({
showOptions: false,
fileId: '',
}),
computed: {
...mapGetters('modal', [
'config',
]),
currentFileName() {
return this.$store.getters['file/current'].name;
},
folderId: {
get() {
return this.$store.getters['data/localSettings'].googleDriveFolderId;
},
set(value) {
this.$store.dispatch('data/patchLocalSettings', {
googleDriveFolderId: value,
});
},
},
},
created() {
this.showOptions = this.folderId || this.fileId;
computedLocalSettings: {
folderId: 'googleDriveFolderId',
},
methods: {
openFolder() {
@ -86,5 +58,5 @@ export default {
this.config.resolve(location);
},
},
};
});
</script>

View File

@ -2,18 +2,12 @@
<div class="modal__inner-1 modal__inner-1--google-photo">
<div class="modal__inner-2">
<div class="google-photo__tumbnail" :style="{'background-image': thumbnailUrl}"></div>
<div class="form-entry">
<label class="form-entry__label" for="title">Title (optional)</label>
<div class="form-entry__field">
<input id="title" type="text" class="textfield" v-model.trim="title" @keyup.enter="resolve()">
</div>
</div>
<div class="form-entry">
<label class="form-entry__label" for="size">Size limit (optional)</label>
<div class="form-entry__field">
<input id="size" type="text" class="textfield" v-model="size" @keyup.enter="resolve()">
</div>
</div>
<form-entry label="Title (optional)">
<input slot="field" class="textfield" type="text" v-model.trim="title" @keyup.enter="resolve()">
</form-entry>
<form-entry label="Size limit (optional)">
<input slot="field" class="textfield" type="text" v-model.trim="size" @keyup.enter="resolve()">
</form-entry>
<div class="modal__button-bar">
<button class="button" @click="reject()">Cancel</button>
<button class="button" @click="resolve()">Ok</button>
@ -24,10 +18,14 @@
<script>
import { mapGetters } from 'vuex';
import FormEntry from './FormEntry';
const makeThumbnail = (url, size) => `${url}=s${size}`;
export default {
components: {
FormEntry,
},
data: () => ({
title: '',
size: '',

View File

@ -1,19 +1,16 @@
<template>
<div class="modal__inner-1 modal__inner-1--html-export">
<div class="modal__inner-1">
<div class="modal__inner-2">
<div class="form-entry">
<label class="form-entry__label" for="template">Template</label>
<div class="form-entry__field">
<select class="textfield" id="template" v-model="selectedTemplate" @keyup.enter="resolve()">
<option v-for="(template, id) in allTemplates" :key="id" v-bind:value="id">
{{ template.name }}
</option>
</select>
</div>
<form-entry label="Template">
<select slot="field" class="textfield" v-model="selectedTemplate" @keyup.enter="resolve()">
<option v-for="(template, id) in allTemplates" :key="id" :value="id">
{{ template.name }}
</option>
</select>
<div class="form-entry__actions">
<a href="javascript:void(0)" @click="configureTemplates">Configure templates</a>
</div>
</div>
</form-entry>
<div class="modal__button-bar">
<button class="button button--copy">Copy to clipboard</button>
<button class="button" @click="config.reject()">Cancel</button>
@ -24,31 +21,16 @@
</template>
<script>
import { mapGetters } from 'vuex';
import Clipboard from 'clipboard';
import exportSvc from '../../services/exportSvc';
import modalTemplate from './modalTemplate';
export default {
export default modalTemplate({
data: () => ({
result: '',
}),
computed: {
...mapGetters('modal', [
'config',
]),
...mapGetters('data', [
'allTemplates',
]),
selectedTemplate: {
get() {
return this.$store.getters['data/localSettings'].htmlExportTemplate;
},
set(value) {
this.$store.dispatch('data/patchLocalSettings', {
htmlExportTemplate: value,
});
},
},
computedLocalSettings: {
selectedTemplate: 'htmlExportTemplate',
},
mounted() {
this.$watch('selectedTemplate', (selectedTemplate) => {
@ -68,23 +50,11 @@ export default {
this.clipboard.destroy();
},
methods: {
configureTemplates() {
this.$store.dispatch('modal/open', {
type: 'templates',
selectedId: this.selectedTemplate,
})
.then(({ templates, selectedId }) => {
this.$store.dispatch('data/setTemplates', templates);
this.$store.dispatch('data/patchLocalSettings', {
htmlExportTemplate: selectedId,
});
});
},
resolve() {
const currentFile = this.$store.getters['file/current'];
exportSvc.exportToDisk(currentFile.id, 'html', this.allTemplates[this.selectedTemplate]);
this.config.resolve();
},
},
};
});
</script>

View File

@ -1,13 +1,10 @@
<template>
<div class="modal__inner-1 modal__inner-1--image">
<div class="modal__inner-1">
<div class="modal__inner-2">
<p>Please provide a <b>URL</b> for your image.
<div class="form-entry">
<label class="form-entry__label" for="url">URL</label>
<div class="form-entry__field">
<input id="url" type="text" class="textfield" v-model="url" @keyup.enter="resolve()">
</div>
</div>
<form-entry label="URL">
<input slot="field" class="textfield" type="text" v-model.trim="url" @keyup.enter="resolve()">
</form-entry>
<menu-entry @click.native="openGooglePhotos(token)" v-for="token in googlePhotosTokens" :key="token.sub">
<icon-provider slot="icon" provider-id="googlePhotos"></icon-provider>
<div>Open from Google Photos</div>
@ -28,10 +25,12 @@
<script>
import { mapGetters } from 'vuex';
import MenuEntry from '../menus/MenuEntry';
import FormEntry from './FormEntry';
import googleHelper from '../../services/providers/helpers/googleHelper';
export default {
components: {
FormEntry,
MenuEntry,
},
data: () => ({

View File

@ -1,13 +1,10 @@
<template>
<div class="modal__inner-1 modal__inner-1--link" @keyup.enter="resolve()">
<div class="modal__inner-1">
<div class="modal__inner-2">
<p>Please provide a <b>URL</b> for your link.
<div class="form-entry">
<label class="form-entry__label" for="url">URL</label>
<div class="form-entry__field">
<input id="url" type="text" class="textfield" v-model="url">
</div>
</div>
<form-entry label="URL">
<input slot="field" class="textfield" type="text" v-model.trim="url" @keyup.enter="resolve()">
</form-entry>
<div class="modal__button-bar">
<button class="button" @click="reject()">Cancel</button>
<button class="button" @click="resolve()">Ok</button>
@ -18,8 +15,12 @@
<script>
import { mapGetters } from 'vuex';
import FormEntry from './FormEntry';
export default {
components: {
FormEntry,
},
data: () => ({
url: '',
}),

View File

@ -6,7 +6,7 @@
<div class="form-entry__field">
<input v-if="isEditing" id="template" type="text" class="textfield" v-focus @blur="submitEdit()" @keyup.enter="submitEdit()" @keyup.esc.stop="submitEdit(true)" v-model="editingName">
<select v-else id="template" v-model="selectedId" class="textfield">
<option v-for="(template, id) in templates" :key="id" v-bind:value="id">
<option v-for="(template, id) in templates" :key="id" :value="id">
{{ template.name }}
</option>
</select>

View File

@ -0,0 +1,65 @@
<template>
<div class="modal__inner-1">
<div class="modal__inner-2">
<div class="modal__image">
<icon-provider provider-id="wordpress"></icon-provider>
</div>
<p>This will publish <b>{{currentFileName}}</b> to your <b>WordPress</b> site.</p>
<form-entry label="Site domain">
<input slot="field" class="textfield" type="text" v-model.trim="domain" @keyup.enter="resolve()">
<div class="form-entry__info">
<b>Example:</b> example.wordpress.com<br>
<b>Jetpack plugin</b> is required for self-hosted sites.
</div>
</form-entry>
<form-entry label="Existing post ID (optional)">
<input slot="field" class="textfield" type="text" v-model.trim="postId" @keyup.enter="resolve()">
</form-entry>
<form-entry label="Template">
<select slot="field" class="textfield" v-model="selectedTemplate" @keyup.enter="resolve()">
<option v-for="(template, id) in allTemplates" :key="id" :value="id">
{{ template.name }}
</option>
</select>
<div class="form-entry__actions">
<a href="javascript:void(0)" @click="configureTemplates">Configure templates</a>
</div>
</form-entry>
<div class="modal__tip">
<b>ProTip:</b> You can provide values for <code>title</code>, <code>tags</code>,
<code>categories</code>, <code>excerpt</code>, <code>author</code>, <code>featuredImage</code>,
<code>status</code> and <code>date</code> in the <a href="javascript:void(0)" @click="openFileProperties">file properties</a>.
</div>
<div class="modal__button-bar">
<button class="button" @click="config.reject()">Cancel</button>
<button class="button" @click="resolve()">Ok</button>
</div>
</div>
</div>
</template>
<script>
import wordpressProvider from '../../services/providers/wordpressProvider';
import modalTemplate from './modalTemplate';
export default modalTemplate({
data: () => ({
postId: '',
}),
computedLocalSettings: {
domain: 'wordpressDomain',
selectedTemplate: 'wordpressPublishTemplate',
},
methods: {
resolve() {
if (this.domain) {
// Return new location
const location = wordpressProvider.makeLocation(
this.config.token, this.domain, this.postId);
location.templateId = this.selectedTemplate;
this.config.resolve(location);
}
},
},
});
</script>

View File

@ -0,0 +1,55 @@
<template>
<div class="modal__inner-1">
<div class="modal__inner-2">
<div class="modal__image">
<icon-provider provider-id="zendesk"></icon-provider>
</div>
<p>This will link your <b>Zendesk</b> account to your <b>StackEdit</b> workspace.</p>
<form-entry label="Site URL">
<input slot="field" class="textfield" type="text" v-model.trim="siteUrl" @keyup.enter="resolve()">
<div class="form-entry__info">
<b>Example:</b> https://example.zendesk.com/
</div>
</form-entry>
<form-entry label="Client Unique Identifier">
<input slot="field" class="textfield" type="text" v-model.trim="clientId" @keyup.enter="resolve()">
<div class="form-entry__info">
You have to configure an OAuth Client with redirect URL <b>{{redirectUrl}}</b><br>
<a href="https://support.zendesk.com/hc/en-us/articles/203663836" target="_blank"><b>More info</b></a>
</div>
</form-entry>
<div class="modal__button-bar">
<button class="button" @click="config.reject()">Cancel</button>
<button class="button" @click="resolve()">Ok</button>
</div>
</div>
</div>
</template>
<script>
import modalTemplate from './modalTemplate';
import utils from '../../services/utils';
export default modalTemplate({
data: () => ({
redirectUrl: utils.oauth2RedirectUri,
}),
computedLocalSettings: {
siteUrl: 'zendeskSiteUrl',
clientId: 'zendeskClientId',
},
methods: {
resolve() {
if (this.siteUrl && this.clientId) {
const parsedUrl = this.siteUrl.match(/^https:\/\/([^.]+)\.zendesk\.com/);
if (parsedUrl) {
this.config.resolve({
subdomain: parsedUrl[1],
clientId: this.clientId,
});
}
}
},
},
});
</script>

View File

@ -0,0 +1,70 @@
<template>
<div class="modal__inner-1">
<div class="modal__inner-2">
<div class="modal__image">
<icon-provider provider-id="zendesk"></icon-provider>
</div>
<p>This will publish <b>{{currentFileName}}</b> to your <b>Zendesk Help Center</b>.</p>
<form-entry label="Section ID">
<input slot="field" class="textfield" type="text" v-model.trim="sectionId" @keyup.enter="resolve()">
<div class="form-entry__info">
https://example.zendesk.com/hc/en-us/sections/<b>21857469</b>-Section-name
</div>
</form-entry>
<form-entry label="Existing article ID (optional)">
<input slot="field" class="textfield" type="text" v-model.trim="articleId" @keyup.enter="resolve()">
</form-entry>
<form-entry label="Locale (optional)">
<input slot="field" class="textfield" type="text" v-model.trim="locale" @keyup.enter="resolve()">
<div class="form-entry__info">
<b>Default:</b> en-us
</div>
</form-entry>
<form-entry label="Template">
<select slot="field" class="textfield" v-model="selectedTemplate" @keyup.enter="resolve()">
<option v-for="(template, id) in allTemplates" :key="id" :value="id">
{{ template.name }}
</option>
</select>
<div class="form-entry__actions">
<a href="javascript:void(0)" @click="configureTemplates">Configure templates</a>
</div>
</form-entry>
<div class="modal__tip">
<b>ProTip:</b> You can provide values for <code>title</code>, <code>tags</code> and
<code>status</code> in the <a href="javascript:void(0)" @click="openFileProperties">file properties</a>.
</div>
<div class="modal__button-bar">
<button class="button" @click="config.reject()">Cancel</button>
<button class="button" @click="resolve()">Ok</button>
</div>
</div>
</div>
</template>
<script>
import zendeskProvider from '../../services/providers/zendeskProvider';
import modalTemplate from './modalTemplate';
export default modalTemplate({
data: () => ({
articleId: '',
}),
computedLocalSettings: {
sectionId: 'zendescPublishSectionId',
locale: 'zendescPublishLocale',
selectedTemplate: 'zendeskPublishTemplate',
},
methods: {
resolve() {
if (this.sectionId || this.articleId) {
// Return new location
const location = zendeskProvider.makeLocation(
this.config.token, this.sectionId, this.locale || 'en-us', this.articleId);
location.templateId = this.selectedTemplate;
this.config.resolve(location);
}
},
},
});
</script>

View File

@ -0,0 +1,54 @@
import FormEntry from './FormEntry';
import store from '../../store';
export default (desc) => {
const component = {
...desc,
components: {
FormEntry,
},
computed: {
...desc.computed || {},
config() {
return store.getters['modal/config'];
},
currentFileName() {
return store.getters['file/current'].name;
},
},
methods: {
...desc.methods || {},
openFileProperties: () => store.dispatch('modal/open', 'fileProperties'),
},
};
Object.keys(desc.computedLocalSettings || {}).forEach((key) => {
const id = desc.computedLocalSettings[key];
component.computed[key] = {
get() {
return store.getters['data/localSettings'][id];
},
set(value) {
store.dispatch('data/patchLocalSettings', {
[id]: value,
});
},
};
if (key === 'selectedTemplate') {
component.computed.allTemplates = () => store.getters['data/allTemplates'];
component.methods.configureTemplates = () => {
store.dispatch('modal/open', {
type: 'templates',
selectedId: this.selectedTemplate,
})
.then(({ templates, selectedId }) => {
store.dispatch('data/setTemplates', templates);
store.dispatch('data/patchLocalSettings', {
[id]: selectedId,
});
});
};
}
});
component.computedLocalSettings = null;
return component;
};

View File

@ -1,4 +0,0 @@
> Written with [StackEdit](https://stackedit.io/).

View File

@ -1,4 +1,5 @@
### File properties can contain metadata used for your publications (Wordpress, Blogger...).
### For example:
#title: My article
#author:
#tags: Tag 1, Tag 2
@ -13,6 +14,16 @@ extensions:
# Markdown extensions
markdown:
### For strict CommonMark:
#abbr: false
#deflist: false
#del: false
#footnote: false
#linkify: false
#sub: false
#sup: false
#table: false
#typographer: false
abbr: true
breaks: false
deflist: true

View File

@ -12,10 +12,19 @@ export default () => ({
googleDrivePublishFormat: 'markdown',
googleDrivePublishTemplate: 'styledHtml',
bloggerBlogUrl: '',
bloggerPublishTemplate: 'styledHtml',
bloggerPublishTemplate: 'plainHtml',
dropboxRestrictedAccess: false,
dropboxPublishTemplate: 'styledHtml',
githubRepoFullAccess: false,
githubRepoUrl: '',
githubPublishTemplate: 'jekyllSite',
gistIsPublic: false,
gistPublishTemplate: 'plainText',
wordpressDomain: '',
wordpressPublishTemplate: 'plainHtml',
zendeskSiteUrl: '',
zendeskClientId: '',
zendescPublishSectionId: '',
zendescPublishLocale: '',
zendeskPublishTemplate: 'plainHtml',
});

View File

@ -75,7 +75,7 @@ newFileContent: |
# Default properties for newly created files
newFileProperties: |
#extensions:
# markdown:
# breaks: true
# extensions:
# markdown:
# breaks: true

View File

@ -19,7 +19,7 @@ The following JavaScript context will be passed to the template:
}
You can use Handlebars built-in helpers and some custom StackEdit helpers:
{{#tocToHtml files.0.content.toc}}{{/tocToHtml}} will produce a nice TOC.
{{#tocToHtml files.0.content.toc}}{{/tocToHtml}} will produce a clickable TOC.
{{#tocToHtml files.0.content.toc 3}}{{/tocToHtml}} will limit the TOC depth to 3.
-->

View File

@ -167,7 +167,7 @@ extensionSvc.onInitConverter(0, (markdown, options) => {
markdown.renderer.rules.th_open = markdown.renderer.rules.td_open;
markdown.renderer.rules.footnote_ref = (tokens, idx) => {
const n = Number(tokens[idx].meta.id + 1).toString();
const n = `${Number(tokens[idx].meta.id + 1)}`;
let id = `fnref${n}`;
if (tokens[idx].meta.subId > 0) {
id += `:${tokens[idx].meta.subId}`;

View File

@ -1,5 +1,5 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" class="icon" width="100%" height="100%" viewBox="0 0 24.00 24.00">
<path fill="#000000" fill-opacity="1" stroke-linejoin="round" d="M 6,2L 18,2C 19.1046,2 20,2.89543 20,4L 20,20C 20,21.1046 19.1046,22 18,22L 6,22C 4.89543,22 4,21.1046 4,20L 4,4C 4,2.89543 4.89543,2 6,2 Z M 12,4.00001C 8.68629,4.00001 5.99999,6.6863 5.99999,10C 5.99999,13.3137 8.68629,16 12.1022,15.9992L 11.2221,13.7674C 10.946,13.2891 11.1099,12.6775 11.5882,12.4013L 12.4542,11.9013C 12.9325,11.6252 13.5441,11.7891 13.8202,12.2674L 15.7446,14.6884C 17.1194,13.5889 18,11.8973 18,10C 18,6.6863 15.3137,4.00001 12,4.00001 Z M 12,9.00001C 12.5523,9.00001 13,9.44773 13,10C 13,10.5523 12.5523,11 12,11C 11.4477,11 11,10.5523 11,10C 11,9.44773 11.4477,9.00001 12,9.00001 Z M 7,18C 6.44771,18 6,18.4477 6,19C 6,19.5523 6.44771,20 7,20C 7.55228,20 8,19.5523 8,19C 8,18.4477 7.55228,18 7,18 Z M 12.0882,13.2674L 14.5757,19.5759L 17.1738,18.0759L 12.9542,12.7674L 12.0882,13.2674 Z "/>
<path d="M 6,2L 18,2C 19.1046,2 20,2.89543 20,4L 20,20C 20,21.1046 19.1046,22 18,22L 6,22C 4.89543,22 4,21.1046 4,20L 4,4C 4,2.89543 4.89543,2 6,2 Z M 12,4.00001C 8.68629,4.00001 5.99999,6.6863 5.99999,10C 5.99999,13.3137 8.68629,16 12.1022,15.9992L 11.2221,13.7674C 10.946,13.2891 11.1099,12.6775 11.5882,12.4013L 12.4542,11.9013C 12.9325,11.6252 13.5441,11.7891 13.8202,12.2674L 15.7446,14.6884C 17.1194,13.5889 18,11.8973 18,10C 18,6.6863 15.3137,4.00001 12,4.00001 Z M 12,9.00001C 12.5523,9.00001 13,9.44773 13,10C 13,10.5523 12.5523,11 12,11C 11.4477,11 11,10.5523 11,10C 11,9.44773 11.4477,9.00001 12,9.00001 Z M 7,18C 6.44771,18 6,18.4477 6,19C 6,19.5523 6.44771,20 7,20C 7.55228,20 8,19.5523 8,19C 8,18.4477 7.55228,18 7,18 Z M 12.0882,13.2674L 14.5757,19.5759L 17.1738,18.0759L 12.9542,12.7674L 12.0882,13.2674 Z "/>
</svg>
</template>

View File

@ -40,8 +40,16 @@ export default {
background-image: url(../assets/iconDropbox.svg);
}
.icon-provider--wordpress {
background-image: url(../assets/iconWordpress.svg);
}
.icon-provider--blogger,
.icon-provider--bloggerPage {
background-image: url(../assets/iconBlogger.svg);
}
.icon-provider--zendesk {
background-image: url(../assets/iconZendesk.svg);
}
</style>

5
src/icons/SignalOff.vue Normal file
View File

@ -0,0 +1,5 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" class="icon" width="100%" height="100%" viewBox="0 0 24.00 24.00">
<path d="M 18,3L 18,16.1777L 21,19.1777L 21,3L 18,3 Z M 4.27734,5L 3,6.2676L 10.7324,14L 8,14L 8,21L 11,21L 11,14.2676L 13,16.2676L 13,21L 16,21L 16,19.2676L 19.7324,23L 21,21.7227L 4.27734,5 Z M 13,9L 13,11.1777L 16,14.1777L 16,9L 13,9 Z M 3,18L 3,21L 6,21L 6,18L 3,18 Z "/>
</svg>
</template>

View File

@ -39,6 +39,7 @@ import CodeBraces from './CodeBraces';
import OpenInNew from './OpenInNew';
import Information from './Information';
import Alert from './Alert';
import SignalOff from './SignalOff';
// Providers
import Provider from './Provider';
@ -82,5 +83,6 @@ Vue.component('iconCodeBraces', CodeBraces);
Vue.component('iconOpenInNew', OpenInNew);
Vue.component('iconInformation', Information);
Vue.component('iconAlert', Alert);
Vue.component('iconSignalOff', SignalOff);
// Providers
Vue.component('iconProvider', Provider);

View File

@ -1,10 +1,15 @@
import Vue from 'vue';
import * as OfflinePluginRuntime from 'offline-plugin/runtime';
import './extensions/';
import './services/optional';
import './icons/';
import App from './components/App';
import store from './store';
if (process.env.NODE_ENV === 'production') {
OfflinePluginRuntime.install();
}
Vue.config.productionTip = false;
/* eslint-disable no-new */

View File

@ -182,5 +182,6 @@ function mergeContent(serverContent, clientContent, lastMergedContent = {}) {
export default {
makePatchableText,
restoreDiscussionOffsets,
mergeObjects,
mergeContent,
};

View File

@ -96,9 +96,9 @@ export default {
// e.data can contain unsafe data if helpers attempts to call postMessage
const [err, result] = e.data;
if (err) {
reject(err.toString());
reject(`${err}`);
} else {
resolve(result.toString());
resolve(`${result}`);
}
});
worker.postMessage([template.value, view, template.helpers]);

View File

@ -1,12 +1,16 @@
import Mousetrap from 'mousetrap';
import store from '../../store';
import editorSvc from '../../services/editorSvc';
import editorEngineSvc from '../../services/editorEngineSvc';
import syncSvc from '../../services/syncSvc';
// Skip shortcuts if modal is open or editor is hidden
Mousetrap.prototype.stopCallback = () => store.getters['modal/config'] || !store.getters['layout/styles'].showEditor;
const pagedownHandler = name => () => editorSvc.pagedownEditor.uiManager.doClick(name);
const pagedownHandler = name => () => {
editorSvc.pagedownEditor.uiManager.doClick(name);
return true;
};
const methods = {
bold: pagedownHandler('bold'),
@ -19,7 +23,32 @@ const methods = {
ulist: pagedownHandler('ulist'),
heading: pagedownHandler('heading'),
hr: pagedownHandler('hr'),
sync: () => syncSvc.isSyncPossible() && syncSvc.requestSync(),
sync() {
if (syncSvc.isSyncPossible()) {
syncSvc.requestSync();
}
return true;
},
expand(param1, param2) {
const text = param1 && `${param1}`;
const replacement = param2 && `${param2}`;
if (text && replacement) {
setTimeout(() => {
const selectionMgr = editorEngineSvc.clEditor.selectionMgr;
let offset = editorEngineSvc.clEditor.selectionMgr.selectionStart;
if (offset === selectionMgr.selectionEnd) {
const range = selectionMgr.createRange(offset - text.length, offset);
if (`${range}` === text) {
range.deleteContents();
range.insertNode(document.createTextNode(replacement));
offset = (offset - text.length) + replacement.length;
selectionMgr.setSelectionStartEnd(offset, offset);
selectionMgr.updateCursorCoordinates(true);
}
}
}, 1);
}
},
};
store.watch(
@ -36,10 +65,7 @@ store.watch(
params = [params];
}
if (Object.prototype.hasOwnProperty.call(methods, method)) {
Mousetrap.bind(shortcut.keys.toString(), () => {
methods[method].apply(null, params);
return false; // preventDefault
});
Mousetrap.bind(`${shortcut.keys}`, () => !methods[method].apply(null, params));
}
}
});

View File

@ -4,77 +4,76 @@ import providerUtils from './providerUtils';
import providerRegistry from './providerRegistry';
import utils from '../utils';
const restrictedFolder = '/Applications/StackEdit (restricted)';
const restrictedFolderRegexp = /^\/Applications\/StackEdit \(restricted\)/;
const makePathAbsolute = (token, path) => {
if (!token.fullAccess) {
return `/Applications/StackEdit (restricted)${path}`;
}
return path;
};
const makePathRelative = (token, path) => {
if (!token.fullAccess) {
return path.replace(/^\/Applications\/StackEdit \(restricted\)/, '');
}
return path;
};
export default providerRegistry.register({
id: 'dropbox',
fullAccess: true,
getToken(location) {
const token = store.getters['data/dropboxTokens'][location.sub];
if (token && !!token.fullAccess === this.fullAccess) {
return token;
}
return null;
return store.getters['data/dropboxTokens'][location.sub];
},
getUrl(location) {
const pathComponents = location.path.split('/').map(encodeURIComponent);
const filename = pathComponents.pop();
let baseUrl = 'https://www.dropbox.com/home';
if (!this.fullAccess) {
baseUrl += encodeURIComponent(restrictedFolder);
}
return `${baseUrl}${pathComponents.join('/')}?preview=${filename}`;
return `https://www.dropbox.com/home${pathComponents.join('/')}?preview=${filename}`;
},
getDescription(location) {
const token = this.getToken(location);
if (this.fullAccess) {
return `${location.path}${token.name}`;
}
return `${location.path}${token.name} (restricted)`;
return `${location.path}${location.dropboxFileId}${token.name}`;
},
checkPath(path) {
return path && path.match(/^\/[^\\<>:"|?*]+$/);
},
downloadContent(token, location) {
return dropboxHelper.downloadFile(token, location.path, location.dropboxFileId)
.then(({ content }) => providerUtils.parseContent(content));
downloadContent(token, syncLocation) {
return dropboxHelper.downloadFile(
token,
makePathRelative(token, syncLocation.path),
syncLocation.dropboxFileId,
)
.then(({ content }) => providerUtils.parseContent(content, syncLocation));
},
uploadContent(token, content, location) {
uploadContent(token, content, syncLocation) {
return dropboxHelper.uploadFile(
token,
location.path,
makePathRelative(token, syncLocation.path),
providerUtils.serializeContent(content),
location.dropboxFileId,
syncLocation.dropboxFileId,
)
.then(dropboxFile => ({
...location,
path: dropboxFile.path_display,
...syncLocation,
path: makePathAbsolute(token, dropboxFile.path_display),
dropboxFileId: dropboxFile.id,
}));
},
publish(token, html, metadata, location) {
publish(token, html, metadata, publishLocation) {
return dropboxHelper.uploadFile(
token,
location.path,
publishLocation.path,
html,
location.dropboxFileId,
publishLocation.dropboxFileId,
)
.then(dropboxFile => ({
...location,
path: dropboxFile.path_display,
...publishLocation,
path: makePathAbsolute(token, dropboxFile.path_display),
dropboxFileId: dropboxFile.id,
}));
},
openFiles(token, paths) {
const openOneFile = () => {
let path = paths.pop();
const path = paths.pop();
if (!path) {
return null;
}
if (!token.fullAccess) {
path = path.replace(restrictedFolderRegexp, '');
}
let syncLocation;
// Try to find an existing sync location
store.getters['syncLocation/items'].some((existingSyncLocation) => {

View File

@ -1,8 +0,0 @@
import dropboxProvider from './dropboxProvider';
import providerRegistry from './providerRegistry';
export default providerRegistry.register({
...dropboxProvider,
id: 'dropboxRestricted',
fullAccess: false,
});

View File

@ -17,37 +17,37 @@ export default providerRegistry.register({
const token = this.getToken(location);
return `${location.filename}${location.gistId}${token.name}`;
},
downloadContent(token, location) {
return githubHelper.downloadGist(token, location.gistId, location.filename)
.then(content => providerUtils.parseContent(content));
downloadContent(token, syncLocation) {
return githubHelper.downloadGist(token, syncLocation.gistId, syncLocation.filename)
.then(content => providerUtils.parseContent(content, syncLocation));
},
uploadContent(token, content, location) {
const file = store.state.file.itemMap[location.fileId];
uploadContent(token, content, syncLocation) {
const file = store.state.file.itemMap[syncLocation.fileId];
const description = (file && file.name) || defaultDescription;
return githubHelper.uploadGist(
token,
description,
location.filename,
syncLocation.filename,
providerUtils.serializeContent(content),
location.isPublic,
location.gistId,
syncLocation.isPublic,
syncLocation.gistId,
)
.then(gist => ({
...location,
...syncLocation,
gistId: gist.id,
}));
},
publish(token, html, metadata, location) {
publish(token, html, metadata, publishLocation) {
return githubHelper.uploadGist(
token,
metadata.title,
location.filename,
publishLocation.filename,
html,
location.isPublic,
location.gistId,
publishLocation.isPublic,
publishLocation.gistId,
)
.then(gist => ({
...location,
...publishLocation,
gistId: gist.id,
}));
},

View File

@ -17,46 +17,46 @@ export default providerRegistry.register({
const token = this.getToken(location);
return `${location.path}${location.owner}/${location.repo}${token.name}`;
},
downloadContent(token, location) {
downloadContent(token, syncLocation) {
return githubHelper.downloadFile(
token, location.owner, location.repo, location.branch, location.path,
token, syncLocation.owner, syncLocation.repo, syncLocation.branch, syncLocation.path,
)
.then(({ sha, content }) => {
savedSha[location.id] = sha;
return providerUtils.parseContent(content);
savedSha[syncLocation.id] = sha;
return providerUtils.parseContent(content, syncLocation);
})
.catch(() => null); // Ignore error, without the sha upload is going to fail anyway
},
uploadContent(token, content, location) {
const sha = savedSha[location.id];
delete savedSha[location.id];
uploadContent(token, content, syncLocation) {
const sha = savedSha[syncLocation.id];
delete savedSha[syncLocation.id];
return githubHelper.uploadFile(
token,
location.owner,
location.repo,
location.branch,
location.path,
syncLocation.owner,
syncLocation.repo,
syncLocation.branch,
syncLocation.path,
providerUtils.serializeContent(content),
sha,
)
.then(() => location);
.then(() => syncLocation);
},
publish(token, html, metadata, location) {
return this.downloadContent(token, location) // Get the last sha
publish(token, html, metadata, publishLocation) {
return this.downloadContent(token, publishLocation) // Get the last sha
.then(() => {
const sha = savedSha[location.id];
delete savedSha[location.id];
const sha = savedSha[publishLocation.id];
delete savedSha[publishLocation.id];
return githubHelper.uploadFile(
token,
location.owner,
location.repo,
location.branch,
location.path,
publishLocation.owner,
publishLocation.repo,
publishLocation.branch,
publishLocation.path,
html,
sha,
);
})
.then(() => location);
.then(() => publishLocation);
},
makeLocation(token, owner, repo, branch, path) {
return {

View File

@ -62,7 +62,11 @@ export default providerRegistry.register({
.then(() => syncData);
},
downloadContent(token, syncLocation) {
const syncData = store.getters['data/syncDataByItemId'][`${syncLocation.fileId}/content`];
return this.downloadData(token, `${syncLocation.fileId}/content`)
.then(() => syncLocation);
},
downloadData(token, dataId) {
const syncData = store.getters['data/syncDataByItemId'][dataId];
if (!syncData) {
return Promise.resolve();
}
@ -70,8 +74,7 @@ export default providerRegistry.register({
.then((content) => {
const item = JSON.parse(content);
if (item.hash !== syncData.hash) {
store.dispatch('data/setSyncData', {
...store.getters['data/syncData'],
store.dispatch('data/patchSyncData', {
[syncData.id]: {
...syncData,
hash: item.hash,
@ -82,29 +85,34 @@ export default providerRegistry.register({
});
},
uploadContent(token, content, syncLocation, ifNotTooLate) {
const syncData = store.getters['data/syncDataByItemId'][`${syncLocation.fileId}/content`];
if (syncData && syncData.hash === content.hash) {
return this.uploadData(token, undefined, content, `${syncLocation.fileId}/content`, ifNotTooLate)
.then(() => syncLocation);
},
uploadData(token, sub, item, dataId, ifNotTooLate) {
const syncData = store.getters['data/syncDataByItemId'][dataId];
if (syncData && syncData.hash === item.hash) {
return Promise.resolve();
}
return googleHelper.uploadAppDataFile(
token,
JSON.stringify({
id: content.id,
type: content.type,
hash: content.hash,
}), ['appDataFolder'],
JSON.stringify(content),
id: item.id,
type: item.type,
hash: item.hash,
sub,
}),
['appDataFolder'],
JSON.stringify(item),
syncData && syncData.id,
ifNotTooLate,
)
.then(file => store.dispatch('data/setSyncData', {
...store.getters['data/syncData'],
.then(file => store.dispatch('data/patchSyncData', {
[file.id]: {
// Build sync data
id: file.id,
itemId: content.id,
type: content.type,
hash: content.hash,
itemId: item.id,
type: item.type,
hash: item.hash,
},
}));
},

View File

@ -21,7 +21,7 @@ export default providerRegistry.register({
},
downloadContent(token, syncLocation) {
return googleHelper.downloadFile(token, syncLocation.driveFileId)
.then(content => providerUtils.parseContent(content));
.then(content => providerUtils.parseContent(content, syncLocation));
},
uploadContent(token, content, syncLocation, ifNotTooLate) {
const file = store.state.file.itemMap[syncLocation.fileId];

View File

@ -13,8 +13,9 @@ const getAppKey = (fullAccess) => {
const request = (token, options, args) => utils.request({
...options,
headers: {
...options.headers,
'Content-Type': 'application/octet-stream',
...options.headers || {},
'Content-Type': options.body && (typeof options.body === 'string'
? 'application/octet-stream' : 'application/json; charset=utf-8'),
'Dropbox-API-Arg': args && JSON.stringify(args),
Authorization: `Bearer ${token.accessToken}`,
},
@ -34,17 +35,17 @@ export default {
})
.then((res) => {
// Check the returned sub consistency
if (sub && res.body.account_id !== sub) {
if (sub && `${res.body.account_id}` !== sub) {
throw new Error('Dropbox account ID not expected.');
}
// Build token object including scopes and sub
const token = {
accessToken,
name: res.body.name.display_name,
sub: res.body.account_id,
sub: `${res.body.account_id}`,
fullAccess,
};
// Add token to githubTokens
// Add token to dropboxTokens
store.dispatch('data/setDropboxToken', token);
return token;
}));

View File

@ -10,18 +10,15 @@ const getScopes = token => [token.repoFullAccess ? 'repo' : 'public_repo', 'gist
const request = (token, options) => utils.request({
...options,
headers: {
...options.headers,
...options.headers || {},
Authorization: `token ${token.accessToken}`,
},
params: {
...options.params || {},
t: Date.now(), // Prevent from caching
},
});
const base64Encode = str => btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g,
(match, p1) => String.fromCharCode(`0x${p1}`),
));
const base64Decode = str => decodeURIComponent(atob(str).split('').map(
c => `%${`00${c.charCodeAt(0).toString(16)}`.slice(-2)}`,
).join(''));
export default {
startOauth2(scopes, sub = null, silent = false) {
return utils.startOauth2(
@ -49,7 +46,7 @@ export default {
})
.then((res) => {
// Check the returned sub consistency
if (sub && res.body.id !== sub) {
if (sub && `${res.body.id}` !== sub) {
throw new Error('GitHub account ID not expected.');
}
// Build token object including scopes and sub
@ -57,7 +54,7 @@ export default {
scopes,
accessToken,
name: res.body.name,
sub: res.body.id,
sub: `${res.body.id}`,
repoFullAccess: scopes.indexOf('repo') !== -1,
};
// Add token to githubTokens
@ -74,7 +71,7 @@ export default {
url: `https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/contents/${encodeURIComponent(path)}`,
body: {
message: 'Uploaded by https://stackedit.io/',
content: base64Encode(content),
content: utils.encodeBase64(content),
sha,
branch,
},
@ -87,7 +84,7 @@ export default {
})
.then(res => ({
sha: res.body.sha,
content: base64Decode(res.body.content),
content: utils.decodeBase64(res.body.content),
}));
},
uploadGist(token, description, filename, content, isPublic, gistId) {

View File

@ -20,7 +20,7 @@ const libraries = ['picker'];
const request = (token, options) => utils.request({
...options,
headers: {
...options.headers,
...options.headers || {},
Authorization: `Bearer ${token.accessToken}`,
},
});
@ -106,7 +106,7 @@ export default {
throw new Error('Client ID inconsistent.');
}
// Check the returned sub consistency
if (sub && res.body.sub !== sub) {
if (sub && `${res.body.sub}` !== sub) {
throw new Error('Google account ID not expected.');
}
// Build token object including scopes and sub
@ -114,7 +114,7 @@ export default {
scopes,
accessToken: data.accessToken,
expiresOn: Date.now() + (data.expiresIn * 1000),
sub: res.body.sub,
sub: `${res.body.sub}`,
isLogin: !store.getters['data/loginToken'] &&
scopes.indexOf('https://www.googleapis.com/auth/drive.appdata') !== -1,
isDrive: scopes.indexOf('https://www.googleapis.com/auth/drive') !== -1 ||
@ -168,8 +168,11 @@ export default {
// Try to get a new token in background
return this.startOauth2(mergedScopes, sub, true)
// If it fails try to popup a window
.catch(() => utils.checkOnline()
.then(() => this.startOauth2(mergedScopes, sub)));
.catch(() => utils.checkOnline() // Check that we are online, silent mode is a hack
.then(() => store.dispatch('modal/providerRedirection', {
providerName: 'Google',
onResolve: () => this.startOauth2(mergedScopes, sub),
})));
});
},
loadClientScript() {

View File

@ -0,0 +1,97 @@
import utils from '../../utils';
import store from '../../../store';
const clientId = '23361';
const tokenExpirationMargin = 5 * 60 * 1000; // 5 min (WordPress tokens expire after 2 weeks)
const request = (token, options) => utils.request({
...options,
headers: {
...options.headers || {},
Authorization: `Bearer ${token.accessToken}`,
},
});
export default {
startOauth2(sub = null, silent = false) {
return utils.startOauth2(
'https://public-api.wordpress.com/oauth2/authorize', {
client_id: clientId,
response_type: 'token',
scope: 'global',
}, silent)
// Call the user info endpoint
.then(data => request({ accessToken: data.accessToken }, {
url: 'https://public-api.wordpress.com/rest/v1.1/me',
})
.then((res) => {
// Check the returned sub consistency
if (sub && `${res.body.ID}` !== sub) {
throw new Error('WordPress account ID not expected.');
}
// Build token object including scopes and sub
const token = {
accessToken: data.accessToken,
expiresOn: Date.now() + (data.expiresIn * 1000),
name: res.body.display_name,
sub: `${res.body.ID}`,
};
// Add token to wordpressTokens
store.dispatch('data/setWordpressToken', token);
return token;
}));
},
refreshToken(token) {
const sub = token.sub;
const lastToken = store.getters['data/wordpressTokens'][sub];
return Promise.resolve()
.then(() => {
if (lastToken.expiresOn > Date.now() + tokenExpirationMargin) {
return lastToken;
}
// Existing token is going to expire.
// Try to get a new token in background
return store.dispatch('modal/providerRedirection', {
providerName: 'WordPress',
onResolve: () => this.startOauth2(sub),
});
});
},
addAccount(fullAccess = false) {
return this.startOauth2(fullAccess);
},
uploadPost(
token,
domain,
siteId,
postId,
title,
content,
tags,
categories,
excerpt,
author,
featuredImage,
status,
date,
) {
return this.refreshToken(token)
.then(refreshedToken => request(refreshedToken, {
method: 'POST',
url: `https://public-api.wordpress.com/rest/v1.2/sites/${siteId || domain}/posts/${postId || 'new'}`,
body: {
content,
title,
tags,
categories,
excerpt,
author,
featured_image: featuredImage || '',
status,
date: date && date.toISOString(),
},
})
.then(res => res.body));
},
};

View File

@ -0,0 +1,88 @@
import utils from '../../utils';
import store from '../../../store';
const request = (token, options) => utils.request({
...options,
headers: {
...options.headers || {},
Authorization: `Bearer ${token.accessToken}`,
},
});
export default {
startOauth2(subdomain, clientId, sub = null, silent = false) {
return utils.startOauth2(
`https://${subdomain}.zendesk.com/oauth/authorizations/new`, {
client_id: clientId,
response_type: 'token',
scope: 'read hc:write',
}, silent)
// Call the user info endpoint
.then(({ accessToken }) => request({ accessToken }, {
url: `https://${subdomain}.zendesk.com/api/v2/users/me.json`,
})
.then((res) => {
const uniqueSub = `${subdomain}/${res.body.user.id}`;
// Check the returned sub consistency
if (sub && uniqueSub !== sub) {
throw new Error('Zendesk account ID not expected.');
}
// Build token object including scopes and sub
const token = {
accessToken,
name: res.body.user.name,
subdomain,
sub: uniqueSub,
};
// Add token to zendeskTokens
store.dispatch('data/setZendeskToken', token);
return token;
}));
},
addAccount(subdomain, clientId) {
return this.startOauth2(subdomain, clientId);
},
uploadArticle(
token,
sectionId,
articleId,
title,
content,
labels,
locale,
isDraft,
) {
const article = {
title,
body: content,
locale,
draft: isDraft,
};
if (articleId) {
return request(token, {
method: 'PUT',
url: `https://${token.subdomain}.zendesk.com/api/v2/help_center/articles/${articleId}/translations/${locale}.json`,
body: { translation: article },
})
.then(() => labels && request(token, {
method: 'PUT',
url: `https://${token.subdomain}.zendesk.com/api/v2/help_center/articles/${articleId}.json`,
body: {
article: {
label_names: labels,
},
},
}))
.then(() => articleId);
}
if (labels) {
article.label_names = labels;
}
return request(token, {
method: 'POST',
url: `https://${token.subdomain}.zendesk.com/api/v2/help_center/sections/${sectionId}/articles.json`,
body: { article },
})
.then(res => `${res.body.article.id}`);
},
};

View File

@ -1,13 +1,9 @@
import emptyContent from '../../data/emptyContent';
import store from '../../store';
import utils from '../utils';
const dataExtractor = /<!--stackedit_data:([A-Za-z0-9+/=\s]+)-->$/;
// https://developer.mozilla.org/en/docs/Web/API/WindowBase64/Base64_encoding_and_decoding
const b64Encode = str => btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g,
(match, p1) => String.fromCharCode(`0x${p1}`)));
const b64Decode = str => decodeURIComponent(atob(str).split('').map(
c => `%${`00${c.charCodeAt(0).toString(16)}`.slice(-2)}`).join(''));
export default {
serializeContent(content) {
let result = content.text;
@ -25,20 +21,20 @@ export default {
data.history = content.history;
}
if (Object.keys(data).length) {
const serializedData = b64Encode(JSON.stringify(data)).replace(/(.{50})/g, '$1\n');
const serializedData = utils.encodeBase64(JSON.stringify(data)).replace(/(.{50})/g, '$1\n');
result += `<!--stackedit_data:\n${serializedData}\n-->`;
}
return result;
},
parseContent(serializedContent) {
const result = emptyContent();
parseContent(serializedContent, syncLocation) {
const result = utils.deepCopy(store.state.content.itemMap[`${syncLocation.fileId}/content`]) || emptyContent();
result.text = serializedContent;
result.history = [];
const extractedData = dataExtractor.exec(serializedContent);
if (extractedData) {
try {
const serializedData = extractedData[1].replace(/\s/g, '');
Object.assign(result, JSON.parse(b64Decode(serializedData)));
Object.assign(result, JSON.parse(utils.decodeBase64(serializedData)));
result.text = serializedContent.slice(0, extractedData.index);
} catch (e) {
// Ignore

View File

@ -0,0 +1,50 @@
import store from '../../store';
import wordpressHelper from './helpers/wordpressHelper';
import providerRegistry from './providerRegistry';
export default providerRegistry.register({
id: 'wordpress',
getToken(location) {
return store.getters['data/wordpressTokens'][location.sub];
},
getUrl(location) {
return `https://wordpress.com/post/${location.siteId}/${location.postId}`;
},
getDescription(location) {
const token = this.getToken(location);
return `${location.postId}${location.domain}${token.name}`;
},
publish(token, html, metadata, publishLocation) {
return wordpressHelper.uploadPost(
token,
publishLocation.domain,
publishLocation.siteId,
publishLocation.postId,
metadata.title,
html,
metadata.tags,
metadata.categories,
metadata.excerpt,
metadata.author,
metadata.featuredImage,
metadata.status,
metadata.date,
)
.then(post => ({
...publishLocation,
siteId: `${post.site_ID}`,
postId: `${post.ID}`,
}));
},
makeLocation(token, domain, postId) {
const location = {
providerId: this.id,
sub: token.sub,
domain,
};
if (postId) {
location.postId = postId;
}
return location;
},
});

View File

@ -0,0 +1,46 @@
import store from '../../store';
import zendeskHelper from './helpers/zendeskHelper';
import providerRegistry from './providerRegistry';
export default providerRegistry.register({
id: 'zendesk',
getToken(location) {
return store.getters['data/zendeskTokens'][location.sub];
},
getUrl(location) {
const token = this.getToken(location);
return `https://${token.subdomain}.zendesk.com/hc/${location.locale}/articles/${location.articleId}`;
},
getDescription(location) {
const token = this.getToken(location);
return `${location.articleId}${token.name}${token.subdomain}`;
},
publish(token, html, metadata, publishLocation) {
return zendeskHelper.uploadArticle(
token,
publishLocation.sectionId,
publishLocation.articleId,
metadata.title,
html,
metadata.tags,
publishLocation.locale,
metadata.status === 'draft',
)
.then(articleId => ({
...publishLocation,
articleId,
}));
},
makeLocation(token, sectionId, locale, articleId) {
const location = {
providerId: this.id,
sub: token.sub,
sectionId,
locale,
};
if (articleId) {
location.articleId = articleId;
}
return location;
},
});

View File

@ -18,7 +18,7 @@ const ensureArray = (value) => {
return [];
}
if (!Array.isArray(value)) {
return value.toString().trim().split(/\s*,\s*/);
return `${value}`.trim().split(/\s*,\s*/);
}
return value;
};
@ -27,14 +27,14 @@ const ensureString = (value, defaultValue) => {
if (!value) {
return defaultValue;
}
return value.toString();
return `${value}`;
};
const ensureDate = (value, defaultValue) => {
if (!value) {
return defaultValue;
}
return new Date(value.toString());
return new Date(`${value}`);
};
function publish(publishLocation) {
@ -55,7 +55,7 @@ function publish(publishLocation) {
excerpt: ensureString(properties.excerpt),
featuredImage: ensureString(properties.featuredImage),
status: ensureString(properties.status),
date: ensureDate(properties.date),
date: ensureDate(properties.date, new Date()),
};
return provider.publish(token, html, metadata, publishLocation);
}));

View File

@ -64,6 +64,7 @@ const loadSyncedContent = loader('syncedContent');
const loadContentState = loader('contentState');
function applyChanges(changes) {
const token = mainProvider.getToken();
const storeItemMap = { ...store.getters.allItemMap };
const syncData = { ...store.getters['data/syncData'] };
let syncDataChanged = false;
@ -79,7 +80,10 @@ function applyChanges(changes) {
}
delete syncData[change.fileId];
syncDataChanged = true;
} else if (!change.removed && change.item && change.item.hash) {
} else if (!change.removed && change.item && change.item.hash && (
// Ignore items that belong to another user (like settings)
!change.item.sub || change.item.sub === token.sub
)) {
if (!existingSyncData || (existingSyncData.hash !== change.item.hash && (
!existingItem || existingItem.hash !== change.item.hash
))) {
@ -202,7 +206,7 @@ function syncFile(fileId) {
...mergedContent,
});
// Retrieve content with new `hash` value and freeze it
// Retrieve content with new `hash` and freeze it
mergedContent = utils.deepCopy(getContent());
// Make merged content history
@ -303,13 +307,76 @@ function syncFile(fileId) {
});
}
function syncDataItem(dataId) {
const item = store.state.data.itemMap[dataId];
const syncData = store.getters['data/syncDataByItemId'][dataId];
// Sync if item hash and syncData hash are inconsistent
if (syncData && item && item.hash === syncData.hash) {
return null;
}
const token = mainProvider.getToken();
return token && mainProvider.downloadData(token, dataId)
.then((serverItem = null) => {
const dataSyncData = store.getters['data/dataSyncData'][dataId];
let mergedItem = (() => {
const clientItem = utils.deepCopy(store.getters[`data/${dataId}`]);
if (!serverItem) {
return clientItem;
}
if (!dataSyncData) {
return serverItem;
}
if (dataSyncData.hash !== serverItem.hash) {
// Server version has changed
if (dataSyncData.hash !== clientItem.hash && typeof clientItem.data === 'object') {
// Client version has changed as well, merge data objects
return {
...clientItem,
data: diffUtils.mergeObjects(serverItem.data, clientItem.data),
};
}
return serverItem;
}
return clientItem;
})();
// Update item in store
store.commit('data/setItem', {
id: dataId,
...mergedItem,
});
// Retrieve item with new `hash` and freeze it
mergedItem = utils.deepCopy(store.state.data.itemMap[dataId]);
return Promise.resolve()
.then(() => {
if (serverItem && serverItem.hash === mergedItem.hash) {
return null;
}
return mainProvider.uploadData(
token,
dataId === 'settings' ? token.sub : undefined,
mergedItem,
dataId,
);
})
.then(() => {
store.dispatch('data/patchDataSyncData', {
[dataId]: utils.deepCopy(store.getters['data/syncDataByItemId'][dataId]),
});
});
});
}
function sync() {
const googleToken = store.getters['data/loginToken'];
return mainProvider.getChanges(googleToken)
const mainToken = store.getters['data/loginToken'];
return mainProvider.getChanges(mainToken)
.then((changes) => {
// Apply changes
applyChanges(changes);
mainProvider.setAppliedChanges(googleToken, changes);
mainProvider.setAppliedChanges(mainToken, changes);
// Prevent from sending items too long after changes have been retrieved
const syncStartTime = Date.now();
@ -327,7 +394,7 @@ function sync() {
...store.state.folder.itemMap,
...store.state.syncLocation.itemMap,
...store.state.publishLocation.itemMap,
// Deal with contents later
// Deal with contents and data later
};
const syncDataByItemId = store.getters['data/syncDataByItemId'];
let result;
@ -336,7 +403,7 @@ function sync() {
const existingSyncData = syncDataByItemId[id];
if (!existingSyncData || existingSyncData.hash !== item.hash) {
result = mainProvider.saveItem(
googleToken,
mainToken,
// Use deepCopy to freeze objects
utils.deepCopy(item),
utils.deepCopy(existingSyncData),
@ -360,6 +427,7 @@ function sync() {
...store.state.syncLocation.itemMap,
...store.state.publishLocation.itemMap,
...store.state.content.itemMap,
...store.state.data.itemMap,
};
const syncData = store.getters['data/syncData'];
let result;
@ -372,7 +440,7 @@ function sync() {
// Use deepCopy to freeze objects
const syncDataToRemove = utils.deepCopy(existingSyncData);
result = mainProvider
.removeItem(googleToken, syncDataToRemove, ifNotTooLate)
.removeItem(mainToken, syncDataToRemove, ifNotTooLate)
.then(() => {
const syncDataCopy = { ...store.getters['data/syncData'] };
delete syncDataCopy[syncDataToRemove.id];
@ -393,7 +461,7 @@ function sync() {
const loadedContent = store.state.content.itemMap[contentId];
const hash = loadedContent ? loadedContent.hash : localDbSvc.hashMap.content[contentId];
const syncData = store.getters['data/syncDataByItemId'][contentId];
// Sync if item hash and syncData hash are different
// Sync if item hash and syncData hash are inconsistent
if (!syncData || hash !== syncData.hash) {
[fileId] = contentId.split('/');
}
@ -411,10 +479,13 @@ function sync() {
return Promise.resolve()
.then(() => saveNextItem())
.then(() => removeNextItem())
.then(() => syncDataItem('settings'))
.then(() => syncDataItem('templates'))
.then(() => {
if (store.getters['content/current'].id) {
const currentFileId = store.getters['content/current'].id;
if (currentFileId) {
// Sync current file first
return syncFile(store.getters['file/current'].id)
return syncFile(currentFileId)
.then(() => syncNextFile());
}
return syncNextFile();

View File

@ -91,8 +91,8 @@ self.onmessage = (evt) => {
const context = evt.data[1];
safeEval(evt.data[2]);
self.postMessage([null, template(context)]);
} catch (e) {
self.postMessage([e.toString()]);
} catch (err) {
self.postMessage([`${err}`]);
}
close();
};

View File

@ -2,6 +2,7 @@ import yaml from 'js-yaml';
import defaultProperties from '../data/defaultFileProperties.yml';
const workspaceId = 'main';
const origin = `${location.protocol}//${location.host}`;
// For uid()
const uidLength = 16;
@ -37,9 +38,16 @@ const urlParser = window.document.createElement('a');
// For loadScript()
const scriptLoadingPromises = Object.create(null);
// For startOauth2
const oauth2AuthorizationTimeout = 120 * 1000; // 2 minutes
// For checkOnline
const checkOnlineTimeout = 15 * 1000; // 15 sec
export default {
workspaceId,
origin: `${location.protocol}//${location.host}`,
origin,
oauth2RedirectUri: `${origin}/oauth2/callback`,
types: [
'contentState',
'syncedContent',
@ -79,6 +87,14 @@ export default {
}
return hash;
},
encodeBase64(str) {
return btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g,
(match, p1) => String.fromCharCode(`0x${p1}`)));
},
decodeBase64(str) {
return decodeURIComponent(atob(str).split('').map(
c => `%${`00${c.charCodeAt(0).toString(16)}`.slice(-2)}`).join(''));
},
computeProperties(yamlProperties) {
const customProperties = yaml.safeLoad(yamlProperties);
const properties = yaml.safeLoad(defaultProperties);
@ -147,14 +163,16 @@ export default {
// Build the authorize URL
const state = this.uid();
params.state = state;
params.redirect_uri = `${this.origin}/oauth2/callback.html`;
params.redirect_uri = this.oauth2RedirectUri;
const authorizeUrl = this.addQueryParams(url, params);
if (silent) {
// Use an iframe as wnd for silent mode
oauth2Context.iframeElt = document.createElement('iframe');
oauth2Context.iframeElt.style.position = 'absolute';
oauth2Context.iframeElt.style.left = '-9999px';
oauth2Context.closeTimeout = setTimeout(() => oauth2Context.clean('Unknown error.'), 5 * 1000);
oauth2Context.closeTimeout = setTimeout(
() => oauth2Context.clean('Unknown error.'),
checkOnlineTimeout);
oauth2Context.iframeElt.onerror = () => oauth2Context.clean('Unknown error.');
oauth2Context.iframeElt.src = authorizeUrl;
document.body.appendChild(oauth2Context.iframeElt);
@ -166,7 +184,9 @@ export default {
if (!oauth2Context.wnd) {
return Promise.reject();
}
oauth2Context.closeTimeout = setTimeout(() => oauth2Context.clean('Timeout.'), 120 * 1000);
oauth2Context.closeTimeout = setTimeout(
() => oauth2Context.clean('Timeout.'),
oauth2AuthorizationTimeout);
}
return new Promise((resolve, reject) => {
oauth2Context.clean = (errorMsg) => {
@ -189,32 +209,43 @@ export default {
oauth2Context.msgHandler = (event) => {
if (event.source === oauth2Context.wnd &&
event.origin === this.origin &&
event.data &&
event.data.state === state
event.origin === this.origin
) {
oauth2Context.clean();
if (event.data.accessToken || event.data.code) {
resolve(event.data);
} else {
reject(event.data);
const data = {};
`${event.data}`.slice(1).split('&').forEach((param) => {
const [key, value] = param.split('=').map(decodeURIComponent);
if (key === 'state') {
data.state = value;
} else if (key === 'access_token') {
data.accessToken = value;
} else if (key === 'code') {
data.code = value;
} else if (key === 'expires_in') {
data.expiresIn = value;
}
});
if (data.state === state) {
resolve(data);
return;
}
reject('Could not get required authorization.');
}
};
window.addEventListener('message', oauth2Context.msgHandler);
oauth2Context.checkClosedInterval = !silent && setInterval(() => {
if (oauth2Context.wnd.closed) {
oauth2Context.clean('Authorize window was closed');
oauth2Context.clean('Authorize window was closed.');
}
}, 250);
});
},
request(configParam) {
const timeout = 30 * 1000; // 30 sec
let retryAfter = 500; // 500 ms
const maxRetryAfter = 30 * 1000; // 30 sec
const config = Object.assign({}, configParam);
config.timeout = config.timeout || 30 * 1000; // 30 sec
config.headers = Object.assign({}, config.headers);
if (config.body && typeof config.body === 'object') {
config.body = JSON.stringify(config.body);
@ -283,13 +314,15 @@ export default {
timeoutId = setTimeout(() => {
xhr.abort();
reject(new Error('Network request timeout.'));
}, timeout);
}, config.timeout);
// Add query params to URL
const url = this.addQueryParams(config.url, config.params);
xhr.open(config.method || 'GET', url);
Object.keys(config.headers).forEach((key) => {
xhr.setRequestHeader(key, config.headers[key]);
const value = config.headers[key];
if (value) {
xhr.setRequestHeader(key, `${value}`);
}
});
xhr.send(config.body || null);
})
@ -310,23 +343,15 @@ export default {
return attempt();
},
checkOnline() {
return new Promise((resolve, reject) => {
const script = document.createElement('script');
let timeout;
const cleaner = (cb, res) => () => {
clearTimeout(timeout);
cb(res);
document.head.removeChild(script);
};
script.onload = cleaner(resolve, 'Online.');
script.onerror = cleaner(reject, 'Offline.');
script.src = `https://apis.google.com/js/api.js?${Date.now()}`;
try {
document.head.appendChild(script); // This can fail with bad network
timeout = setTimeout(cleaner(reject), 15000); // 15 sec
} catch (e) {
reject(e);
const checkStatus = (res) => {
if (!res.status || res.status < 200) {
throw new Error('Offline...');
}
});
};
return this.request({
url: 'https://www.googleapis.com/plus/v1/people/me',
timeout: checkOnlineTimeout,
})
.then(checkStatus, checkStatus);
},
};

View File

@ -24,17 +24,21 @@ const module = moduleTemplate(empty, true);
module.mutations.setItem = (state, value) => {
const emptyItem = empty(value.id);
let itemData = value.data || emptyItem.data;
if (typeof itemData === 'object') {
itemData = Object.assign(emptyItem.data, value.data);
}
const data = typeof value.data === 'object'
? Object.assign(emptyItem.data, value.data)
: value.data;
const item = {
...emptyItem,
...value,
data: itemData,
data,
hash: Date.now(),
};
if (!item.hash) {
item.hash = Date.now();
if (item.id === 'settings' || item.id === 'templates') {
// Use a real hash for synced types
item.hash = utils.hash(utils.serializeObject({
...item,
hash: undefined,
}));
}
Vue.set(state.itemMap, item.id, item);
};
@ -42,13 +46,13 @@ module.mutations.setItem = (state, value) => {
const getter = id => state => (state.itemMap[id] || empty(id)).data;
const setter = id => ({ commit }, data) => commit('setItem', itemTemplate(id, data));
const patcher = id => ({ state, commit }, data) => {
const item = state.itemMap[id] || empty(id);
commit('patchOrSetItem', {
...item,
data: {
const item = Object.assign(empty(id), state.itemMap[id]);
commit('setItem', {
...empty(id),
data: typeof data === 'object' ? {
...item.data,
...data,
},
} : data,
});
};
@ -172,11 +176,17 @@ module.getters.syncDataByType = (state, getters) => {
module.actions.patchSyncData = patcher('syncData');
module.actions.setSyncData = setter('syncData');
// Data sync data (used to sync settings and settings)
module.getters.dataSyncData = getter('dataSyncData');
module.actions.patchDataSyncData = patcher('dataSyncData');
// Tokens
module.getters.tokens = getter('tokens');
module.getters.googleTokens = (state, getters) => getters.tokens.google || {};
module.getters.dropboxTokens = (state, getters) => getters.tokens.dropbox || {};
module.getters.githubTokens = (state, getters) => getters.tokens.github || {};
module.getters.wordpressTokens = (state, getters) => getters.tokens.wordpress || {};
module.getters.zendeskTokens = (state, getters) => getters.tokens.zendesk || {};
module.getters.loginToken = (state, getters) => {
// Return the first google token that has the isLogin flag
const googleTokens = getters.googleTokens;
@ -185,29 +195,18 @@ module.getters.loginToken = (state, getters) => {
return googleTokens[loginSubs[0]];
};
module.actions.patchTokens = patcher('tokens');
module.actions.setGoogleToken = ({ getters, dispatch }, token) => {
const tokenSetter = providerId => ({ getters, dispatch }, token) => {
dispatch('patchTokens', {
google: {
...getters.googleTokens,
[token.sub]: token,
},
});
};
module.actions.setDropboxToken = ({ getters, dispatch }, token) => {
dispatch('patchTokens', {
dropbox: {
...getters.dropboxTokens,
[token.sub]: token,
},
});
};
module.actions.setGithubToken = ({ getters, dispatch }, token) => {
dispatch('patchTokens', {
github: {
...getters.githubTokens,
[providerId]: {
...getters[`${providerId}Tokens`],
[token.sub]: token,
},
});
};
module.actions.setGoogleToken = tokenSetter('google');
module.actions.setDropboxToken = tokenSetter('dropbox');
module.actions.setGithubToken = tokenSetter('github');
module.actions.setWordpressToken = tokenSetter('wordpress');
module.actions.setZendeskToken = tokenSetter('zendesk');
export default module;

View File

@ -5,7 +5,7 @@ const editorTopPadding = 10;
const navigationBarEditButtonsWidth = 36 * 12; // 12 buttons
const navigationBarLeftButtonWidth = 38 + 4 + 15;
const navigationBarRightButtonWidth = 38 + 8;
const navigationBarSpinnerWidth = 18 + 15 + 5; // 5 for left margin
const navigationBarSpinnerWidth = 22 + 8 + 5; // 5 for left margin
const navigationBarLocationWidth = 20;
const navigationBarSyncPublishButtonsWidth = 36 + 10;
const navigationBarTitleMargin = 8;

View File

@ -21,11 +21,13 @@ export default {
const config = typeof param === 'object' ? { ...param } : { type: param };
const clean = () => commit('setStack', state.stack.filter((otherConfig => otherConfig !== config)));
config.resolve = (result) => {
if (config.onResolve) {
config.onResolve(result);
}
clean();
resolve(result);
if (config.onResolve) {
config.onResolve(result)
.then(res => resolve(res));
} else {
resolve(result);
}
};
config.reject = (error) => {
clean();
@ -63,5 +65,11 @@ export default {
resolveText: 'Yes, clean',
rejectText: 'No',
}),
providerRedirection: ({ dispatch }, { providerName, onResolve }) => dispatch('open', {
content: `<p>You are about to navigate to the <b>${providerName}</b> authorization page.</p>`,
resolveText: 'Ok, go on!',
rejectText: 'Cancel',
onResolve,
}),
},
};

View File

@ -38,11 +38,6 @@ export default (empty, simpleHash = false) => {
mutations: {
setItem,
patchItem,
patchOrSetItem(state, patch) {
if (!patchItem(state, patch)) {
setItem(state, patch);
}
},
deleteItem(state, id) {
Vue.delete(state.itemMap, id);
},

View File

@ -35,7 +35,7 @@ export default {
item.content = `HTTP error ${error.status}.`;
}
} else {
item.content = error.toString();
item.content = `${error}`;
}
}
if (!item.content || item.content === '[object Object]') {

View File

@ -10,7 +10,8 @@ module.getters = {
const result = {};
getters.items.forEach((item) => {
// Filter items that we can't use
if (providerRegistry.providers[item.providerId].getToken(item)) {
const provider = providerRegistry.providers[item.providerId];
if (provider && provider.getToken(item)) {
const list = result[item.fileId] || [];
list.push(item);
result[item.fileId] = list;

View File

@ -20,6 +20,10 @@ export default {
},
actions: {
enqueue({ state, commit, dispatch }, cb) {
if (state.offline) {
// No need to enqueue
return;
}
const checkOffline = () => {
if (state.offline) {
// Empty queue

View File

@ -10,7 +10,8 @@ module.getters = {
const result = {};
getters.items.forEach((item) => {
// Filter items that we can't use
if (providerRegistry.providers[item.providerId].getToken(item)) {
const provider = providerRegistry.providers[item.providerId];
if (provider && provider.getToken(item)) {
const list = result[item.fileId] || [];
list.push(item);
result[item.fileId] = list;

View File

@ -2,35 +2,8 @@
<html>
<body>
<script>
var state;
var accessToken;
var code;
var expiresIn;
function parse(search) {
(search || '').slice(1).split('&').forEach(function (param) {
var split = param.split('=');
var key = decodeURIComponent(split.shift());
var value = decodeURIComponent(split.join('='));
if (key === 'state') {
state = value;
} else if (key === 'access_token') {
accessToken = value;
} else if (key === 'code') {
code = value;
} else if (key === 'expires_in') {
expiresIn = value;
}
});
}
parse(location.search);
parse(location.hash);
var origin = location.protocol + '//' + location.host;
(window.opener || window.parent).postMessage({
state: state,
accessToken: accessToken,
code: code,
expiresIn: expiresIn
}, origin);
(window.opener || window.parent).postMessage(location.hash || location.search, origin);
</script>
</body>
</html>

369
yarn.lock
View File

@ -188,6 +188,14 @@ asn1.js@^4.0.0:
inherits "^2.0.1"
minimalistic-assert "^1.0.0"
asn1@0.1.11:
version "0.1.11"
resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.1.11.tgz#559be18376d08a4ec4dbe80877d27818639b2df7"
asn1@0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.1.tgz#ecc73f75d31ea3c6ed9d47428db35fecc7b2c6dc"
asn1@~0.2.3:
version "0.2.3"
resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.3.tgz#dac8787713c9966849fc8180777ebe9c1ddf3b86"
@ -196,6 +204,10 @@ assert-plus@1.0.0, assert-plus@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525"
assert-plus@^0.1.5:
version "0.1.5"
resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-0.1.5.tgz#ee74009413002d84cec7219c6ac811812e723160"
assert-plus@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-0.2.0.tgz#d74e1b87e7affc0db8aadb7021f3fe48101ab234"
@ -224,6 +236,10 @@ async@^2.1.2, async@^2.1.5:
dependencies:
lodash "^4.14.0"
async@~0.9.0:
version "0.9.2"
resolved "https://registry.yarnpkg.com/async/-/async-0.9.2.tgz#aea74d5e61c1f899613bf64bda66d4c78f2fd17d"
asynckit@^0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
@ -239,6 +255,10 @@ autoprefixer@^6.0.0, autoprefixer@^6.3.1, autoprefixer@^6.7.2:
postcss "^5.2.16"
postcss-value-parser "^3.2.3"
aws-sign2@~0.5.0:
version "0.5.0"
resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.5.0.tgz#c57103f7a17fc037f02d7c2e64b602ea223f7d63"
aws-sign2@~0.6.0:
version "0.6.0"
resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.6.0.tgz#14342dd38dbcc94d0e5b87d763cd63612c0e794f"
@ -887,6 +907,12 @@ boolbase@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
boom@0.4.x:
version "0.4.2"
resolved "https://registry.yarnpkg.com/boom/-/boom-0.4.2.tgz#7a636e9ded4efcefb19cef4947a3c67dfaee911b"
dependencies:
hoek "0.9.x"
boom@2.x.x:
version "2.10.1"
resolved "https://registry.yarnpkg.com/boom/-/boom-2.10.1.tgz#39c8918ceff5799f83f9492a848f625add0c766f"
@ -905,6 +931,10 @@ boom@5.x.x:
dependencies:
hoek "4.x.x"
bower@^1.8.2:
version "1.8.2"
resolved "https://registry.yarnpkg.com/bower/-/bower-1.8.2.tgz#adf53529c8d4af02ef24fb8d5341c1419d33e2f7"
brace-expansion@^1.0.0:
version "1.1.8"
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.8.tgz#c07b211c7c952ec1f8efd51a77ef0d1d3990a292"
@ -1020,6 +1050,10 @@ builtin-status-codes@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8"
bytes@2.5.0:
version "2.5.0"
resolved "https://registry.yarnpkg.com/bytes/-/bytes-2.5.0.tgz#4c9423ea2d252c270c41b2bdefeff9bb6b62c06a"
caller-path@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/caller-path/-/caller-path-0.1.0.tgz#94085ef63581ecd3daa92444a8fe94e82577751f"
@ -1303,6 +1337,12 @@ combined-stream@^1.0.5, combined-stream@~1.0.5:
dependencies:
delayed-stream "~1.0.0"
combined-stream@~0.0.4:
version "0.0.7"
resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-0.0.7.tgz#0137e657baa5a7541c57ac37ac5fc07d73b4dc1f"
dependencies:
delayed-stream "0.0.5"
commander@2.9.x, commander@^2.9.0, commander@~2.9.0:
version "2.9.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.9.0.tgz#9c99094176e12240cb22d6c5146098400fe0f7d4"
@ -1313,6 +1353,24 @@ commondir@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b"
compressible@~2.0.10:
version "2.0.11"
resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.11.tgz#16718a75de283ed8e604041625a2064586797d8a"
dependencies:
mime-db ">= 1.29.0 < 2"
compression@^1.0.11, compression@^1.7.0:
version "1.7.0"
resolved "https://registry.yarnpkg.com/compression/-/compression-1.7.0.tgz#030c9f198f1643a057d776a738e922da4373012d"
dependencies:
accepts "~1.3.3"
bytes "2.5.0"
compressible "~2.0.10"
debug "2.6.8"
on-headers "~1.0.1"
safe-buffer "5.1.1"
vary "~1.1.1"
concat-map@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
@ -1453,6 +1511,12 @@ cross-spawn@^3.0.0:
lru-cache "^4.0.1"
which "^1.2.9"
cryptiles@0.2.x:
version "0.2.2"
resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-0.2.2.tgz#ed91ff1f17ad13d3748288594f8a48a0d26f325c"
dependencies:
boom "0.4.x"
cryptiles@2.x.x:
version "2.0.5"
resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-2.0.5.tgz#3bdfecdc608147c1c67202fa291e7dca59eaa3b8"
@ -1592,6 +1656,10 @@ csso@~2.3.1:
clap "^1.0.9"
source-map "^0.5.3"
ctype@0.5.3:
version "0.5.3"
resolved "https://registry.yarnpkg.com/ctype/-/ctype-0.5.3.tgz#82c18c2461f74114ef16c135224ad0b9144ca12f"
currently-unhandled@^0.4.1:
version "0.4.1"
resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea"
@ -1634,17 +1702,23 @@ debug@2.6.7:
dependencies:
ms "2.0.0"
debug@^2.1.1, debug@^2.2.0, debug@^2.6.0:
debug@2.6.8, debug@^2.1.1, debug@^2.2.0, debug@^2.6.0:
version "2.6.8"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.8.tgz#e731531ca2ede27d188222427da17821d68ff4fc"
dependencies:
ms "2.0.0"
debug@2.6.9:
version "2.6.9"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
dependencies:
ms "2.0.0"
decamelize@^1.0.0, decamelize@^1.1.1, decamelize@^1.1.2:
version "1.2.0"
resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
deep-extend@~0.4.0:
deep-extend@^0.4.0, deep-extend@~0.4.0:
version "0.4.2"
resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.4.2.tgz#48b699c27e334bf89f10892be432f6e4c7d34a7f"
@ -1674,6 +1748,10 @@ del@^2.0.2:
pinkie-promise "^2.0.0"
rimraf "^2.2.8"
delayed-stream@0.0.5:
version "0.0.5"
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-0.0.5.tgz#d4b1f43a93e8296dfe02694f4680bc37a313c73f"
delayed-stream@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
@ -1690,6 +1768,10 @@ depd@1.1.0, depd@~1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.0.tgz#e1bd82c6aab6ced965b97b88b17ed3e528ca18c3"
depd@1.1.1, depd@~1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.1.tgz#5783b4e1c459f06fa5ca27f991f3d06e7a310359"
deprecated@^0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/deprecated/-/deprecated-0.0.1.tgz#f9c9af5464afa1e7a971458a8bdef2aa94d5bb19"
@ -1846,10 +1928,18 @@ ee-first@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
ejs@^2.3.4:
version "2.5.7"
resolved "https://registry.yarnpkg.com/ejs/-/ejs-2.5.7.tgz#cc872c168880ae3c7189762fd5ffc00896c9518a"
ejs@^2.5.6:
version "2.5.6"
resolved "https://registry.yarnpkg.com/ejs/-/ejs-2.5.6.tgz#479636bfa3fe3b1debd52087f0acb204b4f19c88"
ejs@~0.8.4:
version "0.8.8"
resolved "https://registry.yarnpkg.com/ejs/-/ejs-0.8.8.tgz#ffdc56dcc35d02926dd50ad13439bbc54061d598"
electron-to-chromium@^1.2.7, electron-to-chromium@^1.3.11:
version "1.3.13"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.13.tgz#1b3a5eace6e087bb5e257a100b0cbfe81b2891fc"
@ -2152,6 +2242,10 @@ etag@~1.8.0:
version "1.8.0"
resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.0.tgz#6f631aef336d6c46362b51764044ce216be3c051"
etag@~1.8.1:
version "1.8.1"
resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
event-emitter@~0.3.5:
version "0.3.5"
resolved "https://registry.yarnpkg.com/event-emitter/-/event-emitter-0.3.5.tgz#df8c69eef1647923c7157b9ce83840610b02cc39"
@ -2211,7 +2305,7 @@ expand-tilde@^2.0.2:
dependencies:
homedir-polyfill "^1.0.1"
express@^4.14.1, express@^4.15.2:
express@^4.15.2:
version "4.15.3"
resolved "https://registry.yarnpkg.com/express/-/express-4.15.3.tgz#bab65d0f03aa80c358408972fc700f916944b662"
dependencies:
@ -2244,6 +2338,39 @@ express@^4.14.1, express@^4.15.2:
utils-merge "1.0.0"
vary "~1.1.1"
express@^4.15.5, express@^4.8.5:
version "4.15.5"
resolved "https://registry.yarnpkg.com/express/-/express-4.15.5.tgz#670235ca9598890a5ae8170b83db722b842ed927"
dependencies:
accepts "~1.3.3"
array-flatten "1.1.1"
content-disposition "0.5.2"
content-type "~1.0.2"
cookie "0.3.1"
cookie-signature "1.0.6"
debug "2.6.9"
depd "~1.1.1"
encodeurl "~1.0.1"
escape-html "~1.0.3"
etag "~1.8.0"
finalhandler "~1.0.6"
fresh "0.5.2"
merge-descriptors "1.0.1"
methods "~1.1.2"
on-finished "~2.3.0"
parseurl "~1.3.1"
path-to-regexp "0.1.7"
proxy-addr "~1.1.5"
qs "6.5.0"
range-parser "~1.2.0"
send "0.15.6"
serve-static "1.12.6"
setprototypeof "1.0.3"
statuses "~1.3.1"
type-is "~1.6.15"
utils-merge "1.0.0"
vary "~1.1.1"
extend@^3.0.0, extend@~3.0.0, extend@~3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444"
@ -2340,6 +2467,18 @@ finalhandler@~1.0.3:
statuses "~1.3.1"
unpipe "~1.0.0"
finalhandler@~1.0.6:
version "1.0.6"
resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.0.6.tgz#007aea33d1a4d3e42017f624848ad58d212f814f"
dependencies:
debug "2.6.9"
encodeurl "~1.0.1"
escape-html "~1.0.3"
on-finished "~2.3.0"
parseurl "~1.3.2"
statuses "~1.3.1"
unpipe "~1.0.0"
find-cache-dir@^0.1.1:
version "0.1.1"
resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-0.1.1.tgz#c8defae57c8a52a8a784f9e31c57c742e993a0b9"
@ -2429,10 +2568,22 @@ for-own@^1.0.0:
dependencies:
for-in "^1.0.1"
forever-agent@~0.5.0:
version "0.5.2"
resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.5.2.tgz#6d0e09c4921f94a27f63d3b49c5feff1ea4c5130"
forever-agent@~0.6.1:
version "0.6.1"
resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91"
form-data@~0.1.0:
version "0.1.4"
resolved "https://registry.yarnpkg.com/form-data/-/form-data-0.1.4.tgz#91abd788aba9702b1aabfa8bc01031a2ac9e3b12"
dependencies:
async "~0.9.0"
combined-stream "~0.0.4"
mime "~1.2.11"
form-data@~2.1.1:
version "2.1.4"
resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.1.4.tgz#33c183acf193276ecaa98143a69e94bfee1750d1"
@ -2457,6 +2608,10 @@ fresh@0.5.0:
version "0.5.0"
resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.0.tgz#f474ca5e6a9246d6fd8e0953cfa9b9c805afa78e"
fresh@0.5.2:
version "0.5.2"
resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7"
friendly-errors-webpack-plugin@^1.1.3:
version "1.6.1"
resolved "https://registry.yarnpkg.com/friendly-errors-webpack-plugin/-/friendly-errors-webpack-plugin-1.6.1.tgz#e32781c4722f546a06a9b5d7a7cfa28520375d70"
@ -2868,6 +3023,15 @@ hash.js@^1.0.0, hash.js@^1.0.3:
dependencies:
inherits "^2.0.1"
hawk@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/hawk/-/hawk-1.1.1.tgz#87cd491f9b46e4e2aeaca335416766885d2d1ed9"
dependencies:
boom "0.4.x"
cryptiles "0.2.x"
hoek "0.9.x"
sntp "0.2.x"
hawk@~3.1.3:
version "3.1.3"
resolved "https://registry.yarnpkg.com/hawk/-/hawk-3.1.3.tgz#078444bd7c1640b0fe540d2c9b73d59678e8e1c4"
@ -2898,6 +3062,10 @@ hmac-drbg@^1.0.0:
minimalistic-assert "^1.0.0"
minimalistic-crypto-utils "^1.0.1"
hoek@0.9.x:
version "0.9.1"
resolved "https://registry.yarnpkg.com/hoek/-/hoek-0.9.1.tgz#3d322462badf07716ea7eb85baf88079cddce505"
hoek@2.x.x:
version "2.16.3"
resolved "https://registry.yarnpkg.com/hoek/-/hoek-2.16.3.tgz#20bb7403d3cea398e91dc4710a8ff1b8274a25ed"
@ -2988,6 +3156,15 @@ http-errors@~1.6.1:
setprototypeof "1.0.3"
statuses ">= 1.3.1 < 2"
http-errors@~1.6.2:
version "1.6.2"
resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.2.tgz#0a002cc85707192a7e7946ceedc11155f60ec736"
dependencies:
depd "1.1.1"
inherits "2.0.3"
setprototypeof "1.0.3"
statuses ">= 1.3.1 < 2"
http-proxy-middleware@^0.17.3:
version "0.17.4"
resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-0.17.4.tgz#642e8848851d66f09d4f124912846dbaeb41b833"
@ -3004,6 +3181,14 @@ http-proxy@^1.16.2:
eventemitter3 "1.x.x"
requires-port "1.x.x"
http-signature@~0.10.0:
version "0.10.1"
resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-0.10.1.tgz#4fbdac132559aa8323121e540779c0a012b27e66"
dependencies:
asn1 "0.1.11"
assert-plus "^0.1.5"
ctype "0.5.3"
http-signature@~1.1.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.1.1.tgz#df72e267066cd0ac67fb76adf8e134a8fbcf91bf"
@ -3038,6 +3223,10 @@ ieee754@^1.1.4:
version "1.1.8"
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.8.tgz#be33d40ac10ef1926701f6f08a2d86fbfd1ad3e4"
ignore-loader@^0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/ignore-loader/-/ignore-loader-0.1.2.tgz#d81f240376d0ba4f0d778972c3ad25874117a463"
ignore@^3.2.0:
version "3.3.3"
resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.3.tgz#432352e57accd87ab3110e82d3fea0e47812156d"
@ -3134,6 +3323,10 @@ ipaddr.js@1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.3.0.tgz#1e03a52fdad83a8bbb2b25cbf4998b4cffcd3dec"
ipaddr.js@1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.4.0.tgz#296aca878a821816e5b85d0a285a99bcff4582f0"
irregular-plurals@^1.0.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/irregular-plurals/-/irregular-plurals-1.2.0.tgz#38f299834ba8c00c30be9c554e137269752ff3ac"
@ -3430,7 +3623,7 @@ json-stable-stringify@^1.0.0, json-stable-stringify@^1.0.1:
dependencies:
jsonify "~0.0.0"
json-stringify-safe@~5.0.1:
json-stringify-safe@~5.0.0, json-stringify-safe@~5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb"
@ -3580,7 +3773,7 @@ loader-runner@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.3.0.tgz#f482aea82d543e07921700d5a46ef26fdac6b8a2"
loader-utils@^0.2.15, loader-utils@^0.2.16:
loader-utils@0.2.x, loader-utils@^0.2.15, loader-utils@^0.2.16:
version "0.2.17"
resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-0.2.17.tgz#f86e6374d43205a6e6c60e9196f17c0299bfb348"
dependencies:
@ -3929,20 +4122,24 @@ miller-rabin@^4.0.0:
bn.js "^4.0.0"
brorand "^1.0.1"
"mime-db@>= 1.29.0 < 2", mime-db@~1.30.0:
version "1.30.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.30.0.tgz#74c643da2dd9d6a45399963465b26d5ca7d71f01"
mime-db@~1.27.0:
version "1.27.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.27.0.tgz#820f572296bbd20ec25ed55e5b5de869e5436eb1"
mime-db@~1.30.0:
version "1.30.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.30.0.tgz#74c643da2dd9d6a45399963465b26d5ca7d71f01"
mime-types@^2.1.12, mime-types@~2.1.11, mime-types@~2.1.15, mime-types@~2.1.7:
version "2.1.15"
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.15.tgz#a4ebf5064094569237b8cf70046776d09fc92aed"
dependencies:
mime-db "~1.27.0"
mime-types@~1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-1.0.2.tgz#995ae1392ab8affcbfcb2641dd054e943c0d5dce"
mime-types@~2.1.17:
version "2.1.17"
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.17.tgz#09d7a393f03e995a79f8af857b70a9e0ab16557a"
@ -3957,6 +4154,10 @@ mime@1.3.x, mime@^1.3.4:
version "1.3.6"
resolved "https://registry.yarnpkg.com/mime/-/mime-1.3.6.tgz#591d84d3653a6b0b4a3b9df8de5aa8108e72e5e0"
mime@~1.2.11:
version "1.2.11"
resolved "https://registry.yarnpkg.com/mime/-/mime-1.2.11.tgz#58203eed86e3a5ef17aed2b7d9ebd47f0a60dd10"
mimic-fn@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.1.0.tgz#e667783d92e89dbd342818b5230b9d62a672ad18"
@ -4193,6 +4394,10 @@ node-sass@^4.5.3:
sass-graph "^2.1.1"
stdout-stream "^1.4.0"
node-uuid@~1.4.0:
version "1.4.8"
resolved "https://registry.yarnpkg.com/node-uuid/-/node-uuid-1.4.8.tgz#b040eb0923968afabf8d32fb1f17f1167fdab907"
noop-fn@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/noop-fn/-/noop-fn-1.0.0.tgz#5f33d47f13d2150df93e0cb036699e982f78ffbf"
@ -4269,6 +4474,10 @@ number-is-nan@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d"
oauth-sign@~0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.3.0.tgz#cb540f93bb2b22a7d5941691a288d60e8ea9386e"
oauth-sign@~0.8.1, oauth-sign@~0.8.2:
version "0.8.2"
resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43"
@ -4307,12 +4516,26 @@ object.pick@^1.2.0:
dependencies:
isobject "^2.1.0"
offline-plugin@^4.8.4:
version "4.8.4"
resolved "https://registry.yarnpkg.com/offline-plugin/-/offline-plugin-4.8.4.tgz#1084c59f6606bded5ee5a6bf6208e2b9f5bdd339"
dependencies:
deep-extend "^0.4.0"
ejs "^2.3.4"
loader-utils "0.2.x"
minimatch "^3.0.3"
slash "^1.0.0"
on-finished@~2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947"
dependencies:
ee-first "1.1.1"
on-headers@~1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.1.tgz#928f5d0f470d49342651ea6794b0857c100693f7"
once@^1.3.0, once@^1.3.3:
version "1.4.0"
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
@ -4483,6 +4706,10 @@ parseurl@~1.3.1:
version "1.3.1"
resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.1.tgz#c8ab8c9223ba34888aa64a297b28853bec18da56"
parseurl@~1.3.2:
version "1.3.2"
resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.2.tgz#fc289d4ed8993119460c156253262cdc8de65bf3"
path-browserify@0.0.0:
version "0.0.0"
resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-0.0.0.tgz#a0b870729aae214005b7d5032ec2cbbb0fb4451a"
@ -4959,6 +5186,13 @@ proxy-addr@~1.1.4:
forwarded "~0.1.0"
ipaddr.js "1.3.0"
proxy-addr@~1.1.5:
version "1.1.5"
resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-1.1.5.tgz#71c0ee3b102de3f202f3b64f608d173fcba1a918"
dependencies:
forwarded "~0.1.0"
ipaddr.js "1.4.0"
prr@~0.0.0:
version "0.0.0"
resolved "https://registry.yarnpkg.com/prr/-/prr-0.0.0.tgz#1a84b85908325501411853d0081ee3fa86e2926a"
@ -4993,6 +5227,14 @@ qs@6.4.0, qs@~6.4.0:
version "6.4.0"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233"
qs@6.5.0:
version "6.5.0"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.0.tgz#8d04954d364def3efc55b5a0793e1e2c8b1e6e49"
qs@~1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/qs/-/qs-1.0.2.tgz#50a93e2b5af6691c31bcea5dae78ee6ea1903768"
qs@~6.5.1:
version "6.5.1"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8"
@ -5089,6 +5331,15 @@ readable-stream@1.0, "readable-stream@>=1.0.33-1 <1.1.0-0":
isarray "0.0.1"
string_decoder "~0.10.x"
readable-stream@1.0.27-1:
version "1.0.27-1"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.0.27-1.tgz#6b67983c20357cefd07f0165001a16d710d91078"
dependencies:
core-util-is "~1.0.0"
inherits "~2.0.1"
isarray "0.0.1"
string_decoder "~0.10.x"
readable-stream@^1.0.33, readable-stream@~1.1.9:
version "1.1.14"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9"
@ -5309,6 +5560,25 @@ request@^2.82.0:
tunnel-agent "^0.6.0"
uuid "^3.1.0"
request@~2.40.0:
version "2.40.0"
resolved "https://registry.yarnpkg.com/request/-/request-2.40.0.tgz#4dd670f696f1e6e842e66b4b5e839301ab9beb67"
dependencies:
forever-agent "~0.5.0"
json-stringify-safe "~5.0.0"
mime-types "~1.0.1"
node-uuid "~1.4.0"
qs "~1.0.0"
optionalDependencies:
aws-sign2 "~0.5.0"
form-data "~0.1.0"
hawk "1.1.1"
http-signature "~0.10.0"
oauth-sign "~0.3.0"
stringstream "~0.0.4"
tough-cookie ">=0.12.0"
tunnel-agent "~0.4.0"
require-directory@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
@ -5396,14 +5666,14 @@ rx-lite@^3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-3.1.2.tgz#19ce502ca572665f3b647b10939f97fd1615f102"
safe-buffer@5.1.1, safe-buffer@^5.1.1, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
version "5.1.1"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853"
safe-buffer@^5.0.1, safe-buffer@~5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.0.1.tgz#d263ca54696cd8a306b5ca6551e92de57918fbe7"
safe-buffer@^5.1.1, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
version "5.1.1"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853"
sass-graph@^2.1.1:
version "2.2.4"
resolved "https://registry.yarnpkg.com/sass-graph/-/sass-graph-2.2.4.tgz#13fbd63cd1caf0908b9fd93476ad43a51d1e0b49"
@ -5470,6 +5740,24 @@ send@0.15.3:
range-parser "~1.2.0"
statuses "~1.3.1"
send@0.15.6:
version "0.15.6"
resolved "https://registry.yarnpkg.com/send/-/send-0.15.6.tgz#20f23a9c925b762ab82705fe2f9db252ace47e34"
dependencies:
debug "2.6.9"
depd "~1.1.1"
destroy "~1.0.4"
encodeurl "~1.0.1"
escape-html "~1.0.3"
etag "~1.8.1"
fresh "0.5.2"
http-errors "~1.6.2"
mime "1.3.4"
ms "2.0.0"
on-finished "~2.3.0"
range-parser "~1.2.0"
statuses "~1.3.1"
sequencify@~0.0.7:
version "0.0.7"
resolved "https://registry.yarnpkg.com/sequencify/-/sequencify-0.0.7.tgz#90cff19d02e07027fd767f5ead3e7b95d1e7380c"
@ -5483,6 +5771,15 @@ serve-static@1.12.3:
parseurl "~1.3.1"
send "0.15.3"
serve-static@1.12.6, serve-static@^1.12.6, serve-static@^1.6.5:
version "1.12.6"
resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.12.6.tgz#b973773f63449934da54e5beba5e31d9f4211577"
dependencies:
encodeurl "~1.0.1"
escape-html "~1.0.3"
parseurl "~1.3.2"
send "0.15.6"
set-blocking@^2.0.0, set-blocking@~2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
@ -5538,6 +5835,12 @@ slice-ansi@0.0.4:
version "0.0.4"
resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-0.0.4.tgz#edbf8903f66f7ce2f8eafd6ceed65e264c831b35"
sntp@0.2.x:
version "0.2.4"
resolved "https://registry.yarnpkg.com/sntp/-/sntp-0.2.4.tgz#fb885f18b0f3aad189f824862536bceeec750900"
dependencies:
hoek "0.9.x"
sntp@1.x.x:
version "1.0.9"
resolved "https://registry.yarnpkg.com/sntp/-/sntp-1.0.9.tgz#6541184cc90aeea6c6e7b35e2659082443c66198"
@ -5619,6 +5922,14 @@ sqlite3@^3.1.3:
nan "~2.4.0"
node-pre-gyp "~0.6.31"
ssh2@^0.3.5:
version "0.3.6"
resolved "https://registry.yarnpkg.com/ssh2/-/ssh2-0.3.6.tgz#49034434aee3821ee5fc22b952081e7801ff92ed"
dependencies:
asn1 "0.2.1"
readable-stream "1.0.27-1"
streamsearch "0.1.2"
sshpk@^1.7.0:
version "1.13.0"
resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.13.0.tgz#ff2a3e4fd04497555fed97b39a0fd82fafb3a33c"
@ -5634,6 +5945,18 @@ sshpk@^1.7.0:
jsbn "~0.1.0"
tweetnacl "~0.14.0"
stackedit@^4.3.15:
version "4.3.15"
resolved "https://registry.yarnpkg.com/stackedit/-/stackedit-4.3.15.tgz#0fc3715cd93f6c93f761d797173d6869c0794009"
dependencies:
bower "^1.8.2"
compression "^1.0.11"
ejs "~0.8.4"
express "^4.8.5"
request "~2.40.0"
serve-static "^1.6.5"
ssh2 "^0.3.5"
stackframe@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/stackframe/-/stackframe-1.0.3.tgz#fe64ab20b170e4ce49044b126c119dfa0e5dc7cc"
@ -5676,6 +5999,10 @@ stream-http@^2.3.1:
to-arraybuffer "^1.0.0"
xtend "^4.0.0"
streamsearch@0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-0.1.2.tgz#808b9d0e56fc273d809ba57338e929919a1a9f1a"
strict-uri-encode@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713"
@ -5990,15 +6317,15 @@ toposort@^1.0.0:
version "1.0.3"
resolved "https://registry.yarnpkg.com/toposort/-/toposort-1.0.3.tgz#f02cd8a74bd8be2fc0e98611c3bacb95a171869c"
tough-cookie@~2.3.0:
version "2.3.2"
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.2.tgz#f081f76e4c85720e6c37a5faced737150d84072a"
tough-cookie@>=0.12.0, tough-cookie@~2.3.2:
version "2.3.3"
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.3.tgz#0b618a5565b6dea90bf3425d04d55edc475a7561"
dependencies:
punycode "^1.4.1"
tough-cookie@~2.3.2:
version "2.3.3"
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.3.tgz#0b618a5565b6dea90bf3425d04d55edc475a7561"
tough-cookie@~2.3.0:
version "2.3.2"
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.2.tgz#f081f76e4c85720e6c37a5faced737150d84072a"
dependencies:
punycode "^1.4.1"
@ -6024,6 +6351,10 @@ tunnel-agent@^0.6.0:
dependencies:
safe-buffer "^5.0.1"
tunnel-agent@~0.4.0:
version "0.4.3"
resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.4.3.tgz#6373db76909fe570e08d73583365ed828a74eeeb"
tweetnacl@^0.14.3, tweetnacl@~0.14.0:
version "0.14.5"
resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"