Apply code changes from migration guide for Vue 3

This commit is contained in:
Ramon Wenger 2022-03-23 16:21:06 +01:00
parent 445f09e16a
commit 09d8d36678
30 changed files with 903 additions and 673 deletions

View File

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

View File

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

View File

@ -1,15 +1,22 @@
<template>
<header class="header-bar">
<a class="header-bar__sidebar-link" data-cy="open-sidebar-link" @click.stop="openSidebar('navigation')">
<hamburger class="header-bar__sidebar-icon" />
</a>
<content-navigation class="header-bar__content-navigation" />
<div class="user-header">
<a class="user-header__sidebar-link">
<current-class class="user-header__current-class" @click.native.stop="openSidebar('profile')" />
<a
class="user-header__sidebar-link"
>
<current-class
class="user-header__current-class"
@click.stop="openSidebar('profile')"
/>
</a>
<user-widget v-bind="me" data-cy="header-user-widget" @click.native.stop="openSidebar('profile')" />
<user-widget
v-bind="me"
data-cy="header-user-widget"
@click.stop="openSidebar('profile')"
/>
</div>
</header>
</template>

View File

@ -8,7 +8,7 @@
<logo />
</router-link>
<user-widget v-bind="me" @click.native.stop="openSidebar('profile')" />
<user-widget v-bind="me" @click.stop="openSidebar('profile')" />
</div>
</template>

View File

