Merge remote-tracking branch 'origin/feature/new-content-block-creation-workflow' into develop

This commit is contained in:
Ramon Wenger 2022-01-30 10:26:20 +01:00
commit a626fd9d04
402 changed files with 12196 additions and 9594 deletions

View File

@ -1,3 +1,13 @@
{ {
"schemaPath": "server/schema.graphql" "projects": {
"private": {
"schemaPath": "./server/schema.graphql",
"includes": ["./client/src/graphql/**"],
"excludes": ["./client/src/graphql/gql/public-client/**"]
},
"public": {
"schemaPath": "./server/schema-public.graphql",
"includes": ["./client/src/graphql/gql/public-client/*.gql"]
}
}
} }

View File

@ -1 +0,0 @@
schema: 'server/schema.graphql'

View File

@ -42,6 +42,8 @@ ipython = "*"
requests = "*" requests = "*"
unittest-xml-reporting = "*" unittest-xml-reporting = "*"
django-silk = "*" django-silk = "*"
wagtail-autocomplete = "*" # todo: @django3-update
# wagtail-autocomplete = "*"
wagtail-autocomplete = "==0.6.3"
jedi = "==0.17.2" jedi = "==0.17.2"
Authlib = "*" Authlib = "*"

47
Pipfile.django3.bk Normal file
View File

@ -0,0 +1,47 @@
[[source]]
url = "https://pypi.python.org/simple"
verify_ssl = true
name = "pypi"
[requires]
python_version = "3.8"
[dev-packages]
awscli = "*"
ipdb = "*"
coverage = "*"
django-silk = "*"
[packages]
factory-boy = "==2.11.0"
wagtail_factories = "==2.0.0"
django = "==3.2"
whitenoise = "~=5.3"
psycopg2 = "==2.8.6"
gunicorn = "==19.7.1"
python-dotenv = "==0.13.0"
dj-database-url = "==0.4.1"
raven = "==6.9.0"
django-extensions = "==1.9.8"
graphene-django = "==2.15.0"
django-filter = "~=21.1"
djangorestframework = "~=3.8"
pillow = "==5.0.0"
wagtail = "~=2.15"
django-cors-headers = "~=3.0"
django-storages = "*"
boto3 = "*"
django-compressor = "*"
django-libsass = "*"
bleach = "*"
newrelic = "*"
sentry-sdk = "==0.7.2"
django-sendgrid-v5 = "==0.8.0"
python-http-client = "==3.2.1"
ipython = "*"
requests = "*"
unittest-xml-reporting = "*"
django-silk = "*"
wagtail-autocomplete = "*"
jedi = "==0.17.2"
Authlib = "*"

1148
Pipfile.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -270,3 +270,8 @@ Command:
``` ```
./bin/pg-backup-to-s3 ./bin/pg-backup-to-s3
``` ```
# Note on component
Our own components remain in kebap-case, imported components from third party libraries will be used in PascalCase.
E.g. `<password-change-form/>` vs. `<ValidationProvider/>`

View File

