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 = "*"
unittest-xml-reporting = "*"
django-silk = "*"
wagtail-autocomplete = "*"
# todo: @django3-update
# wagtail-autocomplete = "*"
wagtail-autocomplete = "==0.6.3"
jedi = "==0.17.2"
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
```
# 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 = {
root: true,
parserOptions: {
parser: 'babel-eslint'
parser: '@typescript-eslint/parser',
extraFileExtensions: ['.vue'],
},
env: {
browser: true,
},
globals: {
process: "readonly"
},
extends: [
// 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.
'plugin:vue/recommended',
// 'plugin:vue/recommended',
// 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
plugins: [
'vue'
'vue',
'@typescript-eslint'
],
overrides: [{
files: ['*.ts','*.tsx'],
rules: {
'no-unused-vars': 'off'
}
}],
// add your custom rules here
rules: {
// allow async-await
@ -49,6 +62,9 @@ module.exports = {
'CONTENT'
]
}],
"vue/multi-word-component-names": ["off", {
"ignores": []
}],
'vue/order-in-components': ['error', {
'order': [
'el',

View File

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

View File

@ -1,56 +1,10 @@
'use strict'
const path = require('path')
const config = require('../config')
const ExtractTextPlugin = require('extract-text-webpack-plugin')
const packageConfig = require('../package.json')
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 assetsSubDirectory = isDev
? config.dev.assetsSubDirectory
@ -78,7 +32,6 @@ const createNotifierCallback = () => {
}
module.exports = {
styleRule,
isDev,
assetsPath,
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';
const path = require('path');
const config = require('../config');
var MiniCssExtractPlugin = require('mini-css-extract-plugin')
const {VueLoaderPlugin} = require('vue-loader');
const {isDev, styleRule, assetsPath} = require('./utils');
const {isDev, assetsPath} = require('./utils');
function resolve(dir) {
return path.join(__dirname, '..', dir);
@ -37,13 +38,29 @@ module.exports = {
? config.dev.assetsPublicPath
: config.build.assetsPublicPath,
},
optimization: {
splitChunks: {
chunks: 'all'
}
},
resolve: {
extensions: ['.js', '.vue', '.json', '.gql', '.graphql', '.scss'],
alias: {
'@': resolve('src'),
styles: resolve('src/styles'),
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: {
rules: [
@ -58,6 +75,11 @@ module.exports = {
img: 'src',
image: 'xlink:href',
},
compilerOptions: {
compatConfig: {
MODE: 2,
},
},
},
},
{
@ -105,23 +127,33 @@ module.exports = {
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: [
new VueLoaderPlugin(),
],
node: {
// prevent webpack from injecting useless setImmediate polyfill because Vue
// source contains it (although only uses it if it's native).
setImmediate: false,
// prevent webpack from injecting mocks to Node native modules
// that does not make sense for the client
dgram: 'empty',
fs: 'empty',
net: 'empty',
tls: 'empty',
child_process: 'empty',
},
// node: {
// // prevent webpack from injecting useless setImmediate polyfill because Vue
// // source contains it (although only uses it if it's native).
// setImmediate: false,
// // prevent webpack from injecting mocks to Node native modules
// // that does not make sense for the client
// dgram: 'empty',
// fs: 'empty',
// net: 'empty',
// tls: 'empty',
// child_process: 'empty',
// },
};

View File

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

View File

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

View File

@ -33,7 +33,8 @@ module.exports = {
*/
// 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,
// set this to false - it *may* help
@ -57,7 +58,7 @@ module.exports = {
productionSourceMap: true,
// https://webpack.js.org/configuration/devtool/#production
devtool: '#source-map',
devtool: 'source-map',
// Gzip off by default as many popular static hosts such as
// Surge or Netlify already gzip all static assets for you.

View File

@ -24,19 +24,11 @@
"slug": "geld-und-kauf",
"__typename": "TopicNode"
},
"schoolClasses": {
"edges": [
{
"node": {
"id": "U2Nob29sQ2xhc3NOb2RlOjE=",
"name": "FLID2018a",
"__typename": "SchoolClassNode"
},
"__typename": "SchoolClassNodeEdge"
}
],
"__typename": "SchoolClassNodeConnection"
},
"schoolClasses": [{
"id": "U2Nob29sQ2xhc3NOb2RlOjE=",
"name": "FLID2018a",
"__typename": "SchoolClassNode"
}],
"__typename": "UserNode",
"onboardingVisited": true,
"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 = {
id: btoa('SchoolClassNode:selectedClassId'),
id: SELECTED_CLASS_ID_ENCODED,
name: 'Moordale',
readOnly: false,
code: 'XXXX',
@ -42,7 +44,7 @@ export default {
id: getChapterId(),
title: 'chapter-title',
description: 'chapter-description',
bookmark: null
}),
ContentBlockNode: () => ({
contents: [],
@ -62,11 +64,7 @@ export default {
readOnly: false,
onboardingVisited: true,
selectedClass,
schoolClasses: {
edges: [
{node: selectedClass},
],
},
schoolClasses: [selectedClass],
recentModules: {
edges: [],
},
@ -84,19 +82,18 @@ export default {
}),
ModuleNode: () => ({
title: 'Module Title',
slug: 'some slug',
slug: 'some-slug',
metaTitle: 'Meta Title',
heroImage: '',
teaser: '',
intro: '',
assignments: {nodes: []},
assignments: [],
objectiveGroups: [],
id: getModuleId(),
bookmark: null
}),
TopicNode: () => ({
modules: {
edges: [],
},
modules: [],
}),
RoomNode: () => ({
title: 'A Room',
@ -104,7 +101,7 @@ export default {
appearance: 'blue',
description: 'A Room description',
schoolClass: {
id: 'selectedClassId',
id: SELECTED_CLASS_ID_ENCODED,
},
}),
RoomEntryNode: () => ({

View File

@ -4,9 +4,9 @@ export default {
heroImage: 'heroImage',
teaser: 'A Module Mock Teaser',
intro: 'intro',
assignments: {},
assignments: [],
objectiveGroups: [],
id: '',
id: 'TW9kdWxlTm9kZToxMjM=',
chapters: [],
topic: {
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');
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', () => {

View File

@ -15,9 +15,7 @@ describe('Apply module visibility', () => {
const {me: minimalMe} = getMinimalMe({});
const me = {
...minimalMe,
schoolClasses: {
edges: schoolClasses.map(scn => ({node: scn}))
}
schoolClasses
};
// name: '[\'FLID2018a\', \'Andere Klasse\']'
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 {
...me,
onboardingVisited,
schoolClasses: {edges: schoolClasses},
schoolClasses,
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.visit('/module/lohn-und-budget');

View File

@ -95,10 +95,8 @@ describe('Project Page', () => {
beforeEach(() => {
cy.setup();
cy.task('getSchema').then(schema => {
cy.mockGraphqlOps({
operations,
});
cy.mockGraphqlOps({
operations,
});
});

View File

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

View File

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

View File

@ -3,13 +3,11 @@ import {getMinimalMe} from '../../../support/helpers';
const getOperations = ({readOnly}) => ({
MeQuery: getMinimalMe({readOnly}),
NewsTeasers: {
newsTeasers: {
edges: [
newsTeasers: [
{},
{},
{},
]
}
}
});

View File

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

View File

@ -13,24 +13,18 @@ describe('Room Team Management - Read only', () => {
},
},
RoomsQuery: {
rooms: {
edges: [
{
node: {
id: '',
slug: '',
title: 'some room',
entryCount: 3,
appearance: 'red',
description: 'some description',
schoolClass: {
id: SELECTED_CLASS_ID,
name: 'bla',
},
},
},
],
},
rooms: [{
id: '',
slug: '',
title: 'some room',
entryCount: 3,
appearance: 'red',
description: 'some description',
schoolClass: {
id: SELECTED_CLASS_ID,
name: 'bla',
},
}],
},
});

View File

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

View File

@ -87,7 +87,8 @@ describe('The Room Page', () => {
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({
isTeacher: true,
});
@ -155,9 +156,7 @@ describe('The Room Page', () => {
MeQuery,
RoomsQuery() {
return {
rooms: {
edges: rooms.map(room => ({node: room})),
},
rooms
};
},
RoomEntriesQuery: {
@ -244,23 +243,18 @@ describe('The Room Page', () => {
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 otherClass = {
id: btoa('SchoolClassNode:34'),
name: 'Other Class',
readOnly: false
};
const operations = {
MeQuery: {
me: {
...me,
schoolClasses: {
edges: [
...me.schoolClasses.edges,
{
node: {
id: btoa('SchoolClassNode:other-class'),
name: 'Other Class'
},
},
],
},
schoolClasses: [...me.schoolClasses, otherClass],
},
},
RoomEntriesQuery,
@ -268,6 +262,15 @@ describe('The Room Page', () => {
updateSettings: {
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.selectClass('Other Class');
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 {SELECTED_CLASS_ID_ENCODED} from '../../../fixtures/mocks';
describe('The Rooms Page', () => {
const getOperations = (isTeacher) => ({
MeQuery: getMinimalMe({isTeacher}),
RoomsQuery: {
rooms: {
edges: [
{
node: {
schoolClass: {
id: btoa('SchoolClassNode:selectedClassId'),
},
},
},
],
},
rooms: [{
schoolClass: {
id: SELECTED_CLASS_ID_ENCODED,
},
}],
},
});
@ -23,9 +18,7 @@ describe('The Rooms Page', () => {
return {
...operations,
RoomsQuery: {
rooms: {
edges: [],
},
rooms: [],
},
};
};
@ -105,9 +98,7 @@ describe('The Rooms Page', () => {
MeQuery,
RoomsQuery() {
return {
rooms: {
edges: rooms.map(room => ({node: room})),
},
rooms
};
},
AddRoom({input: {room: {title, appearance, description}}}) {

View File

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

View File

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

View File

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

View File

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

11257
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -2,48 +2,52 @@
<div
:class="{'no-scroll': showModal || showMobileNavigation}"
class="app"
id="app">
<read-only-banner/>
<scroll-up/>
id="app"
>
<read-only-banner />
<scroll-up />
<component
:is="showModalDeprecated"
v-if="showModalDeprecated"/>
v-if="showModalDeprecated"
/>
<component
:is="showModal"
v-if="showModal"/>
<component :is="layout"/>
v-if="showModal"
/>
<component :is="layout" />
</div>
</template>
<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 ScrollUp from '@/components/ScrollUp';
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 {
name: 'App',
@ -93,6 +97,7 @@
<style lang="scss">
@import "~styles/main.scss";
@import "~styles/helpers";
body {
overflow-y: auto;

View File

@ -2,14 +2,20 @@
<div class="add-content">
<a
class="add-content__button"
@click="addContent">
<add-pointer class="add-content__icon"/>
@click="addContent"
>
<add-pointer class="add-content__icon" />
</a>
</div>
</template>
<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 {
props: ['after', 'parent'],
@ -18,15 +24,31 @@
AddPointer
},
methods: {
addContent() {
if (this.parent && this.parent.__typename === 'ObjectiveGroupNode') {
this.$store.dispatch('addObjective', this.parent.id);
} else {
this.$store.dispatch('addContentBlock', {
after: this.after ? this.after.id : undefined,
parent: this.parent ? this.parent.id : undefined
});
let route;
const slug = this.$route.params.slug;
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>
<style scoped lang="scss">
@import "@/styles/_variables.scss";
@import "@/styles/_mixins.scss";
@import "~styles/helpers";
.add-content {
display: none;

View File

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

View File

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

View File

@ -1,46 +1,64 @@
<template>
<!-- eslint-disable vue/no-v-html -->
<div class="assignment-with-submissions">
<p class="assignment-with-submissions__text">{{ assignment.assignment }}</p>
<p class="assignment-with-submissions__text">
{{ assignment.assignment }}
</p>
<div>
<a
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
class="assignment-with-submissions__solution"
v-if="assignment.solution">
<h4 class="assignment-with-submissions__heading">Lösung</h4>
v-if="assignment.solution"
>
<h4 class="assignment-with-submissions__heading">
Lösung
</h4>
<p
class="assignment-with-submissions__solution-text"
data-cy="assignment-solution"
v-html="assignment.solution" />
v-html="assignment.solution"
/>
</div>
<p
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
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">
<p class="submission-header__title">Lernende</p>
<p class="submission-header__title">Ergebnisse</p>
<p class="submission-header__title">Feedback</p>
<p class="submission-header__title">
Lernende
</p>
<p class="submission-header__title">
Ergebnisse
</p>
<p class="submission-header__title">
Feedback
</p>
</div>
<router-link
:to="submissionLink(submission)"
:key="submission.id"
class="assignment-with-submissions__link"
v-for="submission in submissions">
v-for="submission in submissions"
:key="submission.id"
>
<student-submission
:submission="submission"
class="assignment-with-submissions__submission"
/>
</router-link>
</div>
</div>
</template>
@ -81,7 +99,7 @@
if (this.currentFilter.id === '') {
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>
<router-link
:to="to"
class="sub-navigation-item back-link">
<chevron-left class="back-link__icon sub-navigation-item__icon"/>
class="sub-navigation-item back-link"
>
<chevron-left class="back-link__icon sub-navigation-item__icon" />
{{ fullTitle }}
</router-link>
</template>
<script>
import ChevronLeft from '@/components/icons/ChevronLeft';
import {MODULE_PAGE} from '@/router/module.names';
import {ROOMS_PAGE} from '@/router/room.names';
import {PROJECTS_PAGE} from '@/router/portfolio.names';
const ChevronLeft = () => import(/* webpackChunkName: "icons" */'@/components/icons/ChevronLeft');
export default {
props: {
title: {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
<template>
<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__text">
{{ text }}
@ -10,7 +10,7 @@
</template>
<script>
import InfoIcon from '@/components/icons/InfoIcon';
const InfoIcon = () => import(/* webpackChunkName: "icons" */'@/components/icons/InfoIcon');
export default {
props: ['text'],

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,7 +2,8 @@
<div
class="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">
<p class="read-only-banner__text">
{{ readOnlyText }} Sie können Inhalte lesen, aber nicht
@ -13,14 +14,16 @@
:to="licenseActivationLink"
data-cy="license-activation-link"
class="read-only-banner__link button button--primary"
v-if="me.readOnly">Neuen Lizenzcode eingeben
v-if="me.readOnly"
>
Neuen Lizenzcode eingeben
</router-link>
<a
target="_blank"
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>
</template>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,26 +1,29 @@
<template>
<div
:class="{ 'sub-navigation-item--active': show}"
class="sub-navigation-item"
v-click-outside="close"
class="sub-navigation-item">
>
<div
class="sub-navigation-item__title"
@click="show = !show">
@click="show = !show"
>
{{ title }}
<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-down class="sub-navigation-item__icon sub-navigation-item__chevron-down" />
<chevron-up class="sub-navigation-item__icon sub-navigation-item__chevron-up" />
</div>
<div
class="sub-navigation-item__nav-items book-subnavigation"
v-if="show">
<slot/>
v-if="show"
>
<slot />
</div>
</div>
</template>
<script>
import ChevronDown from '@/components/icons/ChevronDown';
import ChevronUp from '@/components/icons/ChevronUp';
const ChevronDown = () => import(/* webpackChunkName: "icons" */'@/components/icons/ChevronDown');
const ChevronUp = () => import(/* webpackChunkName: "icons" */'@/components/icons/ChevronUp');
export default {
props: ['title'],
@ -37,7 +40,7 @@
},
watch: {
$route(to, from) {
$route() {
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>
<modal>
<template slot="header">
<template #header>
<modal-input
:placeholder="titlePlaceholder"
:value="localContentBlock.title"
@ -24,40 +24,15 @@
@add-element="addElement"
/>
<div
:key="index"
class="contents-form__element"
v-for="(element, index) in localContentBlock.contents">
<component
:is="type(element)"
:class="{'contents-form__chooser': type(element) === 'content-block-element-chooser-widget'}"
v-bind="element"
:index="index"
class="contents-form__element-component"
@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"
v-for="(element, index) in localContentBlock.contents"
:key="index"
>
<content-element
:element="element"
@update="update(index, $event)"
@remove="remove(index)"
/>
<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
:index="index"
@ -66,65 +41,56 @@
/>
</div>
<div slot="footer">
<a
:class="{'button--disabled': disableSave}"
class="button button--primary"
data-cy="modal-save-button"
@click="save">Speichern</a>
<a
class="button"
@click="$emit('hide')">Abbrechen</a>
</div>
<template #footer>
<div>
<a
:class="{'button--disabled': disableSave}"
class="button button--primary"
data-cy="modal-save-button"
@click="save"
>Speichern</a>
<a
class="button"
@click="$emit('hide')"
>Abbrechen</a>
</div>
</template>
</modal>
</template>
<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';
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 {
props: {
'content-block': Object,
'block-type': {
contentBlock: Object,
blockType: {
type: String,
default: 'ContentBlock'
default: 'ContentBlock',
},
'show-task-selection': {
showTaskSelection: {
type: Boolean,
default: false
default: false,
},
'disable-save': {
disableSave: {
type: Boolean,
default: false
}
default: false,
},
},
components: {
ContentElement,
Modal,
ContentBlockElementChooserWidget,
ModalInput,
AddContentElement,
LinkForm,
VideoForm,
ImageForm,
DocumentForm,
AssignmentForm,
TextForm,
TrashIcon,
Checkbox
Checkbox,
},
data() {
@ -134,14 +100,14 @@
title: this.contentBlock.title,
contents: [...this.contentBlock.contents],
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: {
me: meQuery
me: meQuery,
},
computed: {
@ -150,123 +116,15 @@
},
taskSelection() {
return this.showTaskSelection && this.me.permissions.includes('users.can_manage_school_class_content');
}
},
},
methods: {
type(element) {
switch (element.type) {
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';
setContentBlockType(checked) {
this.localContentBlock.isAssignment = checked;
},
_updateProperty(value, index, key) {
const content = this.localContentBlock.contents[index];
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);
update(index, element) {
this.localContentBlock.contents.splice(index, 1, element);
},
save() {
if (!this.disableSave) {
@ -277,27 +135,31 @@
this.$emit('save', this.localContentBlock);
}
},
setContentBlockType(checked, localContentBlock) {
this.localContentBlock.isAssignment = checked;
updateTitle(title) {
this.localContentBlock.title = title;
this.error = false;
},
switchToDocument(index, value) {
this.changeType(index, 'document_block', value);
}
}
addElement(index) {
this.localContentBlock.contents.splice(index + 1, 0, {
hideAssignment: this.blockType !== 'ContentBlock',
});
},
remove(index) {
this.localContentBlock.contents.splice(index, 1);
},
},
};
</script>
<style scoped lang="scss">
@import "@/styles/_variables.scss";
@import "~styles/helpers";
.contents-form {
/* top level does not exist, because of the modal */
&__element {
display: grid;
grid-template-columns: 1fr 50px;
grid-auto-rows: auto;
/*width: 95%; // reserve space for scrollbar*/
}
&__element-component {
@ -310,10 +172,6 @@
&__trash-icon {
}
&__chooser {
grid-column: 1 / span 2;
}
&__add {
grid-column: 1 / span 2;
}

View File

@ -2,6 +2,7 @@
<div
:class="componentClass"
:data-scrollto="component.id"
data-cy="content-component"
>
<bookmark-actions
:bookmarked="bookmarked"
@ -9,11 +10,12 @@
v-if="showBookmarkActions"
@add-note="addNote(component.id)"
@edit-note="editNote"
@bookmark="bookmarkContent(component.id, !bookmarked)"/>
@bookmark="bookmarkContent(component.id, !bookmarked)"
/>
<component
:is="component.type"
v-bind="component"
:parent="parent"
:is="component.type"
/>
</div>
</template>
@ -21,28 +23,28 @@
<script>
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';
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 {
props: ['component', 'parent', 'bookmarks', 'notes', 'root'],
@ -92,13 +94,12 @@ export default {
methods: {
addNote(id) {
if (!this.parent.hasOwnProperty('__typename')) {
this.parent.__typename = 'ContentBlockNode';
}
const type = Object.prototype.hasOwnProperty.call(this.parent, '__typename')
? this.parent.__typename : 'ContentBlockNode';
this.$store.dispatch('addNote', {
content: id,
type: this.parent.__typename,
type,
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>
<div class="content-list-block__container">
<div class="content-list-wrapper">
<ol class="content-list">
<li
:key="contentBlock.id"
class="content-list__item contentlist-item"
v-for="(contentBlock, index) in contentBlocks">
<p class="content-list__numbering">{{ alphaIndex(index) }})</p>
<content-block
:content-block="contentBlock"
:parent="parent"
/>
</li>
</ol>
</div>
</div>
<content-list
:items="contentBlocks"
:starting-index="startingIndex"
>
<template #default="{ item }">
<content-block
:content-block="item"
:parent="parent"
/>
</template>
</content-list>
</template>
<script>
const lowerAsciiA = 97;
import ContentList from '@/components/content-blocks/ContentList';
export default {
name: 'ContentBlockList',
props: ['contents', 'parent', 'startingIndex'],
components: {
ContentList,
// https://vuejs.org/v2/guide/components-edge-cases.html#Circular-References-Between-Components
ContentBlock: () => import('@/components/ContentBlock')
},
@ -34,8 +28,9 @@
computed: {
contentBlocks() {
return this.contents.map(contentBlock => {
const contents = contentBlock.value ? [...contentBlock.value] : [];
return Object.assign({}, contentBlock, {
contents: [...contentBlock.value],
contents,
indent: true,
bookmarks: this.parent.bookmarks,
notes: this.parent.notes,
@ -45,36 +40,10 @@
}
},
methods: {
alphaIndex(index) {
return String.fromCharCode(lowerAsciiA + this.startingIndex + index);
}
},
};
};
</script>
<style scoped lang="scss">
@import "@/styles/_variables.scss";
@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;
}
}
}
@import "~styles/helpers";
</style>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,28 +1,31 @@
<template>
<div
class="final-submission"
data-cy="final-submission">
data-cy="final-submission"
>
<document-block
:value="{url: userInput.document}"
class="final-submission__document"
v-if="userInput.document"
/>
<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>
<a
class="final-submission__reopen"
data-cy="final-submission-reopen"
v-if="showReopen"
@click="$emit('reopen')">Bearbeiten</a>
@click="$emit('reopen')"
>Bearbeiten</a>
</div>
</div>
</template>
<script>
import InfoIcon from '@/components/icons/InfoIcon';
import DocumentBlock from '@/components/content-blocks/DocumentBlock';
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 {
props: {

View File

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

View File

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

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