aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRon Lucke <lucke@elan-ev.de>2023-06-22 09:03:10 +0000
committerRon Lucke <lucke@elan-ev.de>2023-06-22 09:03:10 +0000
commit55cb643f4a1cfd71d784e28396c70368163e3d3e (patch)
tree6057e0e6ca9523d4a020281e826136a123c06057
parentcdebb80f05c277c83ef0c43f47bdb148d533b59f (diff)
Courseware - Vorlagen beim hinzufügen eine Seite anbieten
Closes #2021 Merge request studip/studip!1467
-rw-r--r--resources/vue/components/courseware/CoursewareStructuralElement.vue137
-rw-r--r--resources/vue/components/courseware/CoursewareStructuralElementDialogAdd.vue380
-rw-r--r--resources/vue/mixins/courseware/wizard.js17
3 files changed, 407 insertions, 127 deletions
diff --git a/resources/vue/components/courseware/CoursewareStructuralElement.vue b/resources/vue/components/courseware/CoursewareStructuralElement.vue
index e472ee0..94d6ca7 100644
--- a/resources/vue/components/courseware/CoursewareStructuralElement.vue
+++ b/resources/vue/components/courseware/CoursewareStructuralElement.vue
@@ -367,62 +367,12 @@
</courseware-tabs>
</template>
</studip-dialog>
-
- <studip-dialog
+ <courseware-structural-element-dialog-add
v-if="showAddDialog"
- :title="$gettext('Seite hinzufügen')"
- :confirmText="$gettext('Erstellen')"
- confirmClass="accept"
- :closeText="$gettext('Schließen')"
- closeClass="cancel"
- class="cw-structural-element-dialog"
- :height="inCourse ? '300' : '430'"
- @close="closeAddDialog"
- @confirm="createElement"
- >
- <template v-slot:dialogContent>
- <form class="default" @submit.prevent="">
- <label>
- <translate>Position der neuen Seite</translate>
- <select v-model="newChapterParent">
- <option v-if="!isRoot && canEditParent" value="sibling">
- <translate>Neben der aktuellen Seite</translate>
- </option>
- <option value="descendant"><translate>Unterhalb der aktuellen Seite</translate></option>
- </select>
- </label>
- <label>
- <translate>Name der neuen Seite</translate><br />
- <input v-model="newChapterName" type="text" />
- </label>
- <label v-if="!inCourse">
- <translate>Art des Lernmaterials</translate>
- <select v-model="newChapterPurpose">
- <option value="content"><translate>Inhalt</translate></option>
- <option v-if="!inCourse" value="template"><translate>Aufgabenvorlage</translate></option>
- <option value="oer"><translate>OER-Material</translate></option>
- <option value="portfolio"><translate>ePortfolio</translate></option>
- <option value="draft"><translate>Entwurf</translate></option>
- <option value="other"><translate>Sonstiges</translate></option>
- </select>
- </label>
- <label v-if="!inCourse">
- <translate>Lernmaterialvorlage</translate>
- <select v-model="newChapterTemplate">
- <option :value="null"><translate>ohne Vorlage</translate></option>
- <option
- v-for="template in selectableTemplates"
- :key="template.id"
- :value="template"
- >
- {{ template.attributes.name }}
- </option>
- </select>
- </label>
- </form>
- </template>
- </studip-dialog>
-
+ :structuralElement="structuralElement"
+ :isRoot="isRoot"
+ :canEditParent="canEditParent"
+ />
<studip-dialog
v-if="showInfoDialog"
:title="textInfo.title"
@@ -682,6 +632,7 @@
<script>
import ContainerComponents from './container-components.js';
import CoursewarePluginComponents from './plugin-components.js';
+import CoursewareStructuralElementDialogAdd from './CoursewareStructuralElementDialogAdd.vue';
import CoursewareStructuralElementDialogCopy from './CoursewareStructuralElementDialogCopy.vue';
import CoursewareStructuralElementDialogImport from './CoursewareStructuralElementDialogImport.vue';
import CoursewareStructuralElementDialogLink from './CoursewareStructuralElementDialogLink.vue';
@@ -700,6 +651,7 @@ import CoursewareTab from './CoursewareTab.vue';
import CoursewareExport from '@/vue/mixins/courseware/export.js';
import CoursewareOerMessage from '@/vue/mixins/courseware/oermessage.js';
import colorMixin from '@/vue/mixins/courseware/colors.js';
+import wizardMixin from '@/vue/mixins/courseware/wizard.js';
import CoursewareDateInput from './CoursewareDateInput.vue';
import { FocusTrap } from 'focus-trap-vue';
import IsoDate from './IsoDate.vue';
@@ -710,6 +662,7 @@ import { mapActions, mapGetters } from 'vuex';
export default {
name: 'courseware-structural-element',
components: {
+ CoursewareStructuralElementDialogAdd,
CoursewareStructuralElementDialogCopy,
CoursewareStructuralElementDialogImport,
CoursewareStructuralElementDialogLink,
@@ -733,14 +686,10 @@ export default {
},
props: ['canVisit', 'orderedStructuralElements', 'structuralElement'],
- mixins: [CoursewareExport, CoursewareOerMessage, colorMixin],
+ mixins: [CoursewareExport, CoursewareOerMessage, colorMixin, wizardMixin],
data() {
return {
- newChapterName: '',
- newChapterParent: 'descendant',
- newChapterPurpose: 'content',
- newChapterTemplate: null,
currentElement: '',
uploadFileError: '',
textCompanionWrongContext: this.$gettext('Die angeforderte Seite ist nicht Teil dieser Courseware.'),
@@ -1231,11 +1180,6 @@ export default {
ownerName() {
return this.owner?.attributes['formatted-name'] ?? '?';
},
- selectableTemplates() {
- return this.templates.filter(template => {
- return template.attributes.purpose === this.newChapterPurpose
- });
- },
complete() {
return this.elementProgress === 100;
},
@@ -1253,7 +1197,6 @@ export default {
methods: {
...mapActions({
- createStructuralElementWithTemplate: 'createStructuralElementWithTemplate',
updateStructuralElement: 'updateStructuralElement',
deleteStructuralElement: 'deleteStructuralElement',
lockObject: 'lockObject',
@@ -1316,8 +1259,6 @@ export default {
this.showElementEditDialog(true);
break;
case 'addElement':
- this.newChapterName = '';
- this.newChapterParent = 'descendant';
this.errorEmptyChapterName = false;
this.showElementAddDialog(true);
break;
@@ -1372,14 +1313,7 @@ export default {
this.showElementAddDialog(false);
},
checkUploadFile() {
- const file = this.$refs?.upload_image?.files[0];
- if (file.size > 2097152) {
- this.uploadFileError = this.$gettext('Diese Datei ist zu groß. Bitte wählen Sie eine kleinere Datei.');
- } else if (!file.type.includes('image')) {
- this.uploadFileError = this.$gettext('Diese Datei ist kein Bild. Bitte wählen Sie ein Bild aus.');
- } else {
- this.uploadFileError = '';
- }
+ this.uploadFileError = this.checkUploadImageFile(this.$refs?.upload_image?.files[0]);
},
deleteImage() {
if (!this.deletingPreviewImage) {
@@ -1552,57 +1486,6 @@ export default {
this.companionError({ info: this.$gettext('Die Seite konnte nicht gelöscht werden.') });
});
},
- async createElement() {
- const title = this.newChapterName; // this is the title of the new element
- const purpose = this.newChapterPurpose;
- let parent_id = this.currentId; // new page is descandant as default
-
- this.errorEmptyChapterName = title.trim();
- if (this.errorEmptyChapterName === '') {
- return;
- }
- if (this.newChapterParent === 'sibling') {
- parent_id = this.structuralElement.relationships.parent.data.id;
- }
- this.showElementAddDialog(false);
- this.createStructuralElementWithTemplate({
- attributes: {
- title: title,
- purpose: purpose,
- },
- templateId: this.newChapterTemplate ? this.newChapterTemplate.id : null,
- parentId: parent_id,
- currentId: this.currentId,
- })
- .then(() => {
- let newElement = this.$store.getters['courseware-structural-elements/lastCreated'];
- this.companionSuccess({
- info:
- this.$gettextInterpolate(
- this.$gettext('Die Seite %{ pageTitle } wurde erfolgreich angelegt.'),
- { pageTitle: newElement.attributes.title }
- )
- });
- this.$router.push(newElement.id);
- })
- .catch(e => {
- let errorMessage = this.$gettext('Es ist ein Fehler aufgetreten. Die Seite konnte nicht erstellt werden.');
- if (e.status === 403) {
- errorMessage = this.$gettext('Die Seite konnte nicht erstellt werden. Sie haben nicht die notwendigen Schreibrechte.');
- }
-
- this.companionError({ info: errorMessage });
- });
-
- let newElement = this.lastCreatedElement;
- this.companionSuccess({
- info: this.$gettextInterpolate(
- this.$gettext('Die Seite %{ pageTitle } wurde erfolgreich angelegt.'),
- {pageTitle: newElement.attributes.title}
- )
- });
- this.newChapterName = '';
- },
containerComponent(container) {
return 'courseware-' + container.attributes['container-type'] + '-container';
},
diff --git a/resources/vue/components/courseware/CoursewareStructuralElementDialogAdd.vue b/resources/vue/components/courseware/CoursewareStructuralElementDialogAdd.vue
new file mode 100644
index 0000000..d182495
--- /dev/null
+++ b/resources/vue/components/courseware/CoursewareStructuralElementDialogAdd.vue
@@ -0,0 +1,380 @@
+<template>
+ <studip-wizard-dialog
+ :title="$gettext('Seite hinzufügen')"
+ :confirmText="$gettext('Erstellen')"
+ :closeText="$gettext('Abbrechen')"
+ :slots="wizardSlots"
+ :lastRequiredSlotId="1"
+ :requirements="requirements"
+ @close="closeAddDialog"
+ @confirm="createElement"
+ >
+ <template v-slot:basic>
+ <form class="default" @submit.prevent="">
+ <label>
+ {{ $gettext('Position der neuen Seite') }}
+ <select v-model="pageParent">
+ <option v-if="!isRoot && canEditParent" value="sibling">
+ {{ $gettext('Neben der aktuellen Seite') }}
+ </option>
+ <option value="descendant">{{ $gettext('Unterhalb der aktuellen Seite') }}</option>
+ </select>
+ </label>
+ <label>
+ <span>{{ text.title }}</span>
+ <span aria-hidden="true" class="wizard-required">*</span>
+ <input type="text" v-model="title" required />
+ </label>
+ <label>
+ <span>{{ $gettext('Beschreibung') }}</span>
+ <textarea v-model="description" required />
+ </label>
+ </form>
+ </template>
+ <template v-slot:template>
+ <form v-if="hasTemplates" class="default" @submit.prevent="">
+ <label>
+ {{ $gettext('Art der Vorlage') }}
+ <select v-model="templatePurpose">
+ <option value="content">{{ $gettext('Inhalt') }}</option>
+ <option value="oer">{{ $gettext('OER-Material') }}</option>
+ <option value="portfolio">{{ $gettext('ePortfolio') }}</option>
+ <option value="draft">{{ $gettext('Entwurf') }}</option>
+ <option v-if="!inCourseContext" value="template">{{ $gettext('Aufgabenvorlage') }}</option>
+ <option value="other">{{ $gettext('Sonstiges') }}</option>
+ </select>
+ </label>
+ <label>
+ <span>{{ $gettext('Vorlage') }}</span>
+ <select v-model="selectedTemplate">
+ <option :value="null">{{ $gettext('ohne Vorlage') }}</option>
+ <option v-for="template in selectableTemplates" :key="template.id" :value="template">
+ {{ template.attributes.name }}
+ </option>
+ </select>
+ </label>
+ </form>
+ <courseware-companion-box
+ v-else
+ :msgCompanion="$gettext('Es wurden keine Vorlagen gefunden.')"
+ mood="pointing"
+ class="cw-companion-box-in-form"
+ />
+ </template>
+ <template v-slot:layout>
+ <form class="default" @submit.prevent="">
+ <label>
+ {{ $gettext('Bild') }}
+ <input
+ class="cw-file-input"
+ ref="upload_image"
+ type="file"
+ accept="image/*"
+ @change="checkUploadFile"
+ />
+ <courseware-companion-box
+ v-if="uploadFileError"
+ :msgCompanion="uploadFileError"
+ mood="sad"
+ class="cw-companion-box-in-form"
+ />
+ </label>
+ <label>
+ {{ $gettext('Farbe') }}
+ <studip-select v-model="color" :options="colors" :reduce="(color) => color.class" label="class">
+ <template #open-indicator="selectAttributes">
+ <span v-bind="selectAttributes"><studip-icon shape="arr_1down" size="10" /></span>
+ </template>
+ <template #no-options>
+ {{ $gettext('Es steht keine Auswahl zur Verfügung.') }}
+ </template>
+ <template #selected-option="{ name, hex }">
+ <span class="vs__option-color" :style="{ 'background-color': hex }"></span>
+ <span>{{ name }}</span>
+ </template>
+ <template #option="{ name, hex }">
+ <span class="vs__option-color" :style="{ 'background-color': hex }"></span>
+ <span>{{ name }}</span>
+ </template>
+ </studip-select>
+ </label>
+ </form>
+ </template>
+ <template v-slot:advanced>
+ <form class="default" @submit.prevent="">
+ <label>
+ {{ $gettext('Art des Lernmaterials') }}
+ <select v-model="purpose">
+ <option value="content">{{ $gettext('Inhalt') }}</option>
+ <option value="oer">{{ $gettext('OER-Material') }}</option>
+ <option value="portfolio">{{ $gettext('ePortfolio') }}</option>
+ <option value="draft">{{ $gettext('Entwurf') }}</option>
+ <option v-if="!inCourseContext" value="template">{{ $gettext('Aufgabenvorlage') }}</option>
+ <option value="other">{{ $gettext('Sonstiges') }}</option>
+ </select>
+ </label>
+ <label>
+ {{ $gettext('Lizenztyp') }}
+ <select v-model="license_type">
+ <option v-for="license in licenses" :key="license.id" :value="license.id">
+ {{ license.name }}
+ </option>
+ </select>
+ </label>
+ <label>
+ {{ $gettext('Geschätzter zeitlicher Aufwand') }}
+ <input type="text" v-model="required_time" />
+ </label>
+ <label>
+ {{ $gettext('Niveau') }}<br />
+ {{ $gettext('von') }}
+ <select v-model="difficulty_start">
+ <option v-for="difficulty_start in 12" :key="difficulty_start" :value="difficulty_start">
+ {{ difficulty_start }}
+ </option>
+ </select>
+ {{ $gettext('bis') }}
+ <select v-model="difficulty_end">
+ <option v-for="difficulty_end in 12" :key="difficulty_end" :value="difficulty_end">
+ {{ difficulty_end }}
+ </option>
+ </select>
+ </label>
+ </form>
+ </template>
+ </studip-wizard-dialog>
+</template>
+
+<script>
+import CoursewareCompanionBox from './CoursewareCompanionBox.vue';
+import StudipSelect from './../StudipSelect.vue';
+import StudipWizardDialog from './../StudipWizardDialog.vue';
+import colorMixin from '@/vue/mixins/courseware/colors.js';
+import wizardMixin from '@/vue/mixins/courseware/wizard.js';
+import { mapActions, mapGetters } from 'vuex';
+
+export default {
+ name: 'courseware-structural-element-dialog-add',
+ mixins: [colorMixin, wizardMixin],
+ components: {
+ CoursewareCompanionBox,
+ StudipWizardDialog,
+ StudipSelect,
+ },
+ props: {
+ structuralElement: Object,
+ isRoot: Boolean,
+ canEditParent: Boolean,
+ },
+ data() {
+ return {
+ wizardSlots: [
+ {
+ id: 1,
+ valid: false,
+ name: 'basic',
+ title: this.$gettext('Grundeinstellungen'),
+ icon: 'courseware',
+ description: this.$gettext(
+ 'Wählen Sie einen kurzen, prägnanten Titel und beschreiben Sie in einigen Worten den Inhalt der Seite.'
+ ),
+ },
+ {
+ id: 2,
+ valid: true,
+ name: 'template',
+ title: this.$gettext('Vorlage'),
+ icon: 'content2',
+ description: this.$gettext('Vorlagen enthalten Abschnitte und Blöcke, die bereits für bestimmte Zwecke angeordent sind. Beim anlegen der Seite, wird diese mit Abschnitten und Blöcken befüllt.'),
+ },
+ {
+ id: 3,
+ valid: true,
+ name: 'layout',
+ title: this.$gettext('Erscheinung'),
+ icon: 'picture',
+ description: this.$gettext(
+ 'Ein Vorschaubild motiviert Lernende die Seite zu erkunden. Die Kombination aus Bild und Farbe erleichtert das wiederfinden der Seite in einem Inhaltsverzeichnisblock.'
+ ),
+ },
+ {
+ id: 4,
+ valid: true,
+ name: 'advanced',
+ title: this.$gettext('Zusatzangaben'),
+ icon: 'info-list',
+ description: this.$gettext(
+ 'Hier können Sie detaillierte Angaben zur Seite eintragen. Diese sind besonders interessant wenn die Seite als OER geteilt wird.'
+ ),
+ },
+ ],
+ text: {
+ title: this.$gettext('Titel der neuen Seite'),
+ },
+ uploadFileError: '',
+ requirements: [],
+
+ pageParent: '',
+ title: '',
+ description: '',
+ purpose: '',
+ color: '',
+ license_type: '',
+ required_time: '',
+ difficulty_start: '',
+ difficulty_end: '',
+ templatePurpose: '',
+ selectedTemplate: null,
+ };
+ },
+ computed: {
+ ...mapGetters({
+ licenses: 'licenses',
+ context: 'context',
+ lastCreatedStructuralElement: 'courseware-structural-elements/lastCreated',
+ structuralElementById: 'courseware-structural-elements/byId',
+ templates: 'courseware-templates/all',
+ }),
+ inCourseContext() {
+ return this.context.type === 'courses';
+ },
+ colors() {
+ return this.mixinColors.filter((color) => color.darkmode);
+ },
+ selectableTemplates() {
+ return this.templates.filter((template) => {
+ return template.attributes.purpose === this.templatePurpose;
+ });
+ },
+ hasTemplates() {
+ return this.templates.length > 0;
+ },
+ },
+ methods: {
+ ...mapActions({
+ createStructuralElementWithTemplate: 'createStructuralElementWithTemplate',
+ updateStructuralElement: 'updateStructuralElement',
+ companionError: 'companionError',
+ companionInfo: 'companionInfo',
+ companionSuccess: 'companionSuccess',
+ showElementAddDialog: 'showElementAddDialog',
+ loadStructuralElementById: 'courseware-structural-elements/loadById',
+ uploadImageForStructuralElement: 'uploadImageForStructuralElement',
+ }),
+ initWizardData() {
+ this.pageParent = 'descendant';
+ this.title = '';
+ this.description = '';
+ this.purpose = 'content';
+ this.color = 'studip-blue';
+ this.license_type = '';
+ this.required_time = '';
+ this.difficulty_start = '';
+ this.difficulty_end = '';
+ this.templatePurpose = 'content';
+ this.selectedTemplate = null;
+ this.requirements.push({ slot: this.wizardSlots[0], text: this.text.title });
+ },
+ closeAddDialog() {
+ this.showElementAddDialog(false);
+ this.initWizardData();
+ },
+ checkUploadFile() {
+ this.uploadFileError = this.checkUploadImageFile(this.$refs?.upload_image?.files[0]);
+ },
+ async createElement() {
+ let parent_id = this.structuralElement.id; // new page is descandant as default
+
+ this.errorEmptyChapterName = this.title.trim();
+ if (this.errorEmptyChapterName === '') {
+ return;
+ }
+ if (this.pageParent === 'sibling') {
+ parent_id = this.structuralElement.relationships.parent.data.id;
+ }
+ this.showElementAddDialog(false);
+
+ const file = this.$refs?.upload_image?.files[0];
+ const element = {
+ attributes: {
+ title: this.title,
+ purpose: this.purpose,
+ payload: {
+ description: this.description,
+ color: this.color,
+ license_type: this.license_type,
+ required_time: this.required_time,
+ difficulty_start: this.difficulty_start,
+ difficulty_end: this.difficulty_end,
+ },
+ },
+ templateId: this.selectedTemplate ? this.selectedTemplate.id : null,
+ parentId: parent_id,
+ currentId: this.structuralElement.id,
+ };
+
+ try {
+ await this.createStructuralElementWithTemplate(element);
+ } catch (e) {
+ let errorMessage = this.$gettext(
+ 'Es ist ein Fehler aufgetreten. Die Seite konnte nicht erstellt werden.'
+ );
+ if (e.status === 403) {
+ errorMessage = this.$gettext(
+ 'Die Seite konnte nicht erstellt werden. Sie haben nicht die notwendigen Schreibrechte.'
+ );
+ }
+
+ this.companionError({ info: errorMessage });
+ return;
+ }
+
+ const newCreated = this.lastCreatedStructuralElement;
+ await this.loadStructuralElementById({ id: newCreated.id });
+ const newElement = this.structuralElementById({ id: newCreated.id });
+ this.companionSuccess({
+ info: this.$gettextInterpolate(this.$gettext('Die Seite %{ pageTitle } wurde erfolgreich angelegt.'), {
+ pageTitle: newElement.attributes.title,
+ }),
+ });
+
+ if (file && this.uploadFileError === '') {
+ try {
+ await this.uploadImageForStructuralElement({ structuralElement: newElement, file });
+ } catch (error) {
+ console.error(error);
+ this.companionError({
+ info: this.$gettext('Das Bild für das neue Lernmaterial konnte nicht gespeichert werden.'),
+ });
+ }
+ this.loadStructuralElementById({ id: newElement.id, options: { include: 'children' } });
+ }
+ this.initWizardData();
+ this.$router.push(newElement.id);
+ },
+ },
+ mounted() {
+ this.initWizardData();
+ if (!this.hasTemplates) {
+ this.wizardSlots.splice(1,1);
+ this.wizardSlots[1]['id'] = 2;
+ this.wizardSlots[2]['id'] = 3;
+ }
+ },
+ watch: {
+ title(newTitle) {
+ this.requirements = [];
+ const slot = this.wizardSlots[0];
+ if (newTitle === '') {
+ slot.valid = false;
+ this.requirements.push({ slot: slot, text: this.text.title });
+ } else {
+ slot.valid = true;
+ }
+ },
+ templatePurpose(newPurpose) {
+ this.selectedTemplate = null;
+ },
+ },
+};
+</script>
diff --git a/resources/vue/mixins/courseware/wizard.js b/resources/vue/mixins/courseware/wizard.js
new file mode 100644
index 0000000..b3ea222
--- /dev/null
+++ b/resources/vue/mixins/courseware/wizard.js
@@ -0,0 +1,17 @@
+const wizardMixin = {
+ methods: {
+ checkUploadImageFile(file) {
+ let uploadFileError = '';
+ if (file.size > 2097152) {
+ uploadFileError = this.$gettext('Diese Datei ist zu groß. Bitte wählen Sie eine Datei aus, die kleiner als 2MB ist.');
+ }
+ if (!file.type.includes('image')) {
+ uploadFileError = this.$gettext('Diese Datei ist kein Bild. Bitte wählen Sie ein Bild aus.');
+ }
+
+ return uploadFileError;
+ },
+ }
+};
+
+export default wizardMixin; \ No newline at end of file