Merge branch 'feature/vue3-upgrade-2023-01-25' into develop

This commit is contained in:
Ramon Wenger 2023-02-01 16:03:39 +01:00
commit 300cb8681f
129 changed files with 12708 additions and 14370 deletions

View File

@ -42,7 +42,7 @@ module.exports = {
'@': resolve('src'), '@': resolve('src'),
styles: resolve('src/styles'), styles: resolve('src/styles'),
gql: resolve('src/graphql/gql'), gql: resolve('src/graphql/gql'),
// vue: '@vue/compat', vue: '@vue/compat',
}, },
}, },
module: { module: {
@ -60,7 +60,7 @@ module.exports = {
}, },
compilerOptions: { compilerOptions: {
compatConfig: { compatConfig: {
MODE: 2, MODE: 2
}, },
}, },
}, },

View File

@ -4,7 +4,6 @@ import { hasOperationName } from '../../../support/graphql';
let snapshotTitle; let snapshotTitle;
let deleteSuccess; let deleteSuccess;
let page;
const moduleWithSnapshots = { const moduleWithSnapshots = {
...module, ...module,
@ -28,68 +27,71 @@ const moduleWithSnapshots = {
], ],
}; };
const mockDeleteSnapshot = (success) => { // const mockDeleteSnapshot = (success) => {
cy.intercept('POST', '/api/graphql', (req) => { // cy.intercept('POST', '/api/graphql', (req) => {
if (hasOperationName(req, 'DeleteSnapshot')) { // if (hasOperationName(req, 'DeleteSnapshot')) {
let result; // let result;
if (success) { // if (success) {
result = { // result = {
message: 'yay!', // message: 'yay!',
__typename: 'Success', // __typename: 'Success',
}; // };
} else { // } else {
result = { // result = {
reason: 'Not the owner', // reason: 'Not the owner',
__typename: 'NotOwner', // __typename: 'NotOwner',
}; // };
} // }
req.reply({ // req.reply({
data: { // data: {
deleteSnapshot: { // deleteSnapshot: {
result, // result,
}, // },
}, // },
}); // });
} //
}); // }
}; // });
// };
const mockUpdateSnapshot = (title) => { // const mockUpdateSnapshot = (title) => {
cy.intercept('POST', '/api/graphql', (req) => { // cy.intercept('POST', '/api/graphql', (req) => {
if (hasOperationName(req, 'UpdateSnapshot')) { // if (hasOperationName(req, 'UpdateSnapshot')) {
let snapshot; // let snapshot;
if (title) { // if (title) {
snapshot = { // snapshot = {
__typename: 'SnapshotNode', // __typename: 'SnapshotNode',
id: 'U25hcHNob3ROb2RlOjQ=', // id: 'U25hcHNob3ROb2RlOjQ=',
title, // title,
}; // };
} else { // } else {
snapshot = { // snapshot = {
__typename: 'NotOwner', // __typename: 'NotOwner',
reason: 'Not the owner', // reason: 'Not the owner',
}; // };
} // }
req.reply({ // req.reply({
data: { // data: {
updateSnapshot: { // updateSnapshot: {
snapshot, // snapshot,
}, // },
}, // },
}); // });
} // }
}); // });
}; //
// };
// wait for the specified amount of requests in the test, so they don't spill over to the next test // wait for the specified amount of requests in the test, so they don't spill over to the next test
const waitForNRequests = (n) => { const waitNTimes = (n) => {
for (let i = 0; i < n; i++) { for (let i = 0; i < n; i++) {
cy.wait('@graphqlRequest'); cy.wait('@graphqlRequest');
} }
}; };
describe('Snapshot', () => { describe('Snapshot', () => {
const operations = (isTeacher) => ({ const operations = isTeacher => ({
operations: { operations: {
UpdateSnapshot: { UpdateSnapshot: {
updateSnapshot: { updateSnapshot: {
@ -127,6 +129,7 @@ describe('Snapshot', () => {
}; };
} }
return result; return result;
}, },
}, },
}, },
@ -137,12 +140,10 @@ describe('Snapshot', () => {
CreateSnapshot: { CreateSnapshot: {
createSnapshot: { createSnapshot: {
snapshot: { snapshot: {
id: 'snapshot-id', id: '',
title: 'Mi Snapshot', title: '',
created: '2022-01-01', created: '',
creator: 'me', creator: '',
shared: false,
mine: true,
}, },
success: true, success: true,
}, },
@ -178,7 +179,6 @@ describe('Snapshot', () => {
}, },
SnapshotDetail: { SnapshotDetail: {
snapshot: { snapshot: {
title: 'Shared snapshot',
chapters: [], chapters: [],
module: {}, module: {},
}, },
@ -194,7 +194,6 @@ describe('Snapshot', () => {
beforeEach(() => { beforeEach(() => {
snapshotTitle = false; snapshotTitle = false;
deleteSuccess = false; deleteSuccess = false;
page = moduleWithSnapshots;
cy.setup(); cy.setup();
}); });
@ -202,7 +201,7 @@ describe('Snapshot', () => {
cy.mockGraphqlOps(operations(true)); cy.mockGraphqlOps(operations(true));
cy.visit('module/miteinander-reden/'); cy.visit('module/miteinander-reden/');
cy.getByDataCy('snapshot-menu').should('be.visible'); cy.getByDataCy('snapshot-menu').should('be.visible');
waitForNRequests(4); waitNTimes(4);
}); });
it('Menu is not visible for student', () => { it('Menu is not visible for student', () => {
@ -211,27 +210,19 @@ describe('Snapshot', () => {
cy.getByDataCy('module-title').should('be.visible'); cy.getByDataCy('module-title').should('be.visible');
cy.getByDataCy('snapshot-menu').should('not.exist'); cy.getByDataCy('snapshot-menu').should('not.exist');
waitForNRequests(3); waitNTimes(3);
}); });
it('Creates Snapshot', () => { it('Creates Snapshot', () => {
cy.mockGraphqlOps(operations(true)); cy.mockGraphqlOps(operations(true));
cy.visit('module/miteinander-reden/snapshots'); cy.visit('module/miteinander-reden/');
cy.getByDataCy('snapshot-list')
.should('exist')
.within(() => {
cy.get('.snapshots__snapshot').should('have.length', 1);
});
cy.getByDataCy('back-link').click();
cy.getByDataCy('module-snapshots-button').click(); cy.getByDataCy('module-snapshots-button').click();
cy.getByDataCy('create-snapshot-button').click(); cy.getByDataCy('create-snapshot-button').click();
cy.getByDataCy('show-all-snapshots-button').click(); cy.getByDataCy('show-all-snapshots-button').click();
cy.getByDataCy('snapshot-list') cy.getByDataCy('snapshot-list').should('exist').within(() => {
.should('exist') cy.get('.snapshots__snapshot').should('have.length', 1);
.within(() => {
cy.get('.snapshots__snapshot').should('have.length', 2);
}); });
waitForNRequests(7); waitNTimes(7);
}); });
it('Applies Snapshot', () => { it('Applies Snapshot', () => {
@ -243,7 +234,7 @@ describe('Snapshot', () => {
cy.getByDataCy('module-title').should('exist'); cy.getByDataCy('module-title').should('exist');
cy.getByDataCy('snapshot-header').should('not.exist'); cy.getByDataCy('snapshot-header').should('not.exist');
waitForNRequests(8); waitNTimes(9);
}); });
it('Renames Snapshot', () => { it('Renames Snapshot', () => {
@ -252,12 +243,12 @@ describe('Snapshot', () => {
snapshotTitle = newTitle; snapshotTitle = newTitle;
// mockUpdateSnapshot(newTitle); // mockUpdateSnapshot(newTitle);
cy.visit('module/miteinander-reden/snapshots'); cy.visit('module/miteinander-reden/snapshots');
cy.getByDataCy('snapshot-link').should('contain.text', 'Old Title'); cy.getByDataCy('snapshot-link').should('have.text', 'Old Title');
cy.getByDataCy('rename-snapshot-button').click(); cy.getByDataCy('rename-snapshot-button').click();
cy.getByDataCy('edit-name-input').clear().type(newTitle); cy.getByDataCy('edit-name-input').clear().type(newTitle);
cy.getByDataCy('modal-save-button').click(); cy.getByDataCy('modal-save-button').click();
cy.getByDataCy('snapshot-link').should('contain.text', 'New Title'); cy.getByDataCy('snapshot-link').should('have.text', 'New Title');
waitForNRequests(5); waitNTimes(5);
}); });
it('Deletes Snapshot', () => { it('Deletes Snapshot', () => {
@ -269,7 +260,7 @@ describe('Snapshot', () => {
cy.getByDataCy('delete-snapshot-button').click(); cy.getByDataCy('delete-snapshot-button').click();
cy.getByDataCy('modal-save-button').click(); cy.getByDataCy('modal-save-button').click();
cy.getByDataCy('snapshot-entry').should('have.length', 0); cy.getByDataCy('snapshot-entry').should('have.length', 0);
waitForNRequests(6); waitNTimes(6);
}); });
it('Displays the Snapshot list correcly', () => { it('Displays the Snapshot list correcly', () => {
@ -278,16 +269,13 @@ describe('Snapshot', () => {
cy.getByDataCy('snapshot-entry').should('have.length', 1); cy.getByDataCy('snapshot-entry').should('have.length', 1);
cy.getByDataCy('delete-snapshot-button').should('exist'); cy.getByDataCy('delete-snapshot-button').should('exist');
cy.getByDataCy('rename-snapshot-button').should('exist'); cy.getByDataCy('rename-snapshot-button').should('exist');
cy.getByDataCy('snapshot-link').should('contain.text', 'Old Title'); cy.getByDataCy('snapshot-link').should('have.text', 'Old Title');
cy.getByDataCy('team-snapshots-link').click(); cy.getByDataCy('team-snapshots-link').click();
cy.getByDataCy('snapshot-entry').should('have.length', 1); cy.getByDataCy('snapshot-entry').should('have.length', 1);
cy.getByDataCy('snapshot-link').should('contain.text', 'Shared snapshot'); cy.getByDataCy('snapshot-link').should('have.text', 'Shared snapshot');
cy.getByDataCy('delete-snapshot-button').should('not.exist'); cy.getByDataCy('delete-snapshot-button').should('not.exist');
cy.getByDataCy('rename-snapshot-button').should('not.exist'); cy.getByDataCy('rename-snapshot-button').should('not.exist');
cy.getByDataCy('snapshot-link').click(); waitNTimes(4);
cy.getByDataCy('module-title').should('contain.text', 'Shared snapshot');
waitForNRequests(5);
}); });
afterEach(() => {});
}); });

View File

@ -13,10 +13,9 @@ describe('Room Team Management - Read only', () => {
}, },
}, },
RoomsQuery: { RoomsQuery: {
rooms: [ rooms: [{
{
id: '', id: '',
slug: '', slug: 'some-room',
title: 'some room', title: 'some room',
entryCount: 3, entryCount: 3,
appearance: 'red', appearance: 'red',
@ -25,8 +24,7 @@ describe('Room Team Management - Read only', () => {
id: SELECTED_CLASS_ID, id: SELECTED_CLASS_ID,
name: 'bla', name: 'bla',
}, },
}, }],
],
}, },
}); });

View File

@ -27,6 +27,7 @@ describe('The Room Page (Teacher)', () => {
AddRoomEntry: { AddRoomEntry: {
addRoomEntry: { addRoomEntry: {
roomEntry: { roomEntry: {
slug: 'entry-slug',
title: entryTitle, title: entryTitle,
contents: [ contents: [
{ {

View File

@ -321,8 +321,9 @@ describe('Teacher Class Management', () => {
it('tries to create a new class with duplicate name', () => { it('tries to create a new class with duplicate name', () => {
const name = 'Hill Billy Valley'; const name = 'Hill Billy Valley';
const oldName = 'Some stupid class';
let selectedClass = teacher.selectedClass; let selectedClass = teacher.selectedClass;
selectedClass.name = 'Some stupid class'; selectedClass.name = oldName;
const schoolClasses = [teacher.selectedClass]; const schoolClasses = [teacher.selectedClass];
@ -337,10 +338,6 @@ describe('Teacher Class Management', () => {
MeQuery: () => ({ MeQuery: () => ({
me: me(), me: me(),
}), }),
WhateverNode() {
console.log('Through here');
return {};
},
MySchoolClassQuery: () => ({ MySchoolClassQuery: () => ({
me: me(), me: me(),
}), }),
@ -366,6 +363,7 @@ describe('Teacher Class Management', () => {
cy.visit('/me/my-class'); cy.visit('/me/my-class');
cy.get('h1').should('exist'); cy.get('h1').should('exist');
cy.getByDataCy('group-list-name').should('contain', oldName);
cy.get('[data-cy=header-user-widget]').within(() => { cy.get('[data-cy=header-user-widget]').within(() => {
cy.get('[data-cy=user-widget-avatar]').click(); cy.get('[data-cy=user-widget-avatar]').click();

View File

@ -1,21 +1,42 @@
module.exports = { module.exports = {
moduleFileExtensions: ['js', 'jsx', 'ts', 'json', 'vue'], moduleFileExtensions: [
'js',
'jsx',
'ts',
'json',
'vue',
],
transform: { transform: {
'\\.(gql|graphql)$': 'jest-transform-graphql', '\\.(gql|graphql)$': '@graphql-tools/jest-transform',
'^.+\\.js$': 'babel-jest', '^.+\\.js$': 'babel-jest',
'^.+\\.ts$': 'babel-jest', '^.+\\.ts$': 'babel-jest',
'^.+\\.vue$': '@vue/vue2-jest', '^.+\\.vue$': '@vue/vue3-jest',
'.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$': 'jest-transform-stub', '.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$': 'jest-transform-stub',
}, },
modulePaths: ['<rootDir>/src', '<rootDir>/node_modules'], modulePaths: [
transformIgnorePatterns: ['/node_modules/'], '<rootDir>/src',
'<rootDir>/node_modules',
],
transformIgnorePatterns: [
'/node_modules/',
],
moduleNameMapper: { moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1', '^@/(.*)$': '<rootDir>/src/$1',
'^gql/(.*)$': '<rootDir>/src/graphql/gql/$1', '^gql/(.*)$': '<rootDir>/src/graphql/gql/$1',
}, },
snapshotSerializers: ['<rootDir>/node_modules/jest-serializer-vue'], snapshotSerializers: [
'<rootDir>/node_modules/jest-serializer-vue',
],
testEnvironment: 'jsdom', testEnvironment: 'jsdom',
testMatch: ['**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)'], testEnvironmentOptions: {
testURL: 'http://localhost/', url: 'http://localhost/',
watchPlugins: ['jest-watch-typeahead/filename', 'jest-watch-typeahead/testname'], customExportConditions: ['node', 'node-addons'] // needed according to https://github.com/vuejs/vue-jest/issues/479
},
testMatch: [
'**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)',
],
watchPlugins: [
'jest-watch-typeahead/filename',
'jest-watch-typeahead/testname',
],
}; };

13200
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -30,11 +30,10 @@
"prettier:check": "prettier . --check" "prettier:check": "prettier . --check"
}, },
"dependencies": { "dependencies": {
"@apollo/client": "^3.5.8", "@apollo/client": "^3.5.10",
"@babel/core": "^7.16.7", "@babel/core": "^7.16.7",
"@babel/eslint-plugin": "^7.16.5", "@babel/eslint-plugin": "^7.16.5",
"@babel/plugin-transform-runtime": "^7.5.0", "@babel/plugin-transform-runtime": "^7.5.0",
"@babel/polyfill": "^7.4.4",
"@babel/preset-env": "^7.5.4", "@babel/preset-env": "^7.5.4",
"@babel/preset-stage-2": "^7.0.0", "@babel/preset-stage-2": "^7.0.0",
"@babel/preset-typescript": "^7.16.7", "@babel/preset-typescript": "^7.16.7",
@ -48,17 +47,21 @@
"@tiptap/extension-list-item": "^2.0.0-beta.20", "@tiptap/extension-list-item": "^2.0.0-beta.20",
"@tiptap/extension-paragraph": "^2.0.0-beta.23", "@tiptap/extension-paragraph": "^2.0.0-beta.23",
"@tiptap/extension-text": "^2.0.0-beta.15", "@tiptap/extension-text": "^2.0.0-beta.15",
"@tiptap/vue-2": "^2.0.0-beta.77", "@tiptap/vue-3": "^2.0.0-beta.90",
"@typescript-eslint/eslint-plugin": "^5.10.0", "@typescript-eslint/eslint-plugin": "^5.10.0",
"@typescript-eslint/parser": "^5.10.0", "@typescript-eslint/parser": "^5.10.0",
"@vue/test-utils": "^1.3.0", "@vue/apollo-option": "^4.0.0-alpha.16",
"@vue/vue2-jest": "^27.0.0", "@vue/compat": "3.2.30",
"@vue/compiler-sfc": "3.2.30",
"@vue/test-utils": "^2.2.0",
"@vue/vue3-jest": "^29.1.1",
"autoprefixer": "^10.4.12", "autoprefixer": "^10.4.12",
"babel-core": "^7.0.0-bridge.0", "babel-core": "^7.0.0-bridge.0",
"babel-jest": "^27.5.1", "babel-jest": "^29.2.2",
"babel-loader": "^8.0.6", "babel-loader": "^8.0.6",
"chalk": "^2.0.1", "chalk": "^2.0.1",
"copy-webpack-plugin": "^10.1.0", "copy-webpack-plugin": "^10.1.0",
"core-js": "^3.26.0",
"css-loader": "^3.6.0", "css-loader": "^3.6.0",
"css-minimizer-webpack-plugin": "^3.4.1", "css-minimizer-webpack-plugin": "^3.4.1",
"cy2": "^1.2.1", "cy2": "^1.2.1",
@ -76,7 +79,8 @@
"graphql-tag": "^2.10.1", "graphql-tag": "^2.10.1",
"graphql-tools": "^8.2.5", "graphql-tools": "^8.2.5",
"html-webpack-plugin": "^5.5.0", "html-webpack-plugin": "^5.5.0",
"jest": "^27.5.1", "jest": "^29.2.2",
"jest-environment-jsdom": "^29.2.2",
"jest-serializer-vue": "^2.0.2", "jest-serializer-vue": "^2.0.2",
"jest-transform-graphql": "^2.1.0", "jest-transform-graphql": "^2.1.0",
"jest-transform-stub": "^2.0.0", "jest-transform-stub": "^2.0.0",
@ -85,6 +89,7 @@
"loglevel": "^1.8.0", "loglevel": "^1.8.0",
"mini-css-extract-plugin": "^2.4.5", "mini-css-extract-plugin": "^2.4.5",
"mock-apollo-client": "^1.2.0", "mock-apollo-client": "^1.2.0",
"node-sass": "^7.0.3",
"ora": "^1.2.0", "ora": "^1.2.0",
"portfinder": "^1.0.13", "portfinder": "^1.0.13",
"postcss-import": "^15.0.0", "postcss-import": "^15.0.0",
@ -92,26 +97,24 @@
"postcss-url": "^10.1.3", "postcss-url": "^10.1.3",
"prettier": "2.8.2", "prettier": "2.8.2",
"rimraf": "^2.6.0", "rimraf": "^2.6.0",
"sass": "^1.56.1",
"sass-loader": "^12.6.0", "sass-loader": "^12.6.0",
"semver": "^5.3.0", "semver": "^5.3.0",
"shelljs": "^0.8.5", "shelljs": "^0.8.5",
"survey-knockout": "^1.9.41", "survey-core": "1.9.41",
"survey-knockout-ui": "1.9.41",
"ts-loader": "^8.3.0", "ts-loader": "^8.3.0",
"typescript": "^4.5.4", "typescript": "^4.5.4",
"uploadcare-widget": "^3.6.0", "uploadcare-widget": "^3.6.0",
"url-loader": "^4.1.1", "vee-validate": "^4.5.10",
"vee-validate": "^3.4.14", "vue": "3.2.30",
"vue": "^2.7.13", "vue-loader": "^16.8.3",
"vue-apollo": "^3.1.0",
"vue-loader": "^15.10.0",
"vue-matomo": "^4.1.0", "vue-matomo": "^4.1.0",
"vue-router": "^3.5.3", "vue-router": "^4.0.14",
"vue-scrollto": "^2.11.0", "vue-scrollto": "^2.20.0",
"vue-style-loader": "^3.0.1", "vue-style-loader": "^3.0.1",
"vue-template-compiler": "^2.7.13", "vue-vimeo-player": "^1.1.2",
"vue-vimeo-player": "^0.2.2", "vuejs3-logger": "1.0.0",
"vuex": "^3.0.1", "vuex": "4.0.1",
"webpack": "^5.67.0", "webpack": "^5.67.0",
"webpack-bundle-analyzer": "^4.5.0", "webpack-bundle-analyzer": "^4.5.0",
"webpack-cli": "^4.9.1", "webpack-cli": "^4.9.1",
@ -126,11 +129,5 @@
"> 1%", "> 1%",
"last 2 versions", "last 2 versions",
"not ie <= 8" "not ie <= 8"
], ]
"resolutions": {
"vue": "2.6.14"
},
"optionalDependencies": {
"fsevents": "^2.3.2"
}
} }

View File

@ -9,38 +9,54 @@
</template> </template>
<script> <script>
import { defineAsyncComponent } from 'vue';
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import ScrollUp from '@/components/ScrollUp'; import ScrollUp from '@/components/ScrollUp';
import ReadOnlyBanner from '@/components/ReadOnlyBanner'; import ReadOnlyBanner from '@/components/ReadOnlyBanner';
import modals from '@/components/modals'; import modals from '@/components/modals';
const NewContentBlockWizard = () => const NewContentBlockWizard = defineAsyncComponent(() =>
import(/* webpackChunkName: "content-forms" */ '@/components/content-block-form/NewContentBlockWizard'); import(/* webpackChunkName: "content-forms" */ '@/components/content-block-form/NewContentBlockWizard')
const EditContentBlockWizard = () => );
import(/* webpackChunkName: "content-forms" */ '@/components/content-block-form/EditContentBlockWizard'); const EditContentBlockWizard = defineAsyncComponent(() =>
const EditRoomEntryWizard = () => import(/* webpackChunkName: "content-forms" */ '@/components/content-block-form/EditContentBlockWizard')
import(/* webpackChunkName: "content-forms" */ '@/components/rooms/room-entries/EditRoomEntryWizard'); );
const NewProjectEntryWizard = () => const EditRoomEntryWizard = defineAsyncComponent(() =>
import(/* webpackChunkName: "content-forms" */ '@/components/portfolio/NewProjectEntryWizard'); import(/* webpackChunkName: "content-forms" */ '@/components/rooms/room-entries/EditRoomEntryWizard')
const EditProjectEntryWizard = () => );
import(/* webpackChunkName: "content-forms" */ '@/components/portfolio/EditProjectEntryWizard'); const NewProjectEntryWizard = defineAsyncComponent(() =>
const NewObjectiveWizard = () => import(/* webpackChunkName: "content-forms" */ '@/components/portfolio/NewProjectEntryWizard')
import(/* webpackChunkName: "content-forms" */ '@/components/objective-groups/NewObjectiveWizard'); );
const NewNoteWizard = () => import(/* webpackChunkName: "content-forms" */ '@/components/notes/NewNoteWizard'); const EditProjectEntryWizard = defineAsyncComponent(() =>
const EditNoteWizard = () => import(/* webpackChunkName: "content-forms" */ '@/components/notes/EditNoteWizard'); import(/* webpackChunkName: "content-forms" */ '@/components/portfolio/EditProjectEntryWizard')
const EditClassNameWizard = () => );
import(/* webpackChunkName: "content-forms" */ '@/components/school-class/EditClassNameWizard'); const NewObjectiveWizard = defineAsyncComponent(() =>
const EditTeamNameWizard = () => import(/* webpackChunkName: "content-forms" */ '@/components/objective-groups/NewObjectiveWizard')
import(/* webpackChunkName: "content-forms" */ '@/components/profile/EditTeamNameWizard'); );
const EditSnapshotTitleWizard = () => const NewNoteWizard = defineAsyncComponent(() =>
import(/* webpackChunkName: "content-forms" */ '@/components/snapshots/EditSnapshotTitleWizard'); import(/* webpackChunkName: "content-forms" */ '@/components/notes/NewNoteWizard')
const DefaultLayout = () => import(/* webpackChunkName: "layouts" */ '@/layouts/DefaultLayout'); );
const SimpleLayout = () => import(/* webpackChunkName: "layouts" */ '@/layouts/SimpleLayout'); const EditNoteWizard = defineAsyncComponent(() =>
const FullScreenLayout = () => import(/* webpackChunkName: "layouts" */ '@/layouts/FullScreenLayout'); import(/* webpackChunkName: "content-forms" */ '@/components/notes/EditNoteWizard')
const PublicLayout = () => import(/* webpackChunkName: "layouts" */ '@/layouts/PublicLayout'); );
const BlankLayout = () => import(/* webpackChunkName: "layouts" */ '@/layouts/BlankLayout'); const EditClassNameWizard = defineAsyncComponent(() =>
const SplitLayout = () => import(/* webpackChunkName: "layouts" */ '@/layouts/SplitLayout'); import(/* webpackChunkName: "content-forms" */ '@/components/school-class/EditClassNameWizard')
);
const EditTeamNameWizard = defineAsyncComponent(() =>
import(/* webpackChunkName: "content-forms" */ '@/components/profile/EditTeamNameWizard')
);
const EditSnapshotTitleWizard = defineAsyncComponent(() =>
import(/* webpackChunkName: "content-forms" */ '@/components/snapshots/EditSnapshotTitleWizard')
);
const DefaultLayout = defineAsyncComponent(() => import(/* webpackChunkName: "layouts" */ '@/layouts/DefaultLayout'));
const SimpleLayout = defineAsyncComponent(() => import(/* webpackChunkName: "layouts" */ '@/layouts/SimpleLayout'));
const FullScreenLayout = defineAsyncComponent(() =>
import(/* webpackChunkName: "layouts" */ '@/layouts/FullScreenLayout')
);
const PublicLayout = defineAsyncComponent(() => import(/* webpackChunkName: "layouts" */ '@/layouts/PublicLayout'));
const BlankLayout = defineAsyncComponent(() => import(/* webpackChunkName: "layouts" */ '@/layouts/BlankLayout'));
const SplitLayout = defineAsyncComponent(() => import(/* webpackChunkName: "layouts" */ '@/layouts/SplitLayout'));
export default { export default {
name: 'App', name: 'App',

View File

@ -1,30 +1,36 @@
<template> <template>
<div class="add-content"> <div class="add-content">
<a class="add-content__button" @click="addContent"> <a
class="add-content__button"
@click="addContent"
>
<add-pointer class="add-content__icon" /> <add-pointer class="add-content__icon" />
</a> </a>
</div> </div>
</template> </template>
<script> <script>
import { CREATE_CONTENT_BLOCK_AFTER_PAGE, CREATE_CONTENT_BLOCK_UNDER_PARENT_PAGE } from '@/router/module.names'; import {
CREATE_CONTENT_BLOCK_AFTER_PAGE,
CREATE_CONTENT_BLOCK_UNDER_PARENT_PAGE,
} from '@/router/module.names';
import {defineAsyncComponent} from 'vue';
const AddPointer = () => import(/* webpackChunkName: "icons" */ '@/components/icons/AddPointer'); const AddPointer = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/AddPointer'));
export default { export default {
props: { props: {
where: { where: {
type: Object, type: Object,
validator(prop) { validator(prop) {
return ( return Object.prototype.hasOwnProperty.call(prop, 'after' )
Object.prototype.hasOwnProperty.call(prop, 'after') || Object.prototype.hasOwnProperty.call(prop, 'parent') || Object.prototype.hasOwnProperty.call(prop, 'parent');
); }
},
}, },
}, },
components: { components: {
AddPointer, AddPointer
}, },
computed: { computed: {
@ -39,8 +45,9 @@ export default {
}, },
slug() { slug() {
return this.$route.params.slug; return this.$route.params.slug;
}
}, },
},
methods: { methods: {
addContent() { addContent() {
@ -53,26 +60,26 @@ export default {
name: CREATE_CONTENT_BLOCK_AFTER_PAGE, name: CREATE_CONTENT_BLOCK_AFTER_PAGE,
params: { params: {
after: this.after.id, after: this.after.id,
slug: this.slug, slug: this.slug
}, }
}; };
} else { } else {
route = { route = {
name: CREATE_CONTENT_BLOCK_UNDER_PARENT_PAGE, name: CREATE_CONTENT_BLOCK_UNDER_PARENT_PAGE,
params: { params: {
parent: this.parent.id, parent: this.parent.id
}, }
}; };
} }
this.$router.push(route); this.$router.push(route);
} }
}, }
}, }
}; };
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import '~styles/helpers'; @import "~styles/helpers";
.add-content { .add-content {
display: none; display: none;

View File

@ -1,23 +1,27 @@
<template> <template>
<div class="add-content-element" @click="$emit('add-element', index)"> <div
class="add-content-element"
@click="$emit('add-element', index)"
>
<add-icon class="add-content-element__icon" /> <add-icon class="add-content-element__icon" />
</div> </div>
</template> </template>
<script> <script>
const AddIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/AddIcon'); import {defineAsyncComponent} from 'vue';
const AddIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/AddIcon'));
export default { export default {
props: ['index'], props: ['index'],
components: { components: {
AddIcon, AddIcon
}, }
}; };
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import '@/styles/_variables.scss'; @import "@/styles/_variables.scss";
.add-content-element { .add-content-element {
display: flex; display: flex;

View File

@ -11,27 +11,27 @@
</template> </template>
<script> <script>
const AddIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/AddIcon.vue'); import {defineAsyncComponent} from 'vue';
const AddIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/AddIcon.vue'));
export default { export default {
props: { props: {
route: { route: {
type: String, type: String,
default: null, default: null
}, },
reverse: { reverse: { // use reverse colors
// use reverse colors
type: Boolean, type: Boolean,
default: false, default: false
}, },
click: { click: {
type: Function, type: Function,
default: null, default: null
}, }
}, },
components: { components: {
AddIcon, AddIcon
}, },
computed: { computed: {
@ -40,19 +40,17 @@ export default {
return this.route ? 'router-link' : 'a'; return this.route ? 'router-link' : 'a';
}, },
properties() { properties() {
return this.route return this.route ? {
? {
to: this.route, to: this.route,
tag: 'div', tag: 'div'
} : {};
} }
: {};
},
}, },
}; };
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import '~styles/helpers'; @import "~styles/helpers";
.add-widget { .add-widget {
display: none; display: none;

View File

@ -1,5 +1,8 @@
<template> <template>
<router-link :to="to" data-cy="back-link" class="sub-navigation-item back-link"> <router-link
:to="to"
class="sub-navigation-item back-link"
>
<chevron-left class="back-link__icon sub-navigation-item__icon" /> <chevron-left class="back-link__icon sub-navigation-item__icon" />
{{ fullTitle }} {{ fullTitle }}
</router-link> </router-link>
@ -9,8 +12,9 @@
import { MODULE_PAGE } from '@/router/module.names'; import { MODULE_PAGE } from '@/router/module.names';
import { ROOMS_PAGE } from '@/router/room.names'; import { ROOMS_PAGE } from '@/router/room.names';
import { PROJECTS_PAGE } from '@/router/portfolio.names'; import { PROJECTS_PAGE } from '@/router/portfolio.names';
import {defineAsyncComponent} from 'vue';
const ChevronLeft = () => import(/* webpackChunkName: "icons" */ '@/components/icons/ChevronLeft'); const ChevronLeft = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/ChevronLeft'));
export default { export default {
props: { props: {
@ -36,7 +40,11 @@ export default {
to() { to() {
switch (this.type) { switch (this.type) {
case 'topic': case 'topic':
if (this.slug) {
return {name: 'topic', params: {topicSlug: this.slug}}; return {name: 'topic', params: {topicSlug: this.slug}};
} else {
return {};
}
case 'module': case 'module':
return {name: MODULE_PAGE}; return {name: MODULE_PAGE};
case 'portfolio': case 'portfolio':

View File

@ -8,47 +8,54 @@
:key="index" :key="index"
@click="$emit('input', color.name)" @click="$emit('input', color.name)"
> >
<div :class="'color-chooser__color--' + color.name" class="color-chooser__color"> <div
<tick class="color-chooser__selected-icon" v-if="selectedColor === color.name" /> :class="'color-chooser__color--' + color.name"
class="color-chooser__color"
>
<tick
class="color-chooser__selected-icon"
v-if="selectedColor === color.name"
/>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
const Tick = () => import(/* webpackChunkName: "icons" */ '@/components/icons/Tick'); import {defineAsyncComponent} from 'vue';
const Tick = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/Tick'));
export default { export default {
props: ['selectedColor'], props: ['selectedColor'],
components: { components: {
Tick, Tick
}, },
data() { data() {
return { return {
colors: [ colors: [
{ {
name: 'yellow', name: 'yellow'
}, },
{ {
name: 'blue', name: 'blue'
}, },
{ {
name: 'red', name: 'red'
}, },
{ {
name: 'green', name: 'green'
}, }
], ]
}; };
}, },
}; };
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import '@/styles/_variables.scss'; @import "@/styles/_variables.scss";
@import '@/styles/_mixins.scss'; @import "@/styles/_mixins.scss";
.color-chooser { .color-chooser {
display: flex; display: flex;
@ -75,7 +82,7 @@ export default {
display: flex; display: flex;
justify-content: center; justify-content: center;
@supports (display: grid) { @supports (display: grid) {
display: grid; display: grid
} }
justify-items: center; justify-items: center;
align-items: center; align-items: center;

View File

@ -3,18 +3,36 @@
:class="{'hideable-element--greyed-out': hidden}" :class="{'hideable-element--greyed-out': hidden}"
class="content-block__container hideable-element content-list__parent" class="content-block__container hideable-element content-list__parent"
> >
<div :class="specialClass" :style="instrumentStyle" class="content-block" data-cy="content-block"> <div
<div class="block-actions" v-if="canEditModule && !isInstrumentBlock"> :class="specialClass"
<user-widget v-bind="me" class="block-actions__user-widget content-block__user-widget" v-if="isMine" /> :style="instrumentStyle"
class="content-block"
data-cy="content-block"
>
<div
class="block-actions"
v-if="canEditModule && !isInstrumentBlock"
>
<user-widget
v-bind="me"
class="block-actions__user-widget content-block__user-widget"
v-if="isMine"
/>
<more-options-widget> <more-options-widget>
<li class="popover-links__link" v-if="!isInstrumentBlock"> <li
class="popover-links__link"
v-if="!isInstrumentBlock"
>
<popover-link <popover-link
data-cy="duplicate-content-block-link" data-cy="duplicate-content-block-link"
text="Duplizieren" text="Duplizieren"
@link-action="duplicateContentBlock(contentBlock)" @link-action="duplicateContentBlock(contentBlock)"
/> />
</li> </li>
<li class="popover-links__link" v-if="isMine"> <li
class="popover-links__link"
v-if="isMine"
>
<popover-link <popover-link
data-cy="delete-content-block-link" data-cy="delete-content-block-link"
text="Löschen" text="Löschen"
@ -22,13 +40,22 @@
/> />
</li> </li>
<li class="popover-links__link" v-if="isMine"> <li
<popover-link text="Bearbeiten" @link-action="editContentBlock(contentBlock)" /> class="popover-links__link"
v-if="isMine"
>
<popover-link
text="Bearbeiten"
@link-action="editContentBlock(contentBlock)"
/>
</li> </li>
</more-options-widget> </more-options-widget>
</div> </div>
<div class="content-block__visibility"> <div class="content-block__visibility">
<visibility-action :block="contentBlock" v-if="canEditModule" /> <visibility-action
:block="contentBlock"
v-if="canEditModule"
/>
</div> </div>
<h3 <h3
@ -39,7 +66,10 @@
> >
{{ instrumentLabel }} {{ instrumentLabel }}
</h3> </h3>
<h4 class="content-block__title" v-if="!contentBlock.indent"> <h4
class="content-block__title"
v-if="!contentBlock.indent"
>
{{ contentBlock.title }} {{ contentBlock.title }}
</h4> </h4>
@ -55,7 +85,10 @@
/> />
</div> </div>
<add-content-button :where="{ after: contentBlock }" v-if="canEditModule" /> <add-content-button
:where="{after: contentBlock}"
v-if="canEditModule"
/>
</div> </div>
</template> </template>
@ -76,10 +109,11 @@ import { CONTENT_TYPE } from '@/consts/types';
import PopoverLink from '@/components/ui/PopoverLink'; import PopoverLink from '@/components/ui/PopoverLink';
import {insertAtIndex, removeAtIndex} from '@/graphql/immutable-operations'; import {insertAtIndex, removeAtIndex} from '@/graphql/immutable-operations';
import {EDIT_CONTENT_BLOCK_PAGE} from '@/router/module.names'; import {EDIT_CONTENT_BLOCK_PAGE} from '@/router/module.names';
import {defineAsyncComponent} from 'vue';
import {instrumentCategory} from '@/helpers/instrumentType'; import {instrumentCategory} from '@/helpers/instrumentType';
const ContentComponent = () => const ContentComponent = defineAsyncComponent(() => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/ContentComponent'));
import(/* webpackChunkName: "content-components" */ '@/components/content-blocks/ContentComponent');
export default { export default {
name: 'ContentBlock', name: 'ContentBlock',
@ -123,15 +157,14 @@ export default {
instrumentStyle() { instrumentStyle() {
if (this.isInstrumentBlock) { if (this.isInstrumentBlock) {
return { return {
backgroundColor: this.contentBlock.instrumentCategory.background, backgroundColor: this.contentBlock.instrumentCategory.background
}; };
} }
return {}; return {};
}, },
instrumentLabel() { instrumentLabel() {
const contentType = this.contentBlock.type.toLowerCase(); const contentType = this.contentBlock.type.toLowerCase();
if (contentType.startsWith('base')) { if (contentType.startsWith('base')) { // all legacy instruments start with `base`
// all legacy instruments start with `base`
return instrumentCategory(contentType); return instrumentCategory(contentType);
} }
if (this.isInstrumentBlock) { if (this.isInstrumentBlock) {
@ -143,7 +176,7 @@ export default {
instrumentLabelStyle() { instrumentLabelStyle() {
if (this.isInstrumentBlock) { if (this.isInstrumentBlock) {
return { return {
color: this.contentBlock.instrumentCategory.foreground, color: this.contentBlock.instrumentCategory.foreground
}; };
} }
return {}; return {};
@ -174,8 +207,7 @@ export default {
// collect content_list_items // collect content_list_items
if (content.type === 'content_list_item') { if (content.type === 'content_list_item') {
contentList = [...contentList, content]; contentList = [...contentList, content];
if (index === this.contentBlock.contents.length - 1) { if (index === this.contentBlock.contents.length - 1) { // content is last element of contents array
// content is last element of contents array
let updatedContent = [...newContents, ...this.createContentListOrBlocks(contentList)]; let updatedContent = [...newContents, ...this.createContentListOrBlocks(contentList)];
return updatedContent; return updatedContent;
} }
@ -217,21 +249,14 @@ export default {
id, id,
}, },
}, },
update( update(store, {data: {duplicateContentBlock: {contentBlock}}}) {
store,
{
data: {
duplicateContentBlock: { contentBlock },
},
}
) {
if (contentBlock) { if (contentBlock) {
const query = CHAPTER_QUERY; const query = CHAPTER_QUERY;
const variables = { const variables = {
id: parent.id, id: parent.id,
}; };
const {chapter} = store.readQuery({query, variables}); const {chapter} = store.readQuery({query, variables});
const index = chapter.contentBlocks.findIndex((contentBlock) => contentBlock.id === id); const index = chapter.contentBlocks.findIndex(contentBlock => contentBlock.id === id);
const contentBlocks = insertAtIndex(chapter.contentBlocks, index, contentBlock); const contentBlocks = insertAtIndex(chapter.contentBlocks, index, contentBlock);
const data = { const data = {
chapter: { chapter: {
@ -243,6 +268,7 @@ export default {
} }
}, },
}); });
}, },
editContentBlock(contentBlock) { editContentBlock(contentBlock) {
const route = { const route = {
@ -254,9 +280,7 @@ export default {
this.$router.push(route); this.$router.push(route);
}, },
deleteContentBlock(contentBlock) { deleteContentBlock(contentBlock) {
this.$modal this.$modal.open('confirm').then(() => {
.open('confirm')
.then(() => {
this.doDeleteContentBlock(contentBlock); this.doDeleteContentBlock(contentBlock);
}) })
.catch(); .catch();
@ -271,21 +295,14 @@ export default {
id, id,
}, },
}, },
update( update(store, {data: {deleteContentBlock: {success}}}) {
store,
{
data: {
deleteContentBlock: { success },
},
}
) {
if (success) { if (success) {
const query = CHAPTER_QUERY; const query = CHAPTER_QUERY;
const variables = { const variables = {
id: parent.id, id: parent.id,
}; };
const {chapter} = store.readQuery({query, variables}); const {chapter} = store.readQuery({query, variables});
const index = chapter.contentBlocks.findIndex((contentBlock) => contentBlock.id === id); const index = chapter.contentBlocks.findIndex(contentBlock => contentBlock.id === id);
const contentBlocks = removeAtIndex(chapter.contentBlocks, index); const contentBlocks = removeAtIndex(chapter.contentBlocks, index);
const data = { const data = {
chapter: { chapter: {
@ -299,20 +316,18 @@ export default {
}); });
}, },
createContentListOrBlocks(contentList) { createContentListOrBlocks(contentList) {
return [ return [{
{
type: 'content_list', type: 'content_list',
contents: contentList, contents: contentList,
id: contentList[0].id, id: contentList[0].id,
}, }];
];
}, },
}, },
}; };
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import '~styles/helpers'; @import "~styles/helpers";
.content-block { .content-block {
margin-bottom: $section-spacing; margin-bottom: $section-spacing;
@ -383,7 +398,7 @@ export default {
@include content-box-base; @include content-box-base;
} }
:deep(p) { /deep/ p {
line-height: 1.5; line-height: 1.5;
margin-bottom: 1em; margin-bottom: 1em;
@ -392,7 +407,7 @@ export default {
} }
} }
:deep(.text-block) { /deep/ .text-block {
ul { ul {
@include list-parent; @include list-parent;
} }
@ -402,5 +417,6 @@ export default {
line-height: 1.5; line-height: 1.5;
} }
} }
} }
</style> </style>

View File

@ -1,21 +1,23 @@
<template> <template>
<modal :fullscreen="true"> <modal :fullscreen="true">
<component :value="value" :is="type" /> <component
:value="value"
:is="type"
/>
</modal> </modal>
</template> </template>
<script> <script>
import Modal from '@/components/Modal'; import Modal from '@/components/Modal';
const InfogramBlock = () => import {defineAsyncComponent} from 'vue';
import(/* webpackChunkName: "content-components" */ '@/components/content-blocks/InfogramBlock'); const InfogramBlock = defineAsyncComponent(() => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/InfogramBlock'));
const GeniallyBlock = () => const GeniallyBlock = defineAsyncComponent(() => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/GeniallyBlock'));
import(/* webpackChunkName: "content-components" */ '@/components/content-blocks/GeniallyBlock');
export default { export default {
components: { components: {
Modal, Modal,
InfogramBlock, InfogramBlock,
GeniallyBlock, GeniallyBlock
}, },
computed: { computed: {
@ -27,9 +29,9 @@ export default {
}, },
value() { value() {
return { return {
id: this.id, id: this.id
}; };
}, }
}, }
}; };
</script> </script>

View File

@ -1,15 +1,28 @@
<template> <template>
<header class="header-bar"> <header class="header-bar">
<a class="header-bar__sidebar-link" data-cy="open-sidebar-link" @click.stop="openSidebar('navigation')"> <a
class="header-bar__sidebar-link"
data-cy="open-sidebar-link"
@click.stop="openSidebar('navigation')"
>
<hamburger class="header-bar__sidebar-icon" /> <hamburger class="header-bar__sidebar-icon" />
</a> </a>
<content-navigation class="header-bar__content-navigation" /> <content-navigation class="header-bar__content-navigation" />
<div class="user-header"> <div class="user-header">
<a class="user-header__sidebar-link"> <a
<current-class class="user-header__current-class" @click.native.stop="openSidebar('profile')" /> class="user-header__sidebar-link"
>
<current-class
class="user-header__current-class"
@click="openSidebar('profile')"
/>
</a> </a>
<user-widget v-bind="me" data-cy="header-user-widget" @click.native.stop="openSidebar('profile')" /> <user-widget
:avatar-url="me.avatarUrl"
data-cy="header-user-widget"
@click="openSidebar('profile')"
/>
</div> </div>
</header> </header>
</template> </template>
@ -21,8 +34,9 @@ import CurrentClass from '@/components/school-class/CurrentClass';
import openSidebar from '@/mixins/open-sidebar'; import openSidebar from '@/mixins/open-sidebar';
import me from '@/mixins/me'; import me from '@/mixins/me';
import {defineAsyncComponent} from 'vue';
const Hamburger = () => import(/* webpackChunkName: "icons" */ '@/components/icons/Hamburger'); const Hamburger = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/Hamburger'));
export default { export default {
mixins: [openSidebar, me], mixins: [openSidebar, me],
@ -37,7 +51,7 @@ export default {
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import '~styles/helpers'; @import "~styles/helpers";
.header-bar { .header-bar {
display: flex; display: flex;

View File

@ -10,20 +10,21 @@
</template> </template>
<script> <script>
const InfoIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/InfoIcon'); import {defineAsyncComponent} from 'vue';
const InfoIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/InfoIcon'));
export default { export default {
props: ['text'], props: ['text'],
components: { components: {
InfoIcon, InfoIcon
}, }
}; };
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import '@/styles/_variables.scss'; @import "@/styles/_variables.scss";
@import '@/styles/_mixins.scss'; @import "@/styles/_mixins.scss";
.helpful-tooltip { .helpful-tooltip {
position: relative; position: relative;
@ -65,6 +66,7 @@ export default {
height: 10px; height: 10px;
transform: rotate(-45deg) translateY(-50%); transform: rotate(-45deg) translateY(-50%);
} }
} }
&:hover &__tooltip { &:hover &__tooltip {

View File

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

View File

@ -4,11 +4,17 @@
<hamburger class="mobile-header__hamburger" /> <hamburger class="mobile-header__hamburger" />
</a> </a>
<router-link to="/" data-cy="mobile-home-link"> <router-link
to="/"
data-cy="mobile-home-link"
>
<logo /> <logo />
</router-link> </router-link>
<user-widget v-bind="me" @click.native.stop="openSidebar('profile')" /> <user-widget
v-bind="me"
@click.stop="openSidebar('profile')"
/>
</div> </div>
</template> </template>
@ -17,9 +23,10 @@ import UserWidget from '@/components/UserWidget';
import me from '@/mixins/me'; import me from '@/mixins/me';
import openSidebar from '@/mixins/open-sidebar'; import openSidebar from '@/mixins/open-sidebar';
import {defineAsyncComponent} from 'vue';
const Logo = () => import(/* webpackChunkName: "icons" */ '@/components/icons/Logo'); const Logo = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/Logo'));
const Hamburger = () => import(/* webpackChunkName: "icons" */ '@/components/icons/Hamburger'); const Hamburger = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/Hamburger'));
export default { export default {
mixins: [me, openSidebar], mixins: [me, openSidebar],
@ -39,7 +46,7 @@ export default {
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import '~styles/helpers'; @import "~styles/helpers";
.mobile-header { .mobile-header {
justify-content: space-between; justify-content: space-between;

View File

@ -1,11 +1,7 @@
<template> <template>
<div class="modal__backdrop"> <div class="modal__backdrop">
<div <div
:class="{ :class="{'modal--hide-header': hideHeader || fullscreen, 'modal--fullscreen': fullscreen, 'modal--small': small}"
'modal--hide-header': hideHeader || fullscreen,
'modal--fullscreen': fullscreen,
'modal--small': small,
}"
class="modal" class="modal"
> >
<div class="modal__header"> <div class="modal__header">
@ -13,14 +9,20 @@
</div> </div>
<div class="modal__body"> <div class="modal__body">
<slot /> <slot />
<div class="modal__close-button" @click="hideModal"> <div
class="modal__close-button"
@click="hideModal"
>
<cross class="modal__close-icon" /> <cross class="modal__close-icon" />
</div> </div>
</div> </div>
<div class="modal__footer"> <div class="modal__footer">
<slot name="footer"> <slot name="footer">
<!--<a class="button button&#45;&#45;active">Speichern</a>--> <!--<a class="button button&#45;&#45;active">Speichern</a>-->
<a class="button" @click="hideModal">Abbrechen</a> <a
class="button"
@click="hideModal"
>Abbrechen</a>
</slot> </slot>
</div> </div>
</div> </div>
@ -28,38 +30,39 @@
</template> </template>
<script> <script>
const Cross = () => import(/* webpackChunkName: "icons" */ '@/components/icons/CrossIcon'); import {defineAsyncComponent} from 'vue';
const Cross = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/CrossIcon'));
export default { export default {
props: { props: {
hideHeader: { hideHeader: {
type: Boolean, type: Boolean,
default: false, default: false
}, },
fullscreen: { fullscreen: {
type: Boolean, type: Boolean,
default: false, default: false
}, },
small: { small: {
type: Boolean, type: Boolean,
default: false, default: false
}, }
}, },
components: { components: {
Cross, Cross
}, },
methods: { methods: {
hideModal() { hideModal() {
this.$store.dispatch('hideModal'); this.$store.dispatch('hideModal');
}, }
}, }
}; };
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import '@/styles/_variables.scss'; @import "@/styles/_variables.scss";
.modal { .modal {
align-self: center; align-self: center;
@ -75,7 +78,7 @@ export default {
display: grid; display: grid;
} }
grid-template-rows: auto 1fr 65px; grid-template-rows: auto 1fr 65px;
grid-template-areas: 'header' 'body' 'footer'; grid-template-areas: "header" "body" "footer";
-ms-grid-rows: auto 1fr 65px; -ms-grid-rows: auto 1fr 65px;
position: relative; position: relative;
@ -133,7 +136,7 @@ export default {
&--hide-header { &--hide-header {
grid-template-rows: 1fr 65px; grid-template-rows: 1fr 65px;
grid-template-areas: 'body' 'footer'; grid-template-areas: "body" "footer";
#{$parent}__header { #{$parent}__header {
display: none; display: none;
@ -142,6 +145,7 @@ export default {
#{$parent}__body { #{$parent}__body {
padding: $default-padding; padding: $default-padding;
} }
} }
&--fullscreen { &--fullscreen {
@ -149,7 +153,7 @@ export default {
height: auto; height: auto;
grid-template-rows: 1fr; grid-template-rows: 1fr;
-ms-grid-rows: 1fr; -ms-grid-rows: 1fr;
grid-template-areas: 'body'; grid-template-areas: "body";
overflow: hidden; overflow: hidden;
#{$parent}__footer { #{$parent}__footer {

View File

@ -1,9 +1,17 @@
<template> <template>
<div class="more-options"> <div class="more-options">
<a class="more-options__more-link" data-cy="more-options-link" @click.stop="showMenu = !showMenu"> <a
class="more-options__more-link"
data-cy="more-options-link"
@click.stop="showMenu = !showMenu"
>
<ellipses class="more-options__ellipses" /> <ellipses class="more-options__ellipses" />
</a> </a>
<widget-popover class="more-options__popover" v-if="showMenu" @hide-me="showMenu = false"> <widget-popover
class="more-options__popover"
v-if="showMenu"
@hide-me="showMenu = false"
>
<slot /> <slot />
</widget-popover> </widget-popover>
</div> </div>
@ -11,25 +19,26 @@
<script> <script>
import WidgetPopover from '@/components/ui/WidgetPopover'; import WidgetPopover from '@/components/ui/WidgetPopover';
import {defineAsyncComponent} from 'vue';
const Ellipses = () => import(/* webpackChunkName: "icons" */ '@/components/icons/Ellipses.vue'); const Ellipses = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/Ellipses.vue'));
export default { export default {
components: { components: {
WidgetPopover, WidgetPopover,
Ellipses, Ellipses
}, },
data() { data() {
return { return {
showMenu: false, showMenu: false
}; };
}, }
}; };
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import '~styles/helpers'; @import "~styles/helpers";
.more-options { .more-options {
display: flex; display: flex;

View File

@ -1,22 +1,27 @@
<template> <template>
<transition name="fade"> <transition name="fade">
<a class="scroll-up" v-if="scroll > 200" @click="scrollTop"> <a
class="scroll-up"
v-if="scroll>200"
@click="scrollTop"
>
<arrow-up class="scroll-up__icon" /> <arrow-up class="scroll-up__icon" />
</a> </a>
</transition> </transition>
</template> </template>
<script> <script>
const ArrowUp = () => import(/* webpackChunkName: "icons" */ '@/components/icons/ArrowUp'); import {defineAsyncComponent} from 'vue';
const ArrowUp = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/ArrowUp'));
export default { export default {
components: { components: {
ArrowUp, ArrowUp
}, },
data() { data() {
return { return {
scroll: 0, scroll: 0
}; };
}, },
@ -27,14 +32,14 @@ export default {
}; };
}, },
destroyed() { unmounted() {
document.body.onscroll = null; document.body.onscroll = null;
}, },
methods: { methods: {
scrollTop() { scrollTop() {
document.scrollingElement.scrollTop = 0; document.scrollingElement.scrollTop = 0;
}, }
}, },
}; };
</script> </script>
@ -60,14 +65,15 @@ export default {
height: 50px; height: 50px;
fill: $color-brand; fill: $color-brand;
} }
} }
.fade-enter-active, .fade-enter-active, .fade-leave-active {
.fade-leave-active { transition: opacity .3s;
transition: opacity 0.3s;
} }
.fade-enter, .fade-leave-to /* .fade-leave-active below version 2.1.8 */ { .fade-enter-from, .fade-leave-to /* .fade-leave-active below version 2.1.8 */
{
opacity: 0; opacity: 0;
} }
</style> </style>

View File

@ -4,14 +4,14 @@
{{ name }} {{ name }}
</div> </div>
<div class="student-submission__entry entry"> <div class="student-submission__entry entry">
<p>{{ submission.text | trimToLength(50) }}</p> <p>{{ text }}</p>
<p class="entry__document" v-if="submission.document && submission.document.length > 0"> <p class="entry__document" v-if="submission.document && submission.document.length > 0">
<student-submission-document :document="submission.document" class="entry-document" /> <student-submission-document :document="submission.document" class="entry-document" />
</p> </p>
</div> </div>
<div class="student-submission__feedback entry" v-if="submission.submissionFeedback"> <div class="student-submission__feedback entry" v-if="submission.submissionFeedback">
<p :class="{ 'entry__text--final': submission.submissionFeedback.final }" class="entry__text"> <p :class="{ 'entry__text--final': submission.submissionFeedback.final }" class="entry__text">
{{ submission.submissionFeedback.text | trimToLength(50) }} {{ feedback }}
</p> </p>
</div> </div>
</div> </div>
@ -25,7 +25,21 @@ export default {
components: { components: {
StudentSubmissionDocument, StudentSubmissionDocument,
}, },
filters: {
computed: {
text() {
return this.trimToLength(this.submission.text, 50);
},
feedback() {
return this.trimToLength(this.submission.submissionFeedback.text, 50);
},
name() {
return this.submission && this.submission.student
? `${this.submission.student.firstName} ${this.submission.student.lastName}`
: '';
},
},
methods: {
trimToLength: function (text, numberOfChars) { trimToLength: function (text, numberOfChars) {
if (!text) { if (!text) {
return ''; return '';
@ -40,14 +54,6 @@ export default {
return `${text.substring(0, index)}`; return `${text.substring(0, index)}`;
}, },
}, },
computed: {
name() {
return this.submission && this.submission.student
? `${this.submission.student.firstName} ${this.submission.student.lastName}`
: '';
},
},
}; };
</script> </script>

View File

@ -1,14 +1,18 @@
<template> <template>
<div class="submission-document"> <div class="submission-document">
<p class="submission-document__content content" v-if="document && document.length > 0"> <p
class="submission-document__content content"
v-if="document && document.length > 0"
>
<document-icon class="content__icon" /><span class="content__text">{{ filename }}</span> <document-icon class="content__icon" /><span class="content__text">{{ filename }}</span>
</p> </p>
</div> </div>
</template> </template>
<script> <script>
import {defineAsyncComponent} from 'vue';
import filenameFromUrl from '@/helpers/urls'; import filenameFromUrl from '@/helpers/urls';
const DocumentIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/DocumentIcon'); const DocumentIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/DocumentIcon'));
export default { export default {
name: 'StudentSubmissionDocument', name: 'StudentSubmissionDocument',
@ -18,7 +22,7 @@ export default {
computed: { computed: {
filename() { filename() {
return filenameFromUrl(this.document); return filenameFromUrl(this.document);
}, }
}, },
}; };
</script> </script>

View File

@ -1,7 +1,17 @@
<template> <template>
<div :class="{ 'user-widget--is-profile': isProfile }" class="user-widget"> <div
<div class="user-widget__avatar" data-cy="user-widget-avatar"> :class="{'user-widget--is-profile': isProfile}"
<avatar :avatar-url="avatarUrl" :icon-highlighted="isProfile" /> class="user-widget"
@click.stop="$emit('click', $event)"
>
<div
class="user-widget__avatar"
data-cy="user-widget-avatar"
>
<avatar
:avatar-url="avatarUrl"
:icon-highlighted="isProfile"
/>
</div> </div>
</div> </div>
</template> </template>
@ -12,23 +22,25 @@ import Avatar from '@/components/profile/Avatar';
export default { export default {
props: { props: {
avatarUrl: { avatarUrl: {
type: String, type: String
}, }
}, },
emits: ['click'],
components: { components: {
Avatar, Avatar
}, },
computed: { computed: {
isProfile() { isProfile() {
return this.$route.meta.isProfile; return this.$route.meta.isProfile;
}, }
}, }
}; };
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import '~styles/helpers'; @import "~styles/helpers";
.user-widget { .user-widget {
color: $color-silver-dark; color: $color-silver-dark;

View File

@ -1,5 +1,8 @@
<template> <template>
<nav :class="{ 'content-navigation--sidebar': isSidebar }" class="content-navigation"> <nav
:class="{'content-navigation--sidebar': isSidebar}"
class="content-navigation"
>
<div class="content-navigation__primary"> <div class="content-navigation__primary">
<div class="content-navigation__item"> <div class="content-navigation__item">
<router-link <router-link
@ -7,12 +10,14 @@
:to="topicRoute" :to="topicRoute"
active-class="content-navigation__link--active" active-class="content-navigation__link--active"
class="content-navigation__link" class="content-navigation__link"
@click.native="close" @click="close"
> >
{{ $flavor.textTopics }} {{ $flavor.textTopics }}
</router-link> </router-link>
<topic-navigation v-if="isSidebar" /> <topic-navigation
v-if="isSidebar"
/>
</div> </div>
<div class="content-navigation__item"> <div class="content-navigation__item">
@ -20,7 +25,7 @@
to="/instruments" to="/instruments"
active-class="content-navigation__link--active" active-class="content-navigation__link--active"
class="content-navigation__link" class="content-navigation__link"
@click.native="close" @click="close"
> >
{{ $flavor.textInstruments }} {{ $flavor.textInstruments }}
</router-link> </router-link>
@ -33,14 +38,19 @@
class="content-navigation__link" class="content-navigation__link"
data-cy="news-navigation-link" data-cy="news-navigation-link"
v-if="!me.readOnly" v-if="!me.readOnly"
@click.native="close" @click="close"
> >
News News
</router-link> </router-link>
</div> </div>
</div> </div>
<router-link to="/" class="content-navigation__logo" data-cy="home-link" v-if="!isSidebar"> <router-link
to="/"
class="content-navigation__logo"
data-cy="home-link"
v-if="!isSidebar"
>
<logo class="content-navigation__logo-icon" /> <logo class="content-navigation__logo-icon" />
</router-link> </router-link>
@ -51,23 +61,29 @@
to="/rooms" to="/rooms"
active-class="content-navigation__link--active" active-class="content-navigation__link--active"
class="content-navigation__link content-navigation__link--secondary" class="content-navigation__link content-navigation__link--secondary"
@click.native="close" @click="close"
> >
Räume Räume
</router-link> </router-link>
</div> </div>
<div class="content-navigation__item content-navigation__item--secondary" v-if="showPortfolio"> <div
class="content-navigation__item content-navigation__item--secondary"
v-if="showPortfolio"
>
<router-link <router-link
to="/portfolio" to="/portfolio"
active-class="content-navigation__link--active" active-class="content-navigation__link--active"
class="content-navigation__link content-navigation__link--secondary" class="content-navigation__link content-navigation__link--secondary"
@click.native="close" @click="close"
> >
Portfolio Portfolio
</router-link> </router-link>
</div> </div>
<div class="content-navigation__item content-navigation__item--secondary" v-if="isSidebar"> <div
class="content-navigation__item content-navigation__item--secondary"
v-if="isSidebar"
>
<a <a
:href="$flavor.supportLink" :href="$flavor.supportLink"
target="_blank" target="_blank"
@ -85,27 +101,28 @@ import TopicNavigation from '@/components/book-navigation/TopicNavigation';
import sidebarMixin from '@/mixins/sidebar'; import sidebarMixin from '@/mixins/sidebar';
import meMixin from '@/mixins/me'; import meMixin from '@/mixins/me';
import {defineAsyncComponent} from 'vue';
const Logo = () => import(/* webpackChunkName: "icons" */ '@/components/icons/Logo'); const Logo = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/Logo'));
export default { export default {
props: { props: {
isSidebar: { isSidebar: {
default: false, default: false
}, }
}, },
mixins: [sidebarMixin, meMixin], mixins: [sidebarMixin, meMixin],
components: { components: {
TopicNavigation, TopicNavigation,
Logo, Logo
}, },
computed: { computed: {
showPortfolio() { showPortfolio() {
return this.$flavor.showPortfolio; return this.$flavor.showPortfolio;
}, }
}, },
methods: { methods: {
@ -117,14 +134,14 @@ export default {
}, },
close() { close() {
this.closeSidebar('navigation'); this.closeSidebar('navigation');
}, }
}, }
}; };
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import '@/styles/_variables.scss'; @import "@/styles/_variables.scss";
@import '@/styles/_mixins.scss'; @import "@/styles/_mixins.scss";
.content-navigation { .content-navigation {
display: flex; display: flex;
@ -135,8 +152,7 @@ export default {
@include navigation-link; @include navigation-link;
} }
&__primary, &__primary, &__secondary {
&__secondary {
display: none; display: none;
flex-direction: row; flex-direction: row;
@ -146,7 +162,7 @@ export default {
} }
&__logo { &__logo {
color: #17a887; color: #17A887;
font-size: 36px; font-size: 36px;
font-weight: 800; font-weight: 800;
font-family: $sans-serif-font-family; font-family: $sans-serif-font-family;
@ -181,8 +197,7 @@ export default {
&--sidebar { &--sidebar {
flex-direction: column; flex-direction: column;
#{$parent}__primary, #{$parent}__primary, #{$parent}__secondary {
#{$parent}__secondary {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
width: 100%; width: 100%;
@ -198,6 +213,7 @@ export default {
&:only-child { &:only-child {
margin-bottom: 0; margin-bottom: 0;
} }
} }
#{$parent}__item { #{$parent}__item {

View File

@ -1,8 +1,18 @@
<template> <template>
<transition name="slide"> <transition name="slide">
<div class="navigation-sidebar" v-if="sidebar.navigation" v-click-outside="close"> <div
<content-navigation :is-sidebar="true" class="navigation-sidebar__main" /> class="navigation-sidebar"
<div class="navigation-sidebar__close-button" @click="close"> v-if="sidebar.navigation"
v-click-outside="close"
>
<content-navigation
:is-sidebar="true"
class="navigation-sidebar__main"
/>
<div
class="navigation-sidebar__close-button"
@click="close"
>
<cross class="navigation-sidebar__close-icon" /> <cross class="navigation-sidebar__close-icon" />
</div> </div>
</div> </div>
@ -13,28 +23,30 @@
import ContentNavigation from '@/components/book-navigation/ContentNavigation'; import ContentNavigation from '@/components/book-navigation/ContentNavigation';
import sidebarMixin from '@/mixins/sidebar'; import sidebarMixin from '@/mixins/sidebar';
import {defineAsyncComponent} from 'vue';
const Cross = () => import(/* webpackChunkName: "icons" */ '@/components/icons/CrossIcon'); const Cross = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/CrossIcon'));
export default { export default {
mixins: [sidebarMixin], mixins: [sidebarMixin],
components: { components: {
ContentNavigation, ContentNavigation,
Cross, Cross
}, },
methods: { methods: {
close() { close() {
this.closeSidebar('navigation'); this.closeSidebar('navigation');
}
}, },
},
}; };
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import '@/styles/_variables.scss'; @import "@/styles/_variables.scss";
@import '@/styles/_mixins.scss'; @import "@/styles/_mixins.scss";
$desktop-width: 285px; $desktop-width: 285px;
@ -56,10 +68,10 @@ $desktop-width: 285px;
grid-template-columns: 1fr 50px; grid-template-columns: 1fr 50px;
grid-template-rows: 50px max-content auto 100px; grid-template-rows: 50px max-content auto 100px;
grid-template-areas: 'm m' 'm m' 's s' 's s'; grid-template-areas: "m m" "m m" "s s" "s s";
&--with-subnavigation { &--with-subnavigation {
grid-template-areas: 'm m' 'm m' 'sub sub' 's s'; grid-template-areas: "m m" "m m" "sub sub" "s s";
} }
height: 100vh; height: 100vh;
@ -87,13 +99,11 @@ $desktop-width: 285px;
} }
.slide { .slide {
&-enter-active, &-enter-active, &-leave-active {
&-leave-active {
transition: left 0.2s; transition: left 0.2s;
} }
&-enter, &-enter-from, &-leave-to {
&-leave-to {
left: -100vw; left: -100vw;
@include desktop { @include desktop {
left: -$desktop-width; left: -$desktop-width;

View File

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

View File

@ -8,7 +8,7 @@
class="topic-navigation__topic book-subnavigation__item" class="topic-navigation__topic book-subnavigation__item"
v-for="topic in topics" v-for="topic in topics"
:key="topic.id" :key="topic.id"
@click.native="closeSidebar('navigation')" @click="closeSidebar('navigation')"
> >
{{ topic.order }}. {{ topic.order }}.
{{ topic.title }} {{ topic.title }}
@ -56,7 +56,7 @@ export default {
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import '@/styles/_variables.scss'; @import "~styles/helpers";
.topic-navigation { .topic-navigation {
&__topic { &__topic {

View File

@ -105,7 +105,7 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import Vue, { PropType } from 'vue'; import { PropType, defineComponent } from 'vue';
import Toggle from '@/components/ui/Toggle.vue'; import Toggle from '@/components/ui/Toggle.vue';
import ContentFormSection from '@/components/content-block-form/ContentFormSection.vue'; import ContentFormSection from '@/components/content-block-form/ContentFormSection.vue';
import InputWithLabel from '@/components/ui/InputWithLabel.vue'; import InputWithLabel from '@/components/ui/InputWithLabel.vue';
@ -130,7 +130,7 @@ interface ContentBlockFormData {
localContentBlock: any; localContentBlock: any;
} }
export default Vue.extend({ export default defineComponent({
props: { props: {
title: { title: {
type: String, type: String,

View File

@ -26,10 +26,14 @@
:class="['content-element__component']" :class="['content-element__component']"
v-bind="element" v-bind="element"
:is="component" :is="component"
@change-text="changeText" @change-text="changeText"
@link-change-url="changeUrl" @link-change-url="changeUrl"
@change-url="changeUrl" @change-url="changeUrl"
@switch-to-document="switchToDocument" @switch-to-document="switchToDocument"
@assignment-change-title="changeAssignmentTitle" @assignment-change-title="changeAssignmentTitle"
@assignment-change-assignment="changeAssignmentAssignment" @assignment-change-assignment="changeAssignmentAssignment"
/> />
@ -41,32 +45,27 @@
<script> <script>
import ContentFormSection from '@/components/content-block-form/ContentFormSection'; import ContentFormSection from '@/components/content-block-form/ContentFormSection';
import ContentElementActions from '@/components/content-block-form/ContentElementActions'; import ContentElementActions from '@/components/content-block-form/ContentElementActions';
import {defineAsyncComponent} from 'vue';
const TrashIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/TrashIcon'); const TrashIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/TrashIcon'));
const ContentBlockElementChooserWidget = () => const ContentBlockElementChooserWidget = defineAsyncComponent(() => import(/* webpackChunkName: "content-forms" */'@/components/content-forms/ContentBlockElementChooserWidget'));
import(/* webpackChunkName: "content-forms" */ '@/components/content-forms/ContentBlockElementChooserWidget'); const LinkForm = defineAsyncComponent(() => import(/* webpackChunkName: "content-forms" */'@/components/content-forms/LinkForm'));
const LinkForm = () => import(/* webpackChunkName: "content-forms" */ '@/components/content-forms/LinkForm'); const VideoForm = defineAsyncComponent(() => import(/* webpackChunkName: "content-forms" */'@/components/content-forms/VideoForm'));
const VideoForm = () => import(/* webpackChunkName: "content-forms" */ '@/components/content-forms/VideoForm'); const ImageForm = defineAsyncComponent(() => import(/* webpackChunkName: "content-forms" */'@/components/content-forms/ImageForm'));
const ImageForm = () => import(/* webpackChunkName: "content-forms" */ '@/components/content-forms/ImageForm'); const DocumentForm = defineAsyncComponent(() => import(/* webpackChunkName: "content-forms" */'@/components/content-forms/DocumentForm'));
const DocumentForm = () => import(/* webpackChunkName: "content-forms" */ '@/components/content-forms/DocumentForm'); const AssignmentForm = defineAsyncComponent(() => import(/* webpackChunkName: "content-forms" */'@/components/content-forms/AssignmentForm'));
const AssignmentForm = () => const TextForm = defineAsyncComponent(() => import(/* webpackChunkName: "content-forms" */'@/components/content-forms/TipTap.vue'));
import(/* webpackChunkName: "content-forms" */ '@/components/content-forms/AssignmentForm'); const SubtitleForm = defineAsyncComponent(() => import(/* webpackChunkName: "content-forms" */'@/components/content-forms/SubtitleForm'));
const TextForm = () => import(/* webpackChunkName: "content-forms" */ '@/components/content-forms/TipTap.vue');
const SubtitleForm = () => import(/* webpackChunkName: "content-forms" */ '@/components/content-forms/SubtitleForm');
// readonly blocks // readonly blocks
const Assignment = () => const Assignment = defineAsyncComponent(() => import(/* webpackChunkName: "content-forms" */'@/components/content-blocks/assignment/Assignment'));
import(/* webpackChunkName: "content-forms" */ '@/components/content-blocks/assignment/Assignment'); const SurveyBlock = defineAsyncComponent(() => import(/* webpackChunkName: "content-forms" */'@/components/content-blocks/SurveyBlock'));
const SurveyBlock = () => import(/* webpackChunkName: "content-forms" */ '@/components/content-blocks/SurveyBlock'); const Solution = defineAsyncComponent(() => import(/* webpackChunkName: "content-forms" */'@/components/content-blocks/Solution'));
const Solution = () => import(/* webpackChunkName: "content-forms" */ '@/components/content-blocks/Solution'); const ImageBlock = defineAsyncComponent(() => import(/* webpackChunkName: "content-forms" */'@/components/content-blocks/ImageBlock'));
const ImageBlock = () => import(/* webpackChunkName: "content-forms" */ '@/components/content-blocks/ImageBlock'); const Instruction = defineAsyncComponent(() => import(/* webpackChunkName: "content-forms" */'@/components/content-blocks/Instruction'));
const Instruction = () => import(/* webpackChunkName: "content-forms" */ '@/components/content-blocks/Instruction'); const ModuleRoomSlug = defineAsyncComponent(() => import(/* webpackChunkName: "content-forms" */'@/components/content-blocks/ModuleRoomSlug'));
const ModuleRoomSlug = () => const CmsDocumentBlock = defineAsyncComponent(() => import(/* webpackChunkName: "content-forms" */'@/components/content-blocks/CmsDocumentBlock'));
import(/* webpackChunkName: "content-forms" */ '@/components/content-blocks/ModuleRoomSlug'); const ThinglinkBlock = defineAsyncComponent(() => import(/* webpackChunkName: "content-forms" */'@/components/content-blocks/ThinglinkBlock'));
const CmsDocumentBlock = () => const InfogramBlock = defineAsyncComponent(() => import(/* webpackChunkName: "content-forms" */'@/components/content-blocks/InfogramBlock'));
import(/* webpackChunkName: "content-forms" */ '@/components/content-blocks/CmsDocumentBlock');
const ThinglinkBlock = () =>
import(/* webpackChunkName: "content-forms" */ '@/components/content-blocks/ThinglinkBlock');
const InfogramBlock = () => import(/* webpackChunkName: "content-forms" */ '@/components/content-blocks/InfogramBlock');
const CHOOSER = 'content-block-element-chooser-widget'; const CHOOSER = 'content-block-element-chooser-widget';
@ -111,7 +110,7 @@ export default {
CmsDocumentBlock, CmsDocumentBlock,
InfogramBlock, InfogramBlock,
ThinglinkBlock, ThinglinkBlock,
Assignment, Assignment
}, },
computed: { computed: {
@ -217,12 +216,12 @@ export default {
case 'thinglink_block': case 'thinglink_block':
return { return {
component: 'thinglink-block', component: 'thinglink-block',
title: 'Interaktive Grafik', title: 'Interaktive Grafik'
}; };
case 'infogram_block': case 'infogram_block':
return { return {
component: 'infogram-block', component: 'infogram-block',
title: 'Interaktive Grafik', title: 'Interaktive Grafik'
}; };
} }
return { return {
@ -296,12 +295,9 @@ export default {
case 'document_block': case 'document_block':
el = { el = {
...el, ...el,
value: Object.assign( value: Object.assign({
{
url: '', url: '',
}, }, value),
value
),
}; };
break; break;
case 'image_url_block': case 'image_url_block':

View File

@ -55,7 +55,7 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import Vue from 'vue'; import { defineComponent } from 'vue';
import WidgetPopover from '@/components/ui/WidgetPopover.vue'; import WidgetPopover from '@/components/ui/WidgetPopover.vue';
import Ellipses from '@/components/icons/Ellipses.vue'; import Ellipses from '@/components/icons/Ellipses.vue';
import ButtonWithIconAndText from '@/components/ui/ButtonWithIconAndText.vue'; import ButtonWithIconAndText from '@/components/ui/ButtonWithIconAndText.vue';
@ -65,7 +65,7 @@ interface Data {
show: boolean; show: boolean;
} }
export default Vue.extend({ export default defineComponent({
props: { props: {
actions: { actions: {
type: Object as () => ActionOptions, type: Object as () => ActionOptions,

View File

@ -1,8 +1,13 @@
<template> <template>
<div class="content-form-section"> <div class="content-form-section">
<h2 class="content-form-section__heading"> <h2 class="content-form-section__heading">
<component class="content-form-section__icon" :is="icon" /> <component
<span class="content-form-section__title" data-cy="content-form-section-title">{{ title }}</span> class="content-form-section__icon"
:is="icon"
/> <span
class="content-form-section__title"
data-cy="content-form-section-title"
>{{ title }}</span>
</h2> </h2>
<content-element-actions <content-element-actions
@ -22,30 +27,31 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import {defineComponent} from "vue";
import formElementIcons from '@/components/ui/form-element-icons.js'; import formElementIcons from '@/components/ui/form-element-icons.js';
import ContentElementActions from '@/components/content-block-form/ContentElementActions.vue'; import ContentElementActions from '@/components/content-block-form/ContentElementActions.vue';
import { ActionOptions } from '@/@types'; import {ActionOptions} from "@/@types";
export default { export default defineComponent({
props: { props: {
title: { title: {
type: String, type: String,
default: '', default: ''
}, },
icon: { icon: {
type: String, type: String,
default: '', default: ''
}, },
actions: { actions: {
type: Object as () => ActionOptions, type: Object as () => ActionOptions,
default: () => {}, default: () => {}
}, }
}, },
components: { components: {
ContentElementActions, ContentElementActions,
...formElementIcons, ...formElementIcons
}, }
}; });
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">

View File

@ -18,11 +18,27 @@
/> />
</template> </template>
<add-content-element :index="-1" class="contents-form__add" @add-element="addElement" /> <add-content-element
<div class="contents-form__element" v-for="(element, index) in localContentBlock.contents" :key="index"> :index="-1"
<content-element :element="element" @update="update(index, $event)" @remove="remove(index)" /> class="contents-form__add"
@add-element="addElement"
/>
<div
class="contents-form__element"
v-for="(element, index) in localContentBlock.contents"
:key="index"
>
<content-element
:element="element"
@update="update(index, $event)"
@remove="remove(index)"
/>
<add-content-element :index="index" class="contents-form__add" @add-element="addElement" /> <add-content-element
:index="index"
class="contents-form__add"
@add-element="addElement"
/>
</div> </div>
<template #footer> <template #footer>
@ -32,24 +48,26 @@
class="button button--primary" class="button button--primary"
data-cy="modal-save-button" data-cy="modal-save-button"
@click="save" @click="save"
>Speichern</a >Speichern</a>
> <a
<a class="button" @click="$emit('hide')">Abbrechen</a> class="button"
@click="$emit('hide')"
>Abbrechen</a>
</div> </div>
</template> </template>
</modal> </modal>
</template> </template>
<script> <script>
import {defineAsyncComponent} from 'vue';
import {meQuery} from '@/graphql/queries'; import {meQuery} from '@/graphql/queries';
const ModalInput = () => import(/* webpackChunkName: "content-forms" */ '@/components/ModalInput'); const ModalInput = defineAsyncComponent(() => import(/* webpackChunkName: "content-forms" */'@/components/ModalInput'));
const AddContentElement = () => import(/* webpackChunkName: "content-forms" */ '@/components/AddContentElement'); const AddContentElement = defineAsyncComponent(() => import(/* webpackChunkName: "content-forms" */'@/components/AddContentElement'));
const ContentElement = () => const ContentElement = defineAsyncComponent(() => import(/* webpackChunkName: "content-forms" */'@/components/content-block-form/ContentElement'));
import(/* webpackChunkName: "content-forms" */ '@/components/content-block-form/ContentElement');
const Modal = () => import('@/components/Modal.vue'); const Modal = defineAsyncComponent(() => import('@/components/Modal'));
const Checkbox = () => import('@/components/ui/Checkbox.vue'); const Checkbox = defineAsyncComponent(() => import('@/components/ui/Checkbox'));
export default { export default {
props: { props: {
@ -79,15 +97,12 @@ export default {
data() { data() {
return { return {
error: false, error: false,
localContentBlock: Object.assign( localContentBlock: Object.assign({}, {
{},
{
title: this.contentBlock.title, title: this.contentBlock.title,
contents: [...this.contentBlock.contents], contents: [...this.contentBlock.contents],
id: this.contentBlock.id || undefined, id: this.contentBlock.id || undefined,
isAssignment: this.contentBlock.type && this.contentBlock.type.toLowerCase() === 'task', isAssignment: this.contentBlock.type && this.contentBlock.type.toLowerCase() === 'task',
} }),
),
me: {}, me: {},
}; };
}, },
@ -133,17 +148,19 @@ export default {
remove(index) { remove(index) {
this.localContentBlock.contents.splice(index, 1); this.localContentBlock.contents.splice(index, 1);
}, },
}, },
}; };
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import '~styles/helpers'; @import "~styles/helpers";
.contents-form { .contents-form {
/* top level does not exist, because of the modal */ /* top level does not exist, because of the modal */
&__element { &__element {
} }
&__element-component { &__element-component {

View File

@ -1,11 +1,16 @@
<template> <template>
<contents-form :content-block="contentBlock" :show-task-selection="true" @save="saveContentBlock" @hide="hideModal" /> <contents-form
:content-block="contentBlock"
:show-task-selection="true"
@save="saveContentBlock"
@hide="hideModal"
/>
</template> </template>
<script> <script>
import ContentsForm from '@/components/content-block-form/ContentsForm'; import ContentsForm from '@/components/content-block-form/ContentsForm';
import store from '@/store/index'; import {store} from '@/store';
import EDIT_CONTENT_BLOCK_MUTATION from 'gql/mutations/editContentBlock.gql'; import EDIT_CONTENT_BLOCK_MUTATION from 'gql/mutations/editContentBlock.gql';
import MODULE_DETAILS_QUERY from '@/graphql/gql/queries/modules/moduleDetailsQuery.gql'; import MODULE_DETAILS_QUERY from '@/graphql/gql/queries/modules/moduleDetailsQuery.gql';
@ -33,29 +38,25 @@ export default {
this.$store.dispatch('hideModal'); this.$store.dispatch('hideModal');
}, },
saveContentBlock(contentBlock) { saveContentBlock(contentBlock) {
this.$apollo this.$apollo.mutate({
.mutate({
mutation: EDIT_CONTENT_BLOCK_MUTATION, mutation: EDIT_CONTENT_BLOCK_MUTATION,
variables: { variables: {
input: { input: {
contentBlock: { contentBlock: {
title: contentBlock.title, title: contentBlock.title,
contents: contentBlock.contents.filter((value) => Object.keys(value).length > 0), contents: contentBlock.contents.filter(value => Object.keys(value).length > 0),
type: setUserBlockType(contentBlock.isAssignment), type: setUserBlockType(contentBlock.isAssignment),
}, },
id: contentBlock.id, id: contentBlock.id,
}, },
}, },
refetchQueries: [ refetchQueries: [{
{
query: MODULE_DETAILS_QUERY, query: MODULE_DETAILS_QUERY,
variables: { variables: {
slug: this.$route.params.slug, slug: this.$route.params.slug,
}, },
}, }],
], }).then(() => {
})
.then(() => {
this.hideModal(); this.hideModal();
}); });
}, },
@ -71,5 +72,6 @@ export default {
}; };
}, },
}, },
}; };
</script> </script>

View File

@ -1,5 +1,9 @@
<template> <template>
<div :class="componentClass" :data-scrollto="component.id" data-cy="content-component"> <div
:class="componentClass"
:data-scrollto="component.id"
data-cy="content-component"
>
<bookmark-actions <bookmark-actions
:bookmarked="bookmarked" :bookmarked="bookmarked"
:note="note" :note="note"
@ -9,105 +13,97 @@
@edit-note="editNote" @edit-note="editNote"
@bookmark="bookmarkContent(component.id, !bookmarked)" @bookmark="bookmarkContent(component.id, !bookmarked)"
/> />
<component v-bind="component" :parent="parent" :is="component.type" /> <component
v-bind="component"
:parent="parent"
:is="component.type"
/>
</div> </div>
</template> </template>
<script> <script>
import {constructContentComponentBookmarkMutation} from '@/helpers/update-content-bookmark-mutation'; import {constructContentComponentBookmarkMutation} from '@/helpers/update-content-bookmark-mutation';
import {defineAsyncComponent} from 'vue';
const TextBlock = () => import(/* webpackChunkName: "content-components" */ '@/components/content-blocks/TextBlock'); const TextBlock = defineAsyncComponent(() => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/TextBlock'));
const InstrumentWidget = () => const InstrumentWidget = defineAsyncComponent(() => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/InstrumentWidget'));
import(/* webpackChunkName: "content-components" */ '@/components/content-blocks/InstrumentWidget'); const ImageBlock = defineAsyncComponent(() => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/ImageBlock'));
const ImageBlock = () => import(/* webpackChunkName: "content-components" */ '@/components/content-blocks/ImageBlock'); const ImageUrlBlock = defineAsyncComponent(() => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/ImageUrlBlock'));
const ImageUrlBlock = () => const VideoBlock = defineAsyncComponent(() => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/VideoBlock'));
import(/* webpackChunkName: "content-components" */ '@/components/content-blocks/ImageUrlBlock'); const LinkBlock = defineAsyncComponent(() => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/LinkBlock'));
const VideoBlock = () => import(/* webpackChunkName: "content-components" */ '@/components/content-blocks/VideoBlock'); const DocumentBlock = defineAsyncComponent(() => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/DocumentBlock'));
const LinkBlock = () => import(/* webpackChunkName: "content-components" */ '@/components/content-blocks/LinkBlock'); const CmsDocumentBlock = defineAsyncComponent(() => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/CmsDocumentBlock'));
const DocumentBlock = () => const InfogramBlock = defineAsyncComponent(() => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/InfogramBlock'));
import(/* webpackChunkName: "content-components" */ '@/components/content-blocks/DocumentBlock'); const ThinglinkBlock = defineAsyncComponent(() => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/ThinglinkBlock'));
const CmsDocumentBlock = () => const GeniallyBlock = defineAsyncComponent(() => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/GeniallyBlock'));
import(/* webpackChunkName: "content-components" */ '@/components/content-blocks/CmsDocumentBlock'); const SubtitleBlock = defineAsyncComponent(() => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/SubtitleBlock'));
const InfogramBlock = () => const SectionTitleBlock = defineAsyncComponent(() => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/SectionTitleBlock'));
import(/* webpackChunkName: "content-components" */ '@/components/content-blocks/InfogramBlock'); const ContentListBlock = defineAsyncComponent(() => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/ContentListBlock'));
const ThinglinkBlock = () => const ModuleRoomSlug = defineAsyncComponent(() => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/ModuleRoomSlug'));
import(/* webpackChunkName: "content-components" */ '@/components/content-blocks/ThinglinkBlock'); const Assignment = defineAsyncComponent(() => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/assignment/Assignment'));
const GeniallyBlock = () => const Survey = defineAsyncComponent(() => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/SurveyBlock'));
import(/* webpackChunkName: "content-components" */ '@/components/content-blocks/GeniallyBlock'); const Solution = defineAsyncComponent(() => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/Solution'));
const SubtitleBlock = () => const Instruction = defineAsyncComponent(() => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/Instruction'));
import(/* webpackChunkName: "content-components" */ '@/components/content-blocks/SubtitleBlock'); const BookmarkActions = defineAsyncComponent(() => import(/* webpackChunkName: "content-components" */'@/components/notes/BookmarkActions'));
const SectionTitleBlock = () =>
import(/* webpackChunkName: "content-components" */ '@/components/content-blocks/SectionTitleBlock');
const ContentListBlock = () =>
import(/* webpackChunkName: "content-components" */ '@/components/content-blocks/ContentListBlock');
const ModuleRoomSlug = () =>
import(/* webpackChunkName: "content-components" */ '@/components/content-blocks/ModuleRoomSlug');
const Assignment = () =>
import(/* webpackChunkName: "content-components" */ '@/components/content-blocks/assignment/Assignment');
const Survey = () => import(/* webpackChunkName: "content-components" */ '@/components/content-blocks/SurveyBlock');
const Solution = () => import(/* webpackChunkName: "content-components" */ '@/components/content-blocks/Solution');
const Instruction = () =>
import(/* webpackChunkName: "content-components" */ '@/components/content-blocks/Instruction');
const BookmarkActions = () => import(/* webpackChunkName: "content-components" */ '@/components/notes/BookmarkActions');
export default { export default {
props: { props: {
component: { component: {
type: Object, type: Object,
default: () => ({}), default: () => ({})
}, },
parent: { parent: {
type: Object, type: Object,
default: () => ({}), default: () => ({})
}, },
bookmarks: { bookmarks: {
type: Array, type: Array,
default: () => [], default: () => ([])
}, },
notes: { notes: {
type: Array, type: Array,
default: () => [], default: () => ([])
}, },
root: { root: {
type: String, type: String,
default: '', default: ''
}, },
editMode: { editMode: {
type: Boolean, type: Boolean,
default: false, default: false
}, }
}, },
components: { components: {
text_block: TextBlock, 'text_block': TextBlock,
basic_knowledge: InstrumentWidget, // for legacy 'basic_knowledge': InstrumentWidget, // for legacy
instrument: InstrumentWidget, 'instrument': InstrumentWidget,
image_block: ImageBlock, 'image_block': ImageBlock,
image_url_block: ImageUrlBlock, 'image_url_block': ImageUrlBlock,
video_block: VideoBlock, 'video_block': VideoBlock,
link_block: LinkBlock, 'link_block': LinkBlock,
document_block: DocumentBlock, 'document_block': DocumentBlock,
infogram_block: InfogramBlock, 'infogram_block': InfogramBlock,
genially_block: GeniallyBlock, 'genially_block': GeniallyBlock,
subtitle: SubtitleBlock, 'subtitle': SubtitleBlock,
section_title: SectionTitleBlock, 'section_title': SectionTitleBlock,
content_list: ContentListBlock, 'content_list': ContentListBlock,
module_room_slug: ModuleRoomSlug, 'module_room_slug': ModuleRoomSlug,
thinglink_block: ThinglinkBlock, 'thinglink_block': ThinglinkBlock,
cms_document_block: CmsDocumentBlock, 'cms_document_block': CmsDocumentBlock,
Survey, Survey,
Solution, Solution,
Instruction, Instruction,
Assignment, Assignment,
BookmarkActions, BookmarkActions
}, },
computed: { computed: {
bookmarked() { bookmarked() {
return this.bookmarks && !!this.bookmarks.find((bookmark) => bookmark.uuid === this.component.id); return this.bookmarks && !!this.bookmarks.find(bookmark => bookmark.uuid === this.component.id);
}, },
note() { note() {
const bookmark = this.bookmarks && this.bookmarks.find((bookmark) => bookmark.uuid === this.component.id); const bookmark = this.bookmarks && this.bookmarks.find(bookmark => bookmark.uuid === this.component.id);
return bookmark && bookmark.note; return bookmark && bookmark.note;
}, },
showBookmarkActions() { showBookmarkActions() {
@ -119,19 +115,18 @@ export default {
classes.push('content-component--bookmarked'); classes.push('content-component--bookmarked');
} }
return classes; return classes;
}, }
}, },
methods: { methods: {
addNote(id) { addNote(id) {
const type = Object.prototype.hasOwnProperty.call(this.parent, '__typename') const type = Object.prototype.hasOwnProperty.call(this.parent, '__typename')
? this.parent.__typename ? this.parent.__typename : 'ContentBlockNode';
: 'ContentBlockNode';
this.$store.dispatch('addNote', { this.$store.dispatch('addNote', {
content: id, content: id,
type, type,
block: this.root, block: this.root
}); });
}, },
editNote() { editNote() {
@ -139,18 +134,19 @@ export default {
}, },
bookmarkContent(uuid, bookmarked) { bookmarkContent(uuid, bookmarked) {
this.$apollo.mutate(constructContentComponentBookmarkMutation(uuid, bookmarked, this.parent, this.root)); this.$apollo.mutate(constructContentComponentBookmarkMutation(uuid, bookmarked, this.parent, this.root));
}, }
}, }
}; };
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import '~styles/helpers'; @import "~styles/helpers";
.content-component { .content-component {
position: relative; position: relative;
&--bookmarked { &--bookmarked {
} }
&--subtitle { &--subtitle {

View File

@ -1,40 +1,43 @@
<template> <template>
<content-list :items="contentBlocks"> <content-list
:items="contentBlocks"
>
<template #default="{ item }"> <template #default="{ item }">
<content-block :content-block="item" :parent="parent" /> <content-block
:content-block="item"
:parent="parent"
/>
</template> </template>
</content-list> </content-list>
</template> </template>
<script> <script>
import ContentList from '@/components/content-blocks/ContentList.vue'; import {defineAsyncComponent} from 'vue';
const ContentList = defineAsyncComponent(() => import('@/components/content-blocks/ContentList'));
const ContentBlock = defineAsyncComponent(() => import('@/components/ContentBlock'));
export default { export default {
name: 'ContentBlockList', name: 'ContentBlockList',
props: ['contents', 'parent'], props: ['contents', 'parent'],
components: { components: {
ContentList, ContentList,
// https://vuejs.org/v2/guide/components-edge-cases.html#Circular-References-Between-Components ContentBlock
ContentBlock: () => import('@/components/ContentBlock.vue'),
}, },
computed: { computed: {
contentBlocks() { contentBlocks() {
return this.contents.map((contentBlock) => { return this.contents.map(contentBlock => {
const contents = contentBlock.value ? [...contentBlock.value] : []; const contents = contentBlock.value ? [...contentBlock.value] : [];
return Object.assign({}, contentBlock, { return Object.assign({}, contentBlock, {
contents, contents,
indent: true, indent: true,
bookmarks: this.parent.bookmarks, bookmarks: this.parent.bookmarks,
notes: this.parent.notes, notes: this.parent.notes,
root: this.parent.id, root: this.parent.id
}); });
}); });
}, }
}, }
}; };
</script> </script>
<style scoped lang="scss">
@import '~styles/helpers';
</style>

View File

@ -1,16 +1,25 @@
<template> <template>
<div class="document-block"> <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> <a
<a class="document-block__remove" v-if="showTrashIcon" @click="$emit('trash')"> :href="value.url"
class="document-block__link"
target="_blank"
>{{ urlName }}</a>
<a
class="document-block__remove"
v-if="showTrashIcon"
@click="$emit('trash')"
>
<trash-icon class="document-block__trash-icon" /> <trash-icon class="document-block__trash-icon" />
</a> </a>
</div> </div>
</template> </template>
<script> <script>
const DocumentIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/DocumentIcon'); import {defineAsyncComponent} from 'vue';
const TrashIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/TrashIcon'); const DocumentIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/DocumentIcon'));
const TrashIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/TrashIcon'));
export default { export default {
props: { props: {
@ -30,13 +39,13 @@ export default {
return parts[parts.length - 1]; return parts[parts.length - 1];
} }
return null; return null;
}, }
}, }
}; };
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import '~styles/helpers'; @import "~styles/helpers";
.document-block { .document-block {
display: grid; display: grid;

View File

@ -1,13 +1,20 @@
<template> <template>
<div class="instruction" v-if="me.isTeacher"> <div
class="instruction"
v-if="me.isTeacher"
>
<bulb-icon class="instruction__icon" /> <bulb-icon class="instruction__icon" />
<a :href="url" class="instruction__link">{{ text }}</a> <a
:href="url"
class="instruction__link"
>{{ text }}</a>
</div> </div>
</template> </template>
<script> <script>
import me from '@/mixins/me'; import me from '@/mixins/me';
const BulbIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/BulbIcon'); import {defineAsyncComponent} from 'vue';
const BulbIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/BulbIcon'));
export default { export default {
props: ['value'], props: ['value'],
@ -15,7 +22,7 @@ export default {
mixins: [me], mixins: [me],
components: { components: {
BulbIcon, BulbIcon
}, },
computed: { computed: {
@ -24,13 +31,13 @@ export default {
}, },
url() { url() {
return this.value.document ? this.value.document.url : this.value.url; return this.value.document ? this.value.document.url : this.value.url;
}, }
}, }
}; };
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import '@/styles/_mixins.scss'; @import "@/styles/_mixins.scss";
.instruction { .instruction {
margin-bottom: 1rem; margin-bottom: 1rem;

View File

@ -1,31 +1,39 @@
<template> <template>
<div :class="{ 'link-block--no-margin': noMargin }" class="link-block"> <div
:class="{ 'link-block--no-margin': noMargin}"
class="link-block"
>
<link-icon class="link-block__icon" /> <link-icon class="link-block__icon" />
<a :href="href" class="link-block__link" target="_blank">{{ value.text }}</a> <a
:href="href"
class="link-block__link"
target="_blank"
>{{ value.text }}</a>
</div> </div>
</template> </template>
<script> <script>
const LinkIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/LinkIcon'); import {defineAsyncComponent} from 'vue';
const LinkIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/LinkIcon'));
export default { export default {
props: { props: {
value: Object, value: Object,
noMargin: { noMargin: {
default: false, default: false
}, }
}, },
components: { components: {
LinkIcon, LinkIcon
}, },
computed: { computed: {
href() { href() {
const url = this.value.url; const url = this.value.url;
return url.startsWith('http') ? this.value.url : `http://${this.value.url}`; return url.startsWith('http') ? this.value.url : `http://${this.value.url}`;
}, }
}, }
}; };
</script> </script>

View File

@ -1,15 +1,34 @@
<template> <template>
<!-- eslint-disable vue/no-v-html --> <!-- eslint-disable vue/no-v-html -->
<div class="solution" data-cy="solution"> <div
<a class="solution__toggle" data-cy="show-solution" @click="toggle" class="solution"
data-cy="solution"
>
<a
class="solution__toggle"
data-cy="show-solution"
@click="toggle"
>Lösung >Lösung
<template v-if="!visible">anzeigen</template> <template v-if="!visible">anzeigen</template>
<template v-else>ausblenden</template> <template v-else>ausblenden</template>
</a> </a>
<transition name="fade"> <transition name="fade">
<div class="solution__hidden fade" v-if="visible"> <div
<p class="solution__text solution-text" data-cy="solution-text" v-html="sanitizedText" /> class="solution__hidden fade"
<cms-document-block :solution="true" class="solution__document" :value="value.document" v-if="value.document" /> v-if="visible"
>
<p
class="solution__text solution-text"
data-cy="solution-text"
v-html="sanitizedText"
/>
<cms-document-block
:solution="true"
class="solution__document"
:value="value.document"
v-if="value.document"
/>
</div> </div>
</transition> </transition>
</div> </div>
@ -44,7 +63,7 @@ export default {
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import '~styles/helpers'; @import "~styles/helpers";
.solution { .solution {
display: grid; display: grid;
@ -67,12 +86,12 @@ export default {
font-size: toRem(18px); font-size: toRem(18px);
color: $color-silver-dark; color: $color-silver-dark;
:deep(p) { /deep/ p {
font-size: toRem(18px); font-size: toRem(18px);
color: $color-silver-dark; color: $color-silver-dark;
} }
:deep(ul) { /deep/ ul {
padding-left: $medium-spacing; padding-left: $medium-spacing;
> li { > li {
@ -85,10 +104,10 @@ export default {
.fade-enter-active, .fade-enter-active,
.fade-leave-active { .fade-leave-active {
transition: opacity 0.3s; transition: opacity .3s;
} }
.fade-enter, .fade-enter-from,
.fade-leave-active { .fade-leave-active {
opacity: 0; opacity: 0;
} }

View File

@ -1,9 +1,19 @@
<template> <template>
<!-- eslint-disable vue/no-v-html --> <!-- eslint-disable vue/no-v-html -->
<div :data-scrollto="value.id" class="assignment"> <div
<p class="assignment__main-text" data-cy="assignment-main-text" v-html="assignment.assignment" /> :data-scrollto="value.id"
class="assignment"
>
<p
class="assignment__main-text"
data-cy="assignment-main-text"
v-html="assignment.assignment"
/>
<solution :value="solution" v-if="assignment.solution" /> <solution
:value="solution"
v-if="assignment.solution"
/>
<template v-if="isStudent"> <template v-if="isStudent">
<submission-form <submission-form
@ -23,13 +33,24 @@
@spellcheck="spellcheck" @spellcheck="spellcheck"
/> />
<spell-check :corrections="corrections" :text="submission.text" /> <spell-check
:corrections="corrections"
:text="submission.text"
/>
<p class="assignment__feedback" v-if="assignment.submission.submissionFeedback" v-html="feedbackText" /> <p
class="assignment__feedback"
v-if="assignment.submission.submissionFeedback"
v-html="feedbackText"
/>
</template> </template>
<template v-if="!isStudent"> <template v-if="!isStudent">
<router-link :to="{ name: 'submissions', params: { id: assignment.id } }" class="button button--primary"> <router-link
Zu den Ergebnissen :to="{name: 'submissions', params: { id: assignment.id }}"
class="button button--primary"
>
Zu den
Ergebnissen
</router-link> </router-link>
</template> </template>
</div> </div>
@ -45,12 +66,11 @@ import SPELL_CHECK_MUTATION from '@/graphql/gql/mutations/spellCheck.gql';
import debounce from 'lodash/debounce'; import debounce from 'lodash/debounce';
import cloneDeep from 'lodash/cloneDeep'; import cloneDeep from 'lodash/cloneDeep';
import {sanitize} from '@/helpers/text'; import {sanitize} from '@/helpers/text';
import {defineAsyncComponent} from 'vue';
const SubmissionForm = () => const SubmissionForm = defineAsyncComponent(() => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/assignment/SubmissionForm'));
import(/* webpackChunkName: "content-components" */ '@/components/content-blocks/assignment/SubmissionForm'); const Solution = defineAsyncComponent(() => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/Solution'));
const Solution = () => import(/* webpackChunkName: "content-components" */ '@/components/content-blocks/Solution'); const SpellCheck = defineAsyncComponent(() => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/assignment/SpellCheck'));
const SpellCheck = () =>
import(/* webpackChunkName: "content-components" */ '@/components/content-blocks/assignment/SpellCheck');
export default { export default {
props: ['value'], props: ['value'],
@ -107,8 +127,7 @@ export default {
...mapActions(['scrollToAssignmentReady']), ...mapActions(['scrollToAssignmentReady']),
_save: debounce(function (submission) { _save: debounce(function (submission) {
this.saving++; this.saving++;
this.$apollo this.$apollo.mutate({
.mutate({
mutation: UPDATE_ASSIGNMENT_MUTATION_WITH_SUCCESS, mutation: UPDATE_ASSIGNMENT_MUTATION_WITH_SUCCESS,
variables: { variables: {
input: { input: {
@ -119,14 +138,7 @@ export default {
}, },
}, },
}, },
update( update(store, {data: {updateAssignment: {successful, updatedAssignment}}}) {
store,
{
data: {
updateAssignment: { successful, updatedAssignment },
},
}
) {
try { try {
if (successful) { if (successful) {
const query = ASSIGNMENT_QUERY; const query = ASSIGNMENT_QUERY;
@ -137,7 +149,7 @@ export default {
submission, submission,
}); });
const data = { const data = {
assignment, assignment
}; };
store.writeQuery({query, variables, data}); store.writeQuery({query, variables, data});
} }
@ -146,8 +158,7 @@ export default {
// Query did not exist in the cache, and apollo throws a generic Error. Do nothing // Query did not exist in the cache, and apollo throws a generic Error. Do nothing
} }
}, },
}) }).then(() => {
.then(() => {
this.saving--; this.saving--;
if (this.saving === 0) { if (this.saving === 0) {
this.unsaved = false; this.unsaved = false;
@ -211,8 +222,7 @@ export default {
spellcheck() { spellcheck() {
let self = this; let self = this;
this.spellcheckLoading = true; this.spellcheckLoading = true;
this.$apollo this.$apollo.mutate({
.mutate({
mutation: SPELL_CHECK_MUTATION, mutation: SPELL_CHECK_MUTATION,
variables: { variables: {
input: { input: {
@ -220,18 +230,10 @@ export default {
text: this.assignment.submission.text, text: this.assignment.submission.text,
}, },
}, },
update( update(store, {data: {spellCheck: {results}}}) {
store,
{
data: {
spellCheck: { results },
},
}
) {
self.corrections = results; self.corrections = results;
}, },
}) }).then(() => {
.then(() => {
this.spellcheckLoading = false; this.spellcheckLoading = false;
}); });
}, },
@ -276,11 +278,11 @@ export default {
} }
&__main-text { &__main-text {
:deep(ul) { /deep/ ul{
@include list-parent; @include list-parent
} }
:deep(li) { /deep/ li {
@include list-child; @include list-child;
} }
} }
@ -311,5 +313,7 @@ export default {
&__feedback { &__feedback {
@include regular-text; @include regular-text;
} }
} }
</style> </style>

View File

@ -1,37 +1,47 @@
<template> <template>
<div class="final-submission" data-cy="final-submission"> <div
<document-block :value="{ url: userInput.document }" class="final-submission__document" v-if="userInput.document" /> class="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"> <div class="final-submission__explanation">
<info-icon class="final-submission__explanation-icon" /> <info-icon class="final-submission__explanation-icon" />
<span class="final-submission__explanation-text">{{ sharedMsg }}</span> <span class="final-submission__explanation-text">{{ sharedMsg }}</span>
<a class="final-submission__reopen" data-cy="final-submission-reopen" v-if="showReopen" @click="$emit('reopen')" <a
>Bearbeiten</a class="final-submission__reopen"
> data-cy="final-submission-reopen"
v-if="showReopen"
@click="$emit('reopen')"
>Bearbeiten</a>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import {newLineToParagraph} from '@/helpers/text'; import {newLineToParagraph} from '@/helpers/text';
const DocumentBlock = () => import {defineAsyncComponent} from 'vue';
import(/* webpackChunkName: "content-components" */ '@/components/content-blocks/DocumentBlock'); const DocumentBlock = defineAsyncComponent(() => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/DocumentBlock'));
const InfoIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/InfoIcon'); const InfoIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/InfoIcon'));
export default { export default {
props: { props: {
userInput: { userInput: {
type: Object, type: Object,
default: () => ({}), default: () => ({})
}, },
showReopen: { showReopen: {
type: Boolean, type: Boolean,
default: true, default: true
}, },
sharedMsg: { sharedMsg: {
type: String, type: String,
default: '', default: ''
}, }
}, },
components: { components: {
@ -42,13 +52,13 @@ export default {
computed: { computed: {
text() { text() {
return newLineToParagraph(this.userInput.text); return newLineToParagraph(this.userInput.text);
}, }
}, }
}; };
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import '~styles/helpers'; @import "~styles/helpers";
.final-submission { .final-submission {
&__text { &__text {

View File

@ -10,7 +10,10 @@
/> />
</div> </div>
<div class="submission-form-container__actions" v-if="!isFinalOrReadOnly"> <div
class="submission-form-container__actions"
v-if="!isFinalOrReadOnly"
>
<button <button
class="submission-form-container__submit button button--primary button--white-bg" class="submission-form-container__submit button button--primary button--white-bg"
data-cy="submission-form-submit" data-cy="submission-form-submit"
@ -26,7 +29,11 @@
> >
{{ spellcheckText }} {{ spellcheckText }}
</button> </button>
<file-upload :document="userInput.document" v-if="allowsDocuments" @change-document-url="changeDocumentUrl" /> <file-upload
:document="userInput.document"
v-if="allowsDocuments"
@change-document-url="changeDocumentUrl"
/>
<slot /> <slot />
</div> </div>
@ -41,9 +48,11 @@
</template> </template>
<script> <script>
const SubmissionInput = () => import('@/components/content-blocks/assignment/SubmissionInput.vue'); import {defineAsyncComponent} from 'vue';
const FinalSubmission = () => import('@/components/content-blocks/assignment/FinalSubmission.vue'); const SubmissionInput = defineAsyncComponent(() => import('@/components/content-blocks/assignment/SubmissionInput'));
const FileUpload = () => import('@/components/ui/file-upload/FileUpload.vue'); const FinalSubmission = defineAsyncComponent(() => import('@/components/content-blocks/assignment/FinalSubmission'));
const FileUpload = defineAsyncComponent(() => import('@/components/ui/file-upload/FileUpload'));
export default { export default {
props: { props: {
@ -107,6 +116,7 @@ export default {
this.$emit('changeDocumentUrl', documentUrl); this.$emit('changeDocumentUrl', documentUrl);
}, },
}, },
}; };
</script> </script>
@ -149,4 +159,5 @@ export default {
display: inline-block; display: inline-block;
} }
} }
</style> </style>

View File

@ -11,18 +11,25 @@
v-auto-grow v-auto-grow
@input="$emit('input', $event.target.value)" @input="$emit('input', $event.target.value)"
/> />
<div class="submission-form__save-status submission-form__save-status--saved" v-if="saved"> <div
class="submission-form__save-status submission-form__save-status--saved"
v-if="saved"
>
<tick-circle-icon class="submission-form__save-status-icon" /> <tick-circle-icon class="submission-form__save-status-icon" />
</div> </div>
<div class="submission-form__save-status submission-form__save-status--unsaved" v-if="!saved"> <div
class="submission-form__save-status submission-form__save-status--unsaved"
v-if="!saved"
>
<loading-icon class="submission-form__save-status-icon submission-form__saving-icon" /> <loading-icon class="submission-form__save-status-icon submission-form__saving-icon" />
</div> </div>
</div> </div>
</template> </template>
<script> <script>
const TickCircleIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/TickCircleIcon'); import {defineAsyncComponent} from 'vue';
const LoadingIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/LoadingIcon'); const TickCircleIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/TickCircleIcon'));
const LoadingIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/LoadingIcon'));
export default { export default {
props: { props: {
@ -31,18 +38,18 @@ export default {
readonly: Boolean, readonly: Boolean,
placeholder: { placeholder: {
type: String, type: String,
default: 'Ergebnis erfassen', default: 'Ergebnis erfassen'
}, }
}, },
components: { components: {
TickCircleIcon, TickCircleIcon,
LoadingIcon, LoadingIcon
}, }
}; };
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import '~styles/helpers'; @import "~styles/helpers";
.submission-form { .submission-form {
display: flex; display: flex;

View File

@ -5,7 +5,7 @@
class="assignment-form__title skillbox-input" class="assignment-form__title skillbox-input"
placeholder="Aufgabentitel" placeholder="Aufgabentitel"
@input="$emit('assignment-change-title', $event.target.value, index)" @input="$emit('assignment-change-title', $event.target.value, index)"
/> >
<textarea <textarea
:value="value.assignment" :value="value.assignment"
class="assignment-form__exercise-text skillbox-textarea" class="assignment-form__exercise-text skillbox-textarea"
@ -20,18 +20,19 @@
</template> </template>
<script> <script>
const InfoIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/InfoIcon'); import {defineAsyncComponent} from 'vue';
const InfoIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/InfoIcon'));
export default { export default {
props: ['value', 'index'], props: ['value', 'index'],
components: { components: {
InfoIcon, InfoIcon
}, }
}; };
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import '~styles/helpers'; @import "~styles/helpers";
.assignment-form { .assignment-form {
display: grid; display: grid;

View File

@ -1,12 +1,28 @@
<template> <template>
<div class="document-form" ref="documentform"> <div
<div v-if="!value.url" ref="uploadcare-panel" /> class="document-form"
<div class="document-form__spinner" v-if="loading"> ref="documentform"
>
<div
v-if="!value.url"
ref="uploadcare-panel"
/>
<div
class="document-form__spinner"
v-if="loading"
>
<loading-icon class="document-form__loading-icon" /> <loading-icon class="document-form__loading-icon" />
</div> </div>
<div class="document-form__uploaded" v-if="value.url"> <div
class="document-form__uploaded"
v-if="value.url"
>
<document-icon class="document-form__icon" /> <document-icon class="document-form__icon" />
<a :href="previewUrl" class="document-form__link" target="_blank">{{ previewLink }}</a> <a
:href="previewUrl"
class="document-form__link"
target="_blank"
>{{ previewLink }}</a>
</div> </div>
</div> </div>
</template> </template>
@ -14,8 +30,9 @@
<script> <script>
import {uploadcare} from '@/helpers/uploadcare'; import {uploadcare} from '@/helpers/uploadcare';
import LoadingIcon from '@/components/icons/LoadingIcon'; import LoadingIcon from '@/components/icons/LoadingIcon';
import {defineAsyncComponent} from 'vue';
const DocumentIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/DocumentIcon'); const DocumentIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/DocumentIcon'));
export default { export default {
props: ['value', 'index'], props: ['value', 'index'],
@ -48,22 +65,19 @@ export default {
}, },
mounted() { mounted() {
uploadcare( uploadcare(this, url => {
this,
(url) => {
this.$emit('change-url', url, this.index); this.$emit('change-url', url, this.index);
this.loading = false; this.loading = false;
}, }, () => {
() => {
this.loading = true; this.loading = true;
}
); });
}, },
}; };
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import '~styles/helpers'; @import "~styles/helpers";
.document-form { .document-form {
&__uploaded { &__uploaded {

View File

@ -1,20 +1,28 @@
<template> <template>
<div class="tip-tap"> <div class="tip-tap">
<editor-content class="tip-tap__editor-wrapper" :editor="editor" /> <editor-content
class="tip-tap__editor-wrapper"
:editor="editor"
/>
<toggle :bordered="false" :checked="isList" label="Als Liste formatieren" @input="toggleList" /> <toggle
:bordered="false"
:checked="isList"
label="Als Liste formatieren"
@input="toggleList"
/>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import Vue, { PropType } from 'vue'; import {PropType, defineComponent} from 'vue';
import { Editor, EditorContent } from '@tiptap/vue-2'; import {Editor, EditorContent} from "@tiptap/vue-3";
import Document from '@tiptap/extension-document'; import Document from '@tiptap/extension-document';
import Paragraph from '@tiptap/extension-paragraph'; import Paragraph from '@tiptap/extension-paragraph';
import Text from '@tiptap/extension-text'; import Text from '@tiptap/extension-text';
import BulletList from '@tiptap/extension-bullet-list'; import BulletList from '@tiptap/extension-bullet-list';
import ListItem from '@tiptap/extension-list-item'; import ListItem from '@tiptap/extension-list-item';
import Toggle from '@/components/ui/Toggle.vue'; import Toggle from "@/components/ui/Toggle.vue";
interface Data { interface Data {
editor: Editor | undefined; editor: Editor | undefined;
@ -23,11 +31,11 @@ interface Value {
text: string; text: string;
} }
export default Vue.extend({ export default defineComponent({
props: { props: {
value: { value: {
type: Object as PropType<Value>, type: Object as PropType<Value>,
validator(value: Value) { validator: (value: Value) => {
return Object.prototype.hasOwnProperty.call(value, 'text'); return Object.prototype.hasOwnProperty.call(value, 'text');
}, },
}, },
@ -35,7 +43,7 @@ export default Vue.extend({
components: { components: {
Toggle, Toggle,
EditorContent, EditorContent
}, },
data(): Data { data(): Data {
@ -49,8 +57,8 @@ export default Vue.extend({
return this.editor?.isActive('bulletList') || false; return this.editor?.isActive('bulletList') || false;
}, },
text(): string { text(): string {
return this.value.text; return this.value?.text || '';
}, }
}, },
watch: { watch: {
@ -63,27 +71,33 @@ export default Vue.extend({
} }
editor.commands.setContent(text, false); editor.commands.setContent(text, false);
}, }
}, },
mounted() { mounted() {
this.editor = new Editor({ this.editor = new Editor({
editorProps: { editorProps: {
attributes: { attributes: {
class: 'tip-tap__editor', class: 'tip-tap__editor'
}, }
}, },
content: this.text, content: this.text,
extensions: [Document, Paragraph, Text, BulletList, ListItem], extensions: [
Document,
Paragraph,
Text,
BulletList,
ListItem
],
onUpdate: () => { onUpdate: () => {
const text=(this.editor as Editor).getHTML(); const text=(this.editor as Editor).getHTML();
this.$emit('input', text); this.$emit('input', text);
this.$emit('change-text', text); this.$emit('change-text', text);
}, }
}); });
}, },
beforeDestroy() { beforeUnmount() {
this.editor?.destroy(); this.editor?.destroy();
}, },
@ -92,39 +106,42 @@ export default Vue.extend({
const editor = this.editor as Editor; const editor = this.editor as Editor;
editor.chain().selectAll().toggleBulletList().run(); editor.chain().selectAll().toggleBulletList().run();
}, },
}, }
}); });
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import '~styles/helpers'; @import '~styles/helpers';
.tip-tap { .tip-tap {
&__editor-wrapper { &__editor-wrapper {
margin-bottom: $medium-spacing; margin-bottom: $medium-spacing;
} }
:deep(.tip-tap__editor) { /deep/ &__editor {
@include inputstyle; @include inputstyle;
flex-direction: column; flex-direction: column;
min-height: 150px; min-height: 150px;
} }
:deep(ul) { /deep/ ul {
padding-left: $medium-spacing; padding-left: $medium-spacing;
list-style: initial; list-style: initial;
} }
:deep(li) { /deep/ li {
@include inputfont; @include inputfont;
} }
:deep(div) { /deep/ div {
@include inputfont; @include inputfont;
} }
:deep(p) { /deep/ p {
@include inputfont; @include inputfont;
} }
} }
</style> </style>

View File

@ -1,11 +1,21 @@
<template> <template>
<div> <div>
<div class="video-form" v-if="!isVimeo && !isYoutube && !isSrf"> <div
class="video-form"
v-if="!isVimeo && !isYoutube && !isSrf"
>
<info-icon class="video-form__help-icon help-text__icon" /> <info-icon class="video-form__help-icon help-text__icon" />
<p class="video-form__help-description help-text__description"> <p class="video-form__help-description help-text__description">
Sie können Videos auf Sie können Videos auf <a
<a class="video-form__platform-link help-text__link" href="https://youtube.com/" target="_blank">Youtube</a> class="video-form__platform-link help-text__link"
oder <a class="video-form__platform-link help-text__link" href="https://vimeo.com/" target="_blank">Vimeo</a> href="https://youtube.com/"
target="_blank"
>Youtube</a>
oder <a
class="video-form__platform-link help-text__link"
href="https://vimeo.com/"
target="_blank"
>Vimeo</a>
hochladen und anschliessen einen Link hier einfügen. hochladen und anschliessen einen Link hier einfügen.
</p> </p>
@ -14,7 +24,7 @@
class="video-form__video-link skillbox-input" class="video-form__video-link skillbox-input"
placeholder="Bsp: https://www.youtube.com/watch?v=dQw4w9WgXcQ" placeholder="Bsp: https://www.youtube.com/watch?v=dQw4w9WgXcQ"
@input="$emit('change-url', $event.target.value, index)" @input="$emit('change-url', $event.target.value, index)"
/> >
</div> </div>
<div v-if="isYoutube"> <div v-if="isYoutube">
@ -34,7 +44,8 @@ import YoutubeEmbed from '@/components/videos/YoutubeEmbed';
import VimeoEmbed from '@/components/videos/VimeoEmbed'; import VimeoEmbed from '@/components/videos/VimeoEmbed';
import SrfEmbed from '@/components/videos/SrfEmbed'; import SrfEmbed from '@/components/videos/SrfEmbed';
import {isVimeoUrl, isYoutubeUrl, isSrfUrl} from '@/helpers/video'; import {isVimeoUrl, isYoutubeUrl, isSrfUrl} from '@/helpers/video';
const InfoIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/InfoIcon'); import {defineAsyncComponent} from 'vue';
const InfoIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/InfoIcon'));
export default { export default {
props: ['value', 'index'], props: ['value', 'index'],
@ -43,7 +54,7 @@ export default {
InfoIcon, InfoIcon,
YoutubeEmbed, YoutubeEmbed,
VimeoEmbed, VimeoEmbed,
SrfEmbed, SrfEmbed
}, },
computed: { computed: {
@ -55,14 +66,14 @@ export default {
}, },
isSrf() { isSrf() {
return isSrfUrl(this.value.url); return isSrfUrl(this.value.url);
}, }
}, }
}; };
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import '@/styles/_variables.scss'; @import "@/styles/_variables.scss";
@import '@/styles/_functions.scss'; @import "@/styles/_functions.scss";
.video-form { .video-form {
display: grid; display: grid;
@ -73,9 +84,11 @@ export default {
align-items: center; align-items: center;
&__help-icon { &__help-icon {
} }
&__help-description { &__help-description {
} }
&__platform-link { &__platform-link {
@ -87,7 +100,7 @@ export default {
&__video-link { &__video-link {
grid-column: 1 / span 2; grid-column: 1 / span 2;
width: $modal-input-width; width: $modal-input-width
} }
} }
</style> </style>

View File

@ -1,16 +1,29 @@
<template> <template>
<a :class="typeClass" class="filter-entry" data-cy="filter-entry" :style="categoryStyle"> <a
:class="typeClass"
class="filter-entry"
data-cy="filter-entry"
:style="categoryStyle"
@click="$emit('filter')"
>
<span class="filter-entry__text">{{ text }}</span> <span class="filter-entry__text">{{ text }}</span>
<span :style="activeStyle" class="filter-entry__icon-wrapper"> <span
<chevron-right :style="{ fill: category.foreground }" class="filter-entry__icon" /> :style="activeStyle"
class="filter-entry__icon-wrapper"
>
<chevron-right
:style="{fill: category.foreground}"
class="filter-entry__icon"
/>
</span> </span>
</a> </a>
</template> </template>
<script> <script>
import INSTRUMENT_FILTER_QUERY from 'gql/local/instrumentFilter.gql'; import INSTRUMENT_FILTER_QUERY from 'gql/local/instrumentFilter.gql';
import {defineAsyncComponent} from 'vue';
const ChevronRight = () => import(/* webpackChunkName: "icons" */ '@/components/icons/ChevronRight'); const ChevronRight = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/ChevronRight'));
export default { export default {
props: { props: {
@ -36,6 +49,8 @@ export default {
ChevronRight, ChevronRight,
}, },
emits: ['filter'],
apollo: { apollo: {
instrumentFilter: { instrumentFilter: {
query: INSTRUMENT_FILTER_QUERY, query: INSTRUMENT_FILTER_QUERY,

View File

@ -1,12 +1,13 @@
<template> <template>
<div class="filter-group"> <div class="filter-group">
<filter-entry <filter-entry
:text="title"
v-bind="$attrs" v-bind="$attrs"
:text="title"
:type="category"
:category="category" :category="category"
:is-category="true" :is-category="true"
:id="category.id" :id="category.id"
@click.native="setCategoryFilter(category.id)" @filter="setCategoryFilter(category.id)"
/> />
<div class="filter-group__children"> <div class="filter-group__children">
<filter-entry <filter-entry
@ -16,19 +17,20 @@
v-for="type in types" v-for="type in types"
:id="type.id" :id="type.id"
:key="type.id" :key="type.id"
@click.native="setFilter(`type:${type.id}`)" @filter="setFilter(`type:${type.id}`)"
/> />
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import {defineComponent} from 'vue';
import FilterEntry from '@/components/instruments/FilterEntry'; import FilterEntry from '@/components/instruments/FilterEntry';
import SET_FILTER_MUTATION from 'gql/local/mutations/setInstrumentFilter.gql'; import SET_FILTER_MUTATION from 'gql/local/mutations/setInstrumentFilter.gql';
import INSTRUMENT_FILTER_QUERY from 'gql/local/instrumentFilter.gql'; import INSTRUMENT_FILTER_QUERY from 'gql/local/instrumentFilter.gql';
export default { export default defineComponent({
props: { props: {
title: { title: {
type: String, type: String,
@ -49,15 +51,15 @@ export default {
apollo: { apollo: {
instrumentFilter: { instrumentFilter: {
query: INSTRUMENT_FILTER_QUERY, query: INSTRUMENT_FILTER_QUERY
}, }
}, },
data() { data() {
return { return {
instrumentFilter: { instrumentFilter: {
currentFilter: '', currentFilter: ''
}, }
}; };
}, },
inheritAttrs: false, inheritAttrs: false,
@ -74,12 +76,12 @@ export default {
this.$apollo.mutate({ this.$apollo.mutate({
mutation: SET_FILTER_MUTATION, mutation: SET_FILTER_MUTATION,
variables: { variables: {
filter, filter
}
});
}
}, },
}); });
},
},
};
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">

View File

@ -1,11 +1,12 @@
const Modal = () => import(/* webpackChunkName: "modals" */ '@/components/Modal'); import {defineAsyncComponent} from 'vue';
const FullscreenImage = () => import(/* webpackChunkName: "modals" */ '@/components/FullscreenImage'); const Modal = defineAsyncComponent(() => import(/* webpackChunkName: "modals" */'@/components/Modal'));
const FullscreenInfographic = () => import(/* webpackChunkName: "modals" */ '@/components/FullscreenInfographic'); const FullscreenImage = defineAsyncComponent(() => import(/* webpackChunkName: "modals" */'@/components/FullscreenImage'));
const FullscreenVideo = () => import(/* webpackChunkName: "modals" */ '@/components/FullscreenVideo'); const FullscreenInfographic = defineAsyncComponent(() => import(/* webpackChunkName: "modals" */'@/components/FullscreenInfographic'));
const DeactivatePerson = () => import(/* webpackChunkName: "modals" */ '@/components/profile/DeactivatePerson'); const FullscreenVideo = defineAsyncComponent(() => import(/* webpackChunkName: "modals" */'@/components/FullscreenVideo'));
const SnapshotCreated = () => import(/* webpackChunkName: "modals" */ '@/components/modules/SnapshotCreated'); const DeactivatePerson = defineAsyncComponent(() => import(/* webpackChunkName: "modals" */'@/components/profile/DeactivatePerson'));
const ChangeVisibility = () => import(/* webpackChunkName: "modals" */ '@/components/rooms/ChangeVisibility'); const SnapshotCreated = defineAsyncComponent(() => import(/* webpackChunkName: "modals" */'@/components/modules/SnapshotCreated'));
const Confirm = () => import(/* webpackChunkName: "modals" */ '@/components/modals/Confirm'); const ChangeVisibility = defineAsyncComponent(() => import(/* webpackChunkName: "modals" */'@/components/rooms/ChangeVisibility'));
const Confirm = defineAsyncComponent(() => import(/* webpackChunkName: "modals" */'@/components/modals/Confirm'));
export default { export default {
Modal, Modal,

View File

@ -5,28 +5,8 @@
:slug="module.topic.slug" :slug="module.topic.slug"
class="module-navigation__topic-link" class="module-navigation__topic-link"
type="topic" type="topic"
v-if="module.topic"
/> />
<div class="module-navigation__module-content" v-if="false">
<!-- Do not display this for now, might be used later again though -->
<router-link :to="moduleContentLink" tag="h3" class="module-navigation__heading">
Inhalte: {{ module.metaTitle }}
</router-link>
<div class="module-navigation__anchors" v-if="onModulePage">
<a href="#" class="module-navigation__anchor" v-scroll-to="'#meta-title'">Einleitung</a>
<a href="#" class="module-navigation__anchor" v-scroll-to="'#objectives'">Lernziele</a>
<a
href="#"
class="module-navigation__anchor"
v-for="(chapter, index) in module.chapters"
:key="chapter.id"
v-scroll-to="chapterId(index)"
>{{ chapter.title }}</a
>
<a href="#" class="module-navigation__anchor" v-scroll-to="'#objectives-confirm'">Lernzielkontrolle</a>
</div>
</div>
<div <div
class="module-navigation__toggle-menu" class="module-navigation__toggle-menu"
data-cy="module-teacher-menu" data-cy="module-teacher-menu"

View File

@ -1,6 +1,12 @@
<template> <template>
<router-link :to="moduleLink" :class="['module-teaser', { 'module-teaser--small': !teaser }]" tag="div"> <router-link
<div :style="{ backgroundImage: 'url(' + heroImage + ')' }" class="module-teaser__image" /> :to="moduleLink"
:class="['module-teaser', {'module-teaser--small': !teaser}]"
>
<div
:style="{backgroundImage: 'url('+heroImage+')'}"
class="module-teaser__image"
/>
<div class="module-teaser__body"> <div class="module-teaser__body">
<h3 class="module-teaser__meta-title"> <h3 class="module-teaser__meta-title">
{{ metaTitle }} {{ metaTitle }}
@ -21,24 +27,27 @@ export default {
computed: { computed: {
moduleLink() { moduleLink() {
if (this.slug) {
return { return {
name: 'module', name: 'module',
params: { params: {
slug: this.slug, slug: this.slug
}, }
}; };
}, } else {
}, return {};
}
}
}
}; };
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import '@/styles/_variables.scss'; @import "~styles/helpers";
@import '@/styles/_mixins.scss';
.module-teaser { .module-teaser {
box-shadow: 0 3px 9px 0 rgba(0, 0, 0, 0.12); box-shadow: 0 3px 9px 0 rgba(0, 0, 0, 0.12);
border: 1px solid #e2e2e2; border: 1px solid #E2E2E2;
height: 330px; height: 330px;
max-width: 380px; max-width: 380px;
width: 100%; width: 100%;

View File

@ -1,5 +1,8 @@
<template> <template>
<div class="bookmark-actions" v-if="!editMode"> <div
class="bookmark-actions"
v-if="!editMode"
>
<a <a
:class="{'bookmark-actions__action--bookmarked': bookmarked}" :class="{'bookmark-actions__action--bookmarked': bookmarked}"
class="bookmark-actions__action bookmark-actions__bookmark" class="bookmark-actions__action bookmark-actions__bookmark"
@ -29,35 +32,36 @@
</template> </template>
<script> <script>
const BookmarkIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/BookmarkIcon'); import {defineAsyncComponent} from 'vue';
const AddNoteIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/AddNoteIcon'); const BookmarkIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/BookmarkIcon'));
const NoteIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/NoteIcon'); const AddNoteIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/AddNoteIcon'));
const NoteIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/NoteIcon'));
export default { export default {
props: { props: {
bookmarked: { bookmarked: {
type: Boolean, type: Boolean,
default: false, default: false
}, },
note: { note: {
type: [Object, Boolean], type: [Object, Boolean],
default: false, default: false
}, },
editMode: { editMode: {
type: Boolean, type: Boolean,
default: false, default: false
}, }
}, },
components: { components: {
BookmarkIcon, BookmarkIcon,
AddNoteIcon, AddNoteIcon,
NoteIcon, NoteIcon
}, },
}; };
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import '~styles/helpers'; @import "~styles/helpers";
.bookmark-actions { .bookmark-actions {
height: 100%; height: 100%;
@ -84,8 +88,7 @@ export default {
display: flex; display: flex;
justify-content: center; justify-content: center;
&--bookmarked, &--bookmarked, &--noted {
&--noted {
opacity: 1; opacity: 1;
} }
} }
@ -98,4 +101,5 @@ export default {
} }
} }
} }
</style> </style>

View File

@ -6,7 +6,10 @@
placeholder="Lernziel erfassen..." placeholder="Lernziel erfassen..."
@input="$emit('input', $event)" @input="$emit('input', $event)"
/> />
<a class="icon-button" @click="$emit('delete')"> <a
class="icon-button"
@click="$emit('delete')"
>
<trash-icon class="icon-button__icon icon-button__icon--subtle" /> <trash-icon class="icon-button__icon icon-button__icon--subtle" />
</a> </a>
</div> </div>
@ -14,20 +17,21 @@
<script> <script>
import ModalInput from '@/components/ModalInput'; import ModalInput from '@/components/ModalInput';
const TrashIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/TrashIcon'); import {defineAsyncComponent} from 'vue';
const TrashIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/TrashIcon'));
export default { export default {
props: ['objective'], props: ['objective'],
components: { components: {
ModalInput, ModalInput,
TrashIcon, TrashIcon
}, }
}; };
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import '@/styles/_variables.scss'; @import "@/styles/_variables.scss";
.objective-form { .objective-form {
display: grid; display: grid;

View File

@ -2,8 +2,8 @@
<div class="page-form-input"> <div class="page-form-input">
<label :for="id" class="page-form-input__label">{{ label }}</label> <label :for="id" class="page-form-input__label">{{ label }}</label>
<component <component
:value="value"
:class="classes" :class="classes"
:value.prop="value"
:data-cy="cyId" :data-cy="cyId"
:is="type" :is="type"
:id="id" :id="id"

View File

@ -1,12 +1,16 @@
<template> <template>
<a class="add-project-entry" @click="addProjectEntry"> <a
class="add-project-entry"
@click="addProjectEntry"
>
<plus-icon class="add-project-entry__icon" /> <plus-icon class="add-project-entry__icon" />
<span class="add-project-entry__text">Beitrag erfassen</span> <span class="add-project-entry__text">Beitrag erfassen</span>
</a> </a>
</template> </template>
<script> <script>
const PlusIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/PlusIcon'); import {defineAsyncComponent} from 'vue';
const PlusIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/PlusIcon'));
export default { export default {
props: ['project'], props: ['project'],
components: {PlusIcon}, components: {PlusIcon},
@ -14,13 +18,13 @@ export default {
methods: { methods: {
addProjectEntry() { addProjectEntry() {
this.$store.dispatch('addProjectEntry', this.project); this.$store.dispatch('addProjectEntry', this.project);
}, }
}, }
}; };
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import '~styles/helpers'; @import "~styles/helpers";
.add-project-entry { .add-project-entry {
display: flex; display: flex;

View File

@ -1,9 +1,25 @@
<template> <template>
<div class="portfolio-onboarding"> <div class="portfolio-onboarding">
<h1 class="portfolio-onboarding__heading" data-cy="page-title">Portfolio</h1> <h1
<portfolio-illustration data-cy="portfolio-onboarding-illustration" class="portfolio-onboarding__illustration" /> class="portfolio-onboarding__heading"
<h2 class="portfolio-onboarding__subheading" data-cy="portfolio-onboarding-subtitle">Woran denken Sie gerade?</h2> data-cy="page-title"
<p class="portfolio-onboarding__text" data-cy="portfolio-onboarding-text"> >
Portfolio
</h1>
<portfolio-illustration
data-cy="portfolio-onboarding-illustration"
class="portfolio-onboarding__illustration"
/>
<h2
class="portfolio-onboarding__subheading"
data-cy="portfolio-onboarding-subtitle"
>
Woran denken Sie gerade?
</h2>
<p
class="portfolio-onboarding__text"
data-cy="portfolio-onboarding-text"
>
Hier können Sie Projekte erstellen, um Ihre Gedanken festzuhalten oder Ihre Arbeit zu dokumentieren. Hier können Sie Projekte erstellen, um Ihre Gedanken festzuhalten oder Ihre Arbeit zu dokumentieren.
</p> </p>
@ -12,8 +28,9 @@
</template> </template>
<script> <script>
const PortfolioIllustration = () => import('@/components/illustrations/PortfolioIllustration.vue'); import {defineAsyncComponent} from 'vue';
const CreateProjectButton = () => import('@/components/portfolio/CreateProjectButton.vue'); const PortfolioIllustration = defineAsyncComponent(() => import(/* webpackChunkName: "illustrations" */'@/components/illustrations/PortfolioIllustration'));
const CreateProjectButton = defineAsyncComponent(() => import('@/components/portfolio/CreateProjectButton'));
export default { export default {
components: {CreateProjectButton, PortfolioIllustration}, components: {CreateProjectButton, PortfolioIllustration},
}; };

View File

@ -1,20 +1,48 @@
<template> <template>
<div class="project-actions" data-cy="project-actions"> <div
<a class="project-actions__more-link" @click.stop="toggleMenu"> class="project-actions"
data-cy="project-actions"
>
<a
class="project-actions__more-link"
@click.stop="toggleMenu"
>
<ellipses /> <ellipses />
</a> </a>
<widget-popover class="project-actions__popover" v-if="showMenu" @hide-me="showMenu = false"> <widget-popover
class="project-actions__popover"
v-if="showMenu"
@hide-me="showMenu = false"
>
<li class="popover-links__link"> <li class="popover-links__link">
<a data-cy="delete-project" @click="deleteProject(slug)">Projekt löschen</a> <a
data-cy="delete-project"
@click="deleteProject(slug)"
>Projekt löschen</a>
</li> </li>
<li class="popover-links__link"> <li class="popover-links__link">
<a data-cy="edit-project" @click="editProject(slug)">Projekt bearbeiten</a> <a
data-cy="edit-project"
@click="editProject(slug)"
>Projekt bearbeiten</a>
</li> </li>
<li class="popover-links__link" v-if="!final && shareButtons"> <li
<a data-cy="share-project" @click="updateProjectShareState(slug, true)">Projekt teilen</a> class="popover-links__link"
v-if="!final && shareButtons"
>
<a
data-cy="share-project"
@click="updateProjectShareState(slug, true)"
>Projekt teilen</a>
</li> </li>
<li class="popover-links__link" v-if="final && shareButtons"> <li
<a data-cy="unshare-project" @click="updateProjectShareState(slug, false)">Projekt nicht mehr teilen</a> class="popover-links__link"
v-if="final && shareButtons"
>
<a
data-cy="unshare-project"
@click="updateProjectShareState(slug, false)"
>Projekt nicht mehr teilen</a>
</li> </li>
</widget-popover> </widget-popover>
</div> </div>
@ -28,7 +56,8 @@ import PROJECTS_QUERY from '@/graphql/gql/queries/allProjects.gql';
import updateProjectShareState from '@/mixins/update-project-share-state'; import updateProjectShareState from '@/mixins/update-project-share-state';
import {removeAtIndex} from '@/graphql/immutable-operations.ts'; import {removeAtIndex} from '@/graphql/immutable-operations.ts';
const Ellipses = () => import(/* webpackChunkName: "icons" */ '@/components/icons/Ellipses.vue'); import {defineAsyncComponent} from 'vue';
const Ellipses = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/Ellipses.vue'));
export default { export default {
props: { props: {
@ -67,38 +96,30 @@ export default {
this.$router.push({name: 'edit-project', params: {slug}}); this.$router.push({name: 'edit-project', params: {slug}});
}, },
deleteProject(slug) { deleteProject(slug) {
this.$apollo this.$apollo.mutate({
.mutate({
mutation: DELETE_PROJECT_MUTATION, mutation: DELETE_PROJECT_MUTATION,
variables: { variables: {
input: { input: {
slug, slug,
}, },
}, },
update( update(store, {data: {deleteProject: {success}}}) {
store,
{
data: {
deleteProject: { success },
},
}
) {
if (success) { if (success) {
const {projects: prevProjects} = store.readQuery({query: PROJECTS_QUERY}); const {projects: prevProjects} = store.readQuery({query: PROJECTS_QUERY});
if (prevProjects) { if (prevProjects) {
let index = prevProjects.findIndex((project) => project.slug === slug); let index = prevProjects.findIndex(project => project.slug === slug);
const projects = removeAtIndex(prevProjects, index); const projects = removeAtIndex(prevProjects, index);
const data = { const data = {
projects, projects
}; };
store.writeQuery({query: PROJECTS_QUERY, data}); store.writeQuery({query: PROJECTS_QUERY, data});
} }
} }
}, },
}) }).then(() => {
.then(() => {
this.$router.push('/portfolio'); this.$router.push('/portfolio');
}); });
}, },
@ -107,7 +128,7 @@ export default {
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import '~styles/_helpers.scss'; @import "~styles/_helpers.scss";
.project-actions { .project-actions {
position: relative; position: relative;

View File

@ -1,21 +1,43 @@
<template> <template>
<div class="project-entry" data-cy="project-entry"> <div
<more-options-widget class="project-entry__more" data-cy="project-entry-more" v-if="!readOnly"> class="project-entry"
data-cy="project-entry"
>
<more-options-widget
class="project-entry__more"
data-cy="project-entry-more"
v-if="!readOnly"
>
<li class="popover-links__link"> <li class="popover-links__link">
<a data-cy="edit-project-entry" @click="editProjectEntry()">Eintrag bearbeiten</a> <a
data-cy="edit-project-entry"
@click="editProjectEntry()"
>Eintrag bearbeiten</a>
</li> </li>
<li class="popover-links__link"> <li class="popover-links__link">
<a data-cy="delete-project-entry" @click="deleteProjectEntry()">Eintrag löschen</a> <a
data-cy="delete-project-entry"
@click="deleteProjectEntry()"
>Eintrag löschen</a>
</li> </li>
</more-options-widget> </more-options-widget>
<h3 class="project-entry__heading" data-cy="project-entry-date"> <h3
class="project-entry__heading"
data-cy="project-entry-date"
>
{{ createdDateTime }} {{ createdDateTime }}
</h3> </h3>
<p class="project-entry__paragraph" data-cy="project-entry-activity"> <p
class="project-entry__paragraph"
data-cy="project-entry-activity"
>
{{ description }} {{ description }}
</p> </p>
<p class="project-entry__paragraph" v-if="documentUrl"> <p
class="project-entry__paragraph"
v-if="documentUrl"
>
<document-block :value="{url: documentUrl}" /> <document-block :value="{url: documentUrl}" />
</p> </p>
<div class="project-entry__date"> <div class="project-entry__date">
@ -31,9 +53,9 @@ import DELETE_PROJECT_ENTRY_MUTATION from '@/graphql/gql/mutations/deleteProject
import PROJECT_QUERY from '@/graphql/gql/queries/projectQuery.gql'; import PROJECT_QUERY from '@/graphql/gql/queries/projectQuery.gql';
import {dateFilter, dateTimeFilter} from '@/filters/date-filter'; import {dateFilter, dateTimeFilter} from '@/filters/date-filter';
import {removeAtIndex} from '@/graphql/immutable-operations.ts'; import {removeAtIndex} from '@/graphql/immutable-operations.ts';
import {defineAsyncComponent} from 'vue';
const DocumentBlock = defineAsyncComponent(() => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/DocumentBlock'));
const DocumentBlock = () =>
import(/* webpackChunkName: "content-components" */ '@/components/content-blocks/DocumentBlock');
export default { export default {
props: ['description', 'documentUrl', 'created', 'id', 'readOnly'], props: ['description', 'documentUrl', 'created', 'id', 'readOnly'],
@ -64,14 +86,7 @@ export default {
id: this.id, id: this.id,
}, },
}, },
update( update(store, {data: {deleteProjectEntry: {success}}}) {
store,
{
data: {
deleteProjectEntry: { success },
},
}
) {
if (success) { if (success) {
const query = PROJECT_QUERY; const query = PROJECT_QUERY;
const variables = { const variables = {
@ -79,7 +94,7 @@ export default {
}; };
const {project} = store.readQuery({query, variables}); const {project} = store.readQuery({query, variables});
if (project) { if (project) {
const index = project.entries.findIndex((entry) => entry.id === projectEntry.id); const index = project.entries.findIndex(entry => entry.id === projectEntry.id);
const entries = removeAtIndex(project.entries, index); const entries = removeAtIndex(project.entries, index);
const data = { const data = {
project: { project: {
@ -98,7 +113,7 @@ export default {
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import '~styles/helpers'; @import "~styles/helpers";
.project-entry { .project-entry {
background-color: $color-white; background-color: $color-white;
@ -136,5 +151,6 @@ export default {
display: block; display: block;
} }
} }
} }
</style> </style>

View File

@ -1,7 +1,12 @@
<template> <template>
<modal :hide-header="false"> <modal :hide-header="false">
<template #header> <template #header>
<h2 class="project-entry-modal__heading" data-cy="modal-title">Beitrag erfassen</h2> <h2
class="project-entry-modal__heading"
data-cy="modal-title"
>
Beitrag erfassen
</h2>
</template> </template>
<div class="project-entry-modal"> <div class="project-entry-modal">
@ -18,7 +23,7 @@
icon="document-with-lines-icon" icon="document-with-lines-icon"
data-cy="use-template-button" data-cy="use-template-button"
text="Vorlage nutzen" text="Vorlage nutzen"
@click.native="useTemplate" @click="useTemplate"
/> />
<file-upload <file-upload
@ -30,12 +35,17 @@
</div> </div>
</div> </div>
</div> </div>
<div slot="footer"> <template #footer>
<a class="button button--primary" data-cy="modal-save-button" @click="$emit('save', localProjectEntry)" <a
>Speichern</a class="button button--primary"
> data-cy="modal-save-button"
<a class="button" @click="$emit('hide')">Abbrechen</a> @click="$emit('save', localProjectEntry)"
</div> >Speichern</a>
<a
class="button"
@click="$emit('hide')"
>Abbrechen</a>
</template>
</modal> </modal>
</template> </template>
@ -44,7 +54,9 @@ import Modal from '@/components/Modal';
import ButtonWithIconAndText from '@/components/ui/ButtonWithIconAndText'; import ButtonWithIconAndText from '@/components/ui/ButtonWithIconAndText';
import {PROJECT_ENTRY_TEMPLATE} from '@/consts/strings.consts'; import {PROJECT_ENTRY_TEMPLATE} from '@/consts/strings.consts';
const FileUpload = () => import('@/components/ui/file-upload/FileUpload.vue');
import {defineAsyncComponent} from 'vue';
const FileUpload = defineAsyncComponent(() => import('@/components/ui/file-upload/FileUpload'));
export default { export default {
props: { props: {
@ -62,12 +74,9 @@ export default {
data() { data() {
return { return {
localProjectEntry: Object.assign( localProjectEntry: Object.assign({}, {
{},
{
...this.projectEntry, ...this.projectEntry,
} }),
),
}; };
}, },
@ -83,7 +92,7 @@ export default {
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import '~styles/helpers'; @import "~styles/helpers";
.project-entry-modal { .project-entry-modal {
display: flex; display: flex;
@ -126,5 +135,7 @@ export default {
@include heading-3; @include heading-3;
margin-bottom: 0; margin-bottom: 0;
} }
} }
</style> </style>

View File

@ -1,8 +1,18 @@
<template> <template>
<page-form :title="title" @save="$emit('save', localProject)"> <page-form
<page-form-input label="Titel" v-model="localProject.title" /> :title="title"
<page-form-input label="Beschreibung" type="textarea" v-model="localProject.description" /> @save="$emit('save', localProject)"
<template slot="footer"> >
<page-form-input
label="Titel"
v-model="localProject.title"
/>
<page-form-input
label="Beschreibung"
type="textarea"
v-model="localProject.description"
/>
<template #footer>
<button <button
:class="{ 'button--disabled': !formValid }" :class="{ 'button--disabled': !formValid }"
:disabled="!formValid" :disabled="!formValid"
@ -12,7 +22,12 @@
> >
Speichern Speichern
</button> </button>
<router-link to="/portfolio" tag="button" class="button"> Abbrechen </router-link> <router-link
to="/portfolio"
class="button"
>
Abbrechen
</router-link>
</template> </template>
</page-form> </page-form>
</template> </template>

View File

@ -1,11 +1,6 @@
<template> <template>
<li class="project"> <li class="project">
<router-link <router-link :to="{ name: 'project', params: { slug: project.slug } }" class="project__link" data-cy="project-link">
:to="{ name: 'project', params: { slug: project.slug } }"
tag="div"
class="project__link"
data-cy="project-link"
>
<span class="project__title" data-cy="project-title">{{ project.title }}</span> <span class="project__title" data-cy="project-title">{{ project.title }}</span>
<owner-widget :owner="project.student" class="project__owner" /> <owner-widget :owner="project.student" class="project__owner" />

View File

@ -1,5 +1,8 @@
<template> <template>
<a class="share-icon"> <a
class="share-icon"
@click="$emit('share')"
>
<share-icon class="share-icon__icon" /> <share-icon class="share-icon__icon" />
<span class="share-icon__text"> <span class="share-icon__text">
<template v-if="!final">Mit Lehrperson teilen</template> <template v-if="!final">Mit Lehrperson teilen</template>
@ -9,7 +12,8 @@
</template> </template>
<script> <script>
const ShareIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/ShareIcon'); import {defineAsyncComponent} from 'vue';
const ShareIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/ShareIcon'));
export default { export default {
props: { props: {
@ -18,6 +22,7 @@ export default {
default: false, default: false,
}, },
}, },
emits: ['share'],
components: {ShareIcon}, components: {ShareIcon},
}; };
</script> </script>

View File

@ -7,27 +7,31 @@
<slot /> <slot />
</div> </div>
<div class="activity-entry__link" @click="$emit('link')"> <div
class="activity-entry__link"
@click="$emit('link')"
>
<chevron-right class="activity-entry__icon" /> <chevron-right class="activity-entry__icon" />
</div> </div>
</div> </div>
</template> </template>
<script> <script>
const ChevronRight = () => import(/* webpackChunkName: "icons" */ '@/components/icons/ChevronRight'); import {defineAsyncComponent} from 'vue';
const ChevronRight = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/ChevronRight'));
export default { export default {
props: ['title'], props: ['title'],
components: { components: {
ChevronRight, ChevronRight
}, }
}; };
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import '@/styles/_variables.scss'; @import "@/styles/_variables.scss";
@import '@/styles/_mixins.scss'; @import "@/styles/_mixins.scss";
.activity-entry { .activity-entry {
padding: $small-spacing 0; padding: $small-spacing 0;
@ -59,7 +63,7 @@ export default {
width: 30px; width: 30px;
} }
:deep(p) { /deep/ p {
@include regular-text; @include regular-text;
} }
} }

View File

@ -15,9 +15,17 @@
ref="avatarImage" ref="avatarImage"
/> />
</transition> </transition>
<img :src="avatarUrl" class="avatar__fake-image" ref="fakeImage" /> <img
:src="avatarUrl"
class="avatar__fake-image"
ref="fakeImage"
>
<div class="avatar__edit" v-if="editable" @click="closeSidebar"> <div
class="avatar__edit"
v-if="editable"
@click="closeSidebar"
>
<router-link :to="{name: 'profile'}"> <router-link :to="{name: 'profile'}">
<pen-icon /> <pen-icon />
</router-link> </router-link>
@ -27,27 +35,28 @@
<script> <script>
import TOGGLE_SIDEBAR from '@/graphql/gql/local/mutations/toggleSidebar.gql'; import TOGGLE_SIDEBAR from '@/graphql/gql/local/mutations/toggleSidebar.gql';
import {defineAsyncComponent} from 'vue';
const DefaultAvatar = () => import(/* webpackChunkName: "icons" */ '@/components/icons/DefaultAvatar'); const DefaultAvatar = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/DefaultAvatar'));
const PenIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/PenIcon'); const PenIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/PenIcon'));
export default { export default {
props: { props: {
avatarUrl: { avatarUrl: {
type: String, type: String
}, },
iconHighlighted: {}, iconHighlighted: {},
editable: { editable: {
default: false, default: false
}, }
}, },
components: { components: {
DefaultAvatar, DefaultAvatar,
PenIcon, PenIcon
}, },
data() { data() {
return { return {
isAvatarLoaded: false, isAvatarLoaded: false
}; };
}, },
mounted() { mounted() {
@ -66,17 +75,17 @@ export default {
mutation: TOGGLE_SIDEBAR, mutation: TOGGLE_SIDEBAR,
variables: { variables: {
sidebar: { sidebar: {
profile: false, profile: false
}, }
}, }
}); });
}, }
}, }
}; };
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import '~styles/helpers'; @import "~styles/helpers";
$max-width: 100%; $max-width: 100%;
@ -130,12 +139,10 @@ $max-width: 100%;
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.12); box-shadow: 0 3px 10px rgba(0, 0, 0, 0.12);
} }
.fade-leave-active, .fade-leave-active, .show-enter-active {
.show-enter-active { transition: opacity .5s;
transition: opacity 0.5s;
} }
.fade-leave-to, .fade-leave-to, .show-enter-from {
.show-enter {
opacity: 0; opacity: 0;
} }

View File

@ -1,9 +1,15 @@
<template> <template>
<div class="content-bookmark module-activity-entry"> <div class="content-bookmark module-activity-entry">
<!-- eslint-disable vue/no-v-html --> <!-- eslint-disable vue/no-v-html -->
<div v-if="content.type === 'text_block'" v-html="text" /> <div
v-if="content.type === 'text_block'"
v-html="text"
/>
<div v-else-if="content.type === 'link_block'"> <div v-else-if="content.type === 'link_block'">
<link-block :value="content.value" :no-margin="true" /> <link-block
:value="content.value"
:no-margin="true"
/>
</div> </div>
<p v-else> <p v-else>
{{ type }} {{ type }}
@ -12,7 +18,8 @@
</template> </template>
<script> <script>
const LinkBlock = () => import(/* webpackChunkName: "content-components" */ '@/components/content-blocks/LinkBlock'); import {defineAsyncComponent} from 'vue';
const LinkBlock = defineAsyncComponent(() => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/LinkBlock'));
export default { export default {
props: ['bookmark'], props: ['bookmark'],
@ -20,8 +27,8 @@ export default {
computed: { computed: {
content() { content() {
return this.bookmark.contentBlock return this.bookmark.contentBlock
? this.bookmark.contentBlock.contents.find((e) => e.id === this.bookmark.uuid) ? this.bookmark.contentBlock.contents.find(e => e.id === this.bookmark.uuid)
: this.bookmark.instrument.contents.find((e) => e.id === this.bookmark.uuid); : this.bookmark.instrument.contents.find(e => e.id === this.bookmark.uuid);
}, },
text() { text() {
return this.content.value.text ? this.content.value.text : 'TO BE DEFINED'; return this.content.value.text ? this.content.value.text : 'TO BE DEFINED';
@ -39,7 +46,7 @@ export default {
default: default:
return this.content.type; return this.content.type;
} }
}, }
}, }
}; };
</script> </script>

View File

@ -1,21 +1,26 @@
<template> <template>
<a class="edit-group-name" data-cy="edit-group-name-link" @click="$emit('edit')"> <a
class="edit-group-name"
data-cy="edit-group-name-link"
@click="$emit('edit')"
>
<pen-icon class="edit-group-name__icon" /> <pen-icon class="edit-group-name__icon" />
</a> </a>
</template> </template>
<script> <script>
const PenIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/PenIcon'); import {defineAsyncComponent} from 'vue';
const PenIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/PenIcon'));
export default { export default {
components: { components: {
PenIcon, PenIcon
}, }
}; };
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import '~styles/_variables.scss'; @import "~styles/_variables.scss";
.edit-group-name { .edit-group-name {
&__icon { &__icon {

View File

@ -6,10 +6,15 @@
<modal-input :value="name" :placeholder="placeholder" data-cy="edit-name-input" @input="$emit('input', $event)" /> <modal-input :value="name" :placeholder="placeholder" data-cy="edit-name-input" @input="$emit('input', $event)" />
<template #footer> <template #footer>
<div slot="footer"> <a
<a class="button button--primary" data-cy="modal-save-button" @click="$emit('save')">Speichern</a> class="button button--primary"
<a class="button" @click="$emit('cancel')">Abbrechen</a> data-cy="modal-save-button"
</div> @click="$emit('save')"
>Speichern</a>
<a
class="button"
@click="$emit('cancel')"
>Abbrechen</a>
</template> </template>
</modal> </modal>
</template> </template>

View File

@ -1,15 +1,26 @@
<template> <template>
<div class="profile"> <div class="profile">
<h1 class="profile__header">Profilbild</h1> <h1 class="profile__header">
<div class="profile-avatar" v-if="me.avatarUrl"> Profilbild
</h1>
<div
class="profile-avatar"
v-if="me.avatarUrl"
>
<div class="profile-avatar__image"> <div class="profile-avatar__image">
<avatar :avatar-url="me.avatarUrl" /> <avatar :avatar-url="me.avatarUrl" />
</div> </div>
<a class="profile-avatar__remove icon-button" @click="deleteAvatar()"> <a
class="profile-avatar__remove icon-button"
@click="deleteAvatar()"
>
<trash-icon class="profile-avatar__remove-icon icon-button__icon icon-button__icon--subtle" /> <trash-icon class="profile-avatar__remove-icon icon-button__icon icon-button__icon--subtle" />
</a> </a>
</div> </div>
<avatar-upload-form v-else @avatarUpdate="updateAvatar" /> <avatar-upload-form
v-else
@avatarUpdate="updateAvatar"
/>
</div> </div>
</template> </template>
@ -18,20 +29,21 @@ import UPDATE_AVATAR_QUERY from '@/graphql/gql/mutations/updateAvatarUrl.gql';
import ME_QUERY from '@/graphql/gql/queries/meQuery.gql'; import ME_QUERY from '@/graphql/gql/queries/meQuery.gql';
import AvatarUploadForm from '@/components/profile/AvatarUploadForm'; import AvatarUploadForm from '@/components/profile/AvatarUploadForm';
import Avatar from '@/components/profile/Avatar'; import Avatar from '@/components/profile/Avatar';
const TrashIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/TrashIcon'); import {defineAsyncComponent} from 'vue';
const TrashIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/TrashIcon'));
export default { export default {
components: { components: {
AvatarUploadForm, AvatarUploadForm,
Avatar, Avatar,
TrashIcon, TrashIcon
}, },
data() { data() {
return { return {
me: { me: {
avatarUrl: '', avatarUrl: ''
}, }
}; };
}, },
apollo: { apollo: {
@ -44,46 +56,37 @@ export default {
this.updateAvatar(''); this.updateAvatar('');
}, },
updateAvatar (url) { updateAvatar (url) {
this.$apollo this.$apollo.mutate({
.mutate({
mutation: UPDATE_AVATAR_QUERY, mutation: UPDATE_AVATAR_QUERY,
variables: { variables: {
input: { input: {
avatarUrl: url, avatarUrl: url
},
},
update(
store,
{
data: {
updateAvatar: { success },
},
} }
) { },
update(store, {data: {updateAvatar: {success}}}) {
if (success) { if (success) {
const {me} = store.readQuery({query: ME_QUERY}); const {me} = store.readQuery({query: ME_QUERY});
if (me) { if (me) {
const data = { const data = {
me: { me: {
...me, ...me,
avatarUrl: url, avatarUrl: url
}, }
}; };
store.writeQuery({query: ME_QUERY, data}); store.writeQuery({query: ME_QUERY, data});
} }
} }
}, }
}) }).catch((error) => {
.catch((error) => {
console.warn('UploadError', error); console.warn('UploadError', error);
}); });
}, }
}, }
}; };
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import '@/styles/_variables.scss'; @import "@/styles/_variables.scss";
.profile-avatar { .profile-avatar {
display: flex; display: flex;
@ -98,4 +101,5 @@ export default {
.profile-avatar { .profile-avatar {
margin-bottom: $large-spacing; margin-bottom: $large-spacing;
} }
</style> </style>

View File

@ -1,31 +1,69 @@
<template> <template>
<transition name="slide"> <transition name="slide">
<div class="profile-sidebar" data-cy="sidebar" v-if="sidebar.profile" v-click-outside="close"> <div
<a class="profile-sidebar__close-link" data-cy="close-profile-sidebar-link" @click="close"> class="profile-sidebar"
data-cy="sidebar"
v-if="sidebar.profile"
v-click-outside="close"
>
<a
class="profile-sidebar__close-link"
data-cy="close-profile-sidebar-link"
@click="close"
>
<cross class="profile-sidebar__close-icon" /> <cross class="profile-sidebar__close-icon" />
</a> </a>
<div class="profile-sidebar__section"> <div class="profile-sidebar__section">
<profile-widget class="profile-sidebar__item" /> <profile-widget class="profile-sidebar__item" />
<div class="profile-sidebar__item" @click="close"> <div
<router-link to="/me/activity" class="profile-sidebar__link"> Meine Aktivitäten </router-link> class="profile-sidebar__item"
@click="close"
>
<router-link
to="/me/activity"
class="profile-sidebar__link"
>
Meine Aktivitäten
</router-link>
</div> </div>
<div class="profile-sidebar__item" v-if="me.isTeacher && !me.readOnly" @click="close"> <div
<router-link :to="myTeamPage" data-cy="my-team-link" class="profile-sidebar__link"> Mein Team </router-link> class="profile-sidebar__item"
v-if="me.isTeacher && !me.readOnly"
@click="close"
>
<router-link
:to="myTeamPage"
data-cy="my-team-link"
class="profile-sidebar__link"
>
Mein Team
</router-link>
</div> </div>
</div> </div>
<div class="profile-sidebar__section"> <div class="profile-sidebar__section">
<div class="profile-sidebar__item"> <div class="profile-sidebar__item">
<class-selection-widget /> <class-selection-widget />
<div @click="close"> <div @click="close">
<router-link :to="{ name: 'my-class' }" data-cy="class-list-link" class="profile-sidebar__link"> <router-link
:to="{name: 'my-class'}"
data-cy="class-list-link"
class="profile-sidebar__link"
>
Klassenliste Klassenliste
</router-link> </router-link>
</div> </div>
</div> </div>
</div> </div>
<div class="profile-sidebar__section"> <div class="profile-sidebar__section">
<div class="profile-sidebar__item" @click="close"> <div
<router-link :to="{ name: 'join-class' }" data-cy="join-class-link" class="profile-sidebar__link"> class="profile-sidebar__item"
@click="close"
>
<router-link
:to="{name:'join-class'}"
data-cy="join-class-link"
class="profile-sidebar__link"
>
Zugangscode Zugangscode
</router-link> </router-link>
</div> </div>
@ -46,9 +84,11 @@ import sidebar from '@/mixins/sidebar';
import me from '@/mixins/me'; import me from '@/mixins/me';
import LogoutWidget from '@/components/LogoutWidget'; import LogoutWidget from '@/components/LogoutWidget';
import {MY_TEAM} from '@/router/me.names'; import {MY_TEAM} from '@/router/me.names';
const Cross = () => import(/* webpackChunkName: "icons" */ '@/components/icons/CrossIcon'); import {defineAsyncComponent} from 'vue';
const Cross = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/CrossIcon'));
export default { export default {
mixins: [sidebar, me], mixins: [sidebar, me],
components: { components: {
@ -75,7 +115,7 @@ export default {
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import '~styles/helpers'; @import "~styles/helpers";
$desktop-width: 333px; $desktop-width: 333px;
@ -133,13 +173,11 @@ $desktop-width: 333px;
} }
.slide { .slide {
&-enter-active, &-enter-active, &-leave-active {
&-leave-active {
transition: right 0.2s; transition: right 0.2s;
} }
&-enter, &-enter-from, &-leave-to {
&-leave-to {
right: -100vw; right: -100vw;
@include desktop { @include desktop {
right: -$desktop-width; right: -$desktop-width;

View File

@ -1,13 +1,18 @@
<template> <template>
<router-link class="add-room-entry-button" data-cy="add-room-entry-button" :to="addRoomEntryRoute"> <router-link
class="add-room-entry-button"
data-cy="add-room-entry-button"
:to="addRoomEntryRoute"
>
<plus-icon class="add-room-entry-button__icon" /> <plus-icon class="add-room-entry-button__icon" />
<span class="add-room-entry-button__text">Beitrag erfassen</span> <span class="add-room-entry-button__text">Beitrag erfassen</span>
</router-link> </router-link>
</template> </template>
<script> <script>
import {defineAsyncComponent} from 'vue';
const PlusIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/PlusIcon'));
import { ADD_ROOM_ENTRY_PAGE } from '@/router/room.names'; import { ADD_ROOM_ENTRY_PAGE } from '@/router/room.names';
const PlusIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/PlusIcon');
export default { export default {
props: ['parent'], props: ['parent'],
@ -27,7 +32,7 @@ export default {
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import '~styles/helpers'; @import "~styles/helpers";
.add-room-entry-button { .add-room-entry-button {
border: 2px solid $color-white; border: 2px solid $color-white;

View File

@ -1,15 +1,14 @@
<template> <template>
<div class="entry-count-widget"> <div class="entry-count-widget">
<component :is="icon" /> <component :is="icon" />
<span data-cy="entry-count" <span data-cy="entry-count">{{ entryCount }} <template v-if="verbose">{{ entryCount === 1 ? 'Beitrag' : 'Beiträge' }}</template></span>
>{{ entryCount }} <template v-if="verbose">{{ entryCount === 1 ? 'Beitrag' : 'Beiträge' }}</template></span
>
</div> </div>
</template> </template>
<script> <script>
import {defineAsyncComponent} from 'vue';
import SpeechBubbleIcon from '@/components/icons/SpeechBubbleIcon'; import SpeechBubbleIcon from '@/components/icons/SpeechBubbleIcon';
const Cards = () => import(/* webpackChunkName: "icons" */ '@/components/icons/Cards.vue'); const Cards = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/Cards.vue'));
export default { export default {
props: { props: {
@ -22,8 +21,8 @@ export default {
}, },
icon: { icon: {
type: String, type: String,
default: 'cards', default: 'cards'
}, }
}, },
components: { components: {
@ -35,7 +34,7 @@ export default {
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import '~styles/helpers'; @import "~styles/helpers";
.entry-count-widget { .entry-count-widget {
display: flex; display: flex;
@ -53,4 +52,5 @@ export default {
@include room-widget-text-style; @include room-widget-text-style;
} }
} }
</style> </style>

View File

@ -8,7 +8,11 @@
> >
<ellipses /> <ellipses />
</a> </a>
<widget-popover class="more-actions__popover" v-if="showMenu" @hide-me="showMenu = false"> <widget-popover
class="more-actions__popover"
v-if="showMenu"
@hide-me="showMenu = false"
>
<slot :toggle="toggleMenu" /> <slot :toggle="toggleMenu" />
</widget-popover> </widget-popover>
</div> </div>
@ -16,14 +20,15 @@
<script> <script>
import WidgetPopover from '@/components/ui/WidgetPopover'; import WidgetPopover from '@/components/ui/WidgetPopover';
const Ellipses = () => import(/* webpackChunkName: "icons" */ '@/components/icons/Ellipses'); import {defineAsyncComponent} from 'vue';
const Ellipses = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/Ellipses'));
export default { export default {
props: { props: {
background: { background: {
type: Boolean, type: Boolean,
default: false, default: false
}, }
}, },
components: { components: {

View File

@ -1,8 +1,21 @@
<template> <template>
<div class="room-entry" data-cy="room-entry"> <div
<router-link :to="{ name: 'article', params: { slug: slug } }" class="room-entry__router-link" tag="div"> class="room-entry"
<div class="room-entry__header" v-if="image"> data-cy="room-entry"
<img :src="image" :alt="title" class="room-entry__image" /> >
<router-link
:to="{name: 'article', params: { slug: slug }}"
class="room-entry__router-link"
>
<div
class="room-entry__header"
v-if="image"
>
<img
:src="image"
:alt="title"
class="room-entry__image"
>
</div> </div>
<div class="room-entry__content"> <div class="room-entry__content">
<h2 class="room-entry__title"> <h2 class="room-entry__title">

View File

@ -1,16 +1,41 @@
<template> <template>
<page-form class="room-form" title="Neuer Raum" @save="$emit('save', localRoom)"> <page-form
<page-form-input label="Titel" v-model="localRoom.title" /> class="room-form"
title="Neuer Raum"
@save="$emit('save', localRoom)"
>
<page-form-input
label="Titel"
v-model="localRoom.title"
/>
<page-form-input label="Beschreibung" type="textarea" v-model="localRoom.description" /> <page-form-input
label="Beschreibung"
type="textarea"
v-model="localRoom.description"
/>
<h2 class="room-form__property-heading">Farbe</h2> <h2 class="room-form__property-heading">
<color-chooser :selected-color="localRoom.appearance" @input="updateColor" /> Farbe
</h2>
<color-chooser
:selected-color="localRoom.appearance"
@input="updateColor"
/>
<template #footer> <template #footer>
<button type="submit" data-cy="room-form-save" class="button button--primary room-form__save-button"> <button
type="submit"
data-cy="room-form-save"
class="button button--primary room-form__save-button"
>
Speichern Speichern
</button> </button>
<router-link to="/rooms" tag="button" class="button"> Abbrechen </router-link> <router-link
to="/rooms"
class="button"
>
Abbrechen
</router-link>
</template> </template>
</page-form> </page-form>
</template> </template>
@ -28,13 +53,13 @@ export default {
components: { components: {
ColorChooser, ColorChooser,
PageForm, PageForm,
PageFormInput, PageFormInput
}, },
data() { data() {
return { return {
localRoom: Object.assign({}, this.room), localRoom: Object.assign({}, this.room),
me: {}, me: {}
}; };
}, },
@ -42,7 +67,7 @@ export default {
this.$store.dispatch('setSpecialContainerClass', this.localRoom.appearance); this.$store.dispatch('setSpecialContainerClass', this.localRoom.appearance);
}, },
beforeDestroy() { beforeUnmount() {
this.$store.dispatch('setSpecialContainerClass', ''); this.$store.dispatch('setSpecialContainerClass', '');
}, },
@ -50,19 +75,19 @@ export default {
updateColor(newColor) { updateColor(newColor) {
this.localRoom.appearance = newColor; this.localRoom.appearance = newColor;
this.$store.dispatch('setSpecialContainerClass', newColor); this.$store.dispatch('setSpecialContainerClass', newColor);
}, }
}, },
apollo: { apollo: {
me: { me: {
query: ME_QUERY, query: ME_QUERY,
}, }
}, },
}; };
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import '~styles/helpers'; @import "~styles/helpers";
.room-form { .room-form {
&__property-heading { &__property-heading {

View File

@ -8,19 +8,20 @@
</template> </template>
<script> <script>
const Group = () => import(/* webpackChunkName: "icons" */ '@/components/icons/Group.vue'); import {defineAsyncComponent} from 'vue';
const Group = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/Group.vue'));
export default { export default {
props: ['name'], props: ['name'],
components: { components: {
Group, Group
}, }
}; };
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import '~styles/helpers'; @import "~styles/helpers";
.room-group-widget { .room-group-widget {
display: flex; display: flex;
@ -34,7 +35,7 @@ export default {
} }
& > span { & > span {
@include room-widget-text-style; @include room-widget-text-style;;
} }
} }
</style> </style>

View File

@ -12,26 +12,28 @@
</template> </template>
<script> <script>
const EyeIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/EyeIcon');
const ClosedEyeIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/ClosedEyeIcon'); import {defineAsyncComponent} from 'vue';
const EyeIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/EyeIcon'));
const ClosedEyeIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/ClosedEyeIcon'));
export default { export default {
props: { props: {
restricted: { restricted: {
type: Boolean, type: Boolean,
default: false, default: false
}, }
}, },
components: { components: {
ClosedEyeIcon, ClosedEyeIcon,
EyeIcon, EyeIcon
}, }
}; };
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import '~styles/helpers'; @import "~styles/helpers";
.room-visibility-widget { .room-visibility-widget {
display: flex; display: flex;
@ -42,11 +44,11 @@ export default {
width: 30px; width: 30px;
fill: $color-charcoal-dark; fill: $color-charcoal-dark;
margin-right: 15px; margin-right: 15px;
flex-shrink: 0;
} }
& > span { & > span {
@include room-widget-text-style; @include room-widget-text-style;
} }
} }
</style> </style>

View File

@ -1,6 +1,12 @@
<template> <template>
<div :class="roomClass" class="room-widget"> <div
<router-link :to="{ name: 'room', params: { slug: slug } }" tag="div" class="room-widget__content"> :class="roomClass"
class="room-widget"
>
<router-link
:to="{name: 'room', params: {slug: slug}}"
class="room-widget__content"
>
<h2 class="room-widget__title"> <h2 class="room-widget__title">
{{ title }} {{ title }}
</h2> </h2>

View File

@ -1,12 +1,25 @@
<template> <template>
<div class="rooms-onboarding"> <div class="rooms-onboarding">
<h1 class="rooms-onboarding__heading" data-cy="page-title">Räume</h1> <h1
class="rooms-onboarding__heading"
data-cy="page-title"
>
Räume
</h1>
<rooms-illustration class="rooms-onboarding__illustration" /> <rooms-illustration class="rooms-onboarding__illustration" />
<p data-cy="rooms-onboarding-text" class="rooms-onboarding__text"> <p
data-cy="rooms-onboarding-text"
class="rooms-onboarding__text"
>
Hier können Sie Räume erstellen, damit SchülerInnen zusammenarbeiten und Beiträge teilen können. Hier können Sie Räume erstellen, damit SchülerInnen zusammenarbeiten und Beiträge teilen können.
</p> </p>
<div class="rooms-onboarding__button"> <div class="rooms-onboarding__button">
<router-link :to="newRoomRoute" class="button button--primary" data-cy="create-room-button" v-if="isTeacher"> <router-link
:to="newRoomRoute"
class="button button--primary"
data-cy="create-room-button"
v-if="isTeacher"
>
Raum erstellen Raum erstellen
</router-link> </router-link>
</div> </div>
@ -15,8 +28,8 @@
<script> <script>
import {NEW_ROOM_PAGE} from '@/router/room.names'; import {NEW_ROOM_PAGE} from '@/router/room.names';
const RoomsIllustration = () => import {defineAsyncComponent} from 'vue';
import(/* webpackChunkName: "illustrations" */ '@/components/illustrations/RoomsIllustration'); const RoomsIllustration = defineAsyncComponent(() => import(/* webpackChunkName: "illustrations" */'@/components/illustrations/RoomsIllustration'));
export default { export default {
props: { props: {

View File

@ -1,14 +1,24 @@
<template> <template>
<div class="class-selection" v-if="currentClassSelection"> <div
class="class-selection"
v-if="currentClassSelection"
>
<div <div
data-cy="class-selection" data-cy="class-selection"
class="class-selection__selected-class selected-class" class="class-selection__selected-class selected-class"
@click.stop="showPopover = !showPopover" @click.stop="toggle"
> >
<current-class class="selected-class__text" /> <current-class
class="selected-class__text"
/>
<chevron-down class="selected-class__dropdown-icon" /> <chevron-down class="selected-class__dropdown-icon" />
</div> </div>
<widget-popover :mobile="mobile" class="class-selection__popover" v-if="showPopover" @hide-me="showPopover = false"> <widget-popover
:mobile="mobile"
class="class-selection__popover"
v-if="showPopover"
@hide-me="showPopover = false"
>
<li <li
:label="schoolClass.name" :label="schoolClass.name"
:item="schoolClass" :item="schoolClass"
@ -26,13 +36,23 @@
v-if="me.isTeacher && !me.readOnly" v-if="me.isTeacher && !me.readOnly"
@click="closeSidebar" @click="closeSidebar"
> >
<router-link :to="{ name: 'create-class' }" tag="span" class="popover-links__link-with-icon"> <router-link
:to="{name: 'create-class'}"
class="popover-links__link-with-icon"
>
<add-icon class="popover-links__icon" /> <add-icon class="popover-links__icon" />
<span>Klasse erfassen</span> <span>Klasse erfassen</span>
</router-link> </router-link>
</li> </li>
<li class="popover-links__link popover-links__link--large popover-links__divider" @click="closeSidebar"> <li
<router-link :to="{ name: 'old-classes' }" tag="span"> Alte Klassen anzeigen </router-link> class="popover-links__link popover-links__link--large popover-links__divider"
@click="closeSidebar"
>
<router-link
:to="{name: 'old-classes'}"
>
Alte Klassen anzeigen
</router-link>
</li> </li>
</widget-popover> </widget-popover>
</div> </div>
@ -45,15 +65,17 @@ import CurrentClass from '@/components/school-class/CurrentClass';
import updateSelectedClassMixin from '@/mixins/update-selected-class'; import updateSelectedClassMixin from '@/mixins/update-selected-class';
import sidebarMixin from '@/mixins/sidebar'; import sidebarMixin from '@/mixins/sidebar';
import meMixin from '@/mixins/me'; import meMixin from '@/mixins/me';
const ChevronDown = () => import(/* webpackChunkName: "icons" */ '@/components/icons/ChevronDown'); import {defineAsyncComponent} from 'vue';
const AddIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/AddIcon'); const ChevronDown = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/ChevronDown'));
const AddIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/AddIcon'));
export default { export default {
props: { props: {
mobile: { mobile: {
type: Boolean, type: Boolean,
default: false, default: false
}, }
}, },
mixins: [updateSelectedClassMixin, sidebarMixin, meMixin], mixins: [updateSelectedClassMixin, sidebarMixin, meMixin],
@ -61,36 +83,40 @@ export default {
WidgetPopover, WidgetPopover,
ChevronDown, ChevronDown,
CurrentClass, CurrentClass,
AddIcon, AddIcon
}, },
data() { data() {
return { return {
showPopover: false, showPopover: false
}; };
}, },
computed: { computed: {
currentClassSelection() { currentClassSelection() {
let currentClass = this.me.schoolClasses.find((schoolClass) => { let currentClass = this.me.schoolClasses.find(schoolClass => {
return schoolClass.id === this.me.selectedClass.id; return schoolClass.id === this.me.selectedClass.id;
}); });
return currentClass || this.me.schoolClasses[0]; return currentClass || this.me.schoolClasses[0];
}, }
}, },
methods: { methods: {
toggle() {
this.showPopover = !this.showPopover;
},
updateSelectedClassAndHidePopover(selectedClass) { updateSelectedClassAndHidePopover(selectedClass) {
this.updateSelectedClass(selectedClass); this.updateSelectedClass(selectedClass);
this.showPopover = false; this.showPopover = false;
this.closeSidebar('profile'); this.closeSidebar('profile');
}
}, },
},
}; };
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import '~styles/helpers'; @import "~styles/helpers";
.class-selection { .class-selection {
position: relative; position: relative;
@ -104,6 +130,7 @@ export default {
left: 0; left: 0;
transform: translateY($small-spacing); transform: translateY($small-spacing);
} }
} }
.selected-class { .selected-class {

View File

@ -1,5 +1,8 @@
<template> <template>
<span class="current-class" data-cy="current-class-name">{{ currentClassName }}</span> <span
class="current-class"
data-cy="current-class-name"
>{{ currentClassName }}</span>
</template> </template>
<script> <script>
@ -11,8 +14,8 @@ export default {
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import '@/styles/_variables.scss'; @import "@/styles/_variables.scss";
@import '@/styles/_mixins.scss'; @import "@/styles/_mixins.scss";
.current-class { .current-class {
display: flex; display: flex;

View File

@ -6,39 +6,47 @@
class="base-input-container__input" class="base-input-container__input"
data-cy="base-input-input" data-cy="base-input-input"
@change.prevent="$emit('input', $event.target.checked, item)" @change.prevent="$emit('input', $event.target.checked, item)"
/> >
<span <span
:class="{ :class="{'base-input-container__checkbox': type==='checkbox', 'base-input-container__radiobutton': type === 'radiobutton'}"
'base-input-container__checkbox': type === 'checkbox',
'base-input-container__radiobutton': type === 'radiobutton',
}"
class="base-input-container__icon checkbox" class="base-input-container__icon checkbox"
> >
<tick v-if="type === 'checkbox'" /> <tick v-if="type === 'checkbox'" />
<circle-icon data-cy="circle-icon" v-if="type === 'radiobutton'" /> <circle-icon
data-cy="circle-icon"
v-if="type === 'radiobutton'"
/>
</span> </span>
<span class="base-input-container__label" data-cy="base-input-label" v-if="label">{{ label }}</span> <span
<slot class="base-input-container__label" v-if="!label" /> class="base-input-container__label"
data-cy="base-input-label"
v-if="label"
>{{ label }}</span>
<slot
class="base-input-container__label"
v-if="!label"
/>
</label> </label>
</template> </template>
<script> <script>
const Tick = () => import(/* webpackChunkName: "icons" */ '@/components/icons/Tick'); import {defineAsyncComponent} from 'vue';
const CircleIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/CircleIcon'); const Tick = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/Tick'));
const CircleIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/CircleIcon'));
export default { export default {
props: { props: {
label: String, label: String,
checked: { checked: {
type: Boolean, type: Boolean
}, },
item: Object, item: Object,
type: String, type: String
}, },
components: { components: {
Tick, Tick,
CircleIcon, CircleIcon
}, }
}; };
</script> </script>

View File

@ -1,16 +1,25 @@
<template> <template>
<li class="popover-links__link"> <li
<a class="popover-link" @click="$emit('link-action')"> class="popover-links__link"
<component class="popover-link__icon" :is="icon" /> >
<a
class="popover-link"
@click="$emit('link-action')"
>
<component
class="popover-link__icon"
:is="icon"
/>
<span class="popover-link__text">{{ text }}</span> <span class="popover-link__text">{{ text }}</span>
</a> </a>
</li> </li>
</template> </template>
<script> <script>
const EyeIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/EyeIcon'); import {defineAsyncComponent} from 'vue';
const TrashIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/TrashIcon'); const EyeIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/EyeIcon'));
const PenIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/PenIcon'); const TrashIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/TrashIcon'));
const PenIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/PenIcon'));
export default { export default {
props: { props: {

View File

@ -1,7 +1,11 @@
<template> <template>
<div class="file-upload"> <div class="file-upload">
<template v-if="document"> <template v-if="document">
<document-block :value="{ url: document }" show-trash-icon @trash="$emit('change-document-url', '')" /> <document-block
:value="{url: document}"
show-trash-icon
@trash="$emit('change-document-url', '')"
/>
</template> </template>
<template v-else> <template v-else>
<simple-file-upload <simple-file-upload
@ -14,8 +18,9 @@
</template> </template>
<script> <script>
const SimpleFileUpload = () => import('@/components/ui/file-upload/SimpleFileUpload.vue'); import {defineAsyncComponent} from 'vue';
const DocumentBlock = () => import('@/components/content-blocks/DocumentBlock.vue'); const SimpleFileUpload = defineAsyncComponent(() => import('@/components/ui/file-upload/SimpleFileUpload'));
const DocumentBlock = defineAsyncComponent(() => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/DocumentBlock'));
export default { export default {
props: { props: {
@ -25,8 +30,8 @@ export default {
}, },
withText: { withText: {
type: Boolean, type: Boolean,
default: false, default: false
}, }
}, },
components: {SimpleFileUpload, DocumentBlock}, components: {SimpleFileUpload, DocumentBlock},
}; };
@ -34,4 +39,5 @@ export default {
<style scoped lang="scss"> <style scoped lang="scss">
@import '~styles/helpers'; @import '~styles/helpers';
</style> </style>

View File

@ -1,39 +1,43 @@
<template> <template>
<div class="simple-file-upload"> <div class="simple-file-upload">
<component :is="button" @click.native="clickUploadCare" /> <component
:is="button"
@click="clickUploadCare"
/>
<simple-file-upload-hidden-input @link-change-url="$emit('link-change-url', $event)" /> <simple-file-upload-hidden-input @link-change-url="$emit('link-change-url', $event)" />
</div> </div>
</template> </template>
<script> <script>
const SimpleFileUploadHiddenInput = () => import('@/components/ui/file-upload/SimpleFileUploadHiddenInput.vue'); import {defineAsyncComponent} from 'vue';
const SimpleFileUploadIcon = () => import('@/components/ui/file-upload/SimpleFileUploadIcon.vue'); const SimpleFileUploadHiddenInput = defineAsyncComponent(() => import('@/components/ui/file-upload/SimpleFileUploadHiddenInput'));
const SimpleFileUploadIconAndText = () => import('@/components/ui/file-upload/SimpleFileUploadIconAndText.vue'); const SimpleFileUploadIcon = defineAsyncComponent(() => import('@/components/ui/file-upload/SimpleFileUploadIcon'));
const DocumentIcon = () => import('@/components/icons/DocumentIcon.vue'); const SimpleFileUploadIconAndText = defineAsyncComponent(() => import('@/components/ui/file-upload/SimpleFileUploadIconAndText'));
const DocumentIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/DocumentIcon'));
export default { export default {
props: { props: {
value: { value: {
type: String, type: String,
default: '', default: ''
}, },
withText: { withText: {
type: Boolean, type: Boolean,
default: false, default: false
}, }
}, },
components: { components: {
SimpleFileUploadHiddenInput, SimpleFileUploadHiddenInput,
DocumentIcon, DocumentIcon,
SimpleFileUploadIcon, SimpleFileUploadIcon,
SimpleFileUploadIconAndText, SimpleFileUploadIconAndText
}, },
computed: { computed: {
button() { button() {
return this.withText ? 'simple-file-upload-icon-and-text' : 'simple-file-upload-icon'; return this.withText ? 'simple-file-upload-icon-and-text' : 'simple-file-upload-icon';
}, }
}, },
methods: { methods: {
@ -41,13 +45,13 @@ export default {
// workaround for styling the uploadcare widget // workaround for styling the uploadcare widget
let button = this.$el.querySelector('.uploadcare--widget__button'); let button = this.$el.querySelector('.uploadcare--widget__button');
button.click(); button.click();
}, }
}, },
}; };
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import '~styles/_helpers'; @import "~styles/_helpers";
.simple-file-upload { .simple-file-upload {
height: 25px; height: 25px;
@ -62,7 +66,7 @@ export default {
} }
} }
:deep(.uploadcare--widget) { /deep/ .uploadcare--widget {
display: none; display: none;
} }
</style> </style>

View File

@ -5,7 +5,8 @@
</template> </template>
<script> <script>
const DocumentIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/DocumentIcon'); import {defineAsyncComponent} from 'vue';
const DocumentIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/DocumentIcon'));
export default { export default {
components: {DocumentIcon}, components: {DocumentIcon},

View File

@ -4,7 +4,7 @@
icon="document-icon" icon="document-icon"
text="Dokument hochladen" text="Dokument hochladen"
v-if="!value" v-if="!value"
@click.native="clickUploadCare" @click="clickUploadCare"
/> />
<simple-file-upload-hidden-input @link-change-url="$emit('link-change-url', $event)" /> <simple-file-upload-hidden-input @link-change-url="$emit('link-change-url', $event)" />
@ -12,8 +12,9 @@
</template> </template>
<script> <script>
const SimpleFileUploadHiddenInput = () => import('@/components/ui/file-upload/SimpleFileUploadHiddenInput.vue'); import {defineAsyncComponent} from 'vue';
const ButtonWithIconAndText = () => import('@/components/ui/ButtonWithIconAndText.vue'); const SimpleFileUploadHiddenInput = defineAsyncComponent(() => import('@/components/ui/file-upload/SimpleFileUploadHiddenInput'));
const ButtonWithIconAndText = defineAsyncComponent(() => import('@/components/ui/ButtonWithIconAndText'));
export default { export default {
props: ['value'], props: ['value'],
@ -34,7 +35,7 @@ export default {
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import '~styles/_helpers'; @import "~styles/_helpers";
.simple-file-upload { .simple-file-upload {
width: 25px; width: 25px;
@ -54,7 +55,7 @@ export default {
} }
} }
:deep(.uploadcare--widget) { /deep/ .uploadcare--widget {
display: none; display: none;
} }
</style> </style>

View File

@ -1,18 +1,17 @@
const LinkIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/LinkIcon'); import {defineAsyncComponent} from 'vue';
const VideoIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/VideoIcon'); const LinkIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/LinkIcon'));
const ImageIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/ImageIcon'); const VideoIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/VideoIcon'));
const TextIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/TextIcon'); const ImageIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/ImageIcon'));
const SpeechBubbleIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/SpeechBubbleIcon'); const TextIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/TextIcon'));
const DocumentIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/DocumentIcon'); const SpeechBubbleIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/SpeechBubbleIcon'));
const TitleIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/TitleIcon'); const DocumentIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/DocumentIcon'));
const DocumentWithLinesIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/DocumentWithLinesIcon'); const TitleIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/TitleIcon'));
const DocumentWithLinesIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/DocumentWithLinesIcon'));
const ArrowThinBottom = () => import(/* webpackChunkName: "icons" */ '@/components/icons/ArrowThinBottom'); const ArrowThinBottom = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/ArrowThinBottom'));
const ArrowThinDown = () => import(/* webpackChunkName: "icons" */ '@/components/icons/ArrowThinDown'); const ArrowThinDown = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/ArrowThinDown'));
const ArrowThinTop = () => import(/* webpackChunkName: "icons" */ '@/components/icons/ArrowThinTop'); const ArrowThinTop = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/ArrowThinTop'));
const ArrowThinUp = () => import(/* webpackChunkName: "icons" */ '@/components/icons/ArrowThinUp'); const ArrowThinUp = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/ArrowThinUp'));
const TrashIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/TrashIcon'));
const TrashIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/TrashIcon');
/* /*
for icons with a single word, leave the *-icon name, to prevent conflicts for icons with a single word, leave the *-icon name, to prevent conflicts
@ -31,5 +30,5 @@ export default {
ArrowThinDown, ArrowThinDown,
ArrowThinTop, ArrowThinTop,
ArrowThinUp, ArrowThinUp,
TrashIcon, TrashIcon
}; };

View File

@ -0,0 +1,20 @@
<template>
<div
class="skillboxform-input"
>
<label
:for="id"
class="skillboxform-input__label"
>
{{ label }}
</label>
<slot :id="id" />
</div>
</template>
<script setup>
defineProps({
id: String,
label: String
}) ;
</script>

View File

@ -1,85 +0,0 @@
<template>
<ValidationProvider v-slot="{ errors }" :name="name" :rules="rules">
<div class="skillboxform-input">
<label :for="id" class="skillboxform-input__label">{{ label }}</label>
<input
:value="value"
:class="{ 'skillboxform-input__input--error': errors.length }"
v-bind="$attrs"
class="change-form__email skillbox-input skillboxform-input__input"
autocomplete="off"
:id="id"
@input="$emit('input', $event.target.value)"
/>
<small :data-cy="localErrorsCy" class="skillboxform-input__error" v-if="errors.length">{{ errors[0] }}</small>
<small :data-cy="remoteErrorsCy" class="skillboxform-input__error" v-for="error in remoteErrors" :key="error">{{
error
}}</small>
</div>
</ValidationProvider>
</template>
<script>
import { extend, localize, ValidationProvider } from 'vee-validate';
import de from 'vee-validate/dist/locale/de.json';
import { required } from 'vee-validate/dist/rules';
extend('required', required);
localize('de', {
...de,
names: {
password: 'Passwort',
email: 'E-Mail',
coupon: 'Coupon-Code',
},
});
// todo: use this in beta-login, license-activation and PasswordChangeForm
export default {
props: {
value: {
type: String,
default: '',
},
remoteErrors: {
type: Array,
default: undefined,
},
name: {
type: String,
default: '',
},
label: {
type: String,
default: '',
},
rules: {
type: String,
default: 'required',
},
},
components: {
ValidationProvider,
},
inheritAttrs: false,
computed: {
id() {
return this.$attrs.id || this._uid;
},
remoteErrorsCy() {
return `${this.name}-remote-errors`;
},
localErrorsCy() {
return `${this.name}-local-errors`;
},
},
mounted() {},
};
</script>
<style scoped lang="scss">
@import '~styles/helpers';
</style>

View File

@ -1,8 +1,18 @@
<template> <template>
<div class="visibility-action"> <div class="visibility-action">
<a class="visibility-action__action-button" v-if="canManageContent" @click="toggleVisibility()"> <a
<closed-eye-icon class="visibility-action__action-icon action-icon" v-if="hidden" /> class="visibility-action__action-button"
<eye-icon class="visibility-action__action-icon action-icon" v-else /> v-if="canManageContent"
@click="toggleVisibility()"
>
<closed-eye-icon
class="visibility-action__action-icon action-icon"
v-if="hidden"
/>
<eye-icon
class="visibility-action__action-icon action-icon"
v-else
/>
</a> </a>
</div> </div>
</template> </template>
@ -13,36 +23,37 @@ import me from '@/mixins/me';
import {TYPES, CONTENT_TYPE} from '@/consts/types'; import {TYPES, CONTENT_TYPE} from '@/consts/types';
import {createVisibilityMutation, hidden} from '@/helpers/visibility'; import {createVisibilityMutation, hidden} from '@/helpers/visibility';
const EyeIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/EyeIcon'); import {defineAsyncComponent} from 'vue';
const ClosedEyeIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/ClosedEyeIcon'); const EyeIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/EyeIcon'));
const ClosedEyeIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/ClosedEyeIcon'));
export default { export default {
props: { props: {
block: { block: {
type: Object, type: Object,
default: () => ({}), default: () => ({})
}, },
type: { type: {
type: String, type: String,
default: CONTENT_TYPE, default: CONTENT_TYPE,
validator: (value) => { validator: value => {
// value must be one of TYPES // value must be one of TYPES
return TYPES.indexOf(value) !== -1; return TYPES.indexOf(value) !== -1;
}, }
}, }
}, },
mixins: [me], mixins: [me],
components: { components: {
EyeIcon, EyeIcon,
ClosedEyeIcon, ClosedEyeIcon
}, },
computed: { computed: {
hidden() { hidden() {
return hidden({type: this.type, block: this.block, schoolClass: this.schoolClass}); return hidden({type: this.type, block: this.block, schoolClass: this.schoolClass});
}, }
}, },
methods: { methods: {
@ -50,18 +61,16 @@ export default {
const hidden = !this.hidden; const hidden = !this.hidden;
const schoolClassId = this.schoolClass.id; const schoolClassId = this.schoolClass.id;
const visibility = [ const visibility = [{
{
schoolClassId, schoolClassId,
hidden, hidden
}, }];
];
const {mutation, variables} = createVisibilityMutation(this.type, this.block.id, visibility); const {mutation, variables} = createVisibilityMutation(this.type, this.block.id, visibility);
this.$apollo.mutate({ this.$apollo.mutate({
mutation, mutation,
variables, variables
}); });
}, },
}, },

View File

@ -1,4 +1,4 @@
const resizeElement = (el) => { const resizeElement = (el: HTMLElement) => {
el.style.height = `auto`; el.style.height = `auto`;
el.style.height = `${el.clientHeight - el.offsetHeight + el.scrollHeight}px`; el.style.height = `${el.clientHeight - el.offsetHeight + el.scrollHeight}px`;
}; };
@ -6,13 +6,13 @@ const resizeElement = (el) => {
export default { export default {
update: resizeElement, update: resizeElement,
inserted: resizeElement, inserted: resizeElement,
bind(el) { created(el: HTMLElement) {
el.classList.add('skillbox-auto-grow'); el.classList.add('skillbox-auto-grow');
el.addEventListener('input', () => { el.addEventListener('input', () => {
resizeElement(el); resizeElement(el);
}); });
}, },
unbind(el) { unmounted(el: HTMLElement) {
el.classList.remove('skillbox-auto-grow'); el.classList.remove('skillbox-auto-grow');
el.removeEventListener('input', () => { el.removeEventListener('input', () => {
resizeElement(el); resizeElement(el);

View File

@ -1,14 +0,0 @@
// taken from https://stackoverflow.com/questions/36170425/detect-click-outside-element
export default {
bind(el, binding, vnode) {
el.clickOutsideEvent = (event) => {
if (!(el === event.target || el.contains(event.target))) {
vnode.context[binding.expression](event);
}
};
document.body.addEventListener('click', el.clickOutsideEvent);
},
unbind(el) {
document.body.removeEventListener('click', el.clickOutsideEvent);
},
};

View File

@ -0,0 +1,46 @@
// taken from https://stackoverflow.com/questions/36170425/detect-click-outside-element
import {DirectiveBinding, VNode} from "vue";
declare global {
interface HTMLElement {
clickOutsideEvent: (event: Event) => void
}
}
/*
todo:
there is a special interaction with nested elements where the parent has a @click event:
the parent triggers the event, something happens, but the click event bubbles to the child element.
If the event is then used to open some kind of sidebar or modal that has the `click-outside` propert, t
he bubbled event will be outside of it, thereby closing it.
example:
<div
class="sidebar"
v-if="showSidebar"
v-click-outside="showSidebar=false"
>
...
</div>
<a class="sidebar-toggle" @click="showSidebar=true">
<span>Hello</span>
</a>
FIX:
In this example, setting the event on the a-tag as `@click.stop` will solve the problem
*/
export default {
unmounted(el: HTMLElement) {
document.body.removeEventListener('click', el.clickOutsideEvent);
},
created: (el: HTMLElement, binding: DirectiveBinding) => {
el.clickOutsideEvent = (event: Event) => {
if (!(el === event.target || el.contains(event.target as Node))) {
const eventHandler = binding.value;
eventHandler(event);
}
};
document.body.addEventListener('click', el.clickOutsideEvent);
}
};

View File

@ -1,4 +1,4 @@
import log from 'loglevel'; // import log from 'loglevel';
import type { FlavorValues } from '@/helpers/types'; import type { FlavorValues } from '@/helpers/types';
import { defaultFlavorValues, dhaValues, dhfValues, myKvValues } from '@/helpers/app-flavor.constants'; import { defaultFlavorValues, dhaValues, dhfValues, myKvValues } from '@/helpers/app-flavor.constants';
@ -18,6 +18,6 @@ switch (process.env.VUE_APP_FLAVOR) {
flavorValues = defaultFlavorValues; flavorValues = defaultFlavorValues;
} }
log.debug('flavorValues', flavorValues); // log.debug('flavorValues', flavorValues);
export default flavorValues; export default flavorValues;

View File

@ -1,49 +1,78 @@
<template> <template>
<footer class="default-footer" data-cy="page-footer"> <footer
class="default-footer"
data-cy="page-footer"
>
<div class="default-footer__section"> <div class="default-footer__section">
<div class="default-footer__info"> <div class="default-footer__info">
<div class="default-footer__who-are-we who-are-we"> <div class="default-footer__who-are-we who-are-we">
<h5 class="who-are-we__title">Wer sind wir?</h5> <h5 class="who-are-we__title">
Wer sind wir?
</h5>
<p class="who-are-we__text"> <p class="who-are-we__text">
mySkillbox ist ein Angebot des hep Verlags in Zusammenarbeit mit der Eidgenössischen Hochschule für mySkillbox ist ein Angebot des hep Verlags in
Berufsbildung (EHB). Zusammenarbeit mit der Eidgenössischen Hochschule für Berufsbildung (EHB).
</p> </p>
</div> </div>
<a href="https://www.hep-verlag.ch/" target="_blank"> <a
href="https://www.hep-verlag.ch/"
target="_blank"
>
<hep-logo class="default-footer__logo-hep" /> <hep-logo class="default-footer__logo-hep" />
</a> </a>
<a href="https://www.ehb.swiss/" target="_blank"> <a
href="https://www.ehb.swiss/"
target="_blank"
>
<ehb-logo class="default-footer__logo-ehb" /> <ehb-logo class="default-footer__logo-ehb" />
</a> </a>
</div> </div>
</div> </div>
<div class="default-footer__section"> <div class="default-footer__section">
<div class="default-footer__links"> <div class="default-footer__links">
<a href="https://myskillbox.ch/datenschutz" target="_blank" class="default-footer__link">Datenschutz</a> <a
<a href="https://myskillbox.ch/impressum" target="_blank" class="default-footer__link">Impressum</a> href="https://myskillbox.ch/datenschutz"
<a href="https://myskillbox.ch/agb" target="_blank" class="default-footer__link">AGB</a> target="_blank"
<a :href="$flavor.supportLink" target="_blank" class="default-footer__link">Support</a> class="default-footer__link"
>Datenschutz</a>
<a
href="https://myskillbox.ch/impressum"
target="_blank"
class="default-footer__link"
>Impressum</a>
<a
href="https://myskillbox.ch/agb"
target="_blank"
class="default-footer__link"
>AGB</a>
<a
:href="$flavor.supportLink"
target="_blank"
class="default-footer__link"
>Support</a>
</div> </div>
</div> </div>
</footer> </footer>
</template> </template>
<script> <script>
const HepLogo = () => import(/* webpackChunkName: "icons" */ '@/components/icons/HepLogo'); import {defineAsyncComponent} from 'vue';
const EhbLogo = () => import(/* webpackChunkName: "icons" */ '@/components/icons/EhbLogo');
const HepLogo = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/HepLogo'));
const EhbLogo = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/EhbLogo'));
export default { export default {
components: { components: {
HepLogo, HepLogo,
EhbLogo, EhbLogo
}, }
}; };
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import '@/styles/_variables.scss'; @import "@/styles/_variables.scss";
@import '@/styles/_mixins.scss'; @import "@/styles/_mixins.scss";
.default-footer { .default-footer {
background-color: $color-silver-light; background-color: $color-silver-light;

View File

@ -1,6 +1,12 @@
<template> <template>
<div :class="specialContainerClass" class="container layout layout--fullscreen"> <div
<div class="close-button" @click="back"> :class="specialContainerClass"
class="container layout layout--fullscreen"
>
<div
class="close-button"
@click="back"
>
<cross class="close-button__icon" /> <cross class="close-button__icon" />
</div> </div>
@ -9,29 +15,30 @@
</template> </template>
<script> <script>
const Cross = () => import(/* webpackChunkName: "icons" */ '@/components/icons/CrossIcon'); import {defineAsyncComponent} from 'vue';
const Cross = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/CrossIcon'));
export default { export default {
components: { components: {
Cross, Cross
}, },
computed: { computed: {
specialContainerClass() { specialContainerClass() {
let cls = this.$store.state.specialContainerClass; let cls = this.$store.state.specialContainerClass;
return [cls ? `skillbox--${cls}` : '']; return [cls ? `skillbox--${cls}` : ''];
}, }
}, },
methods: { methods: {
back() { back() {
this.$router.go(-1); this.$router.go(-1);
}, }
}, }
}; };
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import '@/styles/_default-layout.scss'; @import "@/styles/_default-layout.scss";
.close-button { .close-button {
margin-top: $medium-spacing; margin-top: $medium-spacing;
@ -42,4 +49,5 @@ export default {
display:flex; display:flex;
justify-content:flex-end; justify-content:flex-end;
} }
</style> </style>

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