@ -3,23 +3,36 @@
module.exports = { module.exports = {
root: true, root: true,
parserOptions: { parserOptions: {
parser: 'babel-eslint' parser: '@typescript-eslint/parser',
extraFileExtensions: ['.vue'],
}, },
env: { env: {
browser: true, browser: true,
}, },
globals: {
process: "readonly"
},
extends: [ extends: [
// https://github.com/vuejs/eslint-plugin-vue#priority-a-essential-error-prevention // https://github.com/vuejs/eslint-plugin-vue#priority-a-essential-error-prevention
// consider switching to `plugin:vue/strongly-recommended` or `plugin:vue/recommended` for stricter rules. // consider switching to `plugin:vue/strongly-recommended` or `plugin:vue/recommended` for stricter rules.
'plugin:vue/recommended', 'plugin:vue/recommended',
// 'plugin:vue/recommended', // 'plugin:vue/recommended',
// https://github.com/standard/standard/blob/master/docs/RULES-en.md // https://github.com/standard/standard/blob/master/docs/RULES-en.md
'standard' //'standard'
'eslint:recommended',
'plugin:@typescript-eslint/eslint-recommended'
], ],
// required to lint *.vue files // required to lint *.vue files
plugins: [ plugins: [
'vue' 'vue',
'@typescript-eslint'
], ],
overrides: [{
files: ['*.ts','*.tsx'],
rules: {
'no-unused-vars': 'off'
}
}],
// add your custom rules here // add your custom rules here
rules: { rules: {
// allow async-await // allow async-await
@ -49,6 +62,9 @@ module.exports = {
'CONTENT' 'CONTENT'
] ]
}], }],
"vue/multi-word-component-names": ["off", {
"ignores": []
}],
'vue/order-in-components': ['error', { 'vue/order-in-components': ['error', {
'order': [ 'order': [
'el', 'el',

View File

@ -17,7 +17,7 @@ spinner.start()
rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => { rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => {
if (err) throw err if (err) throw err
webpack(webpackConfig, (err, stats) => { webpack(webpackConfig, (err, stats) => {
spinner.stop() spinner.succeed()
if (err) throw err if (err) throw err
process.stdout.write(stats.toString({ process.stdout.write(stats.toString({
colors: true, colors: true,

View File

@ -1,56 +1,10 @@
'use strict' 'use strict'
const path = require('path') const path = require('path')
const config = require('../config') const config = require('../config')
const ExtractTextPlugin = require('extract-text-webpack-plugin')
const packageConfig = require('../package.json') const packageConfig = require('../package.json')
const isDev = process.env.NODE_ENV !== 'production'; const isDev = process.env.NODE_ENV !== 'production';
const styleRule = (scss) => {
const test = scss ? /\.scss$/ : /\.css$/;
let use = [
{
loader: 'css-loader',
// options: {importLoaders: scss ? 3 : 2}
options: {
sourceMap: isDev,
importLoaders: scss ? 2 : 1
}
},
'postcss-loader'
];
if (scss) {
use = [
...use,
{
loader: 'sass-loader',
options: {
data: process.env.THEME ? `@import "styles/themes/_${process.env.THEME}.scss";` : '',
sourceMap: isDev
}
}
]
}
if (!isDev) {
return {
test,
loader: ExtractTextPlugin.extract({
use,
fallback: 'vue-style-loader'
})
}
} else {
return {
test,
use: [
'vue-style-loader',
...use
]
}
}
}
const assetsPath = (_path) => { const assetsPath = (_path) => {
const assetsSubDirectory = isDev const assetsSubDirectory = isDev
? config.dev.assetsSubDirectory ? config.dev.assetsSubDirectory
@ -78,7 +32,6 @@ const createNotifierCallback = () => {
} }
module.exports = { module.exports = {
styleRule,
isDev, isDev,
assetsPath, assetsPath,
createNotifierCallback createNotifierCallback

View File

@ -1,6 +0,0 @@
'use strict'
const config = require('../config')
module.exports = {
// cacheBusting: config.dev.cacheBusting,
}

View File

@ -1,10 +1,11 @@
'use strict'; 'use strict';
const path = require('path'); const path = require('path');
const config = require('../config'); const config = require('../config');
var MiniCssExtractPlugin = require('mini-css-extract-plugin')
const {VueLoaderPlugin} = require('vue-loader'); const {VueLoaderPlugin} = require('vue-loader');
const {isDev, styleRule, assetsPath} = require('./utils'); const {isDev, assetsPath} = require('./utils');
function resolve(dir) { function resolve(dir) {
return path.join(__dirname, '..', dir); return path.join(__dirname, '..', dir);
@ -37,13 +38,29 @@ module.exports = {
? config.dev.assetsPublicPath ? config.dev.assetsPublicPath
: config.build.assetsPublicPath, : config.build.assetsPublicPath,
}, },
optimization: {
splitChunks: {
chunks: 'all'
}
},
resolve: { resolve: {
extensions: ['.js', '.vue', '.json', '.gql', '.graphql', '.scss'], extensions: ['.js', '.vue', '.json', '.gql', '.graphql', '.scss'],
alias: { alias: {
'@': resolve('src'), '@': resolve('src'),
styles: resolve('src/styles'), styles: resolve('src/styles'),
gql: resolve('src/graphql/gql'), gql: resolve('src/graphql/gql'),
// vue: '@vue/compat',
}, },
// we probably don't need this anymore
// fallback: {
// // used to be in node: {setImmediate: false,...}
// setImmediate: false,
// dgram: false,
// fs: false,
// net: false,
// tls: false,
// child_process: false,
// },
}, },
module: { module: {
rules: [ rules: [
@ -58,6 +75,11 @@ module.exports = {
img: 'src', img: 'src',
image: 'xlink:href', image: 'xlink:href',
}, },
compilerOptions: {
compatConfig: {
MODE: 2,
},
},
}, },
}, },
{ {
@ -105,23 +127,33 @@ module.exports = {
name: assetsPath('fonts/[name].[hash:7].[ext]'), name: assetsPath('fonts/[name].[hash:7].[ext]'),
}, },
}, },
styleRule(false), // css rule {
styleRule(true), // sass rule test: /\.s?css$/,
use: [
isDev ? 'vue-style-loader' : MiniCssExtractPlugin.loader,
'css-loader',
'postcss-loader',
'sass-loader'
]
}
// styleRule(false), // css rule
// styleRule(true), // sass rule
], ],
}, },
plugins: [ plugins: [
new VueLoaderPlugin(), new VueLoaderPlugin(),
], ],
node: {
// prevent webpack from injecting useless setImmediate polyfill because Vue // node: {
// source contains it (although only uses it if it's native). // // prevent webpack from injecting useless setImmediate polyfill because Vue
setImmediate: false, // // source contains it (although only uses it if it's native).
// prevent webpack from injecting mocks to Node native modules // setImmediate: false,
// that does not make sense for the client // // prevent webpack from injecting mocks to Node native modules
dgram: 'empty', // // that does not make sense for the client
fs: 'empty', // dgram: 'empty',
net: 'empty', // fs: 'empty',
tls: 'empty', // net: 'empty',
child_process: 'empty', // tls: 'empty',
}, // child_process: 'empty',
// },
}; };

View File

@ -1,93 +1,99 @@
'use strict' 'use strict';
const utils = require('./utils') const utils = require('./utils');
const webpack = require('webpack') const webpack = require('webpack');
const config = require('../config') const config = require('../config');
const merge = require('webpack-merge') const path = require('path');
const path = require('path') const baseWebpackConfig = require('./webpack.base.conf');
const baseWebpackConfig = require('./webpack.base.conf') const CopyPlugin = require('copy-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin') const HtmlWebpackPlugin = require('html-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin') const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin') const portfinder = require('portfinder');
const portfinder = require('portfinder') const {merge} = require('webpack-merge');
const HOST = process.env.HOST const HOST = process.env.HOST;
const PORT = process.env.PORT && Number(process.env.PORT) const PORT = process.env.PORT && Number(process.env.PORT);
const devWebpackConfig = merge(baseWebpackConfig, { const devWebpackConfig = merge(baseWebpackConfig, {
// cheap-module-eval-source-map is faster for development // cheap-module-eval-source-map is faster for development
devtool: config.dev.devtool, devtool: config.dev.devtool,
mode: 'development',
// these devServer options should be customized in /config/index.js // these devServer options should be customized in /config/index.js
devServer: { devServer: {
clientLogLevel: 'warning', client: {
logging: 'warn',
overlay: config.dev.errorOverlay ? {errors: true, warnings: false} : false,
progress: true,
},
historyApiFallback: { historyApiFallback: {
rewrites: [ rewrites: [
{ from: /.*/, to: path.posix.join(config.dev.assetsPublicPath, 'index.html') }, {from: /.*/, to: path.posix.join(config.dev.assetsPublicPath, 'index.html')},
], ],
}, },
hot: true, hot: true,
contentBase: false, // since we use CopyWebpackPlugin.
compress: true, compress: true,
host: HOST || config.dev.host, host: HOST || config.dev.host,
port: PORT || config.dev.port, port: PORT || config.dev.port,
open: config.dev.autoOpenBrowser, open: config.dev.autoOpenBrowser,
overlay: config.dev.errorOverlay // publicPath: config.dev.assetsPublicPath,
? { warnings: false, errors: true }
: false,
publicPath: config.dev.assetsPublicPath,
proxy: config.dev.proxyTable, proxy: config.dev.proxyTable,
quiet: true, // necessary for FriendlyErrorsPlugin // quiet: true, // necessary for FriendlyErrorsPlugin
watchOptions: {
poll: config.dev.poll,
}
}, },
plugins: [ plugins: [
new webpack.DefinePlugin({ new webpack.DefinePlugin({
'process.env': require('../config/dev.env') 'process.env': require('../config/dev.env'),
// bundler feature flags https://github.com/vuejs/vue-next/tree/master/packages/vue#bundler-build-feature-flags
__VUE_OPTIONS_API__: true, // default, but explicit
__VUE_PROD_DEVTOOLS__: false, // default, but explicit
}), }),
new webpack.HotModuleReplacementPlugin(),
new webpack.NamedModulesPlugin(), // HMR shows correct file names in console on update.
new webpack.NoEmitOnErrorsPlugin(),
// https://github.com/ampedandwired/html-webpack-plugin // https://github.com/ampedandwired/html-webpack-plugin
new HtmlWebpackPlugin({ new HtmlWebpackPlugin({
filename: 'index.html', filename: 'index.html',
template: 'index.html', template: 'index.html',
inject: true, ...require('../config/dev.env'),
...require('../config/dev.env')
}), }),
// copy custom static assets // copy custom static assets
new CopyWebpackPlugin([ new CopyPlugin({
patterns: [
{ {
from: path.resolve(__dirname, '../static'), from: path.resolve(__dirname, '../static'),
to: config.dev.assetsSubDirectory, to: config.dev.assetsSubDirectory,
ignore: ['.*'] globOptions: {
} ignore: ['.*'],
]) },
] },
}) ],
}),
new BundleAnalyzerPlugin({
analyzerMode: 'disabled' // do nothing by default, but be able to generate stats with --profile
})
],
});
module.exports = new Promise((resolve, reject) => { module.exports = new Promise((resolve, reject) => {
portfinder.basePort = process.env.PORT || config.dev.port portfinder.basePort = process.env.PORT || config.dev.port;
portfinder.getPort((err, port) => { portfinder.getPort((err, port) => {
if (err) { if (err) {
reject(err) reject(err);
} else { } else {
// publish the new Port, necessary for e2e tests // publish the new Port, necessary for e2e tests
process.env.PORT = port process.env.PORT = port;
// add port to devServer config // add port to devServer config
devWebpackConfig.devServer.port = port devWebpackConfig.devServer.port = port;
// Add FriendlyErrorsPlugin // Add FriendlyErrorsPlugin
devWebpackConfig.plugins.push(new FriendlyErrorsPlugin({ // todo: seems to not be maintained anymore, disable for now
compilationSuccessInfo: { // devWebpackConfig.plugins.push(new FriendlyErrorsPlugin({
messages: [`Your application is running here: http://${devWebpackConfig.devServer.host}:${port}`], // compilationSuccessInfo: {
}, // messages: [`Your application is running here: http://${devWebpackConfig.devServer.host}:${port}`],
onErrors: config.dev.notifyOnErrors // },
? utils.createNotifierCallback() // onErrors: config.dev.notifyOnErrors
: undefined // ? utils.createNotifierCallback()
})) // : undefined,
// }));
resolve(devWebpackConfig) resolve(devWebpackConfig);
} }
}) });
}) });

View File

@ -1,54 +1,43 @@
'use strict' 'use strict';
const path = require('path') const path = require('path');
const utils = require('./utils') const utils = require('./utils');
const webpack = require('webpack') const webpack = require('webpack');
const config = require('../config') const config = require('../config');
const merge = require('webpack-merge') const {merge} = require('webpack-merge');
const baseWebpackConfig = require('./webpack.base.conf') const baseWebpackConfig = require('./webpack.base.conf');
const CopyWebpackPlugin = require('copy-webpack-plugin') const CopyWebpackPlugin = require('copy-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin') const HtmlWebpackPlugin = require('html-webpack-plugin');
const ExtractTextPlugin = require('extract-text-webpack-plugin') const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin');
const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin')
const UglifyJsPlugin = require('uglifyjs-webpack-plugin')
const env = require('../config/prod.env') const env = require('../config/prod.env');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const webpackConfig = merge(baseWebpackConfig, { const webpackConfig = merge(baseWebpackConfig, {
devtool: config.build.productionSourceMap ? config.build.devtool : false, devtool: config.build.productionSourceMap ? config.build.devtool : false,
mode: 'production',
output: { output: {
path: config.build.assetsRoot, path: config.build.assetsRoot,
filename: utils.assetsPath('js/[name].[chunkhash].js'), filename: utils.assetsPath('js/[name].[chunkhash].js'),
chunkFilename: utils.assetsPath('js/[id].[chunkhash].js') chunkFilename: utils.assetsPath('js/[id].[chunkhash].js'),
}, },
plugins: [ plugins: [
// http://vuejs.github.io/vue-loader/en/workflow/production.html // http://vuejs.github.io/vue-loader/en/workflow/production.html
new webpack.DefinePlugin({ new webpack.DefinePlugin({
'process.env': env 'process.env': env,
}), // bundler feature flags https://github.com/vuejs/vue-next/tree/master/packages/vue#bundler-build-feature-flags
new UglifyJsPlugin({ __VUE_OPTIONS_API__: true, // default, but explicit
uglifyOptions: { __VUE_PROD_DEVTOOLS__: false, // default, but explicit
compress: {
warnings: false
}
},
sourceMap: config.build.productionSourceMap,
parallel: true
}), }),
// extract css into its own file // extract css into its own file
new ExtractTextPlugin({ new MiniCssExtractPlugin({
filename: utils.assetsPath('css/[name].[contenthash].css'), filename: utils.assetsPath('css/[name].[contenthash].css'),
// Setting the following option to `false` will not extract CSS from codesplit chunks.
// Their CSS will instead be inserted dynamically with style-loader when the codesplit chunk has been loaded by webpack.
// It's currently set to `true` because we are seeing that sourcemaps are included in the codesplit bundle as well when it's `false`,
// increasing file size: https://github.com/vuejs-templates/webpack/issues/1110
allChunks: true,
}), }),
// Compress extracted CSS. We are using this plugin so that possible // Compress extracted CSS. We are using this plugin so that possible
// duplicated CSS from different components can be deduped. // duplicated CSS from different components can be deduped.
new OptimizeCSSPlugin({ new OptimizeCSSPlugin({
cssProcessorOptions: config.build.productionSourceMap cssProcessorOptions: config.build.productionSourceMap
? { safe: true, map: { inline: false } } ? {safe: true, map: {inline: false}}
: { safe: true } : {safe: true},
}), }),
// generate dist index.html with correct asset hash for caching. // generate dist index.html with correct asset hash for caching.
// you can customize output by editing /index.html // you can customize output by editing /index.html
@ -56,65 +45,70 @@ const webpackConfig = merge(baseWebpackConfig, {
new HtmlWebpackPlugin({ new HtmlWebpackPlugin({
filename: config.build.index, filename: config.build.index,
template: 'index.html', template: 'index.html',
inject: true,
...require('../config/prod.env'), ...require('../config/prod.env'),
minify: { minify: { // defaults from https://github.com/jantimon/html-webpack-plugin#minification
removeComments: true,
collapseWhitespace: true, collapseWhitespace: true,
removeAttributeQuotes: true keepClosingSlash: true,
// more options: removeComments: true,
// https://github.com/kangax/html-minifier#options-quick-reference removeRedundantAttributes: true,
removeScriptTypeAttributes: true,
removeStyleLinkTypeAttributes: true,
useShortDoctype: true,
}, },
// necessary to consistently work with multiple chunks via CommonsChunkPlugin // necessary to consistently work with multiple chunks via CommonsChunkPlugin
chunksSortMode: 'dependency' chunksSortMode: 'auto',
}), }),
// keep module.id stable when vendor modules does not change // keep module.id stable when vendor modules does not change
new webpack.HashedModuleIdsPlugin(), new webpack.ids.HashedModuleIdsPlugin(),
// enable scope hoisting
new webpack.optimize.ModuleConcatenationPlugin(),
// split vendor js into its own file // split vendor js into its own file
new webpack.optimize.CommonsChunkPlugin({ // todo: https://gist.github.com/sokra/1522d586b8e5c0f5072d7565c2bee693
name: 'vendor', // todo: do we need this? probably default is fine
minChunks (module) { // new webpack.optimize.CommonsChunkPlugin({
// any required modules inside node_modules are extracted to vendor // name: 'vendor',
return ( // minChunks (module) {
module.resource && // // any required modules inside node_modules are extracted to vendor
/\.js$/.test(module.resource) && // return (
module.resource.indexOf( // module.resource &&
path.join(__dirname, '../node_modules') // /\.js$/.test(module.resource) &&
) === 0 // module.resource.indexOf(
) // path.join(__dirname, '../node_modules')
} // ) === 0
}), // )
// }
// }),
// extract webpack runtime and module manifest to its own file in order to // extract webpack runtime and module manifest to its own file in order to
// prevent vendor hash from being updated whenever app bundle is updated // prevent vendor hash from being updated whenever app bundle is updated
new webpack.optimize.CommonsChunkPlugin({ // new webpack.optimize.CommonsChunkPlugin({
name: 'manifest', // name: 'manifest',
minChunks: Infinity // minChunks: Infinity
}), // }),
// This instance extracts shared chunks from code splitted chunks and bundles them // This instance extracts shared chunks from code splitted chunks and bundles them
// in a separate chunk, similar to the vendor chunk // in a separate chunk, similar to the vendor chunk
// see: https://webpack.js.org/plugins/commons-chunk-plugin/#extra-async-commons-chunk // see: https://webpack.js.org/plugins/commons-chunk-plugin/#extra-async-commons-chunk
new webpack.optimize.CommonsChunkPlugin({ // new webpack.optimize.CommonsChunkPlugin({
name: 'app', // name: 'app',
async: 'vendor-async', // async: 'vendor-async',
children: true, // children: true,
minChunks: 3 // minChunks: 3
}), // }),
// copy custom static assets // copy custom static assets
new CopyWebpackPlugin([ new CopyWebpackPlugin({
patterns: [
{ {
from: path.resolve(__dirname, '../static'), from: path.resolve(__dirname, '../static'),
to: config.build.assetsSubDirectory, to: config.build.assetsSubDirectory,
ignore: ['.*'] globOptions: {
} ignore: ['.*'],
]) },
] },
}) ],
}),
],
});
if (config.build.productionGzip) { if (config.build.productionGzip) {
const CompressionWebpackPlugin = require('compression-webpack-plugin') const CompressionWebpackPlugin = require('compression-webpack-plugin');
webpackConfig.plugins.push( webpackConfig.plugins.push(
new CompressionWebpackPlugin({ new CompressionWebpackPlugin({
@ -123,17 +117,17 @@ if (config.build.productionGzip) {
test: new RegExp( test: new RegExp(
'\\.(' + '\\.(' +
config.build.productionGzipExtensions.join('|') + config.build.productionGzipExtensions.join('|') +
')$' ')$',
), ),
threshold: 10240, threshold: 10240,
minRatio: 0.8 minRatio: 0.8,
}) }),
) );
} }
if (config.build.bundleAnalyzerReport) { if (config.build.bundleAnalyzerReport) {
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
webpackConfig.plugins.push(new BundleAnalyzerPlugin()) webpackConfig.plugins.push(new BundleAnalyzerPlugin());
} }
module.exports = webpackConfig module.exports = webpackConfig;

2
client/bundle-analysis/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
*
!.gitignore

View File

@ -1,5 +1,5 @@
'use strict' 'use strict'
const merge = require('webpack-merge') const {merge} = require('webpack-merge')
const prodEnv = require('./prod.env') const prodEnv = require('./prod.env')
module.exports = merge(prodEnv, { module.exports = merge(prodEnv, {

View File

@ -33,7 +33,8 @@ module.exports = {
*/ */
// https://webpack.js.org/configuration/devtool/#development // https://webpack.js.org/configuration/devtool/#development
devtool: 'cheap-module-eval-source-map', // devtool: 'cheap-module-eval-source-map',
devtool: 'eval-cheap-module-source-map',
// If you have problems debugging vue-files in devtools, // If you have problems debugging vue-files in devtools,
// set this to false - it *may* help // set this to false - it *may* help
@ -57,7 +58,7 @@ module.exports = {
productionSourceMap: true, productionSourceMap: true,
// https://webpack.js.org/configuration/devtool/#production // https://webpack.js.org/configuration/devtool/#production
devtool: '#source-map', devtool: 'source-map',
// Gzip off by default as many popular static hosts such as // Gzip off by default as many popular static hosts such as
// Surge or Netlify already gzip all static assets for you. // Surge or Netlify already gzip all static assets for you.

View File

@ -24,19 +24,11 @@
"slug": "geld-und-kauf", "slug": "geld-und-kauf",
"__typename": "TopicNode" "__typename": "TopicNode"
}, },
"schoolClasses": { "schoolClasses": [{
"edges": [
{
"node": {
"id": "U2Nob29sQ2xhc3NOb2RlOjE=", "id": "U2Nob29sQ2xhc3NOb2RlOjE=",
"name": "FLID2018a", "name": "FLID2018a",
"__typename": "SchoolClassNode" "__typename": "SchoolClassNode"
}, }],
"__typename": "SchoolClassNodeEdge"
}
],
"__typename": "SchoolClassNodeConnection"
},
"__typename": "UserNode", "__typename": "UserNode",
"onboardingVisited": true, "onboardingVisited": true,
"permissions": [] "permissions": []

View File

@ -1,5 +1,7 @@
export const SELECTED_CLASS_ID = 987;
export const SELECTED_CLASS_ID_ENCODED = btoa(`SchoolClassNode:${SELECTED_CLASS_ID}`);
const selectedClass = { const selectedClass = {
id: btoa('SchoolClassNode:selectedClassId'), id: SELECTED_CLASS_ID_ENCODED,
name: 'Moordale', name: 'Moordale',
readOnly: false, readOnly: false,
code: 'XXXX', code: 'XXXX',
@ -42,7 +44,7 @@ export default {
id: getChapterId(), id: getChapterId(),
title: 'chapter-title', title: 'chapter-title',
description: 'chapter-description', description: 'chapter-description',
bookmark: null
}), }),
ContentBlockNode: () => ({ ContentBlockNode: () => ({
contents: [], contents: [],
@ -62,11 +64,7 @@ export default {
readOnly: false, readOnly: false,
onboardingVisited: true, onboardingVisited: true,
selectedClass, selectedClass,
schoolClasses: { schoolClasses: [selectedClass],
edges: [
{node: selectedClass},
],
},
recentModules: { recentModules: {
edges: [], edges: [],
}, },
@ -84,19 +82,18 @@ export default {
}), }),
ModuleNode: () => ({ ModuleNode: () => ({
title: 'Module Title', title: 'Module Title',
slug: 'some slug', slug: 'some-slug',
metaTitle: 'Meta Title', metaTitle: 'Meta Title',
heroImage: '', heroImage: '',
teaser: '', teaser: '',
intro: '', intro: '',
assignments: {nodes: []}, assignments: [],
objectiveGroups: [], objectiveGroups: [],
id: getModuleId(), id: getModuleId(),
bookmark: null
}), }),
TopicNode: () => ({ TopicNode: () => ({
modules: { modules: [],
edges: [],
},
}), }),
RoomNode: () => ({ RoomNode: () => ({
title: 'A Room', title: 'A Room',
@ -104,7 +101,7 @@ export default {
appearance: 'blue', appearance: 'blue',
description: 'A Room description', description: 'A Room description',
schoolClass: { schoolClass: {
id: 'selectedClassId', id: SELECTED_CLASS_ID_ENCODED,
}, },
}), }),
RoomEntryNode: () => ({ RoomEntryNode: () => ({

View File

@ -4,9 +4,9 @@ export default {
heroImage: 'heroImage', heroImage: 'heroImage',
teaser: 'A Module Mock Teaser', teaser: 'A Module Mock Teaser',
intro: 'intro', intro: 'intro',
assignments: {}, assignments: [],
objectiveGroups: [], objectiveGroups: [],
id: '', id: 'TW9kdWxlTm9kZToxMjM=',
chapters: [], chapters: [],
topic: { topic: {
title: 'A Topic Mock Title', title: 'A Topic Mock Title',

View File

@ -1,36 +0,0 @@
describe('Bookmarks', () => {
beforeEach(() => {
// todo: mock all the graphql queries and mutations
cy.exec('python ../server/manage.py prepare_bookmarks_for_cypress');
cy.viewport('macbook-15');
cy.apolloLogin('rachel.green', 'test');
});
it('should bookmark content block', () => {
cy.visit('/module/lohn-und-budget/');
cy.get('.content-component').contains('Das folgende Interview').parent().parent().as('interviewContent');
cy.get('@interviewContent').within(() => {
cy.get('.bookmark-actions__bookmark').click();
cy.get('.bookmark-actions__add-note').click();
});
cy.get('[data-cy=bookmark-note]').within(() => {
cy.get('.skillbox-input').type('Hallo Velo');
});
cy.get('[data-cy=modal-save-button]').click();
cy.get('@interviewContent').within(() => {
cy.get('.bookmark-actions__edit-note').click();
});
cy.get('[data-cy=bookmark-note]').within(() => {
cy.get('.skillbox-input').clear().type('Hello Bike');
});
cy.get('[data-cy=modal-save-button]').click();
});
});

View File

@ -40,7 +40,7 @@ describe('Email Verification', () => {
cy.visit('/license-activation'); cy.visit('/license-activation');
redeemCoupon(''); redeemCoupon('');
cy.get('[data-cy="coupon-local-errors"]').contains('Coupon ist ein Pflichtfeld'); cy.get('[data-cy="coupon-local-errors"]').contains('Coupon-Code ist ein Pflichtfeld');
}); });
it('displays error if coupon input is wrong', () => { it('displays error if coupon input is wrong', () => {

View File

@ -15,9 +15,7 @@ describe('Apply module visibility', () => {
const {me: minimalMe} = getMinimalMe({}); const {me: minimalMe} = getMinimalMe({});
const me = { const me = {
...minimalMe, ...minimalMe,
schoolClasses: { schoolClasses
edges: schoolClasses.map(scn => ({node: scn}))
}
}; };
// name: '[\'FLID2018a\', \'Andere Klasse\']' // name: '[\'FLID2018a\', \'Andere Klasse\']'
const operations = { const operations = {

View File

@ -0,0 +1,152 @@
import {getMinimalMe} from '../../support/helpers';
import minimalModule from '../../fixtures/module.minimal';
const {me: minimalMe} = getMinimalMe({});
describe('Bookmarks', () => {
beforeEach(() => {
cy.setup();
cy.mockGraphqlOps({
operations: {
MeQuery: {
me: minimalMe
},
ModuleDetailsQuery: {
module: {
...minimalModule,
slug: 'my-module-slug',
chapters: [
{
title: 'My super Chapter',
contentBlocks: [
{
contents: [
{
type: 'text_block',
value: {
text: 'Das folgende Interview'
},
id: "df8212ee-3e82-49fa-977e-c4b60789163e"
}
]
}
]
}
]
}
},
UpdateLastModule: {},
UpdateContentBookmark: {
updateContentBookmark: {
success: true
}
},
UpdateModuleBookmark: {
updateModuleBookmark: {
success: true
}
},
UpdateChapterBookmark: {
updateChapterBookmark: {
success: true
}
},
AddNote: ({input: {note}}) => ({
addNote: {
note
}
}),
UpdateNote: ({input: {note}}) => ({
updateNote: {
note
}
})
}
});
});
it('should bookmark instrument', () => {
cy.visit();
});
it('should bookmark module', () => {
cy.visit('/module/lohn-und-budget/');
cy.getByDataCy('module-bookmark-actions').as('moduleBookmark');
cy.get('@moduleBookmark').within(() => {
cy.getByDataCy('bookmark-action').click();
cy.getByDataCy('add-note-action').click();
});
cy.get('[data-cy=bookmark-note]').within(() => {
cy.get('.skillbox-input').type('Hallo Velo');
});
cy.get('[data-cy=modal-save-button]').click();
cy.get('@moduleBookmark').within(() => {
cy.getByDataCy('edit-note-action').click();
});
cy.get('[data-cy=bookmark-note]').within(() => {
cy.get('.skillbox-input').clear().type('Hello Bike');
});
cy.get('[data-cy=modal-save-button]').click();
});
it('should bookmark chapter', () => {
cy.visit('/module/lohn-und-budget/');
cy.getByDataCy('chapter-bookmark-actions').as('chapterBookmark');
cy.get('@chapterBookmark').within(() => {
cy.getByDataCy('bookmark-action').click();
cy.getByDataCy('add-note-action').click();
});
cy.get('[data-cy=bookmark-note]').within(() => {
cy.get('.skillbox-input').type('Hallo Velo');
});
cy.get('[data-cy=modal-save-button]').click();
cy.get('@chapterBookmark').within(() => {
cy.getByDataCy('edit-note-action').click();
});
cy.get('[data-cy=bookmark-note]').within(() => {
cy.get('.skillbox-input').clear().type('Hello Bike');
});
cy.get('[data-cy=modal-save-button]').click();
});
it('should bookmark content block', () => {
cy.visit('/module/lohn-und-budget/');
cy.getByDataCy('content-component').contains('Das folgende Interview').parent().parent().as('interviewContent');
cy.get('@interviewContent').within(() => {
cy.get('.bookmark-actions__bookmark').click();
cy.get('.bookmark-actions__add-note').click();
});
cy.get('[data-cy=bookmark-note]').within(() => {
cy.get('.skillbox-input').type('Hallo Velo');
});
cy.get('[data-cy=modal-save-button]').click();
cy.get('@interviewContent').within(() => {
cy.get('.bookmark-actions__edit-note').click();
});
cy.get('[data-cy=bookmark-note]').within(() => {
cy.get('.skillbox-input').clear().type('Hello Bike');
});
cy.get('[data-cy=modal-save-button]').click();
});
});

View File

@ -0,0 +1,23 @@
describe('Create Content Block', () => {
it('visits the page', () => {
// todo:
// add mocks
// cy.visit('/module/some-module/add/bliblablub');
// add title
// add text element
// add list item
// add text element to list item
// add second list item
// add text element to second list item
// add another text element to second list item
// save
// another test
// go to pase
// click cancel, go back
});
});
// todo: another test
// edit existing content block

View File

@ -28,7 +28,7 @@ describe('New student', () => {
return { return {
...me, ...me,
onboardingVisited, onboardingVisited,
schoolClasses: {edges: schoolClasses}, schoolClasses,
selectedClass: getSelectedClass(), selectedClass: getSelectedClass(),
}; };
}; };

View File

@ -79,7 +79,8 @@ describe('Objective Visibility', () => {
}); });
}); });
it('should display the correct objectives', () => { //todo: finish writing this test, this does nothing
it.skip('should display the correct objectives', () => {
cy.fakeLogin('rachel.green', 'test'); cy.fakeLogin('rachel.green', 'test');
cy.visit('/module/lohn-und-budget'); cy.visit('/module/lohn-und-budget');

View File

@ -95,12 +95,10 @@ describe('Project Page', () => {
beforeEach(() => { beforeEach(() => {
cy.setup(); cy.setup();
cy.task('getSchema').then(schema => {
cy.mockGraphqlOps({ cy.mockGraphqlOps({
operations, operations,
}); });
}); });
});
it('has the correct layout', () => { it('has the correct layout', () => {
cy.visit('/portfolio/groot'); cy.visit('/portfolio/groot');

View File

@ -46,6 +46,7 @@ const getOperations = ({final, readOnly, classReadOnly = false}) => ({
ModuleDetailsQuery: { ModuleDetailsQuery: {
module, module,
}, },
UpdateLastModule: {},
AssignmentQuery: { AssignmentQuery: {
assignment: { assignment: {
submission: { submission: {

View File

@ -8,6 +8,7 @@ const getOperations = ({readOnly, classReadOnly = false}) => ({
...minimalModule, ...minimalModule,
}, },
}, },
UpdateLastModule: {}
}); });
const moduleNavigationTest = ({readOnly, classReadOnly = false, displayMenu}) => { const moduleNavigationTest = ({readOnly, classReadOnly = false, displayMenu}) => {

View File

@ -3,14 +3,12 @@ import {getMinimalMe} from '../../../support/helpers';
const getOperations = ({readOnly}) => ({ const getOperations = ({readOnly}) => ({
MeQuery: getMinimalMe({readOnly}), MeQuery: getMinimalMe({readOnly}),
NewsTeasers: { NewsTeasers: {
newsTeasers: { newsTeasers: [
edges: [
{}, {},
{}, {},
{}, {},
] ]
} }
}
}); });
describe('Read Only News', () => { describe('Read Only News', () => {

View File

@ -5,6 +5,7 @@ const getOperations = ({readOnly = false, classReadOnly = false}) => ({
ProjectQuery: { ProjectQuery: {
project: { project: {
id: 'projectId', id: 'projectId',
slug: 'project-name',
final: false, final: false,
student: { student: {
id: btoa('PrivateUserNode:1'), id: btoa('PrivateUserNode:1'),

View File

@ -13,10 +13,7 @@ describe('Room Team Management - Read only', () => {
}, },
}, },
RoomsQuery: { RoomsQuery: {
rooms: { rooms: [{
edges: [
{
node: {
id: '', id: '',
slug: '', slug: '',
title: 'some room', title: 'some room',
@ -27,10 +24,7 @@ describe('Room Team Management - Read only', () => {
id: SELECTED_CLASS_ID, id: SELECTED_CLASS_ID,
name: 'bla', name: 'bla',
}, },
}, }],
},
],
},
}, },
}); });

View File

@ -6,9 +6,7 @@ describe('Article page', () => {
slug, slug,
id: 'room-entry-id', id: 'room-entry-id',
title: 'Some Room Entry, yay!', title: 'Some Room Entry, yay!',
comments: { comments: [],
edges: [],
},
}; };
const operations = { const operations = {

View File

@ -87,7 +87,8 @@ describe('The Room Page', () => {
cy.getByDataCy('room-actions').should('not.exist'); cy.getByDataCy('room-actions').should('not.exist');
}); });
it('changes visibility of a room', () => { // todo: re-enable once cypress can do it correctly
it.skip('changes visibility of a room', () => {
const MeQuery = getMinimalMe({ const MeQuery = getMinimalMe({
isTeacher: true, isTeacher: true,
}); });
@ -155,9 +156,7 @@ describe('The Room Page', () => {
MeQuery, MeQuery,
RoomsQuery() { RoomsQuery() {
return { return {
rooms: { rooms
edges: rooms.map(room => ({node: room})),
},
}; };
}, },
RoomEntriesQuery: { RoomEntriesQuery: {
@ -244,23 +243,18 @@ describe('The Room Page', () => {
cy.getByDataCy('add-room-entry-modal').should('exist'); cy.getByDataCy('add-room-entry-modal').should('exist');
}); });
it('changes class while on room page', () => { it.only('changes class while on room page', () => {
const {me} = MeQuery; const {me} = MeQuery;
const otherClass = {
id: btoa('SchoolClassNode:34'),
name: 'Other Class',
readOnly: false
};
const operations = { const operations = {
MeQuery: { MeQuery: {
me: { me: {
...me, ...me,
schoolClasses: { schoolClasses: [...me.schoolClasses, otherClass],
edges: [
...me.schoolClasses.edges,
{
node: {
id: btoa('SchoolClassNode:other-class'),
name: 'Other Class'
},
},
],
},
}, },
}, },
RoomEntriesQuery, RoomEntriesQuery,
@ -268,6 +262,15 @@ describe('The Room Page', () => {
updateSettings: { updateSettings: {
success: true success: true
} }
},
ModuleDetailsQuery: {
me: {
selectedClass: otherClass
}
},
MySchoolClassQuery: {},
RoomsQuery: {
rooms: []
} }
}; };
@ -278,5 +281,6 @@ describe('The Room Page', () => {
cy.getByDataCy('room-title').should('contain', 'A Room'); cy.getByDataCy('room-title').should('contain', 'A Room');
cy.selectClass('Other Class'); cy.selectClass('Other Class');
cy.url().should('include', 'rooms'); cy.url().should('include', 'rooms');
cy.getByDataCy('selected-class-name').should('contain', 'Other Class');
}); });
}); });

View File

@ -1,20 +1,15 @@
import {getMinimalMe} from '../../../support/helpers'; import {getMinimalMe} from '../../../support/helpers';
import {SELECTED_CLASS_ID_ENCODED} from '../../../fixtures/mocks';
describe('The Rooms Page', () => { describe('The Rooms Page', () => {
const getOperations = (isTeacher) => ({ const getOperations = (isTeacher) => ({
MeQuery: getMinimalMe({isTeacher}), MeQuery: getMinimalMe({isTeacher}),
RoomsQuery: { RoomsQuery: {
rooms: { rooms: [{
edges: [
{
node: {
schoolClass: { schoolClass: {
id: btoa('SchoolClassNode:selectedClassId'), id: SELECTED_CLASS_ID_ENCODED,
},
},
},
],
}, },
}],
}, },
}); });
@ -23,9 +18,7 @@ describe('The Rooms Page', () => {
return { return {
...operations, ...operations,
RoomsQuery: { RoomsQuery: {
rooms: { rooms: [],
edges: [],
},
}, },
}; };
}; };
@ -105,9 +98,7 @@ describe('The Rooms Page', () => {
MeQuery, MeQuery,
RoomsQuery() { RoomsQuery() {
return { return {
rooms: { rooms
edges: rooms.map(room => ({node: room})),
},
}; };
}, },
AddRoom({input: {room: {title, appearance, description}}}) { AddRoom({input: {room: {title, appearance, description}}}) {

View File

@ -254,17 +254,13 @@ describe('Teacher Class Management', () => {
let selectedClass = teacher.selectedClass; let selectedClass = teacher.selectedClass;
const schoolClasses = [ const schoolClasses = [
{ teacher.selectedClass
node: teacher.selectedClass
}
]; ];
const me = () => ({ const me = () => ({
...teacher, ...teacher,
selectedClass, selectedClass,
schoolClasses: { schoolClasses
edges: schoolClasses
}
}); });
cy.mockGraphqlOps({ cy.mockGraphqlOps({
@ -278,9 +274,7 @@ describe('Teacher Class Management', () => {
name, name,
readOnly: false readOnly: false
}; };
schoolClasses.push({ schoolClasses.push(schoolClass);
node: schoolClass
});
selectedClass = schoolClass; selectedClass = schoolClass;
return { return {
createSchoolClass: { createSchoolClass: {

View File

@ -11,12 +11,7 @@ describe('Sidebar', () => {
MeQuery: { MeQuery: {
me: { me: {
...me, ...me,
schoolClasses: { schoolClasses: [...me.schoolClasses, {}],
edges: [
...me.schoolClasses.edges,
{node: {}},
],
},
}, },
}, },
ProjectsQuery: { ProjectsQuery: {

View File

@ -19,6 +19,7 @@ describe('Snapshot', () => {
success: true, success: true,
}, },
}, },
UpdateLastModule: {},
ModuleSnapshotsQuery: { ModuleSnapshotsQuery: {
module: { module: {
...module, ...module,

View File

@ -3,7 +3,7 @@
export const getMinimalMe = ({readOnly = false, classReadOnly = false, isTeacher = true} = {}) => { export const getMinimalMe = ({readOnly = false, classReadOnly = false, isTeacher = true} = {}) => {
const selectedClass = { const selectedClass = {
name: 'Selected Class', name: 'Selected Class',
id: btoa('SchoolClassNode:selectedClassId'), id: btoa('SchoolClassNode:987'),
readOnly: classReadOnly, readOnly: classReadOnly,
}; };
return { return {
@ -12,11 +12,7 @@ export const getMinimalMe = ({readOnly = false, classReadOnly = false, isTeacher
readOnly, readOnly,
isTeacher, isTeacher,
selectedClass, selectedClass,
schoolClasses: { schoolClasses: [selectedClass],
edges: [
{node: selectedClass},
],
},
}, },
}; };
}; };
@ -69,13 +65,7 @@ export const getMe = ({schoolClasses, teacher}) => {
'slug': 'geld-und-kauf', 'slug': 'geld-und-kauf',
'__typename': 'TopicNode', '__typename': 'TopicNode',
}, },
'schoolClasses': { 'schoolClasses': schoolClassNodes,
'edges': schoolClassNodes.map(scn => ({
node: scn,
'__typename': 'SchoolClassNodeEdge',
})),
'__typename': 'SchoolClassNodeConnection',
},
'__typename': 'UserNode', '__typename': 'UserNode',
'onboardingVisited': true, 'onboardingVisited': true,
'permissions': teacher ? ['users.can_manage_school_class_content'] : [], 'permissions': teacher ? ['users.can_manage_school_class_content'] : [],

11227
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -5,10 +5,11 @@
"author": "ramon / chrigu", "author": "ramon / chrigu",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js", "dev": "webpack serve --progress --config build/webpack.dev.conf.js",
"analyze": "webpack --profile --json --config build/webpack.dev.conf.js > dist/stats.json && webpack-bundle-analyzer dist/stats.json",
"start": ". ../server/.env && npm run dev", "start": ". ../server/.env && npm run dev",
"lint": "eslint --ext .js,.vue src", "lint": "eslint --ext .js,.vue,.ts src",
"fix-lint": "eslint --ext .js,.vue --fix src", "fix-lint": "eslint --ext .js,.vue,.ts --fix src",
"build": "node build/build.js", "build": "node build/build.js",
"open:cypress:e2e": "npm run cypress:e2e:open", "open:cypress:e2e": "npm run cypress:e2e:open",
"open:cypress:frontend": "npm run cypress:frontend:open", "open:cypress:frontend": "npm run cypress:frontend:open",
@ -24,52 +25,48 @@
"cypress:parallel:run": "cy2 run --parallel --record --config-file cypress.frontend.json --ci-build-id " "cypress:parallel:run": "cy2 run --parallel --record --config-file cypress.frontend.json --ci-build-id "
}, },
"dependencies": { "dependencies": {
"@babel/core": "^7.5.4", "@apollo/client": "^3.5.8",
"@babel/core": "^7.16.7",
"@babel/eslint-plugin": "^7.16.5",
"@babel/plugin-transform-runtime": "^7.5.0", "@babel/plugin-transform-runtime": "^7.5.0",
"@babel/polyfill": "^7.4.4", "@babel/polyfill": "^7.4.4",
"@babel/preset-env": "^7.5.4", "@babel/preset-env": "^7.5.4",
"@babel/preset-stage-2": "^7.0.0", "@babel/preset-stage-2": "^7.0.0",
"@babel/runtime": "^7.5.4", "@babel/runtime": "^7.5.4",
"@iam4x/cypress-graphql-mock": "0.0.1", "@iam4x/cypress-graphql-mock": "0.0.1",
"apollo-cache-inmemory": "^1.6.5", "@vue/composition-api": "^1.4.2",
"apollo-client": "^2.6.8",
"apollo-link": "^1.2.13",
"apollo-link-error": "^1.1.12",
"apollo-link-http": "^1.5.16",
"appolo": "^6.0.19", "appolo": "^6.0.19",
"autoprefixer": "^7.1.2", "autoprefixer": "^7.1.2",
"babel-eslint": "^8.2.1",
"babel-helper-vue-jsx-merge-props": "^2.0.3", "babel-helper-vue-jsx-merge-props": "^2.0.3",
"babel-loader": "^8.0.6", "babel-loader": "^8.0.6",
"babel-plugin-syntax-jsx": "^6.18.0", "babel-plugin-syntax-jsx": "^6.18.0",
"babel-plugin-transform-vue-jsx": "^3.5.0", "babel-plugin-transform-vue-jsx": "^3.5.0",
"chalk": "^2.0.1", "chalk": "^2.0.1",
"copy-webpack-plugin": "^4.0.1", "copy-webpack-plugin": "^10.1.0",
"css-loader": "^0.28.0", "css-loader": "^0.28.0",
"cy2": "^1.2.1", "cy2": "^1.2.1",
"dayjs": "^1.10.4", "dayjs": "^1.10.7",
"debounce": "^1.2.0", "debounce": "^1.2.0",
"eslint": "^4.15.0", "eslint": "^7.32.0",
"eslint-config-standard": "^10.2.1", "eslint-config-standard": "^16.0.3",
"eslint-friendly-formatter": "^3.0.0", "eslint-friendly-formatter": "^4.0.1",
"eslint-loader": "^1.7.1", "eslint-loader": "^4.0.2",
"eslint-plugin-cypress": "^2.11.2", "eslint-plugin-cypress": "^2.12.1",
"eslint-plugin-import": "^2.7.0", "eslint-plugin-import": "^2.25.4",
"eslint-plugin-node": "^5.2.0", "eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^3.4.0", "eslint-plugin-promise": "^6.0.0",
"eslint-plugin-standard": "^3.0.1", "eslint-plugin-standard": "^5.0.0",
"eslint-plugin-vue": "^4.0.0", "eslint-plugin-vue": "^8.3.0",
"extract-text-webpack-plugin": "^3.0.0",
"file-loader": "^1.1.4", "file-loader": "^1.1.4",
"friendly-errors-webpack-plugin": "^1.6.1", "friendly-errors-webpack-plugin": "^1.7.0",
"graphql": "^0.13.2", "graphql": "^16.1.0",
"graphql-tag": "^2.10.1", "graphql-tag": "^2.10.1",
"html-webpack-plugin": "^2.30.1", "html-webpack-plugin": "^5.5.0",
"lodash": "^4.17.10", "lodash": "^4.17.10",
"moment": "^2.24.0", "mini-css-extract-plugin": "^2.4.5",
"node-notifier": "^5.1.2", "node-notifier": "^5.1.2",
"node-sass": "^4.13.1", "node-sass": "^4.13.1",
"optimize-css-assets-webpack-plugin": "^3.2.0", "optimize-css-assets-webpack-plugin": "^6.0.1",
"ora": "^1.2.0", "ora": "^1.2.0",
"portfinder": "^1.0.13", "portfinder": "^1.0.13",
"postcss-import": "^11.0.0", "postcss-import": "^11.0.0",
@ -79,31 +76,30 @@
"sass-loader": "^7.1.0", "sass-loader": "^7.1.0",
"semver": "^5.3.0", "semver": "^5.3.0",
"shelljs": "^0.7.6", "shelljs": "^0.7.6",
"survey-vue": "^1.8.77", "survey-vue": "^1.9.2",
"uglifyjs-webpack-plugin": "^1.1.1",
"unfetch": "^3.1.1", "unfetch": "^3.1.1",
"uploadcare-widget": "^3.6.0", "uploadcare-widget": "^3.6.0",
"url-loader": "^1.0.1", "url-loader": "^1.0.1",
"uuid": "^3.2.1", "uuid": "^3.2.1",
"vee-validate": "^2.2.0", "vee-validate": "^3.4.14",
"vue": "^2.5.17", "vue": "^2.6.14",
"vue-analytics": "^5.16.2", "vue-analytics": "^5.16.2",
"vue-apollo": "^3.0.0-beta.16", "vue-apollo": "^3.1.0",
"vue-loader": "^15.9.6", "vue-loader": "^15.9.8",
"vue-matomo": "^3.13.4-0", "vue-matomo": "^3.13.4-0",
"vue-router": "^3.0.1", "vue-router": "^3.5.3",
"vue-scrollto": "^2.11.0", "vue-scrollto": "^2.11.0",
"vue-style-loader": "^3.0.1", "vue-style-loader": "^3.0.1",
"vue-template-compiler": "^2.5.17", "vue-template-compiler": "^2.6.14",
"vue-toast-notification": "^0.4.1", "vue-toast-notification": "^0.4.1",
"vue-vimeo-player": "0.0.6", "vue-vimeo-player": "0.0.6",
"vuejs-logger": "1.5.5", "vuejs-logger": "1.5.5",
"vuetify": "^1.1.8", "vuetify": "^1.1.8",
"vuex": "^3.0.1", "vuex": "^3.0.1",
"webpack": "^3.6.0", "webpack": "^5.65.0",
"webpack-bundle-analyzer": "^2.9.0", "webpack-bundle-analyzer": "^4.5.0",
"webpack-dev-server": "^2.9.1", "webpack-dev-server": "^4.6.0",
"webpack-merge": "^4.1.0", "webpack-merge": "^5.8.0",
"whatwg-fetch": "^3.0.0" "whatwg-fetch": "^3.0.0"
}, },
"engines": { "engines": {
@ -116,6 +112,8 @@
"not ie <= 8" "not ie <= 8"
], ],
"devDependencies": { "devDependencies": {
"@typescript-eslint/eslint-plugin": "^5.10.0",
"@typescript-eslint/parser": "^5.10.0",
"@vue/test-utils": "^1.0.0-beta.29", "@vue/test-utils": "^1.0.0-beta.29",
"babel-bridge": "^1.12.11", "babel-bridge": "^1.12.11",
"babel-core": "^7.0.0-bridge.0", "babel-core": "^7.0.0-bridge.0",
@ -128,9 +126,10 @@
"jest-transform-graphql": "^2.1.0", "jest-transform-graphql": "^2.1.0",
"jest-transform-stub": "^2.0.0", "jest-transform-stub": "^2.0.0",
"jest-watch-typeahead": "^0.3.1", "jest-watch-typeahead": "^0.3.1",
"mock-apollo-client": "^0.7.0", "mock-apollo-client": "^1.2.0",
"ts-loader": "^8.3.0", "ts-loader": "^8.3.0",
"typescript": "^4.4.3", "typescript": "^4.5.4",
"vue-jest": "^3.0.4" "vue-jest": "^3.0.4",
"webpack-cli": "^4.9.1"
} }
} }

View File

@ -2,48 +2,52 @@
<div <div
:class="{'no-scroll': showModal || showMobileNavigation}" :class="{'no-scroll': showModal || showMobileNavigation}"
class="app" class="app"
id="app"> id="app"
<read-only-banner/> >
<scroll-up/> <read-only-banner />
<scroll-up />
<component <component
:is="showModalDeprecated" :is="showModalDeprecated"
v-if="showModalDeprecated"/> v-if="showModalDeprecated"
/>
<component <component
:is="showModal" :is="showModal"
v-if="showModal"/> v-if="showModal"
<component :is="layout"/> />
<component :is="layout" />
</div> </div>
</template> </template>
<script> <script>
import DefaultLayout from '@/layouts/DefaultLayout';
import SimpleLayout from '@/layouts/SimpleLayout';
import FullScreenLayout from '@/layouts/FullScreenLayout';
import PublicLayout from '@/layouts/PublicLayout';
import BlankLayout from '@/layouts/BlankLayout';
import SplitLayout from '@/layouts/SplitLayout';
import Modal from '@/components/Modal';
import NewContentBlockWizard from '@/components/content-block-form/NewContentBlockWizard';
import EditContentBlockWizard from '@/components/content-block-form/EditContentBlockWizard';
import NewRoomEntryWizard from '@/components/rooms/room-entries/NewRoomEntryWizard';
import EditRoomEntryWizard from '@/components/rooms/room-entries/EditRoomEntryWizard';
import NewProjectEntryWizard from '@/components/portfolio/NewProjectEntryWizard';
import EditProjectEntryWizard from '@/components/portfolio/EditProjectEntryWizard';
import NewObjectiveWizard from '@/components/objective-groups/NewObjectiveWizard';
import NewNoteWizard from '@/components/notes/NewNoteWizard';
import EditNoteWizard from '@/components/notes/EditNoteWizard';
import EditClassNameWizard from '@/components/school-class/EditClassNameWizard';
import EditTeamNameWizard from '@/components/profile/EditTeamNameWizard';
import FullscreenImage from '@/components/FullscreenImage';
import FullscreenInfographic from '@/components/FullscreenInfographic';
import FullscreenVideo from '@/components/FullscreenVideo';
import DeactivatePerson from '@/components/profile/DeactivatePerson';
import SnapshotCreated from '@/components/modules/SnapshotCreated';
import ChangeVisibility from '@/components/rooms/ChangeVisibility';
import {mapGetters} from 'vuex'; import {mapGetters} from 'vuex';
import ScrollUp from '@/components/ScrollUp'; import ScrollUp from '@/components/ScrollUp';
import ReadOnlyBanner from '@/components/ReadOnlyBanner'; import ReadOnlyBanner from '@/components/ReadOnlyBanner';
const Modal = () => import(/* webpackChunkName: "modals" */'@/components/Modal');
const FullscreenImage = () => import(/* webpackChunkName: "modals" */'@/components/FullscreenImage');
const FullscreenInfographic = () => import(/* webpackChunkName: "modals" */'@/components/FullscreenInfographic');
const FullscreenVideo = () => import(/* webpackChunkName: "modals" */'@/components/FullscreenVideo');
const DeactivatePerson = () => import(/* webpackChunkName: "modals" */'@/components/profile/DeactivatePerson');
const SnapshotCreated = () => import(/* webpackChunkName: "modals" */'@/components/modules/SnapshotCreated');
const ChangeVisibility = () => import(/* webpackChunkName: "modals" */'@/components/rooms/ChangeVisibility');
const NewContentBlockWizard = () => import(/* webpackChunkName: "content-forms" */'@/components/content-block-form/NewContentBlockWizard');
const EditContentBlockWizard = () => import(/* webpackChunkName: "content-forms" */'@/components/content-block-form/EditContentBlockWizard');
const NewRoomEntryWizard = () => import(/* webpackChunkName: "content-forms" */'@/components/rooms/room-entries/NewRoomEntryWizard');
const EditRoomEntryWizard = () => import(/* webpackChunkName: "content-forms" */'@/components/rooms/room-entries/EditRoomEntryWizard');
const NewProjectEntryWizard = () => import(/* webpackChunkName: "content-forms" */'@/components/portfolio/NewProjectEntryWizard');
const EditProjectEntryWizard = () => import(/* webpackChunkName: "content-forms" */'@/components/portfolio/EditProjectEntryWizard');
const NewObjectiveWizard = () => import(/* webpackChunkName: "content-forms" */'@/components/objective-groups/NewObjectiveWizard');
const NewNoteWizard = () => import(/* webpackChunkName: "content-forms" */'@/components/notes/NewNoteWizard');
const EditNoteWizard = () => import(/* webpackChunkName: "content-forms" */'@/components/notes/EditNoteWizard');
const EditClassNameWizard = () => import(/* webpackChunkName: "content-forms" */'@/components/school-class/EditClassNameWizard');
const EditTeamNameWizard = () => import(/* webpackChunkName: "content-forms" */'@/components/profile/EditTeamNameWizard');
const DefaultLayout = () => import(/* webpackChunkName: "layouts" */'@/layouts/DefaultLayout');
const SimpleLayout = () => import(/* webpackChunkName: "layouts" */'@/layouts/SimpleLayout');
const FullScreenLayout = () => import(/* webpackChunkName: "layouts" */'@/layouts/FullScreenLayout');
const PublicLayout = () => import(/* webpackChunkName: "layouts" */'@/layouts/PublicLayout');
const BlankLayout = () => import(/* webpackChunkName: "layouts" */'@/layouts/BlankLayout');
const SplitLayout = () => import(/* webpackChunkName: "layouts" */'@/layouts/SplitLayout');
export default { export default {
name: 'App', name: 'App',
@ -93,6 +97,7 @@
<style lang="scss"> <style lang="scss">
@import "~styles/main.scss"; @import "~styles/main.scss";
@import "~styles/helpers";
body { body {
overflow-y: auto; overflow-y: auto;

View File

@ -2,14 +2,20 @@
<div class="add-content"> <div class="add-content">
<a <a
class="add-content__button" class="add-content__button"
@click="addContent"> @click="addContent"
<add-pointer class="add-content__icon"/> >
<add-pointer class="add-content__icon" />
</a> </a>
</div> </div>
</template> </template>
<script> <script>
import AddPointer from '@/components/icons/AddPointer'; import {
CREATE_CONTENT_BLOCK_AFTER_PAGE,
CREATE_CONTENT_BLOCK_UNDER_PARENT_PAGE,
} from '@/router/module.names';
const AddPointer = () => import(/* webpackChunkName: "icons" */'@/components/icons/AddPointer');
export default { export default {
props: ['after', 'parent'], props: ['after', 'parent'],
@ -18,15 +24,31 @@
AddPointer AddPointer
}, },
methods: { methods: {
addContent() { addContent() {
if (this.parent && this.parent.__typename === 'ObjectiveGroupNode') { if (this.parent && this.parent.__typename === 'ObjectiveGroupNode') {
this.$store.dispatch('addObjective', this.parent.id); this.$store.dispatch('addObjective', this.parent.id);
} else { } else {
this.$store.dispatch('addContentBlock', { let route;
after: this.after ? this.after.id : undefined, const slug = this.$route.params.slug;
parent: this.parent ? this.parent.id : undefined if (this.after.id) {
}); route = {
name: CREATE_CONTENT_BLOCK_AFTER_PAGE,
params: {
after: this.after.id,
slug
}
};
} else {
route = {
name: CREATE_CONTENT_BLOCK_UNDER_PARENT_PAGE,
params: {
parent: this.parent.id
}
};
}
this.$router.push(route);
} }
} }
} }
@ -34,8 +56,7 @@
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "@/styles/_variables.scss"; @import "~styles/helpers";
@import "@/styles/_mixins.scss";
.add-content { .add-content {
display: none; display: none;

View File

@ -1,13 +1,14 @@
<template> <template>
<div <div
class="add-content-element" class="add-content-element"
@click="$emit('add-element', index)"> @click="$emit('add-element', index)"
<add-icon class="add-content-element__icon"/> >
<add-icon class="add-content-element__icon" />
</div> </div>
</template> </template>
<script> <script>
import AddIcon from '@/components/icons/AddIcon'; const AddIcon = () => import(/* webpackChunkName: "icons" */'@/components/icons/AddIcon');
export default { export default {
props: ['index'], props: ['index'],

View File

@ -1,16 +1,17 @@
<template> <template>
<component <component
:is="component"
v-bind="properties" v-bind="properties"
:class="{ 'add-widget--reverse': reverse }" :class="{ 'add-widget--reverse': reverse }"
class="add-widget" class="add-widget"
@click="$emit('click')"> :is="component"
<add-icon class="add-widget__add"/> @click="$emit('click')"
>
<add-icon class="add-widget__add" />
</component> </component>
</template> </template>
<script> <script>
import AddIcon from '@/components/icons/AddIcon.vue'; const AddIcon = () => import(/* webpackChunkName: "icons" */'@/components/icons/AddIcon.vue');
export default { export default {
props: { props: {

View File

@ -1,46 +1,64 @@
<template> <template>
<!-- eslint-disable vue/no-v-html -->
<div class="assignment-with-submissions"> <div class="assignment-with-submissions">
<p class="assignment-with-submissions__text">{{ assignment.assignment }}</p> <p class="assignment-with-submissions__text">
{{ assignment.assignment }}
</p>
<div> <div>
<a <a
class="button button--primary submissions-page__back" class="button button--primary submissions-page__back"
@click="$emit('back')">Aufgabe im Modul anzeigen</a> @click="$emit('back')"
>Aufgabe im Modul anzeigen</a>
</div> </div>
<div <div
class="assignment-with-submissions__solution" class="assignment-with-submissions__solution"
v-if="assignment.solution"> v-if="assignment.solution"
<h4 class="assignment-with-submissions__heading">Lösung</h4> >
<h4 class="assignment-with-submissions__heading">
Lösung
</h4>
<p <p
class="assignment-with-submissions__solution-text" class="assignment-with-submissions__solution-text"
data-cy="assignment-solution" data-cy="assignment-solution"
v-html="assignment.solution" /> v-html="assignment.solution"
/>
</div> </div>
<p <p
class="assignment-with-submissions__no-submissions" class="assignment-with-submissions__no-submissions"
v-if="!assignment.submissions.length">Zu diesem Auftrag sind noch keine Ergebnisse vorhanden.</p> v-if="!assignment.submissions.length"
>
Zu diesem Auftrag sind noch keine Ergebnisse vorhanden.
</p>
<div <div
class="assignment-with-submissions__submissions submissions" class="assignment-with-submissions__submissions submissions"
v-if="assignment.submissions.length"> v-if="assignment.submissions.length"
>
<div class="submissions__header student-submission-row submission-header"> <div class="submissions__header student-submission-row submission-header">
<p class="submission-header__title">Lernende</p> <p class="submission-header__title">
<p class="submission-header__title">Ergebnisse</p> Lernende
<p class="submission-header__title">Feedback</p> </p>
<p class="submission-header__title">
Ergebnisse
</p>
<p class="submission-header__title">
Feedback
</p>
</div> </div>
<router-link <router-link
:to="submissionLink(submission)" :to="submissionLink(submission)"
:key="submission.id"
class="assignment-with-submissions__link" class="assignment-with-submissions__link"
v-for="submission in submissions"> v-for="submission in submissions"
:key="submission.id"
>
<student-submission <student-submission
:submission="submission" :submission="submission"
class="assignment-with-submissions__submission" class="assignment-with-submissions__submission"
/> />
</router-link> </router-link>
</div> </div>
</div> </div>
</template> </template>
@ -81,7 +99,7 @@
if (this.currentFilter.id === '') { if (this.currentFilter.id === '') {
return true; return true;
} }
return submission.student.schoolClasses.edges.some(edge => edge.node.id === this.currentFilter.id); return submission.student.schoolClasses.some(schoolClass => schoolClass .id === this.currentFilter.id);
} }
}, },

View File

@ -1,18 +1,20 @@
<template> <template>
<router-link <router-link
:to="to" :to="to"
class="sub-navigation-item back-link"> class="sub-navigation-item back-link"
<chevron-left class="back-link__icon sub-navigation-item__icon"/> >
<chevron-left class="back-link__icon sub-navigation-item__icon" />
{{ fullTitle }} {{ fullTitle }}
</router-link> </router-link>
</template> </template>
<script> <script>
import ChevronLeft from '@/components/icons/ChevronLeft';
import {MODULE_PAGE} from '@/router/module.names'; import {MODULE_PAGE} from '@/router/module.names';
import {ROOMS_PAGE} from '@/router/room.names'; import {ROOMS_PAGE} from '@/router/room.names';
import {PROJECTS_PAGE} from '@/router/portfolio.names'; import {PROJECTS_PAGE} from '@/router/portfolio.names';
const ChevronLeft = () => import(/* webpackChunkName: "icons" */'@/components/icons/ChevronLeft');
export default { export default {
props: { props: {
title: { title: {

View File

@ -1,14 +1,19 @@
<template> <template>
<div <div
:data-scrollto="chapter.id" :data-scrollto="chapter.id"
class="chapter"> class="chapter"
data-cy="chapter"
>
<div <div
:class="{'hideable-element--greyed-out': titleGreyedOut}" :class="{'hideable-element--greyed-out': titleGreyedOut}"
class="hideable-element" class="hideable-element"
v-if="!titleHidden"> v-if="!titleHidden"
>
<h3 <h3
:id="'chapter-' + index" :id="'chapter-' + index"
>{{ chapter.title }}</h3> >
{{ chapter.title }}
</h3>
</div> </div>
<visibility-action <visibility-action
@ -21,6 +26,7 @@
:bookmarked="chapter.bookmark" :bookmarked="chapter.bookmark"
:note="note" :note="note"
class="chapter__bookmark-actions" class="chapter__bookmark-actions"
data-cy="chapter-bookmark-actions"
@add-note="addNote" @add-note="addNote"
@edit-note="editNote" @edit-note="editNote"
@bookmark="bookmark(!chapter.bookmark)" @bookmark="bookmark(!chapter.bookmark)"
@ -37,20 +43,23 @@
v-if="editModule" v-if="editModule"
/> />
<p <p
class="chapter__description"> class="chapter__description"
>
{{ chapter.description }} {{ chapter.description }}
</p> </p>
</div> </div>
<add-content-button <add-content-button
:parent="chapter" :parent="chapter"
v-if="editModule"/> v-if="editModule"
/>
<content-block <content-block
:content-block="contentBlock" :content-block="contentBlock"
:parent="chapter.id" :parent="chapter.id"
v-for="contentBlock in filteredContentBlocks"
:key="contentBlock.id" :key="contentBlock.id"
v-for="contentBlock in filteredContentBlocks"/> />
</div> </div>
</template> </template>
@ -100,6 +109,7 @@
if (this.chapter && this.chapter.bookmark) { if (this.chapter && this.chapter.bookmark) {
return this.chapter.bookmark.note; return this.chapter.bookmark.note;
} }
return false;
}, },
titleGreyedOut() { titleGreyedOut() {
return this.textHidden(CHAPTER_TITLE_TYPE) && this.editModule; return this.textHidden(CHAPTER_TITLE_TYPE) && this.editModule;
@ -134,26 +144,31 @@
bookmarked, bookmarked,
}, },
}, },
update: (store, response) => { update: (store) => {
const query = CHAPTER_QUERY; const query = CHAPTER_QUERY;
const variables = {id}; const variables = {id};
const data = store.readQuery({ const {chapter} = store.readQuery({
query, query,
variables, variables,
}); });
const chapter = data.chapter; let bookmark;
if (bookmarked) { if (bookmarked) {
chapter.bookmark = { bookmark = {
__typename: 'ChapterBookmarkNode', __typename: 'ChapterBookmarkNode',
note: null, note: null,
}; };
} else { } else {
chapter.bookmark = null; bookmark = null;
} }
data.chapter = chapter; const data = {
chapter: {
...chapter,
bookmark
}
};
store.writeQuery({ store.writeQuery({
data, data,
@ -192,7 +207,7 @@
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "@/styles/_mixins.scss"; @import "~styles/helpers";
.chapter { .chapter {
position: relative; position: relative;

View File

@ -1,28 +1,31 @@
<template> <template>
<div class="color-chooser"> <div class="color-chooser">
<div <div
:key="index"
:class="{'color-chooser__color-wrapper--selected': selectedColor === color.name}" :class="{'color-chooser__color-wrapper--selected': selectedColor === color.name}"
class="color-chooser__color-wrapper" class="color-chooser__color-wrapper"
data-cy="color-select" data-cy="color-select"
v-for="(color, index) in colors" v-for="(color, index) in colors"
@click="$emit('input', color.name)"> :key="index"
@click="$emit('input', color.name)"
>
<div <div
:class="'color-chooser__color--' + color.name" :class="'color-chooser__color--' + color.name"
class="color-chooser__color"> class="color-chooser__color"
>
<tick <tick
class="color-chooser__selected-icon" class="color-chooser__selected-icon"
v-if="selectedColor === color.name"/> v-if="selectedColor === color.name"
/>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import Tick from '@/components/icons/Tick'; const Tick = () => import(/* webpackChunkName: "icons" */'@/components/icons/Tick');
export default { export default {
props: ['selected-color'], props: ['selectedColor'],
components: { components: {
Tick Tick

View File

@ -1,63 +1,73 @@
<template> <template>
<div <div
:class="{'hideable-element--greyed-out': hidden}" :class="{'hideable-element--greyed-out': hidden}"
class="content-block__container hideable-element"> class="content-block__container hideable-element content-list__parent"
>
<div <div
:class="specialClass" :class="specialClass"
class="content-block"> class="content-block"
>
<div <div
class="block-actions" class="block-actions"
v-if="canEditContentBlock && editModule"> v-if="canEditContentBlock && editModule"
>
<user-widget <user-widget
v-bind="me" v-bind="me"
class="block-actions__user-widget content-block__user-widget"/> class="block-actions__user-widget content-block__user-widget"
/>
<more-options-widget> <more-options-widget>
<li class="popover-links__link"> <li class="popover-links__link">
<popover-link <popover-link
data-cy="delete-content-block-link" data-cy="delete-content-block-link"
text="Löschen" text="Löschen"
@link-action="deleteContentBlock(contentBlock)" /> @link-action="deleteContentBlock(contentBlock)"
/>
</li> </li>
<li class="popover-links__link"> <li class="popover-links__link">
<popover-link <popover-link
text="Bearbeiten" text="Bearbeiten"
@link-action="editContentBlock(contentBlock)" /> @link-action="editContentBlock(contentBlock)"
/>
</li> </li>
</more-options-widget> </more-options-widget>
</div> </div>
<div class="content-block__visibility"> <div class="content-block__visibility">
<visibility-action <visibility-action
:block="contentBlock" :block="contentBlock"
v-if="canEditModule"/> v-if="canEditModule"
/>
</div> </div>
<h3 <h3
class="content-block__instrument-label" class="content-block__instrument-label"
v-if="instrumentLabel !== ''">{{ instrumentLabel }}</h3> v-if="instrumentLabel !== ''"
>
{{ instrumentLabel }}
</h3>
<h4 <h4
class="content-block__title" class="content-block__title"
v-if="!contentBlock.indent">{{ contentBlock.title }}</h4> v-if="!contentBlock.indent"
>
{{ contentBlock.title }}
</h4>
<content-component <content-component
:key="component.id"
:component="component" :component="component"
:root="root" :root="root"
:parent="contentBlock" :parent="contentBlock"
:bookmarks="contentBlock.bookmarks" :bookmarks="contentBlock.bookmarks"
:notes="contentBlock.notes" :notes="contentBlock.notes"
v-for="component in contentBlocksWithContentLists.contents" v-for="component in contentBlocksWithContentLists.contents"
:key="component.id"
/> />
</div> </div>
<add-content-button <add-content-button
:after="contentBlock" :after="contentBlock"
v-if="canEditModule"/> v-if="canEditModule"
/>
</div> </div>
</template> </template>
<script> <script>
@ -65,7 +75,6 @@
import MoreOptionsWidget from '@/components/MoreOptionsWidget'; import MoreOptionsWidget from '@/components/MoreOptionsWidget';
import UserWidget from '@/components/UserWidget'; import UserWidget from '@/components/UserWidget';
import VisibilityAction from '@/components/visibility/VisibilityAction'; import VisibilityAction from '@/components/visibility/VisibilityAction';
import ContentComponent from '@/components/content-blocks/ContentComponent';
import CHAPTER_QUERY from '@/graphql/gql/queries/chapterQuery.gql'; import CHAPTER_QUERY from '@/graphql/gql/queries/chapterQuery.gql';
import DELETE_CONTENT_BLOCK_MUTATION from '@/graphql/gql/mutations/deleteContentBlock.gql'; import DELETE_CONTENT_BLOCK_MUTATION from '@/graphql/gql/mutations/deleteContentBlock.gql';
@ -76,6 +85,7 @@
import {hidden} from '@/helpers/visibility'; import {hidden} from '@/helpers/visibility';
import {CONTENT_TYPE} from '@/consts/types'; import {CONTENT_TYPE} from '@/consts/types';
import PopoverLink from '@/components/ui/PopoverLink'; import PopoverLink from '@/components/ui/PopoverLink';
const ContentComponent = () => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/ContentComponent');
const instruments = { const instruments = {
base_communication: 'Sprache & Kommunikation', base_communication: 'Sprache & Kommunikation',

View File

@ -2,12 +2,13 @@
<modal <modal
:hide-header="true" :hide-header="true"
:fullscreen="true" :fullscreen="true"
class="fullscreen-image"> class="fullscreen-image"
>
<img <img
:src="imageUrl" :src="imageUrl"
class="fullscreen-image__image"> class="fullscreen-image__image"
>
</modal> </modal>
</template> </template>
<script> <script>

View File

@ -1,15 +1,16 @@
<template> <template>
<modal :fullscreen="true"> <modal :fullscreen="true">
<component <component
:value="value"
:is="type" :is="type"
:value="value"/> />
</modal> </modal>
</template> </template>
<script> <script>
import Modal from '@/components/Modal'; import Modal from '@/components/Modal';
import InfogramBlock from '@/components/content-blocks/InfogramBlock'; const InfogramBlock = () => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/InfogramBlock');
import GeniallyBlock from '@/components/content-blocks/GeniallyBlock'; const GeniallyBlock = () => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/GeniallyBlock');
export default { export default {
components: { components: {

View File

@ -2,7 +2,8 @@
<modal <modal
:hide-header="true" :hide-header="true"
:fullscreen="true" :fullscreen="true"
class="fullscreen-video"> class="fullscreen-video"
>
<iframe <iframe
:src="src" :src="src"
width="2000" width="2000"
@ -11,9 +12,9 @@
frameborder="0" frameborder="0"
webkitallowfullscreen webkitallowfullscreen
mozallowfullscreen mozallowfullscreen
allowfullscreen/> allowfullscreen
/>
</modal> </modal>
</template> </template>
<script> <script>

View File

@ -3,22 +3,26 @@
<a <a
class="header-bar__sidebar-link" class="header-bar__sidebar-link"
data-cy="open-sidebar-link" data-cy="open-sidebar-link"
@click.stop="openSidebar('navigation')"> @click.stop="openSidebar('navigation')"
<hamburger class="header-bar__sidebar-icon"/> >
<hamburger class="header-bar__sidebar-icon" />
</a> </a>
<content-navigation class="header-bar__content-navigation"/> <content-navigation class="header-bar__content-navigation" />
<div class="user-header"> <div class="user-header">
<a <a
class="user-header__sidebar-link" > class="user-header__sidebar-link"
>
<current-class <current-class
class="user-header__current-class" class="user-header__current-class"
@click.native.stop="openSidebar('profile')"/> @click.native.stop="openSidebar('profile')"
/>
</a> </a>
<user-widget <user-widget
v-bind="me" v-bind="me"
data-cy="header-user-widget" data-cy="header-user-widget"
@click.native.stop="openSidebar('profile')"/> @click.native.stop="openSidebar('profile')"
/>
</div> </div>
</header> </header>
</template> </template>
@ -26,20 +30,19 @@
<script> <script>
import ContentNavigation from '@/components/book-navigation/ContentNavigation.vue'; import ContentNavigation from '@/components/book-navigation/ContentNavigation.vue';
import UserWidget from '@/components/UserWidget.vue'; import UserWidget from '@/components/UserWidget.vue';
import Logo from '@/components/icons/Logo';
import CurrentClass from '@/components/school-class/CurrentClass'; import CurrentClass from '@/components/school-class/CurrentClass';
import Hamburger from '@/components/icons/Hamburger';
import openSidebar from '@/mixins/open-sidebar'; import openSidebar from '@/mixins/open-sidebar';
import me from '@/mixins/me'; import me from '@/mixins/me';
const Hamburger = () => import(/* webpackChunkName: "icons" */'@/components/icons/Hamburger');
export default { export default {
mixins: [openSidebar, me], mixins: [openSidebar, me],
components: { components: {
ContentNavigation, ContentNavigation,
UserWidget, UserWidget,
Logo,
CurrentClass, CurrentClass,
Hamburger, Hamburger,
}, },

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="helpful-tooltip"> <div class="helpful-tooltip">
<info-icon class="helpful-tooltip__icon"/> <info-icon class="helpful-tooltip__icon" />
<div class="helpful-tooltip__tooltip"> <div class="helpful-tooltip__tooltip">
<div class="helpful-tooltip__text"> <div class="helpful-tooltip__text">
{{ text }} {{ text }}
@ -10,7 +10,7 @@
</template> </template>
<script> <script>
import InfoIcon from '@/components/icons/InfoIcon'; const InfoIcon = () => import(/* webpackChunkName: "icons" */'@/components/icons/InfoIcon');
export default { export default {
props: ['text'], props: ['text'],

View File

@ -1,16 +1,20 @@
<template> <template>
<button <button
:disabled="loading" :disabled="loading || disabled"
class="loading-button button button--primary button--big"> class="loading-button button button--primary button--big"
<template v-if="!loading">{{ label }}</template> >
<template v-if="!loading">
{{ label }}
</template>
<loading-icon <loading-icon
class="loading-button__icon" class="loading-button__icon"
v-else/> v-else
/>
</button> </button>
</template> </template>
<script> <script>
import LoadingIcon from '@/components/icons/LoadingIcon'; const LoadingIcon = () => import(/* webpackChunkName: "icons" */'@/components/icons/LoadingIcon');
export default { export default {
props: { props: {
@ -18,6 +22,10 @@
type: Boolean, type: Boolean,
default: false default: false
}, },
disabled: {
type: Boolean,
default: false
},
label: { label: {
type: String, type: String,
default: '' default: ''
@ -30,7 +38,7 @@
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "@/styles/_helpers.scss"; @import "~styles/helpers";
.loading-button { .loading-button {
height: 52px; height: 52px;

View File

@ -3,7 +3,8 @@
<a <a
class="logout-widget__logout" class="logout-widget__logout"
data-cy="logout" data-cy="logout"
@click="logout()">Abmelden</a> @click="logout()"
>Abmelden</a>
</div> </div>
</template> </template>

View File

@ -1,29 +1,32 @@
<template> <template>
<div class="mobile-header"> <div class="mobile-header">
<a @click="showMobileNavigation"> <a @click="showMobileNavigation">
<hamburger class="mobile-header__hamburger"/> <hamburger class="mobile-header__hamburger" />
</a> </a>
<router-link <router-link
to="/" to="/"
data-cy="mobile-home-link"> data-cy="mobile-home-link"
<logo/> >
<logo />
</router-link> </router-link>
<user-widget <user-widget
v-bind="me" v-bind="me"
@click.native.stop="openSidebar('profile')"/> @click.native.stop="openSidebar('profile')"
/>
</div> </div>
</template> </template>
<script> <script>
import Logo from '@/components/icons/Logo';
import Hamburger from '@/components/icons/Hamburger';
import UserWidget from '@/components/UserWidget'; import UserWidget from '@/components/UserWidget';
import me from '@/mixins/me'; import me from '@/mixins/me';
import openSidebar from '@/mixins/open-sidebar'; import openSidebar from '@/mixins/open-sidebar';
const Logo = () => import(/* webpackChunkName: "icons" */'@/components/icons/Logo');
const Hamburger = () => import(/* webpackChunkName: "icons" */'@/components/icons/Hamburger');
export default { export default {
mixins: [me, openSidebar], mixins: [me, openSidebar],

View File

@ -2,16 +2,18 @@
<div class="modal__backdrop"> <div class="modal__backdrop">
<div <div
:class="{'modal--hide-header': hideHeader || fullscreen, 'modal--fullscreen': fullscreen, 'modal--small': small}" :class="{'modal--hide-header': hideHeader || fullscreen, 'modal--fullscreen': fullscreen, 'modal--small': small}"
class="modal"> class="modal"
>
<div class="modal__header"> <div class="modal__header">
<slot name="header"/> <slot name="header" />
</div> </div>
<div class="modal__body"> <div class="modal__body">
<slot/> <slot />
<div <div
class="modal__close-button" class="modal__close-button"
@click="hideModal"> @click="hideModal"
<cross class="modal__close-icon"/> >
<cross class="modal__close-icon" />
</div> </div>
</div> </div>
<div class="modal__footer"> <div class="modal__footer">
@ -19,7 +21,8 @@
<!--<a class="button button&#45;&#45;active">Speichern</a>--> <!--<a class="button button&#45;&#45;active">Speichern</a>-->
<a <a
class="button" class="button"
@click="hideModal">Abbrechen</a> @click="hideModal"
>Abbrechen</a>
</slot> </slot>
</div> </div>
</div> </div>
@ -27,7 +30,7 @@
</template> </template>
<script> <script>
import Cross from '@/components/icons/Cross'; const Cross = () => import(/* webpackChunkName: "icons" */'@/components/icons/Cross');
export default { export default {
props: { props: {

View File

@ -5,10 +5,12 @@
:class="{'skillbox-input--error': error}" :class="{'skillbox-input--error': error}"
:value="value" :value="value"
class="modal-input__inputfield skillbox-input" class="modal-input__inputfield skillbox-input"
@input="$emit('input', $event.target.value)"> @input="$emit('input', $event.target.value)"
>
<div <div
class="modal-input__error" class="modal-input__error"
v-if="error"> v-if="error"
>
Für Inhaltsblöcke muss zwingend ein Titel erfasst werden. Für Inhaltsblöcke muss zwingend ein Titel erfasst werden.
</div> </div>
</div> </div>

View File

@ -3,21 +3,24 @@
<a <a
class="more-options__more-link" class="more-options__more-link"
data-cy="more-options-link" data-cy="more-options-link"
@click.stop="showMenu = !showMenu"> @click.stop="showMenu = !showMenu"
<ellipses class="more-options__ellipses"/> >
<ellipses class="more-options__ellipses" />
</a> </a>
<widget-popover <widget-popover
class="more-options__popover" class="more-options__popover"
v-if="showMenu" v-if="showMenu"
@hide-me="showMenu = false"> @hide-me="showMenu = false"
<slot/> >
<slot />
</widget-popover> </widget-popover>
</div> </div>
</template> </template>
<script> <script>
import WidgetPopover from '@/components/ui/WidgetPopover'; import WidgetPopover from '@/components/ui/WidgetPopover';
import Ellipses from '@/components/icons/Ellipses.vue';
const Ellipses = () => import(/* webpackChunkName: "icons" */'@/components/icons/Ellipses.vue');
export default { export default {
components: { components: {

View File

@ -2,7 +2,8 @@
<div <div
class="read-only-banner" class="read-only-banner"
data-cy="read-only-banner" data-cy="read-only-banner"
v-if="me.readOnly || me.selectedClass.readOnly"> v-if="me.readOnly || me.selectedClass.readOnly"
>
<div class="read-only-banner__content"> <div class="read-only-banner__content">
<p class="read-only-banner__text"> <p class="read-only-banner__text">
{{ readOnlyText }} Sie können Inhalte lesen, aber nicht {{ readOnlyText }} Sie können Inhalte lesen, aber nicht
@ -13,14 +14,16 @@
:to="licenseActivationLink" :to="licenseActivationLink"
data-cy="license-activation-link" data-cy="license-activation-link"
class="read-only-banner__link button button--primary" class="read-only-banner__link button button--primary"
v-if="me.readOnly">Neuen Lizenzcode eingeben v-if="me.readOnly"
>
Neuen Lizenzcode eingeben
</router-link> </router-link>
<a <a
target="_blank" target="_blank"
href="https://myskillbox.ch/lesemodus" href="https://myskillbox.ch/lesemodus"
class="button button--secondary">Mehr Informationen zum Lesemodus</a> class="button button--secondary"
>Mehr Informationen zum Lesemodus</a>
</div> </div>
</div> </div>
</div> </div>
</template> </template>

View File

@ -3,14 +3,15 @@
<a <a
class="scroll-up" class="scroll-up"
v-if="scroll>200" v-if="scroll>200"
@click="scrollTop"> @click="scrollTop"
<arrow-up class="scroll-up__icon"/> >
<arrow-up class="scroll-up__icon" />
</a> </a>
</transition> </transition>
</template> </template>
<script> <script>
import ArrowUp from '@/components/icons/ArrowUp'; const ArrowUp = () => import(/* webpackChunkName: "icons" */'@/components/icons/ArrowUp');
export default { export default {
components: { components: {

View File

@ -3,16 +3,22 @@
<div <div
:class="{'section-block--navigatable': route}" :class="{'section-block--navigatable': route}"
class="section-block__illustration" class="section-block__illustration"
@click="navigate()"> @click="navigate()"
<slot/> >
<slot />
</div> </div>
<div <div
:class="{'section-block--navigatable': route}" :class="{'section-block--navigatable': route}"
class="section-block__title block-title" class="section-block__title block-title"
@click="navigate()"> @click="navigate()"
<h2 class="block-title__title">{{ title }}</h2> >
<h3 class="block-title__subtitle small-emph">{{ subtitle }}</h3> <h2 class="block-title__title">
{{ title }}
</h2>
<h3 class="block-title__subtitle small-emph">
{{ subtitle }}
</h3>
</div> </div>
<div class="section-block__content section-content"> <div class="section-block__content section-content">
<div class="section-content__subsection subsection"> <div class="section-content__subsection subsection">
@ -20,12 +26,14 @@
:class="{'section-block--navigatable': route}" :class="{'section-block--navigatable': route}"
class="subsection__content button button--primary" class="subsection__content button button--primary"
v-if="route" v-if="route"
@click="navigate()"> @click="navigate()"
>
{{ linkText }} {{ linkText }}
</a> </a>
<span <span
class="subsection__content subsection__content--disabled" class="subsection__content subsection__content--disabled"
v-if="!route">Noch nicht verfügbar</span> v-if="!route"
>Noch nicht verfügbar</span>
</div> </div>
</div> </div>
</div> </div>
@ -33,7 +41,7 @@
<script> <script>
export default { export default {
props: ['title', 'subtitle', 'route', 'link-text'], props: ['title', 'subtitle', 'route', 'linkText'],
methods: { methods: {
navigate() { navigate() {
if (this.route) { if (this.route) {

View File

@ -7,18 +7,24 @@
<p>{{ submission.text | trimToLength(50) }}</p> <p>{{ submission.text | trimToLength(50) }}</p>
<p <p
class="entry__document" class="entry__document"
v-if="submission.document && submission.document.length > 0"> v-if="submission.document && submission.document.length > 0"
>
<student-submission-document <student-submission-document
:document="submission.document" :document="submission.document"
class="entry-document"/> class="entry-document"
/>
</p> </p>
</div> </div>
<div <div
class="student-submission__feedback entry" class="student-submission__feedback entry"
v-if="submission.submissionFeedback"> v-if="submission.submissionFeedback"
>
<p <p
:class="{'entry__text--final': submission.submissionFeedback.final}" :class="{'entry__text--final': submission.submissionFeedback.final}"
class="entry__text">{{ submission.submissionFeedback.text | trimToLength(50) }}</p> class="entry__text"
>
{{ submission.submissionFeedback.text | trimToLength(50) }}
</p>
</div> </div>
</div> </div>
</template> </template>

View File

@ -2,16 +2,17 @@
<div class="submission-document"> <div class="submission-document">
<p <p
class="submission-document__content content" class="submission-document__content content"
v-if="document && document.length > 0"> v-if="document && document.length > 0"
<document-icon class="content__icon"/><span class="content__text">{{ filename }}</span> >
<document-icon class="content__icon" /><span class="content__text">{{ filename }}</span>
</p> </p>
</div> </div>
</template> </template>
<script> <script>
import DocumentIcon from '@/components/icons/DocumentIcon';
import filenameFromUrl from '@/helpers/urls'; import filenameFromUrl from '@/helpers/urls';
const DocumentIcon = () => import(/* webpackChunkName: "icons" */'@/components/icons/DocumentIcon');
export default { export default {
name: 'StudentSubmissionDocument', name: 'StudentSubmissionDocument',

View File

@ -6,7 +6,8 @@
<span class="user-widget__name">{{ firstName }} {{ lastName }}</span> <span class="user-widget__name">{{ firstName }} {{ lastName }}</span>
<span <span
class="user-widget__date" class="user-widget__date"
v-if="date">{{ date }}</span> v-if="date"
>{{ date }}</span>
</div> </div>
</template> </template>

View File

@ -1,10 +1,12 @@
<template> <template>
<div <div
:class="{'user-widget--is-profile': isProfile}" :class="{'user-widget--is-profile': isProfile}"
class="user-widget"> class="user-widget"
>
<div <div
class="user-widget__avatar" class="user-widget__avatar"
data-cy="user-widget-avatar"> data-cy="user-widget-avatar"
>
<avatar <avatar
:avatar-url="avatarUrl" :avatar-url="avatarUrl"
:icon-highlighted="isProfile" :icon-highlighted="isProfile"

View File

@ -3,12 +3,13 @@
<router-link <router-link
:to="{name: 'topic', params: {topicSlug: topic.slug}}" :to="{name: 'topic', params: {topicSlug: topic.slug}}"
:class="{'book-topics__topic--active': topic.active, 'book-subnavigation__item--mobile': mobile}" :class="{'book-topics__topic--active': topic.active, 'book-subnavigation__item--mobile': mobile}"
:key="topic.id"
tag="div" tag="div"
active-class="book-subnavigation__item--active" active-class="book-subnavigation__item--active"
class="book-topics__topic book-subnavigation__item" class="book-topics__topic book-subnavigation__item"
v-for="topic in topics" v-for="topic in topics"
@click.native="closeSidebar('navigation')"> :key="topic.id"
@click.native="closeSidebar('navigation')"
>
{{ topic.order }}. {{ topic.order }}.
{{ topic.title }} {{ topic.title }}
</router-link> </router-link>
@ -44,7 +45,7 @@
topics: { topics: {
query: ALL_TOPICS_QUERY, query: ALL_TOPICS_QUERY,
manual: true, manual: true,
result({data, loading, networkStatus}) { result({data, loading}) {
if (!loading) { if (!loading) {
this.topics = this.$getRidOfEdges(data).topics; this.topics = this.$getRidOfEdges(data).topics;
} }

View File

@ -1,7 +1,8 @@
<template> <template>
<nav <nav
:class="{'content-navigation--sidebar': isSidebar}" :class="{'content-navigation--sidebar': isSidebar}"
class="content-navigation"> class="content-navigation"
>
<div class="content-navigation__primary"> <div class="content-navigation__primary">
<div class="content-navigation__item"> <div class="content-navigation__item">
<router-link <router-link
@ -9,13 +10,14 @@
:to="topicRoute" :to="topicRoute"
active-class="content-navigation__link--active" active-class="content-navigation__link--active"
class="content-navigation__link" class="content-navigation__link"
@click.native="close">Themen @click.native="close"
>
Themen
</router-link> </router-link>
<book-topic-navigation <book-topic-navigation
v-if="isSidebar" v-if="isSidebar"
/> />
</div> </div>
<div class="content-navigation__item"> <div class="content-navigation__item">
@ -23,7 +25,9 @@
to="/instruments" to="/instruments"
active-class="content-navigation__link--active" active-class="content-navigation__link--active"
class="content-navigation__link" class="content-navigation__link"
@click.native="close">Instrumente @click.native="close"
>
Instrumente
</router-link> </router-link>
</div> </div>
@ -34,7 +38,9 @@
class="content-navigation__link" class="content-navigation__link"
data-cy="news-navigation-link" data-cy="news-navigation-link"
v-if="!me.readOnly" v-if="!me.readOnly"
@click.native="close">News @click.native="close"
>
News
</router-link> </router-link>
</div> </div>
</div> </div>
@ -45,7 +51,7 @@
data-cy="home-link" data-cy="home-link"
v-if="!isSidebar" v-if="!isSidebar"
> >
<logo class="content-navigation__logo-icon"/> <logo class="content-navigation__logo-icon" />
</router-link> </router-link>
<div class="content-navigation__secondary"> <div class="content-navigation__secondary">
@ -55,28 +61,35 @@
to="/rooms" to="/rooms"
active-class="content-navigation__link--active" active-class="content-navigation__link--active"
class="content-navigation__link content-navigation__link--secondary" class="content-navigation__link content-navigation__link--secondary"
@click.native="close">Räume @click.native="close"
>
Räume
</router-link> </router-link>
</div> </div>
<div <div
class="content-navigation__item content-navigation__item--secondary" class="content-navigation__item content-navigation__item--secondary"
v-if="showPortfolio"> v-if="showPortfolio"
>
<router-link <router-link
to="/portfolio" to="/portfolio"
active-class="content-navigation__link--active" active-class="content-navigation__link--active"
class="content-navigation__link content-navigation__link--secondary" class="content-navigation__link content-navigation__link--secondary"
@click.native="close">Portfolio @click.native="close"
>
Portfolio
</router-link> </router-link>
</div> </div>
<div <div
class="content-navigation__item content-navigation__item--secondary" class="content-navigation__item content-navigation__item--secondary"
v-if="isSidebar"> v-if="isSidebar"
>
<a <a
href="https://myskillbox.ch/support" href="https://myskillbox.ch/support"
target="_blank" target="_blank"
class="content-navigation__link content-navigation__link--secondary" class="content-navigation__link content-navigation__link--secondary"
@click="close">Support @click="close"
>Support
</a> </a>
</div> </div>
</div> </div>
@ -84,12 +97,13 @@
</template> </template>
<script> <script>
import Logo from '@/components/icons/Logo';
import BookTopicNavigation from '@/components/book-navigation/BookTopicNavigation'; import BookTopicNavigation from '@/components/book-navigation/BookTopicNavigation';
import sidebarMixin from '@/mixins/sidebar'; import sidebarMixin from '@/mixins/sidebar';
import meMixin from '@/mixins/me'; import meMixin from '@/mixins/me';
const Logo = () => import(/* webpackChunkName: "icons" */'@/components/icons/Logo');
export default { export default {
props: { props: {
isSidebar: { isSidebar: {

View File

@ -1,29 +1,30 @@
<template> <template>
<transition name="slide"> <transition name="slide">
<div <div
v-click-outside="close"
class="navigation-sidebar" class="navigation-sidebar"
v-if="sidebar.navigation" v-if="sidebar.navigation"
v-click-outside="close"
> >
<content-navigation <content-navigation
:is-sidebar="true" :is-sidebar="true"
class="navigation-sidebar__main"/> class="navigation-sidebar__main"
/>
<div <div
class="navigation-sidebar__close-button" class="navigation-sidebar__close-button"
@click="close"> @click="close"
<cross class="navigation-sidebar__close-icon"/> >
<cross class="navigation-sidebar__close-icon" />
</div> </div>
</div> </div>
</transition> </transition>
</template> </template>
<script> <script>
import Cross from '@/components/icons/Cross';
import ContentNavigation from '@/components/book-navigation/ContentNavigation'; import ContentNavigation from '@/components/book-navigation/ContentNavigation';
import sidebarMixin from '@/mixins/sidebar'; import sidebarMixin from '@/mixins/sidebar';
import {meQuery} from '@/graphql/queries'; const Cross = () => import(/* webpackChunkName: "icons" */'@/components/icons/Cross');
export default { export default {
mixins: [sidebarMixin], mixins: [sidebarMixin],
@ -33,21 +34,12 @@
Cross Cross
}, },
data() {
return {
me: {}
};
},
methods: { methods: {
close() { close() {
this.closeSidebar('navigation'); this.closeSidebar('navigation');
} }
}, },
apollo: {
me: meQuery
},
}; };
</script> </script>

View File

@ -1,26 +1,29 @@
<template> <template>
<div <div
:class="{ 'sub-navigation-item--active': show}" :class="{ 'sub-navigation-item--active': show}"
class="sub-navigation-item"
v-click-outside="close" v-click-outside="close"
class="sub-navigation-item"> >
<div <div
class="sub-navigation-item__title" class="sub-navigation-item__title"
@click="show = !show"> @click="show = !show"
>
{{ title }} {{ title }}
<chevron-down class="sub-navigation-item__icon sub-navigation-item__chevron-down"/> <chevron-down class="sub-navigation-item__icon sub-navigation-item__chevron-down" />
<chevron-up class="sub-navigation-item__icon sub-navigation-item__chevron-up"/> <chevron-up class="sub-navigation-item__icon sub-navigation-item__chevron-up" />
</div> </div>
<div <div
class="sub-navigation-item__nav-items book-subnavigation" class="sub-navigation-item__nav-items book-subnavigation"
v-if="show"> v-if="show"
<slot/> >
<slot />
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import ChevronDown from '@/components/icons/ChevronDown'; const ChevronDown = () => import(/* webpackChunkName: "icons" */'@/components/icons/ChevronDown');
import ChevronUp from '@/components/icons/ChevronUp'; const ChevronUp = () => import(/* webpackChunkName: "icons" */'@/components/icons/ChevronUp');
export default { export default {
props: ['title'], props: ['title'],
@ -37,7 +40,7 @@
}, },
watch: { watch: {
$route(to, from) { $route() {
this.show = false; this.show = false;
} }
}, },

View File

@ -0,0 +1,38 @@
<template>
<a
class="add-content-link"
@click="$emit('click')"
><plus-icon class="add-content-link__icon" /> <span class="add-content-link__text">Neuer Inhalt</span></a>
</template>
<script>
import PlusIcon from '@/components/icons/PlusIcon';
export default {
components: { PlusIcon }
//
};
</script>
<style scoped lang="scss">
@import '~styles/helpers';
$color: $color-silver-dark;
.add-content-link {
display: flex;
align-items: center;
&__icon {
width: 20px;
height: 20px;
margin-right: $small-spacing;
fill: $color;
}
&__text {
// custom style, because the view needs this
@include link-base;
font-weight: $font-weight-semibold;
color: $color;
}
}
</style>

View File

@ -0,0 +1,247 @@
<template>
<div class="content-element">
<content-block-element-chooser-widget
:class="['content-element__component', 'content-element__chooser']"
v-bind="element"
v-if="isChooser"
@change-type="changeType"
/>
<content-form-section
:title="title"
:icon="icon"
v-else
>
<div class="content-element__section">
<component
:class="['content-element__component']"
v-bind="element"
:is="component"
@change-text="changeText"
@link-change-url="changeUrl"
@change-url="changeUrl"
@switch-to-document="switchToDocument"
@assignment-change-title="changeAssignmentTitle"
@assignment-change-assignment="changeAssignmentAssignment"
/>
<a
class="contents-form__remove icon-button"
@click="$emit('remove')"
>
<trash-icon
class="contents-form__trash-icon icon-button__icon"
/>
</a>
</div>
</content-form-section>
</div>
</template>
<script>
import ContentFormSection from '@/components/content-block-form/ContentFormSection';
const TrashIcon = () => import(/* webpackChunkName: "icons" */'@/components/icons/TrashIcon');
const ContentBlockElementChooserWidget = () => import(/* webpackChunkName: "content-forms" */'@/components/content-forms/ContentBlockElementChooserWidget');
const LinkForm = () => import(/* webpackChunkName: "content-forms" */'@/components/content-forms/LinkForm');
const VideoForm = () => import(/* webpackChunkName: "content-forms" */'@/components/content-forms/VideoForm');
const ImageForm = () => import(/* webpackChunkName: "content-forms" */'@/components/content-forms/ImageForm');
const DocumentForm = () => import(/* webpackChunkName: "content-forms" */'@/components/content-forms/DocumentForm');
const AssignmentForm = () => import(/* webpackChunkName: "content-forms" */'@/components/content-forms/AssignmentForm');
const TextForm = () => import(/* webpackChunkName: "content-forms" */'@/components/content-forms/TextForm');
const CHOOSER = 'content-block-element-chooser-widget';
export default {
props: {
element: {
type: Object,
default: null
}
},
components: {
ContentFormSection,
TrashIcon,
ContentBlockElementChooserWidget,
LinkForm,
VideoForm,
ImageForm,
DocumentForm,
AssignmentForm,
TextForm,
},
computed: {
isChooser() {
return this.component === CHOOSER;
},
type() {
return this.getType(this.element);
},
component() {
return this.type.component;
},
title() {
return this.type.title;
},
icon() {
return this.type.icon;
}
},
methods: {
getType(element) {
switch (element.type) {
case 'link_block':
return {
component: 'link-form',
title: 'Link',
icon: 'link-icon'
};
case 'video_block':
return {
component: 'video-form',
title: 'Video',
icon: 'video-icon'
};
case 'image_url_block':
return {
component: 'image-form',
title: 'Bild',
icon: 'image-icon'
};
case 'text_block':
return {
component: 'text-form',
title: 'Text',
icon: 'text-icon'
};
case 'assignment':
return {
component: 'assignment-form',
title: 'Aufgabe & Ergebnis',
icon: 'speech-bubble-icon'
};
case 'document_block':
return {
component: 'document-form',
title: 'Dokument',
icon: 'document-icon'
};
}
return {
component: CHOOSER,
title: '',
icon: ''
};
},
_updateProperty(value, key) {
// const content = this.localContentBlock.contents[index];
const content = this.element;
this.update({
...content,
value: {
...content.value,
[key]: value,
},
});
},
changeUrl(value) {
this._updateProperty(value, 'url');
},
changeText(value) {
this._updateProperty(value, 'text');
},
changeAssignmentTitle(value) {
this._updateProperty(value, 'title');
},
changeAssignmentAssignment(value) {
this._updateProperty(value, 'assignment');
},
changeType({type, convertToList}, value) {
let el = {
type: type,
value: Object.assign({}, value),
};
switch (type) {
case 'text_block':
el = {
...el,
value: {
text: '',
},
};
break;
case 'link_block':
el = {
...el,
value: {
text: '',
url: '',
},
};
break;
case 'video_block':
el = {
...el,
value: {
url: '',
},
};
break;
case 'document_block':
el = {
...el,
value: Object.assign({
url: '',
}, value),
};
break;
case 'image_url_block':
el = {
...el,
value: {
url: '',
},
};
break;
}
if (convertToList) {
el = {
type: 'content_list_item',
contents: [el]
};
}
this.update(el);
},
update(element) {
this.$emit('update', element);
},
switchToDocument(value) {
this.changeType('document_block', value);
},
}
};
</script>
<style scoped lang="scss">
@import '~styles/helpers';
.content-element {
&__section {
display: grid;
grid-template-columns: 1fr 50px;
grid-auto-rows: auto;
/*width: 95%; // reserve space for scrollbar*/
}
&__chooser {
grid-column: 1 / span 2;
}
}
</style>

View File

@ -0,0 +1,58 @@
<template>
<div class="content-form-section">
<h2 class="content-form-section__heading">
<component
class="content-form-section__icon"
:is="icon"
/> <span class="content-form-section__title">{{ title }}</span>
</h2>
<slot />
</div>
</template>
<script>
import formElementIcons from '@/components/ui/form-element-icons';
export default {
props: {
title: {
type: String,
default: ''
},
icon: {
type: String,
default: ''
}
},
components: {
...formElementIcons
}
};
</script>
<style scoped lang="scss">
@import '~styles/helpers';
.content-form-section {
@include default-box-shadow;
border-radius: $default-border-radius;
padding: $small-spacing $medium-spacing;
margin-bottom: $medium-spacing;
&__heading {
display: flex;
align-items: center;
}
&__title {
@include heading-4;
margin-bottom: 0;
}
&__icon {
width: 28px;
height: 28px;
margin-right: $small-spacing;
}
}
</style>

View File

@ -1,6 +1,6 @@
<template> <template>
<modal> <modal>
<template slot="header"> <template #header>
<modal-input <modal-input
:placeholder="titlePlaceholder" :placeholder="titlePlaceholder"
:value="localContentBlock.title" :value="localContentBlock.title"
@ -24,40 +24,15 @@
@add-element="addElement" @add-element="addElement"
/> />
<div <div
:key="index"
class="contents-form__element" class="contents-form__element"
v-for="(element, index) in localContentBlock.contents"> v-for="(element, index) in localContentBlock.contents"
<component :key="index"
:is="type(element)" >
:class="{'contents-form__chooser': type(element) === 'content-block-element-chooser-widget'}" <content-element
v-bind="element" :element="element"
:index="index" @update="update(index, $event)"
class="contents-form__element-component" @remove="remove(index)"
@change-type="changeType"
@link-change-url="changeLinkUrl"
@link-change-text="changeLinkText"
@text-change-value="changeTextValue"
@document-change-url="changeDocumentUrl"
@image-change-url="changeImageUrl"
@video-change-url="changeVideoUrl"
@switch-to-document="switchToDocument"
@assignment-change-title="changeAssignmentTitle"
@assignment-change-assignment="changeAssignmentAssignment"
/> />
<a
class="contents-form__remove icon-button"
@click="removeElement(index)">
<trash-icon
class="contents-form__trash-icon icon-button__icon"
v-if="type(element) !== 'content-block-element-chooser-widget'"/>
</a>
<add-content-element <add-content-element
:index="index" :index="index"
@ -66,65 +41,56 @@
/> />
</div> </div>
<div slot="footer"> <template #footer>
<div>
<a <a
:class="{'button--disabled': disableSave}" :class="{'button--disabled': disableSave}"
class="button button--primary" class="button button--primary"
data-cy="modal-save-button" data-cy="modal-save-button"
@click="save">Speichern</a> @click="save"
>Speichern</a>
<a <a
class="button" class="button"
@click="$emit('hide')">Abbrechen</a> @click="$emit('hide')"
>Abbrechen</a>
</div> </div>
</template>
</modal> </modal>
</template> </template>
<script> <script>
import Modal from '@/components/Modal';
import ContentBlockElementChooserWidget from '@/components/content-forms/ContentBlockElementChooserWidget';
import ModalInput from '@/components/ModalInput';
import AddContentElement from '@/components/AddContentElement';
import LinkForm from '@/components/content-forms/LinkForm';
import VideoForm from '@/components/content-forms/VideoForm';
import ImageForm from '@/components/content-forms/ImageForm';
import DocumentForm from '@/components/content-forms/DocumentForm';
import AssignmentForm from '@/components/content-forms/AssignmentForm';
import TextForm from '@/components/content-forms/TextForm';
import TrashIcon from '@/components/icons/TrashIcon';
import Checkbox from '@/components/ui/Checkbox';
import {meQuery} from '@/graphql/queries'; import {meQuery} from '@/graphql/queries';
const ModalInput = () => import(/* webpackChunkName: "content-forms" */'@/components/ModalInput');
const AddContentElement = () => import(/* webpackChunkName: "content-forms" */'@/components/AddContentElement');
const ContentElement = () => import(/* webpackChunkName: "content-forms" */'@/components/content-block-form/ContentElement');
const Modal = () => import('@/components/Modal');
const Checkbox = () => import('@/components/ui/Checkbox');
export default { export default {
props: { props: {
'content-block': Object, contentBlock: Object,
'block-type': { blockType: {
type: String, type: String,
default: 'ContentBlock' default: 'ContentBlock',
}, },
'show-task-selection': { showTaskSelection: {
type: Boolean, type: Boolean,
default: false default: false,
}, },
'disable-save': { disableSave: {
type: Boolean, type: Boolean,
default: false default: false,
} },
}, },
components: { components: {
ContentElement,
Modal, Modal,
ContentBlockElementChooserWidget,
ModalInput, ModalInput,
AddContentElement, AddContentElement,
LinkForm, Checkbox,
VideoForm,
ImageForm,
DocumentForm,
AssignmentForm,
TextForm,
TrashIcon,
Checkbox
}, },
data() { data() {
@ -134,14 +100,14 @@
title: this.contentBlock.title, title: this.contentBlock.title,
contents: [...this.contentBlock.contents], contents: [...this.contentBlock.contents],
id: this.contentBlock.id || undefined, id: this.contentBlock.id || undefined,
isAssignment: this.contentBlock.type && this.contentBlock.type.toLowerCase() === 'task' isAssignment: this.contentBlock.type && this.contentBlock.type.toLowerCase() === 'task',
}), }),
me: {} me: {},
}; };
}, },
apollo: { apollo: {
me: meQuery me: meQuery,
}, },
computed: { computed: {
@ -150,123 +116,15 @@
}, },
taskSelection() { taskSelection() {
return this.showTaskSelection && this.me.permissions.includes('users.can_manage_school_class_content'); return this.showTaskSelection && this.me.permissions.includes('users.can_manage_school_class_content');
} },
}, },
methods: { methods: {
type(element) { setContentBlockType(checked) {
switch (element.type) { this.localContentBlock.isAssignment = checked;
case 'link_block':
return 'link-form';
case 'video_block':
return 'video-form';
case 'image_url_block':
return 'image-form';
case 'text_block':
return 'text-form';
case 'assignment':
return 'assignment-form';
case 'document_block':
return 'document-form';
}
return 'content-block-element-chooser-widget';
}, },
_updateProperty(value, index, key) { update(index, element) {
const content = this.localContentBlock.contents[index]; this.localContentBlock.contents.splice(index, 1, element);
this.localContentBlock.contents.splice(index, 1, {
...content,
value: {
...content.value,
[key]: value
}
});
},
changeLinkUrl(value, index) {
this._updateProperty(value, index, 'url');
},
changeLinkText(value, index) {
this._updateProperty(value, index, 'text');
},
changeVideoUrl(value, index) {
this._updateProperty(value, index, 'url');
},
changeImageUrl(value, index) {
this._updateProperty(value, index, 'url');
},
changeDocumentUrl(value, index) {
this._updateProperty(value, index, 'url');
},
changeTextValue(value, index) {
this._updateProperty(value, index, 'text');
},
changeAssignmentTitle(value, index) {
this._updateProperty(value, index, 'title');
},
changeAssignmentAssignment(value, index) {
this._updateProperty(value, index, 'assignment');
},
removeElement(index) {
this.localContentBlock.contents.splice(index, 1);
},
addElement(index) {
this.localContentBlock.contents.splice(index + 1, 0, {
hideAssignment: this.blockType !== 'ContentBlock'
});
},
updateTitle(title) {
this.localContentBlock.title = title;
this.error = false;
},
changeType(index, type, value) {
let el = {
type: type,
value: Object.assign({}, value)
};
switch (type) {
case 'text_block':
el = {
...el,
value: {
text: ''
}
};
break;
case 'link_block':
el = {
...el,
value: {
text: '',
url: ''
}
};
break;
case 'video_block':
el = {
...el,
value: {
url: ''
}
};
break;
case 'document_block':
el = {
...el,
value: Object.assign({
url: ''
}, value)
};
break;
case 'image_url_block':
el = {
...el,
value: {
url: ''
}
};
break;
}
this.localContentBlock.contents.splice(index, 1, el);
}, },
save() { save() {
if (!this.disableSave) { if (!this.disableSave) {
@ -277,27 +135,31 @@
this.$emit('save', this.localContentBlock); this.$emit('save', this.localContentBlock);
} }
}, },
setContentBlockType(checked, localContentBlock) { updateTitle(title) {
this.localContentBlock.isAssignment = checked; this.localContentBlock.title = title;
this.error = false;
},
addElement(index) {
this.localContentBlock.contents.splice(index + 1, 0, {
hideAssignment: this.blockType !== 'ContentBlock',
});
},
remove(index) {
this.localContentBlock.contents.splice(index, 1);
},
}, },
switchToDocument(index, value) {
this.changeType(index, 'document_block', value);
}
}
}; };
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "@/styles/_variables.scss"; @import "~styles/helpers";
.contents-form { .contents-form {
/* top level does not exist, because of the modal */ /* top level does not exist, because of the modal */
&__element { &__element {
display: grid;
grid-template-columns: 1fr 50px;
grid-auto-rows: auto;
/*width: 95%; // reserve space for scrollbar*/
} }
&__element-component { &__element-component {
@ -310,10 +172,6 @@
&__trash-icon { &__trash-icon {
} }
&__chooser {
grid-column: 1 / span 2;
}
&__add { &__add {
grid-column: 1 / span 2; grid-column: 1 / span 2;
} }

View File

@ -2,6 +2,7 @@
<div <div
:class="componentClass" :class="componentClass"
:data-scrollto="component.id" :data-scrollto="component.id"
data-cy="content-component"
> >
<bookmark-actions <bookmark-actions
:bookmarked="bookmarked" :bookmarked="bookmarked"
@ -9,11 +10,12 @@
v-if="showBookmarkActions" v-if="showBookmarkActions"
@add-note="addNote(component.id)" @add-note="addNote(component.id)"
@edit-note="editNote" @edit-note="editNote"
@bookmark="bookmarkContent(component.id, !bookmarked)"/> @bookmark="bookmarkContent(component.id, !bookmarked)"
/>
<component <component
:is="component.type"
v-bind="component" v-bind="component"
:parent="parent" :parent="parent"
:is="component.type"
/> />
</div> </div>
</template> </template>
@ -21,28 +23,28 @@
<script> <script>
import {mapState} from 'vuex'; import {mapState} from 'vuex';
import TextBlock from '@/components/content-blocks/TextBlock';
import InstrumentWidget from '@/components/content-blocks/InstrumentWidget';
import ImageBlock from '@/components/content-blocks/ImageBlock';
import ImageUrlBlock from '@/components/content-blocks/ImageUrlBlock';
import VideoBlock from '@/components/content-blocks/VideoBlock';
import LinkBlock from '@/components/content-blocks/LinkBlock';
import DocumentBlock from '@/components/content-blocks/DocumentBlock';
import InfogramBlock from '@/components/content-blocks/InfogramBlock';
import ThinglinkBlock from '@/components/content-blocks/ThinglinkBlock';
import GeniallyBlock from '@/components/content-blocks/GeniallyBlock';
import SubtitleBlock from '@/components/content-blocks/SubtitleBlock';
import SectionTitleBlock from '@/components/content-blocks/SectionTitleBlock';
import ContentListBlock from '@/components/content-blocks/ContentListBlock';
import ModuleRoomSlug from '@/components/content-blocks/ModuleRoomSlug';
import Assignment from '@/components/content-blocks/assignment/Assignment';
import Survey from '@/components/content-blocks/SurveyBlock';
import Solution from '@/components/content-blocks/Solution';
import Instruction from '@/components/content-blocks/Instruction';
import BookmarkActions from '@/components/notes/BookmarkActions';
import {constructContentComponentBookmarkMutation} from '@/helpers/update-content-bookmark-mutation'; import {constructContentComponentBookmarkMutation} from '@/helpers/update-content-bookmark-mutation';
const TextBlock = () => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/TextBlock');
const InstrumentWidget = () => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/InstrumentWidget');
const ImageBlock = () => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/ImageBlock');
const ImageUrlBlock = () => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/ImageUrlBlock');
const VideoBlock = () => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/VideoBlock');
const LinkBlock = () => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/LinkBlock');
const DocumentBlock = () => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/DocumentBlock');
const InfogramBlock = () => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/InfogramBlock');
const ThinglinkBlock = () => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/ThinglinkBlock');
const GeniallyBlock = () => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/GeniallyBlock');
const SubtitleBlock = () => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/SubtitleBlock');
const SectionTitleBlock = () => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/SectionTitleBlock');
const ContentListBlock = () => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/ContentListBlock');
const ModuleRoomSlug = () => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/ModuleRoomSlug');
const Assignment = () => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/assignment/Assignment');
const Survey = () => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/SurveyBlock');
const Solution = () => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/Solution');
const Instruction = () => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/Instruction');
const BookmarkActions = () => import(/* webpackChunkName: "content-components" */'@/components/notes/BookmarkActions');
export default { export default {
props: ['component', 'parent', 'bookmarks', 'notes', 'root'], props: ['component', 'parent', 'bookmarks', 'notes', 'root'],
@ -92,13 +94,12 @@ export default {
methods: { methods: {
addNote(id) { addNote(id) {
if (!this.parent.hasOwnProperty('__typename')) { const type = Object.prototype.hasOwnProperty.call(this.parent, '__typename')
this.parent.__typename = 'ContentBlockNode'; ? this.parent.__typename : 'ContentBlockNode';
}
this.$store.dispatch('addNote', { this.$store.dispatch('addNote', {
content: id, content: id,
type: this.parent.__typename, type,
block: this.root block: this.root
}); });
}, },

View File

@ -0,0 +1,28 @@
<template>
<ol class="content-list">
<li
class="content-list__item"
v-for="(item, index) in items"
:key="item.id"
>
<slot
:item="item"
:index="index"
>
{{ item.id }}
</slot>
</li>
</ol>
</template>
<script>
export default {
//
props: {
items: {
type: Array,
default: () => ([])
}
},
};
</script>

View File

@ -1,32 +1,26 @@
<template> <template>
<div class="content-list-block__container"> <content-list
<div class="content-list-wrapper"> :items="contentBlocks"
<ol class="content-list"> :starting-index="startingIndex"
<li >
:key="contentBlock.id" <template #default="{ item }">
class="content-list__item contentlist-item"
v-for="(contentBlock, index) in contentBlocks">
<p class="content-list__numbering">{{ alphaIndex(index) }})</p>
<content-block <content-block
:content-block="contentBlock" :content-block="item"
:parent="parent" :parent="parent"
/> />
</li> </template>
</ol> </content-list>
</div>
</div>
</template> </template>
<script> <script>
const lowerAsciiA = 97; import ContentList from '@/components/content-blocks/ContentList';
export default { export default {
name: 'ContentBlockList', name: 'ContentBlockList',
props: ['contents', 'parent', 'startingIndex'], props: ['contents', 'parent', 'startingIndex'],
components: { components: {
ContentList,
// https://vuejs.org/v2/guide/components-edge-cases.html#Circular-References-Between-Components // https://vuejs.org/v2/guide/components-edge-cases.html#Circular-References-Between-Components
ContentBlock: () => import('@/components/ContentBlock') ContentBlock: () => import('@/components/ContentBlock')
}, },
@ -34,8 +28,9 @@
computed: { computed: {
contentBlocks() { contentBlocks() {
return this.contents.map(contentBlock => { return this.contents.map(contentBlock => {
const contents = contentBlock.value ? [...contentBlock.value] : [];
return Object.assign({}, contentBlock, { return Object.assign({}, contentBlock, {
contents: [...contentBlock.value], contents,
indent: true, indent: true,
bookmarks: this.parent.bookmarks, bookmarks: this.parent.bookmarks,
notes: this.parent.notes, notes: this.parent.notes,
@ -45,36 +40,10 @@
} }
}, },
methods: { };
alphaIndex(index) {
return String.fromCharCode(lowerAsciiA + this.startingIndex + index);
}
},
};
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "@/styles/_variables.scss"; @import "~styles/helpers";
@import "@/styles/_mixins.scss";
.content-list-wrapper {
.content-list {
/* https://stackoverflow.com/questions/1632005/ordered-list-html-lower-alpha-with-right-parentheses */
&__item {
list-style: none;
position: relative;
padding: 0 0 0 2*15px;
}
&__numbering {
position: absolute;
font-weight: 600;
left: 0;
color: $color-brand;
line-height: 27px;
}
}
}
</style> </style>

View File

@ -1,22 +1,24 @@
<template> <template>
<div class="document-block"> <div class="document-block">
<document-icon class="document-block__icon"/> <document-icon class="document-block__icon" />
<a <a
:href="value.url" :href="value.url"
class="document-block__link" class="document-block__link"
target="_blank">{{ urlName }}</a> target="_blank"
>{{ urlName }}</a>
<a <a
class="document-block__remove" class="document-block__remove"
v-if="showTrashIcon" v-if="showTrashIcon"
@click="$emit('trash')"> @click="$emit('trash')"
<trash-icon class="document-block__trash-icon"/> >
<trash-icon class="document-block__trash-icon" />
</a> </a>
</div> </div>
</template> </template>
<script> <script>
import DocumentIcon from '@/components/icons/DocumentIcon'; const DocumentIcon = () => import(/* webpackChunkName: "icons" */'@/components/icons/DocumentIcon');
import TrashIcon from '@/components/icons/TrashIcon'; const TrashIcon = () => import(/* webpackChunkName: "icons" */'@/components/icons/TrashIcon');
export default { export default {
props: { props: {

View File

@ -11,7 +11,8 @@
allowscriptaccess="always" allowscriptaccess="always"
allowfullscreen="true" allowfullscreen="true"
scrolling="yes" scrolling="yes"
allownetworking="all"/> allownetworking="all"
/>
</div> </div>
</div> </div>
</template> </template>

View File

@ -3,7 +3,8 @@
:src="value.path" :src="value.path"
alt="" alt=""
class="image-block" class="image-block"
@click="openFullscreen"> @click="openFullscreen"
>
</template> </template>
<script> <script>

View File

@ -3,7 +3,8 @@
:src="value.url" :src="value.url"
alt="" alt=""
class="image-block" class="image-block"
@click="openFullscreen"> @click="openFullscreen"
>
</template> </template>
<script> <script>

View File

@ -7,7 +7,8 @@
class="infogram-block__iframe" class="infogram-block__iframe"
scrolling="no" scrolling="no"
frameborder="0" frameborder="0"
style="border:none;"/> style="border:none;"
/>
</div> </div>
</template> </template>

View File

@ -1,17 +1,19 @@
<template> <template>
<div <div
class="instruction" class="instruction"
v-if="me.isTeacher"> v-if="me.isTeacher"
<bulb-icon class="instruction__icon"/> >
<bulb-icon class="instruction__icon" />
<a <a
:href="value.url" :href="value.url"
class="instruction__link">{{ text }}</a> class="instruction__link"
>{{ text }}</a>
</div> </div>
</template> </template>
<script> <script>
import me from '@/mixins/me'; import me from '@/mixins/me';
import BulbIcon from '@/components/icons/BulbIcon'; const BulbIcon = () => import(/* webpackChunkName: "icons" */'@/components/icons/BulbIcon');
export default { export default {
props: ['value'], props: ['value'],

View File

@ -1,11 +1,15 @@
<template> <template>
<!-- eslint-disable vue/no-v-html -->
<div class="instrument-widget"> <div class="instrument-widget">
<div <div
class="instrument-widget__description" class="instrument-widget__description"
v-html="value.description"/> v-html="value.description"
/>
<router-link <router-link
:to="{name: 'instrument', params: { slug: value.slug }}" :to="{name: 'instrument', params: { slug: value.slug }}"
class="instrument-widget__button button">Instrument anzeigen class="instrument-widget__button button"
>
Instrument anzeigen
</router-link> </router-link>
</div> </div>
</template> </template>

View File

@ -1,22 +1,24 @@
<template> <template>
<div <div
:class="{ 'link-block--no-margin': noMargin}" :class="{ 'link-block--no-margin': noMargin}"
class="link-block"> class="link-block"
<link-icon class="link-block__icon"/> >
<link-icon class="link-block__icon" />
<a <a
:href="href" :href="href"
class="link-block__link" class="link-block__link"
target="_blank">{{ value.text }}</a> target="_blank"
>{{ value.text }}</a>
</div> </div>
</template> </template>
<script> <script>
import LinkIcon from '@/components/icons/LinkIcon'; const LinkIcon = () => import(/* webpackChunkName: "icons" */'@/components/icons/LinkIcon');
export default { export default {
props: { props: {
value: Object, value: Object,
'no-margin': { noMargin: {
default: false default: false
} }
}, },

View File

@ -2,7 +2,9 @@
<div class="module-slug"> <div class="module-slug">
<router-link <router-link
:to="{name: 'moduleRoom', params: { slug: value.slug }}" :to="{name: 'moduleRoom', params: { slug: value.slug }}"
class="button button--primary">Raum anzeigen class="button button--primary"
>
Raum anzeigen
</router-link> </router-link>
</div> </div>
</template> </template>

View File

@ -1,7 +1,9 @@
<template> <template>
<!-- eslint-disable vue/no-v-html -->
<h4 <h4
class="section-title" class="section-title"
v-html="value.text"/> v-html="value.text"
/>
</template> </template>
<script> <script>

View File

@ -1,11 +1,13 @@
<template> <template>
<div <div
class="solution" class="solution"
data-cy="solution"> data-cy="solution"
>
<a <a
class="solution__toggle" class="solution__toggle"
data-cy="show-solution" data-cy="show-solution"
@click="toggle">Lösung @click="toggle"
>Lösung
<template v-if="!visible">anzeigen</template> <template v-if="!visible">anzeigen</template>
<template v-else>ausblenden</template> <template v-else>ausblenden</template>
</a> </a>
@ -15,7 +17,8 @@
data-cy="solution-text" data-cy="solution-text"
v-if="visible" v-if="visible"
v-html="value.text"/> v-html="value.text"
/>
</transition> </transition>
</div> </div>
</template> </template>

View File

@ -1,10 +1,12 @@
<template> <template>
<h5 <h5
class="subtitle" class="subtitle"
v-html="value.text"/> v-html="value.text"
/>
</template> </template>
<script> <script>
//todo: esacpe value.text
export default { export default {
props: ['value'] props: ['value']
}; };

View File

@ -1,10 +1,13 @@
<template> <template>
<div <div
:data-scrollto="value.id" :data-scrollto="value.id"
class="survey-block"> class="survey-block"
>
<router-link <router-link
:to="{name: 'survey', params: {id:value.id}}" :to="{name: 'survey', params: {id:value.id}}"
class="button button--primary">Übung anzeigen class="button button--primary"
>
Übung anzeigen
</router-link> </router-link>
</div> </div>
</template> </template>

View File

@ -1,8 +1,10 @@
<template> <template>
<!-- eslint-disable vue/no-v-html -->
<div class="task"> <div class="task">
<div <div
class="task__text" class="task__text"
v-html="value.text"/> v-html="value.text"
/>
</div> </div>
</template> </template>

View File

@ -1,10 +1,12 @@
<template> <template>
<div <div
class="text-block" class="text-block"
v-html="value.text"/> v-html="value.text"
/>
</template> </template>
<script> <script>
// todo: escape text maybe
export default { export default {
props: ['value'] props: ['value']
}; };

View File

@ -13,7 +13,8 @@
scrolling="no" scrolling="no"
allowscriptaccess="always" allowscriptaccess="always"
allowfullscreen="true" allowfullscreen="true"
allownetworking="all"/> allownetworking="all"
/>
</div> </div>
</div> </div>
</template> </template>

View File

@ -2,14 +2,16 @@
<div class="video-block"> <div class="video-block">
<youtube-embed <youtube-embed
:url="value.url" :url="value.url"
v-if="isYoutube"/> v-if="isYoutube"
/>
<vimeo-embed <vimeo-embed
:url="value.url" :url="value.url"
v-if="isVimeo"/> v-if="isVimeo"
/>
<srf-embed <srf-embed
:url="value.url" :url="value.url"
v-if="isSrf"/> v-if="isSrf"
/>
</div> </div>
</template> </template>

View File

@ -1,14 +1,16 @@
<template> <template>
<div <div
:data-scrollto="value.id" :data-scrollto="value.id"
class="assignment"> class="assignment"
>
<p class="assignment__assignment-text"> <p class="assignment__assignment-text">
{{ assignment.assignment }} {{ assignment.assignment }}
</p> </p>
<solution <solution
:value="solution" :value="solution"
v-if="assignment.solution"/> v-if="assignment.solution"
/>
<template v-if="isStudent"> <template v-if="isStudent">
<submission-form <submission-form
@ -30,17 +32,21 @@
<spell-check <spell-check
:corrections="corrections" :corrections="corrections"
:text="submission.text"/> :text="submission.text"
/>
<p <p
class="assignment__feedback" class="assignment__feedback"
v-if="assignment.submission.submissionFeedback" v-if="assignment.submission.submissionFeedback"
v-html="feedbackText"/> v-html="feedbackText"
/>
</template> </template>
<template v-if="!isStudent"> <template v-if="!isStudent">
<router-link <router-link
:to="{name: 'submissions', params: { id: assignment.id }}" :to="{name: 'submissions', params: { id: assignment.id }}"
class="button button--primary">Zu den class="button button--primary"
>
Zu den
Ergebnissen Ergebnissen
</router-link> </router-link>
</template> </template>
@ -57,9 +63,9 @@
import debounce from 'lodash/debounce'; import debounce from 'lodash/debounce';
import cloneDeep from 'lodash/cloneDeep'; import cloneDeep from 'lodash/cloneDeep';
import SubmissionForm from '@/components/content-blocks/assignment/SubmissionForm'; const SubmissionForm = () => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/assignment/SubmissionForm');
import Solution from '@/components/content-blocks/Solution'; const Solution = () => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/Solution');
import SpellCheck from '@/components/content-blocks/assignment/SpellCheck'; const SpellCheck = () => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/assignment/SpellCheck');
export default { export default {
props: ['value'], props: ['value'],
@ -106,6 +112,7 @@
return this.assignment.id ? this.assignment.id.replace(/=/g, '') : ''; return this.assignment.id ? this.assignment.id.replace(/=/g, '') : '';
}, },
feedbackText() { feedbackText() {
// todo: should we maybe clean up this feedback text?
let feedback = this.assignment.submission.submissionFeedback; let feedback = this.assignment.submission.submissionFeedback;
return `<span class="inline-title">Feedback von ${feedback.teacher.firstName} ${feedback.teacher.lastName}:</span> ${feedback.text}`; return `<span class="inline-title">Feedback von ${feedback.teacher.firstName} ${feedback.teacher.lastName}:</span> ${feedback.text}`;
}, },

View File

@ -1,28 +1,31 @@
<template> <template>
<div <div
class="final-submission" class="final-submission"
data-cy="final-submission"> data-cy="final-submission"
>
<document-block <document-block
:value="{url: userInput.document}" :value="{url: userInput.document}"
class="final-submission__document" class="final-submission__document"
v-if="userInput.document" v-if="userInput.document"
/> />
<div class="final-submission__explanation"> <div class="final-submission__explanation">
<info-icon class="final-submission__explanation-icon"/> <info-icon class="final-submission__explanation-icon" />
<span class="final-submission__explanation-text">{{ sharedMsg }}</span> <span class="final-submission__explanation-text">{{ sharedMsg }}</span>
<a <a
class="final-submission__reopen" class="final-submission__reopen"
data-cy="final-submission-reopen" data-cy="final-submission-reopen"
v-if="showReopen" v-if="showReopen"
@click="$emit('reopen')">Bearbeiten</a> @click="$emit('reopen')"
>Bearbeiten</a>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import InfoIcon from '@/components/icons/InfoIcon';
import DocumentBlock from '@/components/content-blocks/DocumentBlock';
import {newLineToParagraph} from '@/helpers/text'; import {newLineToParagraph} from '@/helpers/text';
const DocumentBlock = () => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/DocumentBlock');
const InfoIcon = () => import(/* webpackChunkName: "icons" */'@/components/icons/InfoIcon');
export default { export default {
props: { props: {

View File

@ -1,8 +1,10 @@
<template> <template>
<!-- eslint-disable vue/no-v-html -->
<p <p
class="spellcheck" class="spellcheck"
v-if="corrections"> v-if="corrections"
<span class="inline-title">Rechtschreibung:</span> <span v-html="highlightedText"/> >
<span class="inline-title">Rechtschreibung:</span> <span v-html="highlightedText" />
</p> </p>
</template> </template>

View File

@ -12,25 +12,29 @@
<div <div
class="submission-form-container__actions" class="submission-form-container__actions"
v-if="!isFinalOrReadOnly"> v-if="!isFinalOrReadOnly"
>
<button <button
class="submission-form-container__submit button button--primary button--white-bg" class="submission-form-container__submit button button--primary button--white-bg"
data-cy="submission-form-submit" data-cy="submission-form-submit"
@click="$emit('turnIn')" @click="$emit('turnIn')"
>{{ action }} >
{{ action }}
</button> </button>
<button <button
class="submission-form-container__submit submission-form-container__spellcheck button button--primary button--white-bg" class="submission-form-container__submit submission-form-container__spellcheck button button--primary button--white-bg"
data-cy="spellcheck-button" data-cy="spellcheck-button"
v-if="showSpellcheckButton" v-if="showSpellcheckButton"
@click="$emit('spellcheck')" @click="$emit('spellcheck')"
>{{ spellcheckText }} >
{{ spellcheckText }}
</button> </button>
<file-upload <file-upload
:document="userInput.document" :document="userInput.document"
v-if="allowsDocuments" v-if="allowsDocuments"
@change-document-url="changeDocumentUrl"/> @change-document-url="changeDocumentUrl"
<slot/> />
<slot />
</div> </div>
<final-submission <final-submission
@ -38,15 +42,16 @@
:shared-msg="sharedMsg" :shared-msg="sharedMsg"
:show-reopen="!readOnly" :show-reopen="!readOnly"
v-if="isFinalOrReadOnly" v-if="isFinalOrReadOnly"
@reopen="$emit('reopen')"/> @reopen="$emit('reopen')"
/>
</div> </div>
</template> </template>
<script> <script>
import SubmissionInput from '@/components/content-blocks/assignment/SubmissionInput'; const SubmissionInput = () => import('@/components/content-blocks/assignment/SubmissionInput');
import FinalSubmission from '@/components/content-blocks/assignment/FinalSubmission'; const FinalSubmission = () => import('@/components/content-blocks/assignment/FinalSubmission');
import DocumentBlock from '@/components/content-blocks/DocumentBlock'; const FileUpload = () => import('@/components/ui/file-upload/FileUpload');
import FileUpload from '@/components/ui/file-upload/FileUpload';
export default { export default {
props: { props: {
@ -75,7 +80,6 @@
FileUpload, FileUpload,
SubmissionInput, SubmissionInput,
FinalSubmission, FinalSubmission,
DocumentBlock,
}, },
computed: { computed: {

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