@ -27,7 +27,7 @@ export default {
};
},
destroyed() {
unmounted() {
document.body.onscroll = null;
},

View File

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

View File

@ -7,7 +7,7 @@
:to="topicRoute"
active-class="content-navigation__link--active"
class="content-navigation__link"
@click.native="close"
@click="close"
>
{{ $flavor.textTopics }}
</router-link>
@ -20,7 +20,7 @@
to="/instruments"
active-class="content-navigation__link--active"
class="content-navigation__link"
@click.native="close"
@click="close"
>
{{ $flavor.textInstruments }}
</router-link>
@ -33,7 +33,7 @@
class="content-navigation__link"
data-cy="news-navigation-link"
v-if="!me.readOnly"
@click.native="close"
@click="close"
>
News
</router-link>
@ -51,7 +51,7 @@
to="/rooms"
active-class="content-navigation__link--active"
class="content-navigation__link content-navigation__link--secondary"
@click.native="close"
@click="close"
>
Räume
</router-link>
@ -62,7 +62,7 @@
to="/portfolio"
active-class="content-navigation__link--active"
class="content-navigation__link content-navigation__link--secondary"
@click.native="close"
@click="close"
>
Portfolio
</router-link>

View File

@ -8,7 +8,7 @@
class="topic-navigation__topic book-subnavigation__item"
v-for="topic in topics"
:key="topic.id"
@click.native="closeSidebar('navigation')"
@click="closeSidebar('navigation')"
>
{{ topic.order }}.
{{ topic.title }}

View File

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

View File

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

View File

@ -7,6 +7,7 @@
</template>
<script lang="ts">
<<<<<<< HEAD
import Vue, { PropType } from 'vue';
import { Editor, EditorContent } from '@tiptap/vue-2';
import Document from '@tiptap/extension-document';
@ -15,6 +16,25 @@ import Text from '@tiptap/extension-text';
import BulletList from '@tiptap/extension-bullet-list';
import ListItem from '@tiptap/extension-list-item';
import Toggle from '@/components/ui/Toggle.vue';
||||||| parent of a423cfde (Apply code changes from migration guide for Vue 3)
import Vue, {PropType} from 'vue';
import {Editor, EditorContent} from "@tiptap/vue-2";
import Document from '@tiptap/extension-document';
import Paragraph from '@tiptap/extension-paragraph';
import Text from '@tiptap/extension-text';
import BulletList from '@tiptap/extension-bullet-list';
import ListItem from '@tiptap/extension-list-item';
import Toggle from "@/components/ui/Toggle.vue";
=======
import {PropType, defineComponent} from 'vue';
import {Editor, EditorContent} from "@tiptap/vue-2";
import Document from '@tiptap/extension-document';
import Paragraph from '@tiptap/extension-paragraph';
import Text from '@tiptap/extension-text';
import BulletList from '@tiptap/extension-bullet-list';
import ListItem from '@tiptap/extension-list-item';
import Toggle from "@/components/ui/Toggle.vue";
>>>>>>> a423cfde (Apply code changes from migration guide for Vue 3)
interface Data {
editor: Editor | undefined;
@ -23,6 +43,7 @@ interface Value {
text: string;
}
<<<<<<< HEAD
export default Vue.extend({
props: {
value: {
@ -71,6 +92,21 @@ export default Vue.extend({
editorProps: {
attributes: {
class: 'tip-tap__editor',
||||||| parent of a423cfde (Apply code changes from migration guide for Vue 3)
export default Vue.extend({
props: {
value: {
type: Object as PropType<Value>,
validator(value: Value) {
return Object.prototype.hasOwnProperty.call(value, 'text');
=======
export default defineComponent({
props: {
value: {
type: Object as PropType<Value>,
validator: (value: Value) => {
return Object.prototype.hasOwnProperty.call(value, 'text');
>>>>>>> a423cfde (Apply code changes from migration guide for Vue 3)
},
},
content: this.text,
@ -80,6 +116,7 @@ export default Vue.extend({
this.$emit('input', text);
this.$emit('change-text', text);
},
<<<<<<< HEAD
});
},
@ -91,6 +128,15 @@ export default Vue.extend({
toggleList() {
const editor = this.editor as Editor;
editor.chain().selectAll().toggleBulletList().run();
||||||| parent of a423cfde (Apply code changes from migration guide for Vue 3)
text(): string {
return this.value.text;
}
=======
text(): string {
return this.value?.text || '';
}
>>>>>>> a423cfde (Apply code changes from migration guide for Vue 3)
},
},
});

View File

@ -3,10 +3,11 @@
<filter-entry
:text="title"
v-bind="$attrs"
:type="category"
:category="category"
:is-category="true"
:id="category.id"
@click.native="setCategoryFilter(category.id)"
@click="setCategoryFilter(category.id)"
/>
<div class="filter-group__children">
<filter-entry
@ -16,7 +17,7 @@
v-for="type in types"
:id="type.id"
:key="type.id"
@click.native="setFilter(`type:${type.id}`)"
@click="setFilter(`type:${type.id}`)"
/>
</div>
</div>

View File

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

View File

@ -18,7 +18,7 @@
icon="document-with-lines-icon"
data-cy="use-template-button"
text="Vorlage nutzen"
@click.native="useTemplate"
@click="useTemplate"
/>
<file-upload
@ -30,12 +30,38 @@
</div>
</div>
</div>
<<<<<<< HEAD
<div slot="footer">
<a class="button button--primary" data-cy="modal-save-button" @click="$emit('save', localProjectEntry)"
>Speichern</a
>
<a class="button" @click="$emit('hide')">Abbrechen</a>
</div>
||||||| parent of a423cfde (Apply code changes from migration guide for Vue 3)
<div slot="footer">
<a
class="button button--primary"
data-cy="modal-save-button"
@click="$emit('save', localProjectEntry)"
>Speichern</a>
<a
class="button"
@click="$emit('hide')"
>Abbrechen</a>
</div>
=======
<template #footer>
<a
class="button button--primary"
data-cy="modal-save-button"
@click="$emit('save', localProjectEntry)"
>Speichern</a>
<a
class="button"
@click="$emit('hide')"
>Abbrechen</a>
</template>
>>>>>>> a423cfde (Apply code changes from migration guide for Vue 3)
</modal>
</template>
@ -43,8 +69,17 @@
import Modal from '@/components/Modal';
import ButtonWithIconAndText from '@/components/ui/ButtonWithIconAndText';
<<<<<<< HEAD
import { PROJECT_ENTRY_TEMPLATE } from '@/consts/strings.consts';
const FileUpload = () => import('@/components/ui/file-upload/FileUpload.vue');
||||||| parent of a423cfde (Apply code changes from migration guide for Vue 3)
import {PROJECT_ENTRY_TEMPLATE} from '@/consts/strings.consts';
const FileUpload = () => import('@/components/ui/file-upload/FileUpload');
=======
import {PROJECT_ENTRY_TEMPLATE} from '@/consts/strings.consts';
const FileUpload = () => import('@/components/ui/file-upload/FileUpload');
>>>>>>> a423cfde (Apply code changes from migration guide for Vue 3)
export default {
props: {

View File

@ -1,8 +1,40 @@
<template>
<<<<<<< HEAD
<page-form :title="title" @save="$emit('save', localProject)">
<page-form-input label="Titel" v-model="localProject.title" />
<page-form-input label="Beschreibung" type="textarea" v-model="localProject.description" />
<template slot="footer">
||||||| parent of a423cfde (Apply code changes from migration guide for Vue 3)
<page-form
:title="title"
@save="$emit('save', localProject)"
>
<page-form-input
label="Titel"
v-model="localProject.title"
/>
<page-form-input
label="Beschreibung"
type="textarea"
v-model="localProject.description"
/>
<template slot="footer">
=======
<page-form
:title="title"
@save="$emit('save', localProject)"
>
<page-form-input
label="Titel"
v-model="localProject.title"
/>
<page-form-input
label="Beschreibung"
type="textarea"
v-model="localProject.description"
/>
<template #footer>
>>>>>>> a423cfde (Apply code changes from migration guide for Vue 3)
<button
:class="{ 'button--disabled': !formValid }"
:disabled="!formValid"

View File

@ -6,10 +6,34 @@
<modal-input :value="name" :placeholder="placeholder" data-cy="edit-name-input" @input="$emit('input', $event)" />
<template #footer>
<<<<<<< HEAD
<div slot="footer">
<a class="button button--primary" data-cy="modal-save-button" @click="$emit('save')">Speichern</a>
<a class="button" @click="$emit('cancel')">Abbrechen</a>
</div>
||||||| parent of a423cfde (Apply code changes from migration guide for Vue 3)
<div slot="footer">
<a
class="button button--primary"
data-cy="modal-save-button"
@click="$emit('save')"
>Speichern</a>
<a
class="button"
@click="$emit('cancel')"
>Abbrechen</a>
</div>
=======
<a
class="button button--primary"
data-cy="modal-save-button"
@click="$emit('save')"
>Speichern</a>
<a
class="button"
@click="$emit('cancel')"
>Abbrechen</a>
>>>>>>> a423cfde (Apply code changes from migration guide for Vue 3)
</template>
</modal>
</template>

View File

@ -1,6 +1,18 @@
<template>
<div class="simple-file-upload">
<<<<<<< HEAD
<component :is="button" @click.native="clickUploadCare" />
||||||| parent of a423cfde (Apply code changes from migration guide for Vue 3)
<component
:is="button"
@click.native="clickUploadCare"
/>
=======
<component
:is="button"
@click="clickUploadCare"
/>
>>>>>>> a423cfde (Apply code changes from migration guide for Vue 3)
<simple-file-upload-hidden-input @link-change-url="$emit('link-change-url', $event)" />
</div>
</template>

View File

@ -4,7 +4,7 @@
icon="document-icon"
text="Dokument hochladen"
v-if="!value"
@click.native="clickUploadCare"
@click="clickUploadCare"
/>
<simple-file-upload-hidden-input @link-change-url="$emit('link-change-url', $event)" />

View File

@ -1,4 +1,4 @@
const resizeElement = (el) => {
const resizeElement = (el: HTMLElement) => {
el.style.height = `auto`;
el.style.height = `${el.clientHeight - el.offsetHeight + el.scrollHeight}px`;
};
@ -6,13 +6,13 @@ const resizeElement = (el) => {
export default {
update: resizeElement,
inserted: resizeElement,
bind(el) {
created(el: HTMLElement) {
el.classList.add('skillbox-auto-grow');
el.addEventListener('input', () => {
resizeElement(el);
});
},
unbind(el) {
unmounted(el: HTMLElement) {
el.classList.remove('skillbox-auto-grow');
el.removeEventListener('input', () => {
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,23 @@
// taken from https://stackoverflow.com/questions/36170425/detect-click-outside-element
import {DirectiveBinding, VNode} from "vue";
declare global {
interface HTMLElement {
clickOutsideEvent: (event: Event) => void
}
}
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,131 +1,160 @@
<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__info">
<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">
mySkillbox ist ein Angebot des hep Verlags in Zusammenarbeit mit der Eidgenössischen Hochschule für
Berufsbildung (EHB).
mySkillbox ist ein Angebot des hep Verlags in
Zusammenarbeit mit der Eidgenössischen Hochschule für Berufsbildung (EHB).
</p>
</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" />
</a>
<a href="https://www.ehb.swiss/" target="_blank">
<a
href="https://www.ehb.swiss/"
target="_blank"
>
<ehb-logo class="default-footer__logo-ehb" />
</a>
</div>
</div>
<div class="default-footer__section">
<div class="default-footer__links">
<a href="https://myskillbox.ch/datenschutz" target="_blank" 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>
<a
href="https://myskillbox.ch/datenschutz"
target="_blank"
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>
</footer>
</template>
<script>
const HepLogo = () => import(/* webpackChunkName: "icons" */ '@/components/icons/HepLogo');
const EhbLogo = () => import(/* webpackChunkName: "icons" */ '@/components/icons/EhbLogo');
import {defineAsyncComponent} from 'vue';
export default {
components: {
HepLogo,
EhbLogo,
},
};
const HepLogo = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/HepLogo'));
const EhbLogo = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/EhbLogo'));
export default {
components: {
HepLogo,
EhbLogo
}
};
</script>
<style scoped lang="scss">
@import '@/styles/_variables.scss';
@import '@/styles/_mixins.scss';
@import "@/styles/_variables.scss";
@import "@/styles/_mixins.scss";
.default-footer {
background-color: $color-silver-light;
max-width: 100vw;
overflow: hidden;
.default-footer {
background-color: $color-silver-light;
max-width: 100vw;
overflow: hidden;
&__section {
width: 100%;
border-bottom: $color-silver 1px solid;
display: flex;
justify-content: center;
}
&__section {
width: 100%;
border-bottom: $color-silver 1px solid;
display: flex;
justify-content: center;
}
&__info {
width: 100%;
max-width: $footer-width;
padding: 2 * $large-spacing 0;
display: flex;
flex-direction: column;
&__info {
width: 100%;
max-width: $footer-width;
padding: 2*$large-spacing 0;
display: flex;
flex-direction: column;
@include desktop {
flex-direction: row;
justify-content: space-between;
@include desktop {
flex-direction: row;
justify-content: space-between;
}
}
&__who-are-we {
width: 100%;
margin-bottom: $large-spacing;
@include desktop {
width: 330px;
margin-bottom: 0;
}
}
&__logo-hep {
width: auto;
height: 35px;
margin-bottom: $large-spacing;
@include desktop {
width: 147px;
margin-bottom: 0;
}
}
&__logo-ehb {
width: 100px;
height: 32px;
}
&__links {
width: 100%;
max-width: $footer-width;
padding: $large-spacing 0;
display: flex;
flex-direction: column;
@include desktop {
flex-direction: row;
}
}
&__link {
@include aside-with-cheese;
margin-right: $large-spacing;
margin-bottom: $small-spacing;
@include desktop {
margin-bottom: 0;
}
}
}
&__who-are-we {
width: 100%;
margin-bottom: $large-spacing;
.who-are-we {
&__title {
@include heading-4;
}
@include desktop {
width: 330px;
margin-bottom: 0;
&__text {
@include aside-text;
}
}
&__logo-hep {
width: auto;
height: 35px;
margin-bottom: $large-spacing;
@include desktop {
width: 147px;
margin-bottom: 0;
}
}
&__logo-ehb {
width: 100px;
height: 32px;
}
&__links {
width: 100%;
max-width: $footer-width;
padding: $large-spacing 0;
display: flex;
flex-direction: column;
@include desktop {
flex-direction: row;
}
}
&__link {
@include aside-with-cheese;
margin-right: $large-spacing;
margin-bottom: $small-spacing;
@include desktop {
margin-bottom: 0;
}
}
}
.who-are-we {
&__title {
@include heading-4;
}
&__text {
@include aside-text;
}
}
</style>

View File

@ -1,73 +1,80 @@
<template>
<div class="layout layout--public public">
<div class="public__logo">
<router-link :to="{ name: 'hello' }" class="hep-link">
<router-link
:to="{name: 'hello'}"
class="hep-link"
>
<logo />
</router-link>
</div>
<router-view class="public__content layout__content" />
<default-footer class="skillbox__footer public__footer footer" v-if="$flavor.showFooter" />
<default-footer
class="skillbox__footer public__footer footer"
v-if="$flavor.showFooter"
/>
</div>
</template>
<script>
import DefaultFooter from '@/layouts/DefaultFooter';
import {defineAsyncComponent} from 'vue';
import DefaultFooter from '@/layouts/DefaultFooter';
const Logo = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/Logo'));
const Logo = () => import(/* webpackChunkName: "icons" */ '@/components/icons/Logo');
export default {
components: {
Logo,
DefaultFooter,
},
};
export default {
components: {
Logo,
DefaultFooter
},
};
</script>
<style lang="scss" scoped>
@import '@/styles/_variables.scss';
@import '@/styles/_mixins.scss';
@import '@/styles/_default-layout.scss';
@import "@/styles/_variables.scss";
@import "@/styles/_mixins.scss";
@import "@/styles/_default-layout.scss";
@mixin content-block {
padding-right: $medium-spacing;
padding-left: $medium-spacing;
max-width: 800px;
min-width: 320px;
width: 100%;
margin: 0 auto;
}
@mixin content-block {
padding-right: $medium-spacing;
padding-left: $medium-spacing;
max-width: 800px;
min-width: 320px;
width: 100%;
margin: 0 auto;
}
.logo {
position: relative;
.logo {
position: relative;
width: auto;
height: 43px;
}
.public {
grid-template-areas: 'h' 'c' 'f';
&__content {
@include content-block();
margin-bottom: $large-spacing;
width: auto;
height: 43px;
}
&__logo {
@include content-block();
margin-top: $medium-spacing;
.public {
grid-template-areas: "h" "c" "f";
&__content {
@include content-block();
margin-bottom: $large-spacing;
width: auto;
}
&__logo {
@include content-block();
margin-top: $medium-spacing
}
&__footer {
background-color: $color-silver-light;
display: block;
}
}
&__footer {
background-color: $color-silver-light;
display: block;
}
}
.footer {
padding: $large-spacing $medium-spacing 0;
.footer {
padding: $large-spacing $medium-spacing 0;
&__content {
@include content-block();
&__content {
@include content-block();
}
}
}
</style>

View File

@ -1,5 +1,5 @@
import '@babel/polyfill';
import Vue from 'vue';
import {createApp, inject} from 'vue';
import VueVimeoPlayer from 'vue-vimeo-player';
import apolloClientFactory from './graphql/client';
import VueApollo from 'vue-apollo';
@ -13,42 +13,9 @@ import ME_QUERY from '@/graphql/gql/queries/meQuery.gql';
import VueModal from '@/plugins/modal';
import VueRemoveEdges from '@/plugins/edges';
import VueMatomo from 'vue-matomo';
import VueLogger from 'vuejs3-logger';
import { joiningClass, loginRequired, unauthorizedAccess } from '@/router/guards';
import flavorPlugin from '@/plugins/flavor';
import log from 'loglevel';
window.log = log; // make log available in app when built, to change log level: log.setLevel('debug')
Vue.config.productionTip = false;
const isProduction = process.env.NODE_ENV === 'production';
const logLevel = isProduction ? 'error' : 'warn';
log.setDefaultLevel(logLevel);
Vue.use(VueModal);
Vue.use(VueRemoveEdges);
Vue.use(VueApollo);
Vue.use(VueVimeoPlayer);
Vue.use(VueScrollTo, {
duration: 500,
easing: 'ease-out',
offset: -50,
});
Vue.use(flavorPlugin);
if (process.env.MATOMO_HOST) {
Vue.use(VueMatomo, {
host: process.env.MATOMO_HOST,
siteId: process.env.MATOMO_SITE_ID,
router: router,
});
}
Vue.directive('click-outside', clickOutside);
Vue.directive('auto-grow', autoGrow);
const publicApolloClient = apolloClientFactory('/api/graphql-public/', null);
const privateApolloClient = apolloClientFactory('/api/graphql/', networkErrorCallback);
@ -60,56 +27,96 @@ const apolloProvider = new VueApollo({
defaultClient: privateApolloClient,
});
const app = createApp({
store,
router,
apolloProvider,
render: h => h(App),
});
const isProduction = process.env.NODE_ENV === 'production';
app.use(VueModal);
app.use(VueRemoveEdges);
app.use(VueApollo);
app.use(VueVimeoPlayer);
app.use(VueLogger, {
isEnabled: true,
logLevel: isProduction ? 'error' : 'debug',
stringifyArguments: false,
showConsoleColors: true,
});
// VueScrollTo.setDefaults({
// duration: 500,
// easing: 'ease-out',
// offset: -50,
// });
app.directive('scroll-to', VueScrollTo);
app.use(flavorPlugin);
if (process.env.MATOMO_HOST) {
app.use(VueMatomo, {
host: process.env.MATOMO_HOST,
siteId: process.env.MATOMO_SITE_ID,
router: router,
});
}
app.directive('click-outside', clickOutside);
app.directive('auto-grow', autoGrow);
/* guards */
function redirectUsersWithoutValidLicense() {
return privateApolloClient
.query({
query: ME_QUERY,
})
.then(({ data }) => data.me.expiryDate == null);
return privateApolloClient.query({
query: ME_QUERY,
}).then(({data}) => data.me.expiryDate == null);
}
function redirectStudentsWithoutClass() {
return privateApolloClient
.query({
query: ME_QUERY,
})
.then(({ data }) => data.me.schoolClasses.length === 0 && !data.me.isTeacher);
return privateApolloClient.query({
query: ME_QUERY,
}).then(({data}) => data.me.schoolClasses.length === 0 && !data.me.isTeacher);
}
function redirectUsersToOnboarding() {
return privateApolloClient
.query({
query: ME_QUERY,
})
.then(({ data }) => !data.me.onboardingVisited);
return privateApolloClient.query({
query: ME_QUERY,
}).then(({data}) => !data.me.onboardingVisited);
}
function networkErrorCallback(statusCode) {
if (statusCode === 402) {
log.debug('status code 402, redirecting');
router.push({ name: 'licenseActivation' });
router.push({name: 'licenseActivation'});
}
}
router.beforeEach(async (to, from, next) => {
log.debug('navigation guard called', to, from);
// todo: make logger work outside vue app
// const logger = inject('vuejs3-logger');
// logger.$log.debug('navigation guard called', to, from);
if (to.path === '/logout') {
log.debug('logout', to);
publicApolloClient.resetStore();
await publicApolloClient.resetStore();
if (process.env.LOGOUT_REDIRECT_URL) {
location.replace(`https://sso.hep-verlag.ch/logout?return_to=${process.env.LOGOUT_REDIRECT_URL}`);
next(false);
return;
} else {
next({ name: 'hello' });
next({name: 'hello'});
return;
}
}
if (unauthorizedAccess(to)) {
log.debug('unauthorized', to);
//logger.$log.debug('unauthorized', to);
const postLoginRedirectionUrl = to.path;
const redirectUrl = `/hello/`;
@ -117,46 +124,33 @@ router.beforeEach(async (to, from, next) => {
localStorage.setItem(postLoginRedirectUrlKey, postLoginRedirectionUrl);
}
log.debug('redirecting to hello', to);
// logger.$log.debug('redirecting to hello', to);
next(redirectUrl);
return;
}
if (to.name && to.name !== 'licenseActivation' && loginRequired(to) && (await redirectUsersWithoutValidLicense())) {
log.debug('redirecting to licenseActivation', to, null);
if (to.name && to.name !== 'licenseActivation' && loginRequired(to) && await redirectUsersWithoutValidLicense()) {
// logger.$log.debug('redirecting to licenseActivation', to, null);
console.log('redirecting to licenseActivation', to, null);
next({ name: 'licenseActivation' });
next({name: 'licenseActivation'});
return;
}
if (!joiningClass(to) && loginRequired(to) && (await redirectStudentsWithoutClass())) {
log.debug('redirecting to join-class', to);
log.debug('await redirectStudentsWithoutClass()', await redirectStudentsWithoutClass());
next({ name: 'join-class' });
if (!joiningClass(to) && loginRequired(to) && await redirectStudentsWithoutClass()) {
//logger.$log.debug('redirecting to join-class', to);
//logger.$log.debug('await redirectStudentsWithoutClass()', await redirectStudentsWithoutClass());
next({name: 'join-class'});
return;
}
if (
to.name &&
to.name.indexOf('onboarding') === -1 &&
!joiningClass(to) &&
loginRequired(to) &&
(await redirectUsersToOnboarding())
) {
log.debug('redirecting to onboarding-start', to);
next({ name: 'onboarding-start' });
if ((to.name && to.name.indexOf('onboarding') === -1) && !joiningClass(to) && loginRequired(to) && await redirectUsersToOnboarding()) {
//logger.$log.debug('redirecting to onboarding-start', to);
next({name: 'onboarding-start'});
return;
}
log.debug('End of Guard reached', to);
//logger.$log.debug('End of Guard reached', to);
next();
});
/* eslint-disable no-new */
new Vue({
el: '#app',
store,
router,
apolloProvider,
render: (h) => h(App),
});
app.mount('#app');

File diff suppressed because one or more lines are too long

View File

@ -1,82 +1,85 @@
<template>
<content-block-form title="Inhaltsblock erfassen" :content-block="contentBlock" @back="goToModule" @save="save" />
<content-block-form
title="Inhaltsblock erfassen"
:content-block="contentBlock"
@back="goToModule"
@save="save"
/>
</template>
<script>
import Vue from 'vue';
import {defineComponent} from 'vue';
import ContentBlockForm from '@/components/content-block-form/ContentBlockForm';
import { setUserBlockType } from '@/helpers/content-block';
import NEW_CONTENT_BLOCK_MUTATION from '@/graphql/gql/mutations/addContentBlock.gql';
import MODULE_DETAILS_QUERY from '@/graphql/gql/queries/modules/moduleDetailsQuery.gql';
import { cleanUpContents } from '@/components/content-block-form/helpers';
import ContentBlockForm from '@/components/content-block-form/ContentBlockForm';
import {setUserBlockType} from '@/helpers/content-block';
import NEW_CONTENT_BLOCK_MUTATION from '@/graphql/gql/mutations/addContentBlock.gql';
import MODULE_DETAILS_QUERY from '@/graphql/gql/queries/modules/moduleDetailsQuery.gql';
import {cleanUpContents} from '@/components/content-block-form/helpers';
export default Vue.extend({
props: {
parent: {
type: String,
default: '',
},
after: {
type: String,
default: '',
},
},
components: {
ContentBlockForm,
},
data: () => ({
contentBlock: {
title: '',
isAssignment: false,
contents: [],
},
}),
methods: {
save({ title, contents, isAssignment }) {
let cleanedContents = cleanUpContents(contents);
const contentBlock = {
title: title,
contents: cleanedContents,
type: setUserBlockType(isAssignment),
};
let input;
const { parent, after, slug } = this.$route.params;
if (after) {
input = {
contentBlock,
after,
};
} else {
input = {
contentBlock,
parent,
};
export default defineComponent({
props: {
parent: {
type: String,
default: ''
},
after: {
type: String,
default: ''
}
this.$apollo
.mutate({
},
components: {
ContentBlockForm,
},
data: () => ({
contentBlock: {
title: '',
isAssignment: false,
contents: [
]},
}),
methods: {
save({title, contents, isAssignment}) {
let cleanedContents = cleanUpContents(contents);
const contentBlock = {
title: title,
contents: cleanedContents,
type: setUserBlockType(isAssignment),
};
let input;
const { parent, after, slug } = this.$route.params;
if(after) {
input = {
contentBlock,
after
};
} else {
input = {
contentBlock,
parent
};
}
this.$apollo.mutate({
mutation: NEW_CONTENT_BLOCK_MUTATION,
variables: {
input,
input
},
refetchQueries: [
{
query: MODULE_DETAILS_QUERY,
variables: {
slug,
},
},
],
})
.then(this.goToModule);
},
goToModule() {
// use the history, so the scroll position is preserved
this.$router.go(-1);
},
},
});
refetchQueries: [{
query: MODULE_DETAILS_QUERY,
variables: {
slug
}
}]
}).then(this.goToModule);
},
goToModule() {
// use the history, so the scroll position is preserved
this.$router.go(-1);
}
}
});
</script>

View File

@ -8,7 +8,7 @@
:final="project.final"
data-cy="project-share-link"
class="project__share"
@click.native="updateProjectShareState(project.slug, !project.final)"
@click="updateProjectShareState(project.slug, !project.final)"
/>
<project-actions :share-buttons="false" class="project__more" :slug="project.slug" v-if="canEdit" />

View File

@ -5,14 +5,21 @@
</div>
<div class="topic__content">
<h1 data-cy="topic-title" class="topic__title">
<h1
data-cy="topic-title"
class="topic__title"
>
{{ topic.title }}
</h1>
<p class="topic__teaser">
{{ topic.teaser }}
</p>
<div class="topic__links">
<div class="topic__video-link topic__link" v-if="topic.vimeoId" @click="openVideo">
<div
class="topic__video-link topic__link"
v-if="topic.vimeoId"
@click="openVideo"
>
<play-icon class="topic__video-link-icon topic__link-icon" />
<span class="topic__link-description">Video schauen</span>
</div>
@ -27,187 +34,184 @@
</a>
</div>
<div class="topic__modules">
<module-teaser v-for="module in modules" :key="module.slug" v-bind="module" />
<module-teaser
v-for="module in modules"
v-bind="module"
:key="module.slug"
/>
</div>
</div>
</div>
</template>
<script>
import ModuleTeaser from '@/components/modules/ModuleTeaser.vue';
import TOPIC_QUERY from '@/graphql/gql/queries/topicQuery.gql';
import me from '@/mixins/me';
import TopicNavigation from '@/components/book-navigation/TopicNavigation';
import ModuleTeaser from '@/components/modules/ModuleTeaser.vue';
import TOPIC_QUERY from '@/graphql/gql/queries/topicQuery.gql';
import me from '@/mixins/me';
import TopicNavigation from '@/components/book-navigation/TopicNavigation';
import UPDATE_LAST_TOPIC_MUTATION from '@/graphql/gql/mutations/updateLastTopic.gql';
import ME_QUERY from '@/graphql/gql/queries/meQuery.gql';
import UPDATE_LAST_TOPIC_MUTATION from '@/graphql/gql/mutations/updateLastTopic.gql';
import ME_QUERY from '@/graphql/gql/queries/meQuery.gql';
const PlayIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/Play');
const BulbIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/BulbIcon');
const PlayIcon = () => import(/* webpackChunkName: "icons" */'@/components/icons/Play');
const BulbIcon = () => import(/* webpackChunkName: "icons" */'@/components/icons/BulbIcon');
export default {
mixins: [me],
components: {
TopicNavigation,
ModuleTeaser,
PlayIcon,
BulbIcon,
},
export default {
apollo: {
topic() {
return {
query: TOPIC_QUERY,
variables: {
slug: this.$route.params.topicSlug,
},
update(data) {
return this.$getRidOfEdges(data).topic || {};
},
result() {
if (this.saveMe) {
this.saveMe = false;
this.updateLastVisitedTopic(this.topic.id);
}
},
};
mixins: [me],
components: {
TopicNavigation,
ModuleTeaser,
PlayIcon,
BulbIcon,
},
},
data() {
return {
topic: {
modules: {
edges: [],
},
apollo: {
topic() {
return {
query: TOPIC_QUERY,
variables: {
slug: this.$route.params.topicSlug,
},
update(data) {
return this.$getRidOfEdges(data).topic || {};
},
result() {
if (this.saveMe) {
this.saveMe = false;
this.updateLastVisitedTopic(this.topic.id);
}
},
};
},
saveMe: false,
};
},
computed: {
modules() {
return this.topic.modules;
},
},
mounted() {
if (!this.topic.id) {
// component was loaded before topic, apollo not ready yet
this.saveMe = true; // needs saving, apollo will do this
} else {
this.updateLastVisitedTopic(this.topic.id);
}
},
methods: {
openVideo() {
this.$store.dispatch('showFullscreenVideo', this.topic.vimeoId);
},
updateLastVisitedTopic(topicId) {
if (!topicId) {
return;
}
this.$apollo.mutate({
mutation: UPDATE_LAST_TOPIC_MUTATION,
variables: {
input: {
id: topicId,
data() {
return {
topic: {
modules: {
edges: [],
},
},
update(
store,
{
data: {
updateLastTopic: { topic },
},
}
) {
if (topic) {
const query = ME_QUERY;
const { me } = store.readQuery({ query });
if (me) {
const data = {
me: {
...me,
lastTopic: topic,
},
};
store.writeQuery({ query, data });
}
}
},
});
saveMe: false,
};
},
},
};
computed: {
modules() {
return this.topic.modules;
},
},
mounted() {
if (!this.topic.id) { // component was loaded before topic, apollo not ready yet
this.saveMe = true; // needs saving, apollo will do this
} else {
this.updateLastVisitedTopic(this.topic.id);
}
},
methods: {
openVideo() {
this.$store.dispatch('showFullscreenVideo', this.topic.vimeoId);
},
updateLastVisitedTopic(topicId) {
if (!topicId) {
return;
}
this.$apollo.mutate({
mutation: UPDATE_LAST_TOPIC_MUTATION,
variables: {
input: {
id: topicId,
},
},
update(store, {data: {updateLastTopic: {topic}}}) {
if (topic) {
const query = ME_QUERY;
const {me} = store.readQuery({query});
if (me) {
const data = {
me: {
...me,
lastTopic: topic,
},
};
store.writeQuery({query, data});
}
}
},
});
},
},
};
</script>
<style scoped lang="scss">
@import '@/styles/_variables.scss';
@import '@/styles/_mixins.scss';
@import "@/styles/_variables.scss";
@import "@/styles/_mixins.scss";
.topic {
display: grid;
padding: $large-spacing 0;
grid-template-columns: 1fr;
@include desktop {
grid-template-columns: 300px 1fr;
}
&__navigation {
padding: 0 $medium-spacing;
display: none;
.topic {
display: grid;
padding: $large-spacing 0;
grid-template-columns: 1fr;
@include desktop {
display: block;
grid-template-columns: 300px 1fr;
}
&__navigation {
padding: 0 $medium-spacing;
display: none;
@include desktop {
display: block;
}
}
&__teaser {
color: $color-charcoal-dark;
width: 90%;
@include lead-paragraph;
margin-bottom: $large-spacing;
}
&__links {
margin-bottom: $large-spacing;
display: flex;
}
&__link {
cursor: pointer;
display: flex;
align-items: center;
}
&__video-link {
margin-right: $large-spacing;
}
&__link-icon {
width: 40px;
height: 40px;
margin-right: $medium-spacing;
}
&__link-description {
@include heading-3;
}
&__modules {
margin-top: 40px;
display: flex;
flex-wrap: wrap;
@supports (display: grid) {
display: grid;
}
grid-column-gap: $large-spacing;
grid-row-gap: $large-spacing;
@include desktop {
grid-template-columns: repeat(3, minmax(auto, 380px));
}
}
}
&__teaser {
color: $color-charcoal-dark;
width: 90%;
@include lead-paragraph;
margin-bottom: $large-spacing;
}
&__links {
margin-bottom: $large-spacing;
display: flex;
}
&__link {
cursor: pointer;
display: flex;
align-items: center;
}
&__video-link {
margin-right: $large-spacing;
}
&__link-icon {
width: 40px;
height: 40px;
margin-right: $medium-spacing;
}
&__link-description {
@include heading-3;
}
&__modules {
margin-top: 40px;
display: flex;
flex-wrap: wrap;
@supports (display: grid) {
display: grid;
}
grid-column-gap: $large-spacing;
grid-row-gap: $large-spacing;
@include desktop {
grid-template-columns: repeat(3, minmax(auto, 380px));
}
}
}
</style>

View File

@ -1,51 +1,51 @@
// adapted from
// https://stackoverflow.com/questions/41791193/vuejs-reactive-binding-for-a-plugin-how-to/41801107#41801107
import Vue, { VueConstructor } from 'vue';
import {reactive, App} from 'vue';
class ModalStore {
vm: Vue;
data: any;
constructor() {
this.vm = new Vue({
data: () => ({
component: '',
payload: {},
}),
this.data = reactive({
component: '',
payload: {}
});
}
get state() {
return this.vm.$data;
return this.data;
}
}
interface Modal {
state: any;
component: string;
payload?: any;
confirm: (res: any) => void;
open: (component: string, payload?: any) => Promise<(resolve: () => any, reject: () => any) => void>;
cancel: () => void;
_resolve: (r?: any) => any;
_reject: (r?: any) => any;
state: any,
component: string,
payload?: any,
confirm: (res: any) => void,
open: (component: string, payload?: any) => Promise<(resolve: () => any, reject: () => any) => void>,
cancel: () => void,
_resolve: (r?: any) => any,
_reject: (r?: any) => any
}
declare module 'vue/types/vue' {
interface Vue {
$modal: Modal;
declare module '@vue/runtime-core' {
interface ComponentCustomProperties {
$modal: Modal
}
}
const ModalPlugin = {
install(Vue: VueConstructor) {
install(app: App) {
const store = new ModalStore();
console.log('installing modal plugin');
const reset = () => {
store.state.component = '';
store.state.payload = {};
};
const modal: Modal = {
app.config.globalProperties.$modal = {
state: store.state,
component: store.state.component,
payload: store.state.payload,
@ -65,12 +65,13 @@ const ModalPlugin = {
reset();
this._reject();
},
_resolve: () => {},
_reject: () => {},
_resolve: () => {
},
_reject: () => {
},
};
Vue.prototype.$modal = modal;
},
}
};
export default ModalPlugin;

View File

@ -1,13 +1,13 @@
// from https://stackoverflow.com/questions/64213461/vuejs-typescript-cannot-find-module-components-navigation-or-its-correspon
declare module '*.vue' {
import Vue from 'vue';
export default Vue;
}
// declare module "*.vue" {
// import Vue from 'vue';
// export default Vue;
// }
// for Vue 3
// declare module '*.vue' {
// import type { DefineComponent } from 'vue'
// const component: DefineComponent<{}, {}, any>
// export default component
// }
//
declare module '*.vue' {
import type { DefineComponent } from 'vue';
const component: DefineComponent<{}, {}, any>;
export default component;
}