Added server conf endpoint.
New localDbSvc.getWorkspaceItems method used to export workspaces. Added offline availability in the workspace management modal. New accordion in the badge management modal. Add badge creation checks in unit tests.
This commit is contained in:
parent
3f0597601e
commit
07d824faca
@ -16,9 +16,7 @@ module.exports = {
|
|||||||
],
|
],
|
||||||
globals: {
|
globals: {
|
||||||
"NODE_ENV": false,
|
"NODE_ENV": false,
|
||||||
"VERSION": false,
|
"VERSION": false
|
||||||
"GOOGLE_CLIENT_ID": false,
|
|
||||||
"GITHUB_CLIENT_ID": false
|
|
||||||
},
|
},
|
||||||
// check if imports actually resolve
|
// check if imports actually resolve
|
||||||
'settings': {
|
'settings': {
|
||||||
|
@ -19,9 +19,7 @@ module.exports = merge(baseWebpackConfig, {
|
|||||||
devtool: 'source-map',
|
devtool: 'source-map',
|
||||||
plugins: [
|
plugins: [
|
||||||
new webpack.DefinePlugin({
|
new webpack.DefinePlugin({
|
||||||
NODE_ENV: config.dev.env.NODE_ENV,
|
NODE_ENV: config.dev.env.NODE_ENV
|
||||||
GOOGLE_CLIENT_ID: config.dev.env.GOOGLE_CLIENT_ID,
|
|
||||||
GITHUB_CLIENT_ID: config.dev.env.GITHUB_CLIENT_ID
|
|
||||||
}),
|
}),
|
||||||
// https://github.com/glenjamin/webpack-hot-middleware#installation--usage
|
// https://github.com/glenjamin/webpack-hot-middleware#installation--usage
|
||||||
new webpack.HotModuleReplacementPlugin(),
|
new webpack.HotModuleReplacementPlugin(),
|
||||||
|
@ -2,7 +2,5 @@ var merge = require('webpack-merge')
|
|||||||
var prodEnv = require('./prod.env')
|
var prodEnv = require('./prod.env')
|
||||||
|
|
||||||
module.exports = merge(prodEnv, {
|
module.exports = merge(prodEnv, {
|
||||||
NODE_ENV: '"development"',
|
NODE_ENV: '"development"'
|
||||||
GOOGLE_CLIENT_ID: '"241271498917-c3loeet001r90q6u79q484bsh5clg4fr.apps.googleusercontent.com"',
|
|
||||||
GITHUB_CLIENT_ID: '"cbf0cf25cfd026be23e1"'
|
|
||||||
})
|
})
|
||||||
|
@ -24,7 +24,7 @@ module.exports = {
|
|||||||
dev: {
|
dev: {
|
||||||
env: require('./dev.env'),
|
env: require('./dev.env'),
|
||||||
port: 8080,
|
port: 8080,
|
||||||
autoOpenBrowser: true,
|
autoOpenBrowser: false,
|
||||||
assetsSubDirectory: 'static',
|
assetsSubDirectory: 'static',
|
||||||
assetsPublicPath: '/',
|
assetsPublicPath: '/',
|
||||||
proxyTable: {},
|
proxyTable: {},
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
NODE_ENV: '"production"',
|
NODE_ENV: '"production"'
|
||||||
GOOGLE_CLIENT_ID: '"241271498917-t4t7d07qis7oc0ahaskbif3ft6tk63cd.apps.googleusercontent.com"',
|
|
||||||
GITHUB_CLIENT_ID: '"30c1491057c9ad4dbd56"'
|
|
||||||
}
|
}
|
||||||
|
37
server/conf.js
Normal file
37
server/conf.js
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
const pandocPath = process.env.PANDOC_PATH || 'pandoc';
|
||||||
|
const wkhtmltopdfPath = process.env.WKHTMLTOPDF_PATH || 'wkhtmltopdf';
|
||||||
|
const userBucketName = process.env.USER_BUCKET_NAME || 'stackedit-users';
|
||||||
|
const paypalUri = process.env.PAYPAL_URI || 'https://www.paypal.com/cgi-bin/webscr';
|
||||||
|
const paypalReceiverEmail = process.env.PAYPAL_RECEIVER_EMAIL;
|
||||||
|
|
||||||
|
const dropboxAppKey = process.env.DROPBOX_APP_KEY;
|
||||||
|
const dropboxAppKeyFull = process.env.DROPBOX_APP_KEY_FULL;
|
||||||
|
const githubClientId = process.env.GITHUB_CLIENT_ID;
|
||||||
|
const githubClientSecret = process.env.GITHUB_CLIENT_SECRET;
|
||||||
|
const googleClientId = process.env.GOOGLE_CLIENT_ID;
|
||||||
|
const googleApiKey = process.env.GOOGLE_API_KEY;
|
||||||
|
const wordpressClientId = process.env.WORDPRESS_CLIENT_ID;
|
||||||
|
|
||||||
|
exports.values = {
|
||||||
|
pandocPath,
|
||||||
|
wkhtmltopdfPath,
|
||||||
|
userBucketName,
|
||||||
|
paypalUri,
|
||||||
|
paypalReceiverEmail,
|
||||||
|
dropboxAppKey,
|
||||||
|
dropboxAppKeyFull,
|
||||||
|
githubClientId,
|
||||||
|
githubClientSecret,
|
||||||
|
googleClientId,
|
||||||
|
googleApiKey,
|
||||||
|
wordpressClientId,
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.publicValues = {
|
||||||
|
dropboxAppKey,
|
||||||
|
dropboxAppKeyFull,
|
||||||
|
githubClientId,
|
||||||
|
googleClientId,
|
||||||
|
googleApiKey,
|
||||||
|
wordpressClientId,
|
||||||
|
};
|
@ -1,5 +1,6 @@
|
|||||||
const qs = require('qs'); // eslint-disable-line import/no-extraneous-dependencies
|
const qs = require('qs'); // eslint-disable-line import/no-extraneous-dependencies
|
||||||
const request = require('request');
|
const request = require('request');
|
||||||
|
const conf = require('./conf');
|
||||||
|
|
||||||
function githubToken(clientId, code) {
|
function githubToken(clientId, code) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
@ -8,7 +9,7 @@ function githubToken(clientId, code) {
|
|||||||
url: 'https://github.com/login/oauth/access_token',
|
url: 'https://github.com/login/oauth/access_token',
|
||||||
qs: {
|
qs: {
|
||||||
client_id: clientId,
|
client_id: clientId,
|
||||||
client_secret: process.env.GITHUB_SECRET,
|
client_secret: conf.values.githubClientSecret,
|
||||||
code,
|
code,
|
||||||
},
|
},
|
||||||
}, (err, res, body) => {
|
}, (err, res, body) => {
|
||||||
|
@ -6,6 +6,7 @@ const user = require('./user');
|
|||||||
const github = require('./github');
|
const github = require('./github');
|
||||||
const pdf = require('./pdf');
|
const pdf = require('./pdf');
|
||||||
const pandoc = require('./pandoc');
|
const pandoc = require('./pandoc');
|
||||||
|
const conf = require('./conf');
|
||||||
|
|
||||||
const resolvePath = pathToResolve => path.join(__dirname, '..', pathToResolve);
|
const resolvePath = pathToResolve => path.join(__dirname, '..', pathToResolve);
|
||||||
|
|
||||||
@ -24,6 +25,7 @@ module.exports = (app, serveV4) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
app.get('/oauth2/githubToken', github.githubToken);
|
app.get('/oauth2/githubToken', github.githubToken);
|
||||||
|
app.get('/conf', (req, res) => res.send(conf.publicValues));
|
||||||
app.get('/userInfo', user.userInfo);
|
app.get('/userInfo', user.userInfo);
|
||||||
app.post('/pdfExport', pdf.generate);
|
app.post('/pdfExport', pdf.generate);
|
||||||
app.post('/pandocExport', pandoc.generate);
|
app.post('/pandocExport', pandoc.generate);
|
||||||
|
@ -3,6 +3,7 @@ const { spawn } = require('child_process');
|
|||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const tmp = require('tmp');
|
const tmp = require('tmp');
|
||||||
const user = require('./user');
|
const user = require('./user');
|
||||||
|
const conf = require('./conf');
|
||||||
|
|
||||||
const outputFormats = {
|
const outputFormats = {
|
||||||
asciidoc: 'text/plain',
|
asciidoc: 'text/plain',
|
||||||
@ -90,10 +91,9 @@ exports.generate = (req, res) => {
|
|||||||
reject(error);
|
reject(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
const binPath = process.env.PANDOC_PATH || 'pandoc';
|
|
||||||
const format = outputFormat === 'pdf' ? 'latex' : outputFormat;
|
const format = outputFormat === 'pdf' ? 'latex' : outputFormat;
|
||||||
params.push('-f', 'json', '-t', format, '-o', filePath);
|
params.push('-f', 'json', '-t', format, '-o', filePath);
|
||||||
const pandoc = spawn(binPath, params, {
|
const pandoc = spawn(conf.values.pandocPath, params, {
|
||||||
stdio: [
|
stdio: [
|
||||||
'pipe',
|
'pipe',
|
||||||
'ignore',
|
'ignore',
|
||||||
|
@ -3,6 +3,7 @@ const { spawn } = require('child_process');
|
|||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const tmp = require('tmp');
|
const tmp = require('tmp');
|
||||||
const user = require('./user');
|
const user = require('./user');
|
||||||
|
const conf = require('./conf');
|
||||||
|
|
||||||
/* eslint-disable no-var, prefer-arrow-callback, func-names */
|
/* eslint-disable no-var, prefer-arrow-callback, func-names */
|
||||||
function waitForJavaScript() {
|
function waitForJavaScript() {
|
||||||
@ -127,10 +128,9 @@ exports.generate = (req, res) => {
|
|||||||
params.push('--page-size', !authorizedPageSizes.includes(options.pageSize) ? 'A4' : options.pageSize);
|
params.push('--page-size', !authorizedPageSizes.includes(options.pageSize) ? 'A4' : options.pageSize);
|
||||||
|
|
||||||
// Use a temp file as wkhtmltopdf can't access /dev/stdout on Amazon EC2 for some reason
|
// Use a temp file as wkhtmltopdf can't access /dev/stdout on Amazon EC2 for some reason
|
||||||
const binPath = process.env.WKHTMLTOPDF_PATH || 'wkhtmltopdf';
|
|
||||||
params.push('--run-script', `${waitForJavaScript.toString()}waitForJavaScript()`);
|
params.push('--run-script', `${waitForJavaScript.toString()}waitForJavaScript()`);
|
||||||
params.push('--window-status', 'done');
|
params.push('--window-status', 'done');
|
||||||
const wkhtmltopdf = spawn(binPath, params.concat('-', filePath), {
|
const wkhtmltopdf = spawn(conf.values.wkhtmltopdfPath, params.concat('-', filePath), {
|
||||||
stdio: [
|
stdio: [
|
||||||
'pipe',
|
'pipe',
|
||||||
'ignore',
|
'ignore',
|
||||||
|
@ -1,13 +1,8 @@
|
|||||||
const request = require('request');
|
const request = require('request');
|
||||||
const AWS = require('aws-sdk');
|
const AWS = require('aws-sdk');
|
||||||
const verifier = require('google-id-token-verifier');
|
const verifier = require('google-id-token-verifier');
|
||||||
|
const conf = require('./conf');
|
||||||
|
|
||||||
const {
|
|
||||||
USER_BUCKET_NAME = 'stackedit-users',
|
|
||||||
PAYPAL_URI = 'https://www.paypal.com/cgi-bin/webscr',
|
|
||||||
PAYPAL_RECEIVER_EMAIL = 'stackedit.project@gmail.com',
|
|
||||||
GOOGLE_CLIENT_ID,
|
|
||||||
} = process.env;
|
|
||||||
const s3Client = new AWS.S3();
|
const s3Client = new AWS.S3();
|
||||||
|
|
||||||
const cb = (resolve, reject) => (err, res) => {
|
const cb = (resolve, reject) => (err, res) => {
|
||||||
@ -20,7 +15,7 @@ const cb = (resolve, reject) => (err, res) => {
|
|||||||
|
|
||||||
exports.getUser = id => new Promise((resolve, reject) => {
|
exports.getUser = id => new Promise((resolve, reject) => {
|
||||||
s3Client.getObject({
|
s3Client.getObject({
|
||||||
Bucket: USER_BUCKET_NAME,
|
Bucket: conf.values.userBucketName,
|
||||||
Key: id,
|
Key: id,
|
||||||
}, cb(resolve, reject));
|
}, cb(resolve, reject));
|
||||||
})
|
})
|
||||||
@ -35,7 +30,7 @@ exports.getUser = id => new Promise((resolve, reject) => {
|
|||||||
|
|
||||||
exports.putUser = (id, user) => new Promise((resolve, reject) => {
|
exports.putUser = (id, user) => new Promise((resolve, reject) => {
|
||||||
s3Client.putObject({
|
s3Client.putObject({
|
||||||
Bucket: USER_BUCKET_NAME,
|
Bucket: conf.values.userBucketName,
|
||||||
Key: id,
|
Key: id,
|
||||||
Body: JSON.stringify(user),
|
Body: JSON.stringify(user),
|
||||||
}, cb(resolve, reject));
|
}, cb(resolve, reject));
|
||||||
@ -43,13 +38,13 @@ exports.putUser = (id, user) => new Promise((resolve, reject) => {
|
|||||||
|
|
||||||
exports.removeUser = id => new Promise((resolve, reject) => {
|
exports.removeUser = id => new Promise((resolve, reject) => {
|
||||||
s3Client.deleteObject({
|
s3Client.deleteObject({
|
||||||
Bucket: USER_BUCKET_NAME,
|
Bucket: conf.values.userBucketName,
|
||||||
Key: id,
|
Key: id,
|
||||||
}, cb(resolve, reject));
|
}, cb(resolve, reject));
|
||||||
});
|
});
|
||||||
|
|
||||||
exports.getUserFromToken = idToken => new Promise((resolve, reject) => verifier
|
exports.getUserFromToken = idToken => new Promise((resolve, reject) => verifier
|
||||||
.verify(idToken, GOOGLE_CLIENT_ID, cb(resolve, reject)))
|
.verify(idToken, conf.values.googleClientId, cb(resolve, reject)))
|
||||||
.then(tokenInfo => exports.getUser(tokenInfo.sub));
|
.then(tokenInfo => exports.getUser(tokenInfo.sub));
|
||||||
|
|
||||||
exports.userInfo = (req, res) => exports.getUserFromToken(req.query.idToken)
|
exports.userInfo = (req, res) => exports.getUserFromToken(req.query.idToken)
|
||||||
@ -78,7 +73,7 @@ exports.paypalIpn = (req, res, next) => Promise.resolve()
|
|||||||
sponsorUntil = Date.now() + (5 * 366 * 24 * 60 * 60 * 1000); // 5 years
|
sponsorUntil = Date.now() + (5 * 366 * 24 * 60 * 60 * 1000); // 5 years
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
req.body.receiver_email !== PAYPAL_RECEIVER_EMAIL ||
|
req.body.receiver_email !== conf.values.paypalReceiverEmail ||
|
||||||
req.body.payment_status !== 'Completed' ||
|
req.body.payment_status !== 'Completed' ||
|
||||||
req.body.mc_currency !== 'USD' ||
|
req.body.mc_currency !== 'USD' ||
|
||||||
(req.body.txn_type !== 'web_accept' && req.body.txn_type !== 'subscr_payment') ||
|
(req.body.txn_type !== 'web_accept' && req.body.txn_type !== 'subscr_payment') ||
|
||||||
@ -90,7 +85,7 @@ exports.paypalIpn = (req, res, next) => Promise.resolve()
|
|||||||
// Processing PayPal IPN
|
// Processing PayPal IPN
|
||||||
req.body.cmd = '_notify-validate';
|
req.body.cmd = '_notify-validate';
|
||||||
return new Promise((resolve, reject) => request.post({
|
return new Promise((resolve, reject) => request.post({
|
||||||
uri: PAYPAL_URI,
|
uri: conf.values.paypalUri,
|
||||||
form: req.body,
|
form: req.body,
|
||||||
}, (err, response, body) => {
|
}, (err, response, body) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
|
@ -156,7 +156,7 @@ export default {
|
|||||||
...sourceNode.item,
|
...sourceNode.item,
|
||||||
parentId: targetNode.item.id,
|
parentId: targetNode.item.id,
|
||||||
});
|
});
|
||||||
badgeSvc.addBadge('moveFiles');
|
badgeSvc.addBadge(sourceNode.isFolder ? 'moveFolder' : 'moveFile');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async onContextMenu(evt) {
|
async onContextMenu(evt) {
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
<div class="notification__item flex flex--row flex--align-center" v-for="(item, idx) in items" :key="idx">
|
<div class="notification__item flex flex--row flex--align-center" v-for="(item, idx) in items" :key="idx">
|
||||||
<div class="notification__icon flex flex--column flex--center">
|
<div class="notification__icon flex flex--column flex--center">
|
||||||
<icon-alert v-if="item.type === 'error'"></icon-alert>
|
<icon-alert v-if="item.type === 'error'"></icon-alert>
|
||||||
<icon-check-circle v-if="item.type === 'badge'"></icon-check-circle>
|
<icon-check-circle v-else-if="item.type === 'badge'"></icon-check-circle>
|
||||||
<icon-information v-else></icon-information>
|
<icon-information v-else></icon-information>
|
||||||
</div>
|
</div>
|
||||||
<div class="notification__content">
|
<div class="notification__content">
|
||||||
|
@ -175,8 +175,9 @@ export default {
|
|||||||
|
|
||||||
p {
|
p {
|
||||||
margin: 10px 15px;
|
margin: 10px 15px;
|
||||||
line-height: 1.5;
|
font-size: 0.9rem;
|
||||||
font-style: italic;
|
opacity: 0.67;
|
||||||
|
line-height: 1.3;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -84,25 +84,25 @@
|
|||||||
Print
|
Print
|
||||||
</menu-entry>
|
</menu-entry>
|
||||||
<hr>
|
<hr>
|
||||||
<menu-entry @click.native="settings">
|
<menu-entry @click.native="badges">
|
||||||
<icon-settings slot="icon"></icon-settings>
|
<icon-seal slot="icon"></icon-seal>
|
||||||
<div>Settings</div>
|
<div><div class="menu-entry__label menu-entry__label--count">{{badgeCount}}/{{featureCount}}</div> Badges</div>
|
||||||
<span>Tweak application and keyboard shortcuts.</span>
|
<span>List application features and earned badges.</span>
|
||||||
|
</menu-entry>
|
||||||
|
<menu-entry @click.native="accounts">
|
||||||
|
<icon-key slot="icon"></icon-key>
|
||||||
|
<div><div class="menu-entry__label menu-entry__label--count">{{accountCount}}</div> Accounts</div>
|
||||||
|
<span>Manage access to your external accounts.</span>
|
||||||
</menu-entry>
|
</menu-entry>
|
||||||
<menu-entry @click.native="templates">
|
<menu-entry @click.native="templates">
|
||||||
<icon-code-braces slot="icon"></icon-code-braces>
|
<icon-code-braces slot="icon"></icon-code-braces>
|
||||||
<div><div class="menu-entry__label menu-entry__label--count">{{templateCount}}</div> Templates</div>
|
<div><div class="menu-entry__label menu-entry__label--count">{{templateCount}}</div> Templates</div>
|
||||||
<span>Configure Handlebars templates for your exports.</span>
|
<span>Configure Handlebars templates for your exports.</span>
|
||||||
</menu-entry>
|
</menu-entry>
|
||||||
<menu-entry @click.native="accounts">
|
<menu-entry @click.native="settings">
|
||||||
<icon-key slot="icon"></icon-key>
|
<icon-settings slot="icon"></icon-settings>
|
||||||
<div><div class="menu-entry__label menu-entry__label--count">{{accountCount}}</div> User accounts</div>
|
<div>Settings</div>
|
||||||
<span>Manage access to your external accounts.</span>
|
<span>Tweak application and keyboard shortcuts.</span>
|
||||||
</menu-entry>
|
|
||||||
<menu-entry @click.native="badges">
|
|
||||||
<icon-seal slot="icon"></icon-seal>
|
|
||||||
<div><div class="menu-entry__label menu-entry__label--count">{{badgeCount}}/{{featureCount}}</div> Badges</div>
|
|
||||||
<span>List application features and earned badges.</span>
|
|
||||||
</menu-entry>
|
</menu-entry>
|
||||||
<hr>
|
<hr>
|
||||||
<menu-entry @click.native="setPanel('workspaceBackups')">
|
<menu-entry @click.native="setPanel('workspaceBackups')">
|
||||||
@ -111,10 +111,8 @@
|
|||||||
</menu-entry>
|
</menu-entry>
|
||||||
<menu-entry @click.native="reset">
|
<menu-entry @click.native="reset">
|
||||||
<icon-logout slot="icon"></icon-logout>
|
<icon-logout slot="icon"></icon-logout>
|
||||||
<div>Reset application</div>
|
Reset application
|
||||||
<span>Sign out and clean all workspace data.</span>
|
|
||||||
</menu-entry>
|
</menu-entry>
|
||||||
<hr>
|
|
||||||
<menu-entry @click.native="about">
|
<menu-entry @click.native="about">
|
||||||
<icon-help-circle slot="icon"></icon-help-circle>
|
<icon-help-circle slot="icon"></icon-help-circle>
|
||||||
About StackEdit
|
About StackEdit
|
||||||
@ -218,7 +216,7 @@ export default {
|
|||||||
async reset() {
|
async reset() {
|
||||||
try {
|
try {
|
||||||
await store.dispatch('modal/open', 'reset');
|
await store.dispatch('modal/open', 'reset');
|
||||||
window.location.href = '#reset=true';
|
localStorage.setItem('resetStackEdit', '1');
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
} catch (e) { /* Cancel */ }
|
} catch (e) { /* Cancel */ }
|
||||||
},
|
},
|
||||||
|
@ -4,9 +4,6 @@
|
|||||||
<p>{{currentFileName}} can't be published as it's a temporary file.</p>
|
<p>{{currentFileName}} can't be published as it's a temporary file.</p>
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<div class="side-bar__info" v-if="noToken">
|
|
||||||
<p>You have to <b>link an account</b> to start publishing files.</p>
|
|
||||||
</div>
|
|
||||||
<div class="side-bar__info" v-if="publishLocations.length">
|
<div class="side-bar__info" v-if="publishLocations.length">
|
||||||
<p>{{currentFileName}} is already published.</p>
|
<p>{{currentFileName}} is already published.</p>
|
||||||
<menu-entry @click.native="requestPublish">
|
<menu-entry @click.native="requestPublish">
|
||||||
@ -20,6 +17,9 @@
|
|||||||
<span>Manage publication locations for {{currentFileName}}.</span>
|
<span>Manage publication locations for {{currentFileName}}.</span>
|
||||||
</menu-entry>
|
</menu-entry>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="side-bar__info" v-else-if="noToken">
|
||||||
|
<p>You have to link an account to start publishing files.</p>
|
||||||
|
</div>
|
||||||
<hr>
|
<hr>
|
||||||
<div v-for="token in bloggerTokens" :key="'blogger-' + token.sub">
|
<div v-for="token in bloggerTokens" :key="'blogger-' + token.sub">
|
||||||
<menu-entry @click.native="publishBlogger(token)">
|
<menu-entry @click.native="publishBlogger(token)">
|
||||||
@ -181,8 +181,13 @@ export default {
|
|||||||
return tokensToArray(store.getters['data/zendeskTokensBySub']);
|
return tokensToArray(store.getters['data/zendeskTokensBySub']);
|
||||||
},
|
},
|
||||||
noToken() {
|
noToken() {
|
||||||
return Object.values(store.getters['data/tokensByType'])
|
return !this.bloggerTokens.length
|
||||||
.every(tokens => !Object.keys(tokens).length);
|
&& !this.dropboxTokens.length
|
||||||
|
&& !this.githubTokens.length
|
||||||
|
&& !this.gitlabTokens.length
|
||||||
|
&& !this.googleDriveTokens.length
|
||||||
|
&& !this.wordpressTokens.length
|
||||||
|
&& !this.zendeskTokens.length;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
@ -4,9 +4,6 @@
|
|||||||
<p>{{currentFileName}} can't be synced as it's a temporary file.</p>
|
<p>{{currentFileName}} can't be synced as it's a temporary file.</p>
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<div class="side-bar__info" v-if="noToken">
|
|
||||||
<p>You have to <b>link an account</b> to start syncing files.</p>
|
|
||||||
</div>
|
|
||||||
<div class="side-bar__info" v-if="syncLocations.length">
|
<div class="side-bar__info" v-if="syncLocations.length">
|
||||||
<p>{{currentFileName}} is already synchronized.</p>
|
<p>{{currentFileName}} is already synchronized.</p>
|
||||||
<menu-entry @click.native="requestSync">
|
<menu-entry @click.native="requestSync">
|
||||||
@ -20,6 +17,9 @@
|
|||||||
<span>Manage synchronized locations for {{currentFileName}}.</span>
|
<span>Manage synchronized locations for {{currentFileName}}.</span>
|
||||||
</menu-entry>
|
</menu-entry>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="side-bar__info" v-else-if="noToken">
|
||||||
|
<p>You have to link an account to start syncing files.</p>
|
||||||
|
</div>
|
||||||
<hr>
|
<hr>
|
||||||
<div v-for="token in dropboxTokens" :key="token.sub">
|
<div v-for="token in dropboxTokens" :key="token.sub">
|
||||||
<menu-entry @click.native="openDropbox(token)">
|
<menu-entry @click.native="openDropbox(token)">
|
||||||
|
@ -17,15 +17,19 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import FileSaver from 'file-saver';
|
||||||
import MenuEntry from './common/MenuEntry';
|
import MenuEntry from './common/MenuEntry';
|
||||||
import utils from '../../services/utils';
|
|
||||||
import store from '../../store';
|
import store from '../../store';
|
||||||
import backupSvc from '../../services/backupSvc';
|
import backupSvc from '../../services/backupSvc';
|
||||||
|
import localDbSvc from '../../services/localDbSvc';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
MenuEntry,
|
MenuEntry,
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
workspaceId: () => store.getters['workspace/currentWorkspace'].id,
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
onImportBackup(evt) {
|
onImportBackup(evt) {
|
||||||
const file = evt.target.files[0];
|
const file = evt.target.files[0];
|
||||||
@ -44,11 +48,16 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
exportWorkspace() {
|
exportWorkspace() {
|
||||||
window.location.href = utils.addQueryParams('app', {
|
const allItemsById = {};
|
||||||
...utils.queryParams,
|
localDbSvc.getWorkspaceItems(this.workspaceId, (item) => {
|
||||||
exportWorkspace: true,
|
allItemsById[item.id] = item;
|
||||||
}, true);
|
}, () => {
|
||||||
window.location.reload();
|
const backup = JSON.stringify(allItemsById);
|
||||||
|
const blob = new Blob([backup], {
|
||||||
|
type: 'text/plain;charset=utf-8',
|
||||||
|
});
|
||||||
|
FileSaver.saveAs(blob, 'StackEdit workspace.json');
|
||||||
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -1,5 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="side-bar__panel side-bar__panel--menu">
|
<div class="side-bar__panel side-bar__panel--menu">
|
||||||
|
<menu-entry @click.native="manageWorkspaces">
|
||||||
|
<icon-database slot="icon"></icon-database>
|
||||||
|
<div><div class="menu-entry__label menu-entry__label--count">{{workspaceCount}}</div> Manage workspaces</div>
|
||||||
|
<span>List, rename, remove workspaces</span>
|
||||||
|
</menu-entry>
|
||||||
|
<hr>
|
||||||
<div class="workspace" v-for="(workspace, id) in workspacesById" :key="id">
|
<div class="workspace" v-for="(workspace, id) in workspacesById" :key="id">
|
||||||
<menu-entry :href="workspace.url" target="_blank">
|
<menu-entry :href="workspace.url" target="_blank">
|
||||||
<icon-provider slot="icon" :provider-id="workspace.providerId"></icon-provider>
|
<icon-provider slot="icon" :provider-id="workspace.providerId"></icon-provider>
|
||||||
@ -23,10 +29,6 @@
|
|||||||
<icon-provider slot="icon" provider-id="googleDriveWorkspace"></icon-provider>
|
<icon-provider slot="icon" provider-id="googleDriveWorkspace"></icon-provider>
|
||||||
<span>Add a <b>Google Drive</b> workspace</span>
|
<span>Add a <b>Google Drive</b> workspace</span>
|
||||||
</menu-entry>
|
</menu-entry>
|
||||||
<menu-entry @click.native="manageWorkspaces">
|
|
||||||
<icon-database slot="icon"></icon-database>
|
|
||||||
<span><div class="menu-entry__label menu-entry__label--count">{{workspaceCount}}</div> Manage workspaces</span>
|
|
||||||
</menu-entry>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -22,15 +22,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="account-entry__row">
|
<div class="account-entry__row">
|
||||||
<span v-if="entry.userId">
|
<span class="account-entry__field" v-if="entry.userId">
|
||||||
<b>User ID:</b>
|
<b>User ID:</b>
|
||||||
{{entry.userId}}
|
{{entry.userId}}
|
||||||
</span>
|
</span>
|
||||||
<span v-if="entry.url">
|
<span class="account-entry__field" v-if="entry.url">
|
||||||
<b>URL:</b>
|
<b>URL:</b>
|
||||||
{{entry.url}}
|
{{entry.url}}
|
||||||
</span>
|
</span>
|
||||||
<span v-if="entry.scopes">
|
<span class="account-entry__field" v-if="entry.scopes">
|
||||||
<b>Scopes:</b>
|
<b>Scopes:</b>
|
||||||
{{entry.scopes.join(', ')}}
|
{{entry.scopes.join(', ')}}
|
||||||
</span>
|
</span>
|
||||||
@ -230,8 +230,12 @@ $button-size: 30px;
|
|||||||
|
|
||||||
.account-entry__row {
|
.account-entry__row {
|
||||||
border-top: 1px solid $hr-color;
|
border-top: 1px solid $hr-color;
|
||||||
opacity: 0.5;
|
|
||||||
font-size: 0.67em;
|
font-size: 0.67em;
|
||||||
|
padding: 0.25em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-entry__field {
|
||||||
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.account-entry__icon {
|
.account-entry__icon {
|
||||||
|
@ -6,20 +6,21 @@
|
|||||||
</div>
|
</div>
|
||||||
<p v-if="badgeCount > 1">{{badgeCount}} badges earned</p>
|
<p v-if="badgeCount > 1">{{badgeCount}} badges earned</p>
|
||||||
<p v-else>{{badgeCount}} badge earned</p>
|
<p v-else>{{badgeCount}} badge earned</p>
|
||||||
<div class="badge-entry" :class="{'badge-entry--earned': badge.isEarned}" v-for="badge in badgeTree" :key="badge.featureId">
|
<div class="badge-entry" v-for="badge in badgeTree" :key="badge.featureId">
|
||||||
<div class="flex flex--row">
|
<div class="flex flex--row">
|
||||||
<icon-seal></icon-seal>
|
<icon-seal class="badge-entry__icon" :class="{'badge-entry__icon--earned': badge.isEarned, 'badge-entry__icon--some-earned': badge.hasSomeEarned}"></icon-seal>
|
||||||
<div>
|
<div>
|
||||||
<span class="badge-entry__name">{{badge.name}}</span>
|
<span class="badge-entry__name" :class="{'badge-entry__name--earned': badge.isEarned, 'badge-entry__name--some-earned': badge.hasSomeEarned}">{{badge.name}}</span>
|
||||||
<span class="badge-entry__description">— {{badge.description}}</span>
|
<span class="badge-entry__description">— {{badge.description}}</span>
|
||||||
<div class="badge-entry" :class="{'badge-entry--earned': child.isEarned}" v-for="child in badge.children" :key="child.featureId">
|
<a href="javascript:void(0)" v-if="!shown[badge.featureId]" @click="show(badge.featureId)">Show</a>
|
||||||
|
<div class="badge-entry" v-else v-for="child in badge.children" :key="child.featureId">
|
||||||
<div class="flex flex--row">
|
<div class="flex flex--row">
|
||||||
<icon-seal></icon-seal>
|
<icon-seal class="badge-entry__icon" :class="{'badge-entry__icon--earned': child.isEarned}"></icon-seal>
|
||||||
<div>
|
<div>
|
||||||
<span class="badge-entry__name">{{child.name}}</span>
|
<span class="badge-entry__name" :class="{'badge-entry__name--earned': child.isEarned}">{{child.name}}</span>
|
||||||
<span class="badge-entry__description">— {{child.description}}</span>
|
<span class="badge-entry__description">— {{child.description}}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -32,6 +33,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import Vue from 'vue';
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import ModalInner from './common/ModalInner';
|
import ModalInner from './common/ModalInner';
|
||||||
import store from '../../store';
|
import store from '../../store';
|
||||||
@ -40,6 +42,9 @@ export default {
|
|||||||
components: {
|
components: {
|
||||||
ModalInner,
|
ModalInner,
|
||||||
},
|
},
|
||||||
|
data: () => ({
|
||||||
|
shown: {},
|
||||||
|
}),
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters('modal', [
|
...mapGetters('modal', [
|
||||||
'config',
|
'config',
|
||||||
@ -54,6 +59,11 @@ export default {
|
|||||||
return store.getters['data/allBadges'].length;
|
return store.getters['data/allBadges'].length;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
methods: {
|
||||||
|
show(featureId) {
|
||||||
|
Vue.set(this.shown, featureId, true);
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -76,19 +86,24 @@ export default {
|
|||||||
|
|
||||||
.badge-entry {
|
.badge-entry {
|
||||||
font-size: 0.8em;
|
font-size: 0.8em;
|
||||||
margin: 0.75rem 0 0;
|
margin: 0.75rem 0;
|
||||||
}
|
|
||||||
|
|
||||||
svg {
|
|
||||||
width: 1.67em;
|
|
||||||
height: 1.67em;
|
|
||||||
margin-right: 0.25em;
|
|
||||||
opacity: 0.33;
|
|
||||||
flex: none;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.badge-entry--earned svg {
|
.badge-entry__icon {
|
||||||
|
width: 1.67em;
|
||||||
|
height: 1.67em;
|
||||||
|
margin-right: 0.25em;
|
||||||
|
opacity: 0.3;
|
||||||
|
flex: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-entry__icon--some-earned {
|
||||||
|
opacity: 0.5;
|
||||||
|
color: goldenrod;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-entry__icon--earned {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
color: goldenrod;
|
color: goldenrod;
|
||||||
}
|
}
|
||||||
@ -100,10 +115,10 @@ export default {
|
|||||||
.badge-entry__name {
|
.badge-entry__name {
|
||||||
font-size: 1.2em;
|
font-size: 1.2em;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
opacity: 0.5;
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
|
||||||
.badge-entry--earned & {
|
.badge-entry__name--earned {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
<div class="modal__image">
|
<div class="modal__image">
|
||||||
<icon-database></icon-database>
|
<icon-database></icon-database>
|
||||||
</div>
|
</div>
|
||||||
<p>The following workspaces are locally available:</p>
|
<p>The following workspaces are accessible:</p>
|
||||||
<div class="workspace-entry flex flex--column" v-for="(workspace, id) in workspacesById" :key="id">
|
<div class="workspace-entry flex flex--column" v-for="(workspace, id) in workspacesById" :key="id">
|
||||||
<div class="flex flex--column">
|
<div class="flex flex--column">
|
||||||
<div class="workspace-entry__header flex flex--row flex--align-center">
|
<div class="workspace-entry__header flex flex--row flex--align-center">
|
||||||
@ -48,11 +48,13 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="workspace-entry__offline" v-if="availableOffline[id]">
|
||||||
|
available offline
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal__info">
|
|
||||||
<b>ProTip:</b> Workspaces are accessible offline, try it!
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="modal__button-bar">
|
<div class="modal__button-bar">
|
||||||
<button class="button button--resolve" @click="config.resolve()">Close</button>
|
<button class="button button--resolve" @click="config.resolve()">Close</button>
|
||||||
@ -61,11 +63,13 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import Vue from 'vue';
|
||||||
import { mapGetters, mapActions } from 'vuex';
|
import { mapGetters, mapActions } from 'vuex';
|
||||||
import ModalInner from './common/ModalInner';
|
import ModalInner from './common/ModalInner';
|
||||||
import workspaceSvc from '../../services/workspaceSvc';
|
import workspaceSvc from '../../services/workspaceSvc';
|
||||||
import store from '../../store';
|
import store from '../../store';
|
||||||
import badgeSvc from '../../services/badgeSvc';
|
import badgeSvc from '../../services/badgeSvc';
|
||||||
|
import localDbSvc from '../../services/localDbSvc';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
@ -74,6 +78,7 @@ export default {
|
|||||||
data: () => ({
|
data: () => ({
|
||||||
editedId: null,
|
editedId: null,
|
||||||
editingName: '',
|
editingName: '',
|
||||||
|
availableOffline: {},
|
||||||
}),
|
}),
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters('modal', [
|
...mapGetters('modal', [
|
||||||
@ -124,6 +129,14 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
created() {
|
||||||
|
Object.keys(this.workspacesById).forEach(async (workspaceId) => {
|
||||||
|
const cancel = localDbSvc.getWorkspaceItems(workspaceId, () => {
|
||||||
|
Vue.set(this.availableOffline, workspaceId, true);
|
||||||
|
cancel();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -208,4 +221,13 @@ $small-button-size: 22px;
|
|||||||
background-color: rgba(0, 0, 0, 0.1);
|
background-color: rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.workspace-entry__offline {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
line-height: 1;
|
||||||
|
padding: 0.15em 0.35em;
|
||||||
|
border-radius: 3px;
|
||||||
|
color: #fff;
|
||||||
|
background-color: darken($error-color, 10);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -19,7 +19,8 @@ export default {
|
|||||||
'settings',
|
'settings',
|
||||||
'layoutSettings',
|
'layoutSettings',
|
||||||
'tokens',
|
'tokens',
|
||||||
'badges',
|
'badgeCreations',
|
||||||
|
'serverConf',
|
||||||
],
|
],
|
||||||
textMaxLength: 250000,
|
textMaxLength: 250000,
|
||||||
defaultName: 'Untitled',
|
defaultName: 'Untitled',
|
||||||
|
@ -1,13 +1,3 @@
|
|||||||
class Badge {
|
|
||||||
constructor(featureId, name, description, children, isEarned) {
|
|
||||||
this.featureId = featureId;
|
|
||||||
this.name = name;
|
|
||||||
this.description = description;
|
|
||||||
this.children = children;
|
|
||||||
this.isEarned = isEarned;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class Feature {
|
class Feature {
|
||||||
constructor(id, badgeName, description, children = null) {
|
constructor(id, badgeName, description, children = null) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
@ -16,13 +6,20 @@ class Feature {
|
|||||||
this.children = children;
|
this.children = children;
|
||||||
}
|
}
|
||||||
|
|
||||||
toBadge(earnings) {
|
toBadge(badgeCreations) {
|
||||||
const children = this.children
|
const children = this.children
|
||||||
? this.children.map(child => child.toBadge(earnings))
|
? this.children.map(child => child.toBadge(badgeCreations))
|
||||||
: null;
|
: null;
|
||||||
return new Badge(this.id, this.badgeName, this.description, children, children
|
return {
|
||||||
? children.every(child => child.isEarned)
|
featureId: this.id,
|
||||||
: !!earnings[this.id]);
|
name: this.badgeName,
|
||||||
|
description: this.description,
|
||||||
|
children,
|
||||||
|
isEarned: children
|
||||||
|
? children.every(child => child.isEarned)
|
||||||
|
: !!badgeCreations[this.id],
|
||||||
|
hasSomeEarned: children && children.some(child => child.isEarned),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -42,6 +39,16 @@ export default [
|
|||||||
'Renamer',
|
'Renamer',
|
||||||
'Use the name field in the navigation bar to rename the current file.',
|
'Use the name field in the navigation bar to rename the current file.',
|
||||||
),
|
),
|
||||||
|
new Feature(
|
||||||
|
'toggleExplorer',
|
||||||
|
'Explorer toggler',
|
||||||
|
'Use the navigation bar to toggle the explorer.',
|
||||||
|
),
|
||||||
|
new Feature(
|
||||||
|
'toggleSideBar',
|
||||||
|
'Side bar toggler',
|
||||||
|
'Use the navigation bar to toggle the side bar.',
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
new Feature(
|
new Feature(
|
||||||
@ -65,9 +72,14 @@ export default [
|
|||||||
'Use the file explorer to create a new folder in your workspace.',
|
'Use the file explorer to create a new folder in your workspace.',
|
||||||
),
|
),
|
||||||
new Feature(
|
new Feature(
|
||||||
'moveFiles',
|
'moveFile',
|
||||||
'File mover',
|
'File mover',
|
||||||
'Drag files in the file explorer to move them around.',
|
'Drag a file in the file explorer to move it in another folder.',
|
||||||
|
),
|
||||||
|
new Feature(
|
||||||
|
'moveFolder',
|
||||||
|
'Folder mover',
|
||||||
|
'Drag a folder in the file explorer to move it in another folder.',
|
||||||
),
|
),
|
||||||
new Feature(
|
new Feature(
|
||||||
'renameFile',
|
'renameFile',
|
||||||
@ -80,15 +92,57 @@ export default [
|
|||||||
'Use the file explorer to rename a folder in your workspace.',
|
'Use the file explorer to rename a folder in your workspace.',
|
||||||
),
|
),
|
||||||
new Feature(
|
new Feature(
|
||||||
'removeFiles',
|
'removeFile',
|
||||||
'File remover',
|
'File remover',
|
||||||
'Use the file explorer to remove files in your workspace.',
|
'Use the file explorer to remove a file in your workspace.',
|
||||||
|
),
|
||||||
|
new Feature(
|
||||||
|
'removeFolder',
|
||||||
|
'Folder remover',
|
||||||
|
'Use the file explorer to remove a folder in your workspace.',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
new Feature(
|
||||||
|
'buttonBar',
|
||||||
|
'Button bar expert',
|
||||||
|
'Use the button bar to customize the editor layout and to toggle features.',
|
||||||
|
[
|
||||||
|
new Feature(
|
||||||
|
'toggleNavigationBar',
|
||||||
|
'Navigation bar toggler',
|
||||||
|
'Use the button bar to toggle the navigation bar.',
|
||||||
|
),
|
||||||
|
new Feature(
|
||||||
|
'toggleSidePreview',
|
||||||
|
'Side preview toggler',
|
||||||
|
'Use the button bar to toggle the side preview.',
|
||||||
|
),
|
||||||
|
new Feature(
|
||||||
|
'toggleEditor',
|
||||||
|
'Editor toggler',
|
||||||
|
'Use the button bar to toggle the editor.',
|
||||||
|
),
|
||||||
|
new Feature(
|
||||||
|
'toggleFocusMode',
|
||||||
|
'Focused',
|
||||||
|
'Use the button bar to toggle the focus mode. This mode keeps the caret vertically centered while typing.',
|
||||||
|
),
|
||||||
|
new Feature(
|
||||||
|
'toggleScrollSync',
|
||||||
|
'Scroll sync toggler',
|
||||||
|
'Use the button bar to toggle the scroll sync feature. This feature links the editor and the preview scrollbars.',
|
||||||
|
),
|
||||||
|
new Feature(
|
||||||
|
'toggleStatusBar',
|
||||||
|
'Status bar toggler',
|
||||||
|
'Use the button bar to toggle the status bar.',
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
new Feature(
|
new Feature(
|
||||||
'signIn',
|
'signIn',
|
||||||
'Logged in',
|
'Signed in',
|
||||||
'Sign in with Google, sync your main workspace and unlock functionalities.',
|
'Sign in with Google, sync your main workspace and unlock functionalities.',
|
||||||
[
|
[
|
||||||
new Feature(
|
new Feature(
|
||||||
@ -143,7 +197,7 @@ export default [
|
|||||||
new Feature(
|
new Feature(
|
||||||
'manageAccounts',
|
'manageAccounts',
|
||||||
'Account manager',
|
'Account manager',
|
||||||
'Link all kinds of external accounts and use the "User accounts" dialog to manage them.',
|
'Link all kinds of external accounts and use the "Accounts" dialog to manage them.',
|
||||||
[
|
[
|
||||||
new Feature(
|
new Feature(
|
||||||
'addBloggerAccount',
|
'addBloggerAccount',
|
||||||
@ -188,7 +242,7 @@ export default [
|
|||||||
new Feature(
|
new Feature(
|
||||||
'removeAccount',
|
'removeAccount',
|
||||||
'Revoker',
|
'Revoker',
|
||||||
'Use the "User accounts" dialog to remove access to an external account.',
|
'Use the "Accounts" dialog to remove access to an external account.',
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@ -13,7 +13,7 @@ The file explorer is accessible using the button in left corner of the navigatio
|
|||||||
|
|
||||||
## Switch to another file
|
## Switch to another file
|
||||||
|
|
||||||
All your files are listed in the file explorer. You can switch from one to another by clicking a file in the list.
|
All your files and folders are presented as a tree in the file explorer. You can switch from one to another by clicking a file in the tree.
|
||||||
|
|
||||||
## Rename a file
|
## Rename a file
|
||||||
|
|
||||||
|
@ -16,7 +16,7 @@ const showInfo = () => {
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
addBadge(featureId) {
|
addBadge(featureId) {
|
||||||
if (!store.getters['data/badges'][featureId]) {
|
if (!store.getters['data/badgeCreations'][featureId]) {
|
||||||
if (!lastEarnedFeatureIds) {
|
if (!lastEarnedFeatureIds) {
|
||||||
const earnedFeatureIds = store.getters['data/allBadges']
|
const earnedFeatureIds = store.getters['data/allBadges']
|
||||||
.filter(badge => badge.isEarned)
|
.filter(badge => badge.isEarned)
|
||||||
@ -24,7 +24,7 @@ export default {
|
|||||||
lastEarnedFeatureIds = new Set(earnedFeatureIds);
|
lastEarnedFeatureIds = new Set(earnedFeatureIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
store.dispatch('data/patchBadges', {
|
store.dispatch('data/patchBadgeCreations', {
|
||||||
[featureId]: {
|
[featureId]: {
|
||||||
created: Date.now(),
|
created: Date.now(),
|
||||||
},
|
},
|
||||||
|
@ -62,7 +62,6 @@ export default {
|
|||||||
} else {
|
} else {
|
||||||
workspaceSvc.deleteFile(id);
|
workspaceSvc.deleteFile(id);
|
||||||
}
|
}
|
||||||
badgeSvc.addBadge('removeFiles');
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (selectedNode === store.getters['explorer/selectedNode']) {
|
if (selectedNode === store.getters['explorer/selectedNode']) {
|
||||||
@ -78,8 +77,10 @@ export default {
|
|||||||
store.commit('folder/deleteItem', folderNode.item.id);
|
store.commit('folder/deleteItem', folderNode.item.id);
|
||||||
};
|
};
|
||||||
recursiveDelete(selectedNode);
|
recursiveDelete(selectedNode);
|
||||||
|
badgeSvc.addBadge('removeFolder');
|
||||||
} else {
|
} else {
|
||||||
deleteFile(selectedNode.item.id);
|
deleteFile(selectedNode.item.id);
|
||||||
|
badgeSvc.addBadge('removeFile');
|
||||||
}
|
}
|
||||||
if (doClose) {
|
if (doClose) {
|
||||||
// Close the current file by opening the last opened, not deleted one
|
// Close the current file by opening the last opened, not deleted one
|
||||||
|
@ -1,23 +1,23 @@
|
|||||||
import FileSaver from 'file-saver';
|
|
||||||
import utils from './utils';
|
import utils from './utils';
|
||||||
import store from '../store';
|
import store from '../store';
|
||||||
import welcomeFile from '../data/welcomeFile.md';
|
import welcomeFile from '../data/welcomeFile.md';
|
||||||
import workspaceSvc from './workspaceSvc';
|
import workspaceSvc from './workspaceSvc';
|
||||||
import constants from '../data/constants';
|
import constants from '../data/constants';
|
||||||
|
|
||||||
|
const deleteMarkerMaxAge = 1000;
|
||||||
const dbVersion = 1;
|
const dbVersion = 1;
|
||||||
const dbStoreName = 'objects';
|
const dbStoreName = 'objects';
|
||||||
const { exportWorkspace } = utils.queryParams;
|
|
||||||
const { silent } = utils.queryParams;
|
const { silent } = utils.queryParams;
|
||||||
const resetApp = utils.queryParams.reset;
|
const resetApp = localStorage.getItem('resetStackEdit');
|
||||||
const deleteMarkerMaxAge = 1000;
|
if (resetApp) {
|
||||||
|
localStorage.removeItem('resetStackEdit');
|
||||||
|
}
|
||||||
|
|
||||||
class Connection {
|
class Connection {
|
||||||
constructor() {
|
constructor(workspaceId = store.getters['workspace/currentWorkspace'].id) {
|
||||||
this.getTxCbs = [];
|
this.getTxCbs = [];
|
||||||
|
|
||||||
// Make the DB name
|
// Make the DB name
|
||||||
const workspaceId = store.getters['workspace/currentWorkspace'].id;
|
|
||||||
this.dbName = utils.getDbName(workspaceId);
|
this.dbName = utils.getDbName(workspaceId);
|
||||||
|
|
||||||
// Init connection
|
// Init connection
|
||||||
@ -264,7 +264,7 @@ const localDbSvc = {
|
|||||||
// DB item is different from the corresponding store item
|
// DB item is different from the corresponding store item
|
||||||
this.hashMap[dbItem.type][dbItem.id] = dbItem.hash;
|
this.hashMap[dbItem.type][dbItem.id] = dbItem.hash;
|
||||||
// Update content only if it exists in the store
|
// Update content only if it exists in the store
|
||||||
if (storeItem || !contentTypes[dbItem.type] || exportWorkspace) {
|
if (storeItem || !contentTypes[dbItem.type]) {
|
||||||
// Put item in the store
|
// Put item in the store
|
||||||
dbItem.tx = undefined;
|
dbItem.tx = undefined;
|
||||||
store.commit(`${dbItem.type}/setItem`, dbItem);
|
store.commit(`${dbItem.type}/setItem`, dbItem);
|
||||||
@ -327,7 +327,7 @@ const localDbSvc = {
|
|||||||
* Create the connection and start syncing.
|
* Create the connection and start syncing.
|
||||||
*/
|
*/
|
||||||
async init() {
|
async init() {
|
||||||
// Reset the app if reset flag was passed
|
// Reset the app if the reset flag was passed
|
||||||
if (resetApp) {
|
if (resetApp) {
|
||||||
await Promise.all(Object.keys(store.getters['workspace/workspacesById'])
|
await Promise.all(Object.keys(store.getters['workspace/workspacesById'])
|
||||||
.map(workspaceId => workspaceSvc.removeWorkspace(workspaceId)));
|
.map(workspaceId => workspaceSvc.removeWorkspace(workspaceId)));
|
||||||
@ -344,16 +344,6 @@ const localDbSvc = {
|
|||||||
// Load the DB
|
// Load the DB
|
||||||
await localDbSvc.sync();
|
await localDbSvc.sync();
|
||||||
|
|
||||||
// If exportWorkspace parameter was provided
|
|
||||||
if (exportWorkspace) {
|
|
||||||
const backup = JSON.stringify(store.getters.allItemsById);
|
|
||||||
const blob = new Blob([backup], {
|
|
||||||
type: 'text/plain;charset=utf-8',
|
|
||||||
});
|
|
||||||
FileSaver.saveAs(blob, 'StackEdit workspace.json');
|
|
||||||
throw new Error('RELOAD');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Watch workspace deletions and persist them as soon as possible
|
// Watch workspace deletions and persist them as soon as possible
|
||||||
// to make the changes available to reloading workspace tabs.
|
// to make the changes available to reloading workspace tabs.
|
||||||
store.watch(
|
store.watch(
|
||||||
@ -438,6 +428,27 @@ const localDbSvc = {
|
|||||||
{ immediate: true },
|
{ immediate: true },
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getWorkspaceItems(workspaceId, onItem, onFinish = () => {}) {
|
||||||
|
const connection = new Connection(workspaceId);
|
||||||
|
connection.createTx((tx) => {
|
||||||
|
const dbStore = tx.objectStore(dbStoreName);
|
||||||
|
const index = dbStore.index('tx');
|
||||||
|
index.openCursor().onsuccess = (event) => {
|
||||||
|
const cursor = event.target.result;
|
||||||
|
if (cursor) {
|
||||||
|
onItem(cursor.value);
|
||||||
|
cursor.continue();
|
||||||
|
} else {
|
||||||
|
connection.db.close();
|
||||||
|
onFinish();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Return a cancel function
|
||||||
|
return () => connection.db.close();
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const loader = type => fileId => localDbSvc.loadItem(`${fileId}/${type}`)
|
const loader = type => fileId => localDbSvc.loadItem(`${fileId}/${type}`)
|
||||||
|
@ -8,7 +8,10 @@ const silentAuthorizeTimeout = 15 * 1000; // 15 secondes (which will be reattemp
|
|||||||
const networkTimeout = 30 * 1000; // 30 sec
|
const networkTimeout = 30 * 1000; // 30 sec
|
||||||
let isConnectionDown = false;
|
let isConnectionDown = false;
|
||||||
const userInactiveAfter = 3 * 60 * 1000; // 3 minutes (twice the default sync period)
|
const userInactiveAfter = 3 * 60 * 1000; // 3 minutes (twice the default sync period)
|
||||||
|
let lastActivity = 0;
|
||||||
|
let lastFocus = 0;
|
||||||
|
let isConfLoading = false;
|
||||||
|
let isConfLoaded = false;
|
||||||
|
|
||||||
function parseHeaders(xhr) {
|
function parseHeaders(xhr) {
|
||||||
const pairs = xhr.getAllResponseHeaders().trim().split('\n');
|
const pairs = xhr.getAllResponseHeaders().trim().split('\n');
|
||||||
@ -31,21 +34,20 @@ function isRetriable(err) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
init() {
|
async init() {
|
||||||
// Keep track of the last user activity
|
// Keep track of the last user activity
|
||||||
this.lastActivity = 0;
|
|
||||||
const setLastActivity = () => {
|
const setLastActivity = () => {
|
||||||
this.lastActivity = Date.now();
|
lastActivity = Date.now();
|
||||||
};
|
};
|
||||||
window.document.addEventListener('mousedown', setLastActivity);
|
window.document.addEventListener('mousedown', setLastActivity);
|
||||||
window.document.addEventListener('keydown', setLastActivity);
|
window.document.addEventListener('keydown', setLastActivity);
|
||||||
window.document.addEventListener('touchstart', setLastActivity);
|
window.document.addEventListener('touchstart', setLastActivity);
|
||||||
|
|
||||||
// Keep track of the last window focus
|
// Keep track of the last window focus
|
||||||
this.lastFocus = 0;
|
lastFocus = 0;
|
||||||
const setLastFocus = () => {
|
const setLastFocus = () => {
|
||||||
this.lastFocus = Date.now();
|
lastFocus = Date.now();
|
||||||
localStorage.setItem(store.getters['workspace/lastFocusKey'], this.lastFocus);
|
localStorage.setItem(store.getters['workspace/lastFocusKey'], lastFocus);
|
||||||
setLastActivity();
|
setLastActivity();
|
||||||
};
|
};
|
||||||
if (document.hasFocus()) {
|
if (document.hasFocus()) {
|
||||||
@ -53,7 +55,7 @@ export default {
|
|||||||
}
|
}
|
||||||
window.addEventListener('focus', setLastFocus);
|
window.addEventListener('focus', setLastFocus);
|
||||||
|
|
||||||
// Check browser is online periodically
|
// Check that browser is online periodically
|
||||||
const checkOffline = async () => {
|
const checkOffline = async () => {
|
||||||
const isBrowserOffline = window.navigator.onLine === false;
|
const isBrowserOffline = window.navigator.onLine === false;
|
||||||
if (!isBrowserOffline
|
if (!isBrowserOffline
|
||||||
@ -90,23 +92,42 @@ export default {
|
|||||||
store.dispatch('notification/error', 'You are offline.');
|
store.dispatch('notification/error', 'You are offline.');
|
||||||
} else {
|
} else {
|
||||||
store.dispatch('notification/info', 'You are back online!');
|
store.dispatch('notification/info', 'You are back online!');
|
||||||
|
this.getServerConf();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
utils.setInterval(checkOffline, 1000);
|
utils.setInterval(checkOffline, 1000);
|
||||||
window.addEventListener('online', () => {
|
window.addEventListener('online', () => {
|
||||||
isConnectionDown = false;
|
isConnectionDown = false;
|
||||||
checkOffline();
|
checkOffline();
|
||||||
});
|
});
|
||||||
window.addEventListener('offline', checkOffline);
|
window.addEventListener('offline', checkOffline);
|
||||||
|
await checkOffline();
|
||||||
|
this.getServerConf();
|
||||||
|
},
|
||||||
|
async getServerConf() {
|
||||||
|
if (!store.state.offline && !isConfLoading && !isConfLoaded) {
|
||||||
|
try {
|
||||||
|
isConfLoading = true;
|
||||||
|
const res = await this.request({ url: 'conf' });
|
||||||
|
await store.dispatch('data/setServerConf', res.body);
|
||||||
|
isConfLoaded = true;
|
||||||
|
} finally {
|
||||||
|
isConfLoading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
isWindowFocused() {
|
isWindowFocused() {
|
||||||
// We don't use state.workspace.lastFocus as it's not reactive
|
// We don't use state.workspace.lastFocus as it's not reactive
|
||||||
const storedLastFocus = localStorage.getItem(store.getters['workspace/lastFocusKey']);
|
const storedLastFocus = localStorage.getItem(store.getters['workspace/lastFocusKey']);
|
||||||
return parseInt(storedLastFocus, 10) === this.lastFocus;
|
return parseInt(storedLastFocus, 10) === lastFocus;
|
||||||
},
|
},
|
||||||
isUserActive() {
|
isUserActive() {
|
||||||
return this.lastActivity > Date.now() - userInactiveAfter && this.isWindowFocused();
|
return lastActivity > Date.now() - userInactiveAfter && this.isWindowFocused();
|
||||||
|
},
|
||||||
|
isConfLoaded() {
|
||||||
|
return !!Object.keys(store.getters['data/serverConf']).length;
|
||||||
},
|
},
|
||||||
async loadScript(url) {
|
async loadScript(url) {
|
||||||
if (!scriptLoadingPromises[url]) {
|
if (!scriptLoadingPromises[url]) {
|
||||||
@ -217,15 +238,15 @@ export default {
|
|||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async request(configParam, offlineCheck = false) {
|
async request(config, offlineCheck = false) {
|
||||||
let retryAfter = 500; // 500 ms
|
let retryAfter = 500; // 500 ms
|
||||||
const maxRetryAfter = 10 * 1000; // 10 sec
|
const maxRetryAfter = 10 * 1000; // 10 sec
|
||||||
const config = Object.assign({}, configParam);
|
const sanitizedConfig = Object.assign({}, config);
|
||||||
config.timeout = config.timeout || networkTimeout;
|
sanitizedConfig.timeout = sanitizedConfig.timeout || networkTimeout;
|
||||||
config.headers = Object.assign({}, config.headers);
|
sanitizedConfig.headers = Object.assign({}, sanitizedConfig.headers);
|
||||||
if (config.body && typeof config.body === 'object') {
|
if (sanitizedConfig.body && typeof sanitizedConfig.body === 'object') {
|
||||||
config.body = JSON.stringify(config.body);
|
sanitizedConfig.body = JSON.stringify(sanitizedConfig.body);
|
||||||
config.headers['Content-Type'] = 'application/json';
|
sanitizedConfig.headers['Content-Type'] = 'application/json';
|
||||||
}
|
}
|
||||||
|
|
||||||
const attempt = async () => {
|
const attempt = async () => {
|
||||||
@ -236,7 +257,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const xhr = new window.XMLHttpRequest();
|
const xhr = new window.XMLHttpRequest();
|
||||||
xhr.withCredentials = config.withCredentials || false;
|
xhr.withCredentials = sanitizedConfig.withCredentials || false;
|
||||||
|
|
||||||
const timeoutId = setTimeout(() => {
|
const timeoutId = setTimeout(() => {
|
||||||
xhr.abort();
|
xhr.abort();
|
||||||
@ -247,7 +268,7 @@ export default {
|
|||||||
} else {
|
} else {
|
||||||
reject(new Error('Network request timeout.'));
|
reject(new Error('Network request timeout.'));
|
||||||
}
|
}
|
||||||
}, config.timeout);
|
}, sanitizedConfig.timeout);
|
||||||
|
|
||||||
xhr.onload = () => {
|
xhr.onload = () => {
|
||||||
if (offlineCheck) {
|
if (offlineCheck) {
|
||||||
@ -257,9 +278,9 @@ export default {
|
|||||||
const result = {
|
const result = {
|
||||||
status: xhr.status,
|
status: xhr.status,
|
||||||
headers: parseHeaders(xhr),
|
headers: parseHeaders(xhr),
|
||||||
body: config.blob ? xhr.response : xhr.responseText,
|
body: sanitizedConfig.blob ? xhr.response : xhr.responseText,
|
||||||
};
|
};
|
||||||
if (!config.raw && !config.blob) {
|
if (!sanitizedConfig.raw && !sanitizedConfig.blob) {
|
||||||
try {
|
try {
|
||||||
result.body = JSON.parse(result.body);
|
result.body = JSON.parse(result.body);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -284,17 +305,17 @@ export default {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const url = utils.addQueryParams(config.url, config.params);
|
const url = utils.addQueryParams(sanitizedConfig.url, sanitizedConfig.params);
|
||||||
xhr.open(config.method || 'GET', url);
|
xhr.open(sanitizedConfig.method || 'GET', url);
|
||||||
Object.entries(config.headers).forEach(([key, value]) => {
|
Object.entries(sanitizedConfig.headers).forEach(([key, value]) => {
|
||||||
if (value) {
|
if (value) {
|
||||||
xhr.setRequestHeader(key, `${value}`);
|
xhr.setRequestHeader(key, `${value}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (config.blob) {
|
if (sanitizedConfig.blob) {
|
||||||
xhr.responseType = 'blob';
|
xhr.responseType = 'blob';
|
||||||
}
|
}
|
||||||
xhr.send(config.body || null);
|
xhr.send(sanitizedConfig.body || null);
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Try again later in case of retriable error
|
// Try again later in case of retriable error
|
||||||
|
@ -124,7 +124,11 @@ export default new Provider({
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
async listFileRevisions({ token, syncLocation }) {
|
async listFileRevisions({ token, syncLocation }) {
|
||||||
const entries = await dropboxHelper.listRevisions(token, syncLocation.dropboxFileId);
|
const entries = await dropboxHelper.listRevisions({
|
||||||
|
token,
|
||||||
|
path: makePathRelative(token, syncLocation.path),
|
||||||
|
fileId: syncLocation.dropboxFileId,
|
||||||
|
});
|
||||||
return entries.map(entry => ({
|
return entries.map(entry => ({
|
||||||
id: entry.rev,
|
id: entry.rev,
|
||||||
sub: `${dropboxHelper.subPrefix}:${(entry.sharing_info || {}).modified_by || token.sub}`,
|
sub: `${dropboxHelper.subPrefix}:${(entry.sharing_info || {}).modified_by || token.sub}`,
|
||||||
|
@ -5,9 +5,9 @@ import badgeSvc from '../../badgeSvc';
|
|||||||
|
|
||||||
const getAppKey = (fullAccess) => {
|
const getAppKey = (fullAccess) => {
|
||||||
if (fullAccess) {
|
if (fullAccess) {
|
||||||
return 'lq6mwopab8wskas';
|
return store.getters['data/serverConf'].dropboxAppKeyFull;
|
||||||
}
|
}
|
||||||
return 'sw0hlixhr8q1xk0';
|
return store.getters['data/serverConf'].dropboxAppKey;
|
||||||
};
|
};
|
||||||
|
|
||||||
const httpHeaderSafeJson = args => args && JSON.stringify(args)
|
const httpHeaderSafeJson = args => args && JSON.stringify(args)
|
||||||
@ -60,6 +60,7 @@ export default {
|
|||||||
* https://www.dropbox.com/developers/documentation/http/documentation#users-get_current_account
|
* https://www.dropbox.com/developers/documentation/http/documentation#users-get_current_account
|
||||||
*/
|
*/
|
||||||
async startOauth2(fullAccess, sub = null, silent = false) {
|
async startOauth2(fullAccess, sub = null, silent = false) {
|
||||||
|
// Get an OAuth2 code
|
||||||
const { accessToken } = await networkSvc.startOauth2(
|
const { accessToken } = await networkSvc.startOauth2(
|
||||||
'https://www.dropbox.com/oauth2/authorize',
|
'https://www.dropbox.com/oauth2/authorize',
|
||||||
{
|
{
|
||||||
@ -146,14 +147,21 @@ export default {
|
|||||||
/**
|
/**
|
||||||
* https://www.dropbox.com/developers/documentation/http/documentation#list-revisions
|
* https://www.dropbox.com/developers/documentation/http/documentation#list-revisions
|
||||||
*/
|
*/
|
||||||
async listRevisions(token, fileId) {
|
async listRevisions({
|
||||||
|
token,
|
||||||
|
path,
|
||||||
|
fileId,
|
||||||
|
}) {
|
||||||
const res = await request(token, {
|
const res = await request(token, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: 'https://api.dropboxapi.com/2/files/list_revisions',
|
url: 'https://api.dropboxapi.com/2/files/list_revisions',
|
||||||
body: {
|
body: fileId ? {
|
||||||
path: fileId,
|
path: fileId,
|
||||||
mode: 'id',
|
mode: 'id',
|
||||||
limit: 100,
|
limit: 100,
|
||||||
|
} : {
|
||||||
|
path,
|
||||||
|
limit: 100,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
return res.body.entries;
|
return res.body.entries;
|
||||||
|
@ -4,7 +4,6 @@ import store from '../../../store';
|
|||||||
import userSvc from '../../userSvc';
|
import userSvc from '../../userSvc';
|
||||||
import badgeSvc from '../../badgeSvc';
|
import badgeSvc from '../../badgeSvc';
|
||||||
|
|
||||||
const clientId = GITHUB_CLIENT_ID;
|
|
||||||
const getScopes = token => [token.repoFullAccess ? 'repo' : 'public_repo', 'gist'];
|
const getScopes = token => [token.repoFullAccess ? 'repo' : 'public_repo', 'gist'];
|
||||||
|
|
||||||
const request = (token, options) => networkSvc.request({
|
const request = (token, options) => networkSvc.request({
|
||||||
@ -64,6 +63,9 @@ export default {
|
|||||||
* https://developer.github.com/apps/building-oauth-apps/authorization-options-for-oauth-apps/
|
* https://developer.github.com/apps/building-oauth-apps/authorization-options-for-oauth-apps/
|
||||||
*/
|
*/
|
||||||
async startOauth2(scopes, sub = null, silent = false) {
|
async startOauth2(scopes, sub = null, silent = false) {
|
||||||
|
const clientId = store.getters['data/serverConf'].githubClientId;
|
||||||
|
|
||||||
|
// Get an OAuth2 code
|
||||||
const { code } = await networkSvc.startOauth2(
|
const { code } = await networkSvc.startOauth2(
|
||||||
'https://github.com/login/oauth/authorize',
|
'https://github.com/login/oauth/authorize',
|
||||||
{
|
{
|
||||||
|
@ -51,6 +51,7 @@ export default {
|
|||||||
* https://docs.gitlab.com/ee/api/oauth2.html
|
* https://docs.gitlab.com/ee/api/oauth2.html
|
||||||
*/
|
*/
|
||||||
async startOauth2(serverUrl, applicationId, sub = null, silent = false) {
|
async startOauth2(serverUrl, applicationId, sub = null, silent = false) {
|
||||||
|
// Get an OAuth2 code
|
||||||
const { accessToken } = await networkSvc.startOauth2(
|
const { accessToken } = await networkSvc.startOauth2(
|
||||||
`${serverUrl}/oauth/authorize`,
|
`${serverUrl}/oauth/authorize`,
|
||||||
{
|
{
|
||||||
|
@ -4,8 +4,6 @@ import store from '../../../store';
|
|||||||
import userSvc from '../../userSvc';
|
import userSvc from '../../userSvc';
|
||||||
import badgeSvc from '../../badgeSvc';
|
import badgeSvc from '../../badgeSvc';
|
||||||
|
|
||||||
const clientId = GOOGLE_CLIENT_ID;
|
|
||||||
const apiKey = 'AIzaSyC_M4RA9pY6XmM9pmFxlT59UPMO7aHr9kk';
|
|
||||||
const appsDomain = null;
|
const appsDomain = null;
|
||||||
const tokenExpirationMargin = 5 * 60 * 1000; // 5 min (tokens expire after 1h)
|
const tokenExpirationMargin = 5 * 60 * 1000; // 5 min (tokens expire after 1h)
|
||||||
|
|
||||||
@ -21,6 +19,7 @@ const checkIdToken = (idToken) => {
|
|||||||
try {
|
try {
|
||||||
const token = idToken.split('.');
|
const token = idToken.split('.');
|
||||||
const payload = JSON.parse(utils.decodeBase64(token[1]));
|
const payload = JSON.parse(utils.decodeBase64(token[1]));
|
||||||
|
const clientId = store.getters['data/serverConf'].googleClientId;
|
||||||
return payload.aud === clientId && Date.now() + tokenExpirationMargin < payload.exp * 1000;
|
return payload.aud === clientId && Date.now() + tokenExpirationMargin < payload.exp * 1000;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return false;
|
return false;
|
||||||
@ -40,6 +39,7 @@ if (utils.queryParams.providerId === 'googleDrive') {
|
|||||||
* https://developers.google.com/people/api/rest/v1/people/get
|
* https://developers.google.com/people/api/rest/v1/people/get
|
||||||
*/
|
*/
|
||||||
const getUser = async (sub, token) => {
|
const getUser = async (sub, token) => {
|
||||||
|
const apiKey = store.getters['data/serverConf'].googleApiKey;
|
||||||
const url = `https://people.googleapis.com/v1/people/${sub}?personFields=names,photos&key=${apiKey}`;
|
const url = `https://people.googleapis.com/v1/people/${sub}?personFields=names,photos&key=${apiKey}`;
|
||||||
const { body } = await networkSvc.request(sub === 'me' && token
|
const { body } = await networkSvc.request(sub === 'me' && token
|
||||||
? {
|
? {
|
||||||
@ -111,6 +111,9 @@ export default {
|
|||||||
* https://developers.google.com/identity/protocols/OpenIDConnect
|
* https://developers.google.com/identity/protocols/OpenIDConnect
|
||||||
*/
|
*/
|
||||||
async startOauth2(scopes, sub = null, silent = false) {
|
async startOauth2(scopes, sub = null, silent = false) {
|
||||||
|
const clientId = store.getters['data/serverConf'].googleClientId;
|
||||||
|
|
||||||
|
// Get an OAuth2 code
|
||||||
const { accessToken, expiresIn, idToken } = await networkSvc.startOauth2(
|
const { accessToken, expiresIn, idToken } = await networkSvc.startOauth2(
|
||||||
'https://accounts.google.com/o/oauth2/v2/auth',
|
'https://accounts.google.com/o/oauth2/v2/auth',
|
||||||
{
|
{
|
||||||
@ -265,16 +268,6 @@ export default {
|
|||||||
badgeSvc.addBadge('addGooglePhotosAccount');
|
badgeSvc.addBadge('addGooglePhotosAccount');
|
||||||
return token;
|
return token;
|
||||||
},
|
},
|
||||||
async getSponsorship(token) {
|
|
||||||
const refreshedToken = await this.refreshToken(token);
|
|
||||||
return networkSvc.request({
|
|
||||||
method: 'GET',
|
|
||||||
url: 'userInfo',
|
|
||||||
params: {
|
|
||||||
idToken: refreshedToken.idToken,
|
|
||||||
},
|
|
||||||
}, true);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* https://developers.google.com/drive/v3/reference/files/create
|
* https://developers.google.com/drive/v3/reference/files/create
|
||||||
|
@ -2,7 +2,6 @@ import networkSvc from '../../networkSvc';
|
|||||||
import store from '../../../store';
|
import store from '../../../store';
|
||||||
import badgeSvc from '../../badgeSvc';
|
import badgeSvc from '../../badgeSvc';
|
||||||
|
|
||||||
const clientId = '23361';
|
|
||||||
const tokenExpirationMargin = 5 * 60 * 1000; // 5 min (WordPress tokens expire after 2 weeks)
|
const tokenExpirationMargin = 5 * 60 * 1000; // 5 min (WordPress tokens expire after 2 weeks)
|
||||||
|
|
||||||
const request = (token, options) => networkSvc.request({
|
const request = (token, options) => networkSvc.request({
|
||||||
@ -19,6 +18,9 @@ export default {
|
|||||||
* https://developer.wordpress.com/docs/oauth2/
|
* https://developer.wordpress.com/docs/oauth2/
|
||||||
*/
|
*/
|
||||||
async startOauth2(sub = null, silent = false) {
|
async startOauth2(sub = null, silent = false) {
|
||||||
|
const clientId = store.getters['data/serverConf'].wordpressClientId;
|
||||||
|
|
||||||
|
// Get an OAuth2 code
|
||||||
const { accessToken, expiresIn } = await networkSvc.startOauth2(
|
const { accessToken, expiresIn } = await networkSvc.startOauth2(
|
||||||
'https://public-api.wordpress.com/oauth2/authorize',
|
'https://public-api.wordpress.com/oauth2/authorize',
|
||||||
{
|
{
|
||||||
|
@ -17,6 +17,7 @@ export default {
|
|||||||
* https://support.zendesk.com/hc/en-us/articles/203663836-Using-OAuth-authentication-with-your-application
|
* https://support.zendesk.com/hc/en-us/articles/203663836-Using-OAuth-authentication-with-your-application
|
||||||
*/
|
*/
|
||||||
async startOauth2(subdomain, clientId, sub = null, silent = false) {
|
async startOauth2(subdomain, clientId, sub = null, silent = false) {
|
||||||
|
// Get an OAuth2 code
|
||||||
const { accessToken } = await networkSvc.startOauth2(
|
const { accessToken } = await networkSvc.startOauth2(
|
||||||
`https://${subdomain}.zendesk.com/oauth/authorizations/new`,
|
`https://${subdomain}.zendesk.com/oauth/authorizations/new`,
|
||||||
{
|
{
|
||||||
|
@ -728,7 +728,7 @@ const syncWorkspace = async (skipContents = false) => {
|
|||||||
if (workspace.id === 'main') {
|
if (workspace.id === 'main') {
|
||||||
await syncDataItem('settings');
|
await syncDataItem('settings');
|
||||||
await syncDataItem('workspaces');
|
await syncDataItem('workspaces');
|
||||||
await syncDataItem('badges');
|
await syncDataItem('badgeCreations');
|
||||||
}
|
}
|
||||||
await syncDataItem('templates');
|
await syncDataItem('templates');
|
||||||
|
|
||||||
|
@ -11,6 +11,7 @@ import styledHtmlWithTocTemplate from '../data/templates/styledHtmlWithTocTempla
|
|||||||
import jekyllSiteTemplate from '../data/templates/jekyllSiteTemplate.html';
|
import jekyllSiteTemplate from '../data/templates/jekyllSiteTemplate.html';
|
||||||
import constants from '../data/constants';
|
import constants from '../data/constants';
|
||||||
import features from '../data/features';
|
import features from '../data/features';
|
||||||
|
import badgeSvc from '../services/badgeSvc';
|
||||||
|
|
||||||
const itemTemplate = (id, data = {}) => ({
|
const itemTemplate = (id, data = {}) => ({
|
||||||
id,
|
id,
|
||||||
@ -63,9 +64,12 @@ const patcher = id => ({ state, commit }, data) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// For layoutSettings
|
// For layoutSettings
|
||||||
const layoutSettingsToggler = propertyName => ({ getters, dispatch }, value) => dispatch('patchLayoutSettings', {
|
const layoutSettingsToggler = (propertyName, featureId) => ({ getters, dispatch }, value) => {
|
||||||
[propertyName]: value === undefined ? !getters.layoutSettings[propertyName] : value,
|
dispatch('patchLayoutSettings', {
|
||||||
});
|
[propertyName]: value === undefined ? !getters.layoutSettings[propertyName] : value,
|
||||||
|
});
|
||||||
|
badgeSvc.addBadge(featureId);
|
||||||
|
};
|
||||||
const notEnoughSpace = (getters) => {
|
const notEnoughSpace = (getters) => {
|
||||||
const layoutConstants = getters['layout/constants'];
|
const layoutConstants = getters['layout/constants'];
|
||||||
const showGutter = getters['discussion/currentDiscussion'];
|
const showGutter = getters['discussion/currentDiscussion'];
|
||||||
@ -132,6 +136,7 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
getters: {
|
getters: {
|
||||||
|
serverConf: getter('serverConf'),
|
||||||
workspaces: getter('workspaces'), // Not to be used, prefer workspace/workspacesById
|
workspaces: getter('workspaces'), // Not to be used, prefer workspace/workspacesById
|
||||||
settings: getter('settings'),
|
settings: getter('settings'),
|
||||||
computedSettings: (state, { settings }) => {
|
computedSettings: (state, { settings }) => {
|
||||||
@ -204,8 +209,9 @@ export default {
|
|||||||
gitlabTokensBySub: (state, { tokensByType }) => tokensByType.gitlab || {},
|
gitlabTokensBySub: (state, { tokensByType }) => tokensByType.gitlab || {},
|
||||||
wordpressTokensBySub: (state, { tokensByType }) => tokensByType.wordpress || {},
|
wordpressTokensBySub: (state, { tokensByType }) => tokensByType.wordpress || {},
|
||||||
zendeskTokensBySub: (state, { tokensByType }) => tokensByType.zendesk || {},
|
zendeskTokensBySub: (state, { tokensByType }) => tokensByType.zendesk || {},
|
||||||
badges: getter('badges'),
|
badgeCreations: getter('badgeCreations'),
|
||||||
badgeTree: (state, { badges }) => features.map(feature => feature.toBadge(badges)),
|
badgeTree: (state, { badgeCreations }) => features
|
||||||
|
.map(feature => feature.toBadge(badgeCreations)),
|
||||||
allBadges: (state, { badgeTree }) => {
|
allBadges: (state, { badgeTree }) => {
|
||||||
const result = [];
|
const result = [];
|
||||||
const processBadgeNodes = nodes => nodes.forEach((node) => {
|
const processBadgeNodes = nodes => nodes.forEach((node) => {
|
||||||
@ -219,15 +225,16 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
|
setServerConf: setter('serverConf'),
|
||||||
setSettings: setter('settings'),
|
setSettings: setter('settings'),
|
||||||
patchLocalSettings: patcher('localSettings'),
|
patchLocalSettings: patcher('localSettings'),
|
||||||
patchLayoutSettings: patcher('layoutSettings'),
|
patchLayoutSettings: patcher('layoutSettings'),
|
||||||
toggleNavigationBar: layoutSettingsToggler('showNavigationBar'),
|
toggleNavigationBar: layoutSettingsToggler('showNavigationBar', 'toggleNavigationBar'),
|
||||||
toggleEditor: layoutSettingsToggler('showEditor'),
|
toggleEditor: layoutSettingsToggler('showEditor', 'toggleEditor'),
|
||||||
toggleSidePreview: layoutSettingsToggler('showSidePreview'),
|
toggleSidePreview: layoutSettingsToggler('showSidePreview', 'toggleSidePreview'),
|
||||||
toggleStatusBar: layoutSettingsToggler('showStatusBar'),
|
toggleStatusBar: layoutSettingsToggler('showStatusBar', 'toggleStatusBar'),
|
||||||
toggleScrollSync: layoutSettingsToggler('scrollSync'),
|
toggleScrollSync: layoutSettingsToggler('scrollSync', 'toggleScrollSync'),
|
||||||
toggleFocusMode: layoutSettingsToggler('focusMode'),
|
toggleFocusMode: layoutSettingsToggler('focusMode', 'toggleFocusMode'),
|
||||||
toggleSideBar: ({ getters, dispatch, rootGetters }, value) => {
|
toggleSideBar: ({ getters, dispatch, rootGetters }, value) => {
|
||||||
// Reset side bar
|
// Reset side bar
|
||||||
dispatch('setSideBarPanel');
|
dispatch('setSideBarPanel');
|
||||||
@ -240,6 +247,7 @@ export default {
|
|||||||
patch.showExplorer = false;
|
patch.showExplorer = false;
|
||||||
}
|
}
|
||||||
dispatch('patchLayoutSettings', patch);
|
dispatch('patchLayoutSettings', patch);
|
||||||
|
badgeSvc.addBadge('toggleSideBar');
|
||||||
},
|
},
|
||||||
toggleExplorer: ({ getters, dispatch, rootGetters }, value) => {
|
toggleExplorer: ({ getters, dispatch, rootGetters }, value) => {
|
||||||
// Close side bar if not enough space
|
// Close side bar if not enough space
|
||||||
@ -250,6 +258,7 @@ export default {
|
|||||||
patch.showSideBar = false;
|
patch.showSideBar = false;
|
||||||
}
|
}
|
||||||
dispatch('patchLayoutSettings', patch);
|
dispatch('patchLayoutSettings', patch);
|
||||||
|
badgeSvc.addBadge('toggleExplorer');
|
||||||
},
|
},
|
||||||
setSideBarPanel: ({ dispatch }, value) => dispatch('patchLayoutSettings', {
|
setSideBarPanel: ({ dispatch }, value) => dispatch('patchLayoutSettings', {
|
||||||
sideBarPanel: value === undefined ? 'menu' : value,
|
sideBarPanel: value === undefined ? 'menu' : value,
|
||||||
@ -288,6 +297,6 @@ export default {
|
|||||||
addGitlabToken: tokenAdder('gitlab'),
|
addGitlabToken: tokenAdder('gitlab'),
|
||||||
addWordpressToken: tokenAdder('wordpress'),
|
addWordpressToken: tokenAdder('wordpress'),
|
||||||
addZendeskToken: tokenAdder('zendesk'),
|
addZendeskToken: tokenAdder('zendesk'),
|
||||||
patchBadges: patcher('badges'),
|
patchBadgeCreations: patcher('badgeCreations'),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -28,8 +28,6 @@ module.exports = {
|
|||||||
'!**/node_modules/**',
|
'!**/node_modules/**',
|
||||||
],
|
],
|
||||||
globals: {
|
globals: {
|
||||||
GOOGLE_CLIENT_ID: 'GOOGLE_CLIENT_ID',
|
|
||||||
GITHUB_CLIENT_ID: 'GITHUB_CLIENT_ID',
|
|
||||||
NODE_ENV: 'production',
|
NODE_ENV: 'production',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -3,39 +3,45 @@ import store from '../../../../src/store';
|
|||||||
import specUtils from '../specUtils';
|
import specUtils from '../specUtils';
|
||||||
|
|
||||||
describe('ButtonBar.vue', () => {
|
describe('ButtonBar.vue', () => {
|
||||||
it('should toggle the navigation bar', () => specUtils.checkToggler(
|
it('should toggle the navigation bar', async () => specUtils.checkToggler(
|
||||||
ButtonBar,
|
ButtonBar,
|
||||||
wrapper => wrapper.find('.button-bar__button--navigation-bar-toggler').trigger('click'),
|
wrapper => wrapper.find('.button-bar__button--navigation-bar-toggler').trigger('click'),
|
||||||
() => store.getters['data/layoutSettings'].showNavigationBar,
|
() => store.getters['data/layoutSettings'].showNavigationBar,
|
||||||
|
'toggleNavigationBar',
|
||||||
));
|
));
|
||||||
|
|
||||||
it('should toggle the side preview', () => specUtils.checkToggler(
|
it('should toggle the side preview', async () => specUtils.checkToggler(
|
||||||
ButtonBar,
|
ButtonBar,
|
||||||
wrapper => wrapper.find('.button-bar__button--side-preview-toggler').trigger('click'),
|
wrapper => wrapper.find('.button-bar__button--side-preview-toggler').trigger('click'),
|
||||||
() => store.getters['data/layoutSettings'].showSidePreview,
|
() => store.getters['data/layoutSettings'].showSidePreview,
|
||||||
|
'toggleSidePreview',
|
||||||
));
|
));
|
||||||
|
|
||||||
it('should toggle the editor', () => specUtils.checkToggler(
|
it('should toggle the editor', async () => specUtils.checkToggler(
|
||||||
ButtonBar,
|
ButtonBar,
|
||||||
wrapper => wrapper.find('.button-bar__button--editor-toggler').trigger('click'),
|
wrapper => wrapper.find('.button-bar__button--editor-toggler').trigger('click'),
|
||||||
() => store.getters['data/layoutSettings'].showEditor,
|
() => store.getters['data/layoutSettings'].showEditor,
|
||||||
|
'toggleEditor',
|
||||||
));
|
));
|
||||||
|
|
||||||
it('should toggle the focus mode', () => specUtils.checkToggler(
|
it('should toggle the focus mode', async () => specUtils.checkToggler(
|
||||||
ButtonBar,
|
ButtonBar,
|
||||||
wrapper => wrapper.find('.button-bar__button--focus-mode-toggler').trigger('click'),
|
wrapper => wrapper.find('.button-bar__button--focus-mode-toggler').trigger('click'),
|
||||||
() => store.getters['data/layoutSettings'].focusMode,
|
() => store.getters['data/layoutSettings'].focusMode,
|
||||||
|
'toggleFocusMode',
|
||||||
));
|
));
|
||||||
|
|
||||||
it('should toggle the scroll sync', () => specUtils.checkToggler(
|
it('should toggle the scroll sync', async () => specUtils.checkToggler(
|
||||||
ButtonBar,
|
ButtonBar,
|
||||||
wrapper => wrapper.find('.button-bar__button--scroll-sync-toggler').trigger('click'),
|
wrapper => wrapper.find('.button-bar__button--scroll-sync-toggler').trigger('click'),
|
||||||
() => store.getters['data/layoutSettings'].scrollSync,
|
() => store.getters['data/layoutSettings'].scrollSync,
|
||||||
|
'toggleScrollSync',
|
||||||
));
|
));
|
||||||
|
|
||||||
it('should toggle the status bar', () => specUtils.checkToggler(
|
it('should toggle the status bar', async () => specUtils.checkToggler(
|
||||||
ButtonBar,
|
ButtonBar,
|
||||||
wrapper => wrapper.find('.button-bar__button--status-bar-toggler').trigger('click'),
|
wrapper => wrapper.find('.button-bar__button--status-bar-toggler').trigger('click'),
|
||||||
() => store.getters['data/layoutSettings'].showStatusBar,
|
() => store.getters['data/layoutSettings'].showStatusBar,
|
||||||
|
'toggleStatusBar',
|
||||||
));
|
));
|
||||||
});
|
});
|
||||||
|
@ -14,7 +14,7 @@ const ensureNotExists = file => expect(store.getters.allItemsById).not.toHavePro
|
|||||||
const refreshItem = item => store.getters.allItemsById[item.id];
|
const refreshItem = item => store.getters.allItemsById[item.id];
|
||||||
|
|
||||||
describe('Explorer.vue', () => {
|
describe('Explorer.vue', () => {
|
||||||
it('should create new files in the root folder', () => {
|
it('should create new file in the root folder', async () => {
|
||||||
expect(store.state.explorer.newChildNode.isNil).toBeTruthy();
|
expect(store.state.explorer.newChildNode.isNil).toBeTruthy();
|
||||||
const wrapper = mount();
|
const wrapper = mount();
|
||||||
wrapper.find('.side-title__button--new-file').trigger('click');
|
wrapper.find('.side-title__button--new-file').trigger('click');
|
||||||
@ -25,7 +25,7 @@ describe('Explorer.vue', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should create new files in a folder', async () => {
|
it('should create new file in a folder', async () => {
|
||||||
const folder = await workspaceSvc.storeItem({ type: 'folder' });
|
const folder = await workspaceSvc.storeItem({ type: 'folder' });
|
||||||
const wrapper = mount();
|
const wrapper = mount();
|
||||||
select(folder.id);
|
select(folder.id);
|
||||||
@ -36,7 +36,7 @@ describe('Explorer.vue', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not create new files in the trash folder', () => {
|
it('should not create new files in the trash folder', async () => {
|
||||||
const wrapper = mount();
|
const wrapper = mount();
|
||||||
select('trash');
|
select('trash');
|
||||||
wrapper.find('.side-title__button--new-file').trigger('click');
|
wrapper.find('.side-title__button--new-file').trigger('click');
|
||||||
@ -46,7 +46,7 @@ describe('Explorer.vue', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should create new folders in the root folder', () => {
|
it('should create new folders in the root folder', async () => {
|
||||||
expect(store.state.explorer.newChildNode.isNil).toBeTruthy();
|
expect(store.state.explorer.newChildNode.isNil).toBeTruthy();
|
||||||
const wrapper = mount();
|
const wrapper = mount();
|
||||||
wrapper.find('.side-title__button--new-folder').trigger('click');
|
wrapper.find('.side-title__button--new-folder').trigger('click');
|
||||||
@ -68,7 +68,7 @@ describe('Explorer.vue', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not create new folders in the trash folder', () => {
|
it('should not create new folders in the trash folder', async () => {
|
||||||
const wrapper = mount();
|
const wrapper = mount();
|
||||||
select('trash');
|
select('trash');
|
||||||
wrapper.find('.side-title__button--new-folder').trigger('click');
|
wrapper.find('.side-title__button--new-folder').trigger('click');
|
||||||
@ -78,7 +78,7 @@ describe('Explorer.vue', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not create new folders in the temp folder', () => {
|
it('should not create new folders in the temp folder', async () => {
|
||||||
const wrapper = mount();
|
const wrapper = mount();
|
||||||
select('temp');
|
select('temp');
|
||||||
wrapper.find('.side-title__button--new-folder').trigger('click');
|
wrapper.find('.side-title__button--new-folder').trigger('click');
|
||||||
@ -96,6 +96,7 @@ describe('Explorer.vue', () => {
|
|||||||
wrapper.find('.side-title__button--delete').trigger('click');
|
wrapper.find('.side-title__button--delete').trigger('click');
|
||||||
ensureExists(file);
|
ensureExists(file);
|
||||||
expect(refreshItem(file).parentId).toEqual('trash');
|
expect(refreshItem(file).parentId).toEqual('trash');
|
||||||
|
await specUtils.expectBadge('removeFile');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not delete the trash folder', async () => {
|
it('should not delete the trash folder', async () => {
|
||||||
@ -103,15 +104,17 @@ describe('Explorer.vue', () => {
|
|||||||
select('trash');
|
select('trash');
|
||||||
wrapper.find('.side-title__button--delete').trigger('click');
|
wrapper.find('.side-title__button--delete').trigger('click');
|
||||||
await specUtils.resolveModal('trashDeletion');
|
await specUtils.resolveModal('trashDeletion');
|
||||||
|
await specUtils.expectBadge('removeFile', false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not delete files in the trash folder', async () => {
|
it('should not delete file in the trash folder', async () => {
|
||||||
const file = await workspaceSvc.createFile({ parentId: 'trash' }, true);
|
const file = await workspaceSvc.createFile({ parentId: 'trash' }, true);
|
||||||
const wrapper = mount();
|
const wrapper = mount();
|
||||||
select(file.id);
|
select(file.id);
|
||||||
wrapper.find('.side-title__button--delete').trigger('click');
|
wrapper.find('.side-title__button--delete').trigger('click');
|
||||||
await specUtils.resolveModal('trashDeletion');
|
await specUtils.resolveModal('trashDeletion');
|
||||||
ensureExists(file);
|
ensureExists(file);
|
||||||
|
await specUtils.expectBadge('removeFile', false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should delete the temp folder after confirmation', async () => {
|
it('should delete the temp folder after confirmation', async () => {
|
||||||
@ -121,9 +124,10 @@ describe('Explorer.vue', () => {
|
|||||||
wrapper.find('.side-title__button--delete').trigger('click');
|
wrapper.find('.side-title__button--delete').trigger('click');
|
||||||
await specUtils.resolveModal('tempFolderDeletion');
|
await specUtils.resolveModal('tempFolderDeletion');
|
||||||
ensureNotExists(file);
|
ensureNotExists(file);
|
||||||
|
await specUtils.expectBadge('removeFolder');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should delete temp files after confirmation', async () => {
|
it('should delete temp file after confirmation', async () => {
|
||||||
const file = await workspaceSvc.createFile({ parentId: 'temp' }, true);
|
const file = await workspaceSvc.createFile({ parentId: 'temp' }, true);
|
||||||
const wrapper = mount();
|
const wrapper = mount();
|
||||||
select(file.id);
|
select(file.id);
|
||||||
@ -131,6 +135,7 @@ describe('Explorer.vue', () => {
|
|||||||
ensureExists(file);
|
ensureExists(file);
|
||||||
await specUtils.resolveModal('tempFileDeletion');
|
await specUtils.resolveModal('tempFileDeletion');
|
||||||
ensureNotExists(file);
|
ensureNotExists(file);
|
||||||
|
await specUtils.expectBadge('removeFile');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should delete folder after confirmation', async () => {
|
it('should delete folder after confirmation', async () => {
|
||||||
@ -144,9 +149,10 @@ describe('Explorer.vue', () => {
|
|||||||
// Make sure file has been moved to Trash
|
// Make sure file has been moved to Trash
|
||||||
ensureExists(file);
|
ensureExists(file);
|
||||||
expect(refreshItem(file).parentId).toEqual('trash');
|
expect(refreshItem(file).parentId).toEqual('trash');
|
||||||
|
await specUtils.expectBadge('removeFolder');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should rename files', async () => {
|
it('should rename file', async () => {
|
||||||
const file = await workspaceSvc.createFile({}, true);
|
const file = await workspaceSvc.createFile({}, true);
|
||||||
const wrapper = mount();
|
const wrapper = mount();
|
||||||
select(file.id);
|
select(file.id);
|
||||||
@ -154,7 +160,7 @@ describe('Explorer.vue', () => {
|
|||||||
expect(store.getters['explorer/editingNode'].item.id).toEqual(file.id);
|
expect(store.getters['explorer/editingNode'].item.id).toEqual(file.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should rename folders', async () => {
|
it('should rename folder', async () => {
|
||||||
const folder = await workspaceSvc.storeItem({ type: 'folder' });
|
const folder = await workspaceSvc.storeItem({ type: 'folder' });
|
||||||
const wrapper = mount();
|
const wrapper = mount();
|
||||||
select(folder.id);
|
select(folder.id);
|
||||||
@ -182,6 +188,7 @@ describe('Explorer.vue', () => {
|
|||||||
Explorer,
|
Explorer,
|
||||||
wrapper => wrapper.find('.side-title__button--close').trigger('click'),
|
wrapper => wrapper.find('.side-title__button--close').trigger('click'),
|
||||||
() => store.getters['data/layoutSettings'].showExplorer,
|
() => store.getters['data/layoutSettings'].showExplorer,
|
||||||
|
'toggleExplorer',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -49,15 +49,25 @@ const dragAndDrop = (sourceItem, targetItem) => {
|
|||||||
describe('ExplorerNode.vue', () => {
|
describe('ExplorerNode.vue', () => {
|
||||||
const modifiedName = 'Name';
|
const modifiedName = 'Name';
|
||||||
|
|
||||||
it('should open files on select after a timeout', async () => {
|
it('should open file on select after a timeout', async () => {
|
||||||
const node = await makeFileNode();
|
const node = await makeFileNode();
|
||||||
mountAndSelect(node);
|
mountAndSelect(node);
|
||||||
expect(store.getters['file/current'].id).not.toEqual(node.item.id);
|
expect(store.getters['file/current'].id).not.toEqual(node.item.id);
|
||||||
await new Promise(resolve => setTimeout(resolve, 10));
|
await new Promise(resolve => setTimeout(resolve, 10));
|
||||||
expect(store.getters['file/current'].id).toEqual(node.item.id);
|
expect(store.getters['file/current'].id).toEqual(node.item.id);
|
||||||
|
await specUtils.expectBadge('switchFile');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should open folders on select after a timeout', async () => {
|
it('should not open already open file', async () => {
|
||||||
|
const node = await makeFileNode();
|
||||||
|
store.commit('file/setCurrentId', node.item.id);
|
||||||
|
mountAndSelect(node);
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 10));
|
||||||
|
expect(store.getters['file/current'].id).toEqual(node.item.id);
|
||||||
|
await specUtils.expectBadge('switchFile', false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should open folder on select after a timeout', async () => {
|
||||||
const node = await makeFolderNode();
|
const node = await makeFolderNode();
|
||||||
const wrapper = mountAndSelect(node);
|
const wrapper = mountAndSelect(node);
|
||||||
expect(wrapper.classes()).not.toContain('explorer-node--open');
|
expect(wrapper.classes()).not.toContain('explorer-node--open');
|
||||||
@ -65,7 +75,7 @@ describe('ExplorerNode.vue', () => {
|
|||||||
expect(wrapper.classes()).toContain('explorer-node--open');
|
expect(wrapper.classes()).toContain('explorer-node--open');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should open folders on new child', async () => {
|
it('should open folder on new child', async () => {
|
||||||
const node = await makeFolderNode();
|
const node = await makeFolderNode();
|
||||||
const wrapper = mountAndSelect(node);
|
const wrapper = mountAndSelect(node);
|
||||||
// Close the folder
|
// Close the folder
|
||||||
@ -76,7 +86,7 @@ describe('ExplorerNode.vue', () => {
|
|||||||
expect(wrapper.classes()).toContain('explorer-node--open');
|
expect(wrapper.classes()).toContain('explorer-node--open');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should create new files in a folder', async () => {
|
it('should create new file in a folder', async () => {
|
||||||
const node = await makeFolderNode();
|
const node = await makeFolderNode();
|
||||||
const wrapper = mount(node);
|
const wrapper = mount(node);
|
||||||
wrapper.trigger('contextmenu');
|
wrapper.trigger('contextmenu');
|
||||||
@ -91,9 +101,10 @@ describe('ExplorerNode.vue', () => {
|
|||||||
parentId: node.item.id,
|
parentId: node.item.id,
|
||||||
});
|
});
|
||||||
expect(wrapper.contains('.explorer-node__new-child')).toBe(false);
|
expect(wrapper.contains('.explorer-node__new-child')).toBe(false);
|
||||||
|
await specUtils.expectBadge('createFile');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should cancel a file creation on escape', async () => {
|
it('should cancel file creation on escape', async () => {
|
||||||
const node = await makeFolderNode();
|
const node = await makeFolderNode();
|
||||||
const wrapper = mount(node);
|
const wrapper = mount(node);
|
||||||
wrapper.trigger('contextmenu');
|
wrapper.trigger('contextmenu');
|
||||||
@ -110,15 +121,16 @@ describe('ExplorerNode.vue', () => {
|
|||||||
parentId: node.item.id,
|
parentId: node.item.id,
|
||||||
});
|
});
|
||||||
expect(wrapper.contains('.explorer-node__new-child')).toBe(false);
|
expect(wrapper.contains('.explorer-node__new-child')).toBe(false);
|
||||||
|
await specUtils.expectBadge('createFile', false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not create new files in a file', async () => {
|
it('should not create new file in a file', async () => {
|
||||||
const node = await makeFileNode();
|
const node = await makeFileNode();
|
||||||
mount(node).trigger('contextmenu');
|
mount(node).trigger('contextmenu');
|
||||||
expect(specUtils.getContextMenuItem('New file').disabled).toBe(true);
|
expect(specUtils.getContextMenuItem('New file').disabled).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not create new files in the trash folder', async () => {
|
it('should not create new file in the trash folder', async () => {
|
||||||
const node = store.getters['explorer/nodeMap'].trash;
|
const node = store.getters['explorer/nodeMap'].trash;
|
||||||
mount(node).trigger('contextmenu');
|
mount(node).trigger('contextmenu');
|
||||||
expect(specUtils.getContextMenuItem('New file').disabled).toBe(true);
|
expect(specUtils.getContextMenuItem('New file').disabled).toBe(true);
|
||||||
@ -139,9 +151,10 @@ describe('ExplorerNode.vue', () => {
|
|||||||
parentId: node.item.id,
|
parentId: node.item.id,
|
||||||
});
|
});
|
||||||
expect(wrapper.contains('.explorer-node__new-child--folder')).toBe(false);
|
expect(wrapper.contains('.explorer-node__new-child--folder')).toBe(false);
|
||||||
|
await specUtils.expectBadge('createFolder');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should cancel a folder creation on escape', async () => {
|
it('should cancel folder creation on escape', async () => {
|
||||||
const node = await makeFolderNode();
|
const node = await makeFolderNode();
|
||||||
const wrapper = mount(node);
|
const wrapper = mount(node);
|
||||||
wrapper.trigger('contextmenu');
|
wrapper.trigger('contextmenu');
|
||||||
@ -158,27 +171,28 @@ describe('ExplorerNode.vue', () => {
|
|||||||
parentId: node.item.id,
|
parentId: node.item.id,
|
||||||
});
|
});
|
||||||
expect(wrapper.contains('.explorer-node__new-child--folder')).toBe(false);
|
expect(wrapper.contains('.explorer-node__new-child--folder')).toBe(false);
|
||||||
|
await specUtils.expectBadge('createFolder', false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not create new folders in a file', async () => {
|
it('should not create new folder in a file', async () => {
|
||||||
const node = await makeFileNode();
|
const node = await makeFileNode();
|
||||||
mount(node).trigger('contextmenu');
|
mount(node).trigger('contextmenu');
|
||||||
expect(specUtils.getContextMenuItem('New folder').disabled).toBe(true);
|
expect(specUtils.getContextMenuItem('New folder').disabled).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not create new folders in the trash folder', async () => {
|
it('should not create new folder in the trash folder', async () => {
|
||||||
const node = store.getters['explorer/nodeMap'].trash;
|
const node = store.getters['explorer/nodeMap'].trash;
|
||||||
mount(node).trigger('contextmenu');
|
mount(node).trigger('contextmenu');
|
||||||
expect(specUtils.getContextMenuItem('New folder').disabled).toBe(true);
|
expect(specUtils.getContextMenuItem('New folder').disabled).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not create new folders in the temp folder', async () => {
|
it('should not create new folder in the temp folder', async () => {
|
||||||
const node = store.getters['explorer/nodeMap'].temp;
|
const node = store.getters['explorer/nodeMap'].temp;
|
||||||
mount(node).trigger('contextmenu');
|
mount(node).trigger('contextmenu');
|
||||||
expect(specUtils.getContextMenuItem('New folder').disabled).toBe(true);
|
expect(specUtils.getContextMenuItem('New folder').disabled).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should rename files', async () => {
|
it('should rename file', async () => {
|
||||||
const node = await makeFileNode();
|
const node = await makeFileNode();
|
||||||
const wrapper = mount(node);
|
const wrapper = mount(node);
|
||||||
wrapper.trigger('contextmenu');
|
wrapper.trigger('contextmenu');
|
||||||
@ -187,20 +201,10 @@ describe('ExplorerNode.vue', () => {
|
|||||||
wrapper.setData({ editingValue: modifiedName });
|
wrapper.setData({ editingValue: modifiedName });
|
||||||
wrapper.find('.explorer-node__item-editor .text-input').trigger('blur');
|
wrapper.find('.explorer-node__item-editor .text-input').trigger('blur');
|
||||||
expect(store.getters['explorer/selectedNode'].item.name).toEqual(modifiedName);
|
expect(store.getters['explorer/selectedNode'].item.name).toEqual(modifiedName);
|
||||||
|
await specUtils.expectBadge('renameFile');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should rename folders', async () => {
|
it('should cancel rename file on escape', async () => {
|
||||||
const node = await makeFolderNode();
|
|
||||||
const wrapper = mount(node);
|
|
||||||
wrapper.trigger('contextmenu');
|
|
||||||
await specUtils.resolveContextMenu('Rename');
|
|
||||||
expect(wrapper.contains('.explorer-node__item-editor')).toBe(true);
|
|
||||||
wrapper.setData({ editingValue: modifiedName });
|
|
||||||
wrapper.find('.explorer-node__item-editor .text-input').trigger('blur');
|
|
||||||
expect(store.getters['explorer/selectedNode'].item.name).toEqual(modifiedName);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should cancel rename on escape', async () => {
|
|
||||||
const node = await makeFileNode();
|
const node = await makeFileNode();
|
||||||
const wrapper = mount(node);
|
const wrapper = mount(node);
|
||||||
wrapper.trigger('contextmenu');
|
wrapper.trigger('contextmenu');
|
||||||
@ -211,6 +215,33 @@ describe('ExplorerNode.vue', () => {
|
|||||||
keyCode: 27,
|
keyCode: 27,
|
||||||
});
|
});
|
||||||
expect(store.getters['explorer/selectedNode'].item.name).not.toEqual(modifiedName);
|
expect(store.getters['explorer/selectedNode'].item.name).not.toEqual(modifiedName);
|
||||||
|
await specUtils.expectBadge('renameFile', false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should rename folder', async () => {
|
||||||
|
const node = await makeFolderNode();
|
||||||
|
const wrapper = mount(node);
|
||||||
|
wrapper.trigger('contextmenu');
|
||||||
|
await specUtils.resolveContextMenu('Rename');
|
||||||
|
expect(wrapper.contains('.explorer-node__item-editor')).toBe(true);
|
||||||
|
wrapper.setData({ editingValue: modifiedName });
|
||||||
|
wrapper.find('.explorer-node__item-editor .text-input').trigger('blur');
|
||||||
|
expect(store.getters['explorer/selectedNode'].item.name).toEqual(modifiedName);
|
||||||
|
await specUtils.expectBadge('renameFolder');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should cancel rename folder on escape', async () => {
|
||||||
|
const node = await makeFolderNode();
|
||||||
|
const wrapper = mount(node);
|
||||||
|
wrapper.trigger('contextmenu');
|
||||||
|
await specUtils.resolveContextMenu('Rename');
|
||||||
|
expect(wrapper.contains('.explorer-node__item-editor')).toBe(true);
|
||||||
|
wrapper.setData({ editingValue: modifiedName });
|
||||||
|
wrapper.find('.explorer-node__item-editor .text-input').trigger('keydown', {
|
||||||
|
keyCode: 27,
|
||||||
|
});
|
||||||
|
expect(store.getters['explorer/selectedNode'].item.name).not.toEqual(modifiedName);
|
||||||
|
await specUtils.expectBadge('renameFolder', false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not rename the trash folder', async () => {
|
it('should not rename the trash folder', async () => {
|
||||||
@ -225,23 +256,26 @@ describe('ExplorerNode.vue', () => {
|
|||||||
expect(specUtils.getContextMenuItem('Rename').disabled).toBe(true);
|
expect(specUtils.getContextMenuItem('Rename').disabled).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should move a file into a folder', async () => {
|
it('should move file into a folder', async () => {
|
||||||
const sourceItem = await workspaceSvc.createFile({}, true);
|
const sourceItem = await workspaceSvc.createFile({}, true);
|
||||||
const targetItem = await workspaceSvc.storeItem({ type: 'folder' });
|
const targetItem = await workspaceSvc.storeItem({ type: 'folder' });
|
||||||
dragAndDrop(sourceItem, targetItem);
|
dragAndDrop(sourceItem, targetItem);
|
||||||
|
await specUtils.expectBadge('moveFile');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should move a folder into a folder', async () => {
|
it('should move folder into a folder', async () => {
|
||||||
const sourceItem = await workspaceSvc.storeItem({ type: 'folder' });
|
const sourceItem = await workspaceSvc.storeItem({ type: 'folder' });
|
||||||
const targetItem = await workspaceSvc.storeItem({ type: 'folder' });
|
const targetItem = await workspaceSvc.storeItem({ type: 'folder' });
|
||||||
dragAndDrop(sourceItem, targetItem);
|
dragAndDrop(sourceItem, targetItem);
|
||||||
|
await specUtils.expectBadge('moveFolder');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should move a file into a file parent folder', async () => {
|
it('should move file into a file parent folder', async () => {
|
||||||
const targetItem = await workspaceSvc.storeItem({ type: 'folder' });
|
const targetItem = await workspaceSvc.storeItem({ type: 'folder' });
|
||||||
const file = await workspaceSvc.createFile({ parentId: targetItem.id }, true);
|
const file = await workspaceSvc.createFile({ parentId: targetItem.id }, true);
|
||||||
const sourceItem = await workspaceSvc.createFile({}, true);
|
const sourceItem = await workspaceSvc.createFile({}, true);
|
||||||
dragAndDrop(sourceItem, file);
|
dragAndDrop(sourceItem, file);
|
||||||
|
await specUtils.expectBadge('moveFile');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not move the trash folder', async () => {
|
it('should not move the trash folder', async () => {
|
||||||
@ -256,14 +290,14 @@ describe('ExplorerNode.vue', () => {
|
|||||||
expect(store.state.explorer.dragSourceId).not.toEqual('temp');
|
expect(store.state.explorer.dragSourceId).not.toEqual('temp');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not move a file to the temp folder', async () => {
|
it('should not move file to the temp folder', async () => {
|
||||||
const targetNode = store.getters['explorer/nodeMap'].temp;
|
const targetNode = store.getters['explorer/nodeMap'].temp;
|
||||||
const wrapper = mount(targetNode);
|
const wrapper = mount(targetNode);
|
||||||
wrapper.trigger('dragenter');
|
wrapper.trigger('dragenter');
|
||||||
expect(store.state.explorer.dragTargetId).not.toEqual('temp');
|
expect(store.state.explorer.dragTargetId).not.toEqual('temp');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not move a file to a file in the temp folder', async () => {
|
it('should not move file to a file in the temp folder', async () => {
|
||||||
const file = await workspaceSvc.createFile({ parentId: 'temp' }, true);
|
const file = await workspaceSvc.createFile({ parentId: 'temp' }, true);
|
||||||
const targetNode = store.getters['explorer/nodeMap'][file.id];
|
const targetNode = store.getters['explorer/nodeMap'][file.id];
|
||||||
const wrapper = mount(targetNode);
|
const wrapper = mount(targetNode);
|
||||||
|
@ -3,15 +3,17 @@ import store from '../../../../src/store';
|
|||||||
import specUtils from '../specUtils';
|
import specUtils from '../specUtils';
|
||||||
|
|
||||||
describe('NavigationBar.vue', () => {
|
describe('NavigationBar.vue', () => {
|
||||||
it('should toggle the explorer', () => specUtils.checkToggler(
|
it('should toggle the explorer', async () => specUtils.checkToggler(
|
||||||
NavigationBar,
|
NavigationBar,
|
||||||
wrapper => wrapper.find('.navigation-bar__button--explorer-toggler').trigger('click'),
|
wrapper => wrapper.find('.navigation-bar__button--explorer-toggler').trigger('click'),
|
||||||
() => store.getters['data/layoutSettings'].showExplorer,
|
() => store.getters['data/layoutSettings'].showExplorer,
|
||||||
|
'toggleExplorer',
|
||||||
));
|
));
|
||||||
|
|
||||||
it('should toggle the side bar', () => specUtils.checkToggler(
|
it('should toggle the side bar', async () => specUtils.checkToggler(
|
||||||
NavigationBar,
|
NavigationBar,
|
||||||
wrapper => wrapper.find('.navigation-bar__button--stackedit').trigger('click'),
|
wrapper => wrapper.find('.navigation-bar__button--stackedit').trigger('click'),
|
||||||
() => store.getters['data/layoutSettings'].showSideBar,
|
() => store.getters['data/layoutSettings'].showSideBar,
|
||||||
|
'toggleSideBar',
|
||||||
));
|
));
|
||||||
});
|
});
|
||||||
|
@ -25,12 +25,13 @@ beforeEach(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
checkToggler(Component, toggler, checker) {
|
async checkToggler(Component, toggler, checker, featureId) {
|
||||||
const wrapper = shallowMount(Component, { store });
|
const wrapper = shallowMount(Component, { store });
|
||||||
const valueBefore = checker();
|
const valueBefore = checker();
|
||||||
toggler(wrapper);
|
toggler(wrapper);
|
||||||
const valueAfter = checker();
|
const valueAfter = checker();
|
||||||
expect(valueAfter).toEqual(!valueBefore);
|
expect(valueAfter).toEqual(!valueBefore);
|
||||||
|
await this.expectBadge(featureId);
|
||||||
},
|
},
|
||||||
async resolveModal(type) {
|
async resolveModal(type) {
|
||||||
const config = store.getters['modal/config'];
|
const config = store.getters['modal/config'];
|
||||||
@ -47,5 +48,11 @@ export default {
|
|||||||
expect(item).toBeTruthy();
|
expect(item).toBeTruthy();
|
||||||
store.state.contextMenu.resolve(item);
|
store.state.contextMenu.resolve(item);
|
||||||
await new Promise(resolve => setTimeout(resolve, 1));
|
await new Promise(resolve => setTimeout(resolve, 1));
|
||||||
}
|
},
|
||||||
|
async expectBadge(featureId, isEarned = true) {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1));
|
||||||
|
expect(store.getters['data/allBadges'].filter(badge => badge.featureId === featureId)[0]).toMatchObject({
|
||||||
|
isEarned,
|
||||||
|
});
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user