diff --git a/lib/classes/JsonApi/Routes/Courseware/CoursewareInstancesUpdate.php b/lib/classes/JsonApi/Routes/Courseware/CoursewareInstancesUpdate.php index 5f0269089541d36935bf067c3f3ee339fca8ff18..2da917ec69ef107999d0c38c95dd70ac5f789ceb 100644 --- a/lib/classes/JsonApi/Routes/Courseware/CoursewareInstancesUpdate.php +++ b/lib/classes/JsonApi/Routes/Courseware/CoursewareInstancesUpdate.php @@ -168,6 +168,9 @@ class CoursewareInstancesUpdate extends JsonApiController $resetProgressSettings = $get('data.attributes.reset-progress-settings'); $instance->setResetProgressSettings($resetProgressSettings); + $linkedUnits = $get('data.attributes.linked-units'); + $instance->setLinkedUnits($linkedUnits); + // Store changes in unit configuration. $instance->getUnit()->store(); diff --git a/lib/classes/JsonApi/Schemas/Courseware/Instance.php b/lib/classes/JsonApi/Schemas/Courseware/Instance.php index a30fbae007c4ccce02b4aaafca7ccf97f49a6c0c..6c0e41ef0e37682775f6571eed7373d9615faa23 100644 --- a/lib/classes/JsonApi/Schemas/Courseware/Instance.php +++ b/lib/classes/JsonApi/Schemas/Courseware/Instance.php @@ -46,7 +46,8 @@ class Instance extends SchemaProvider 'reminder-settings' => $resource->getReminderSettings(), 'reset-progress-settings' => $resource->getResetProgressSettings(), 'root-id' => $resource->getRoot()->id, - 'is-teacher' => $GLOBALS['perm']->have_studip_perm($resource->getEditingPermissionLevel(), $resource->getRoot()->range_id) + 'is-teacher' => $GLOBALS['perm']->have_studip_perm($resource->getEditingPermissionLevel(), $resource->getRoot()->range_id), + 'linked-units' => $resource->getLinkedUnits() ]; } diff --git a/lib/models/Courseware/BlockTypes/Link.json b/lib/models/Courseware/BlockTypes/Link.json index a6b6db2c5d79bee61310ad0c4815a4b26c3b9e25..351983f84614c4e1e0039eee62ff6993876124f3 100644 --- a/lib/models/Courseware/BlockTypes/Link.json +++ b/lib/models/Courseware/BlockTypes/Link.json @@ -8,6 +8,9 @@ "target": { "type": "string" }, + "unit-target": { + "type": "string" + }, "url": { "type": "string" }, diff --git a/lib/models/Courseware/BlockTypes/Link.php b/lib/models/Courseware/BlockTypes/Link.php index 3282e2c4bc8e414de9dd6a5769772b4216463e6c..8e140792f22ae9f121d6884d52d1fd35ed5f6d94 100644 --- a/lib/models/Courseware/BlockTypes/Link.php +++ b/lib/models/Courseware/BlockTypes/Link.php @@ -30,8 +30,9 @@ class Link extends BlockType public function initialPayload(): array { return [ - 'type' => '', + 'type' => 'external', 'target' => '', + 'unit-target' => '', 'url' => '', 'title' => '', ]; @@ -61,9 +62,21 @@ class Link extends BlockType public static function getTags(): array { return [ - _('URL'), _('Verlinkung'), _('Webseite'), _('extern'), _('weiterleiten'), - _('Material'), _('Zusatz'), _('Weiterleitung'), _('intern'), _('Verweis'), - _('Index'), _('Hyperlink'), _('Quellenangabe'), _('Linkliste'), _('Linksammlung') + _('URL'), + _('Verlinkung'), + _('Webseite'), + _('extern'), + _('weiterleiten'), + _('Material'), + _('Zusatz'), + _('Weiterleitung'), + _('intern'), + _('Verweis'), + _('Index'), + _('Hyperlink'), + _('Quellenangabe'), + _('Linkliste'), + _('Linksammlung') ]; } } diff --git a/lib/models/Courseware/Instance.php b/lib/models/Courseware/Instance.php index 2676a41e88f7ce64e63cedf5d11ddff67b2daeb0..c9005deda2e9cc0903e3af9de1cf10baac34b8ab 100644 --- a/lib/models/Courseware/Instance.php +++ b/lib/models/Courseware/Instance.php @@ -585,4 +585,37 @@ class Instance return $data; } + /* + * + * LINKED UNITS + * + */ + public function getLinkedUnits(): array + { + $config = $this->unit->config->getArrayCopy(); + if (array_key_exists('linked_units', $config)) { + return $config['linked_units']; + } + + return []; + } + + public function setLinkedUnits(array $units): void + { + $this->validateLinkedUnits($units); + $this->unit->config['linked_units'] = $units; + } + + public function isValidLinkedUnits($units): bool + { + return is_array($units); + } + + private function validateLinkedUnits($units): void + { + if (!$this->isValidLinkedUnits($units)) { + throw new \InvalidArgumentException('Invalid linked units for courseware.'); + } + } + } diff --git a/resources/assets/stylesheets/scss/courseware.scss b/resources/assets/stylesheets/scss/courseware.scss index a5409dce41aeed3d6f91ce8734c3643ab7a29921..c555b6dbeeee57aa1c155f31a70ac6cbbda78b9c 100644 --- a/resources/assets/stylesheets/scss/courseware.scss +++ b/resources/assets/stylesheets/scss/courseware.scss @@ -32,3 +32,4 @@ @import './courseware/layouts/talk-bubble.scss'; @import './courseware/layouts/tile.scss'; @import './courseware/layouts/tree.scss'; +@import './courseware/layouts/tree-units.scss'; diff --git a/resources/assets/stylesheets/scss/courseware/blocks/link.scss b/resources/assets/stylesheets/scss/courseware/blocks/link.scss index eeeab6bb85c42cea967883d7f9a2d7ef639b9721..35f01ae10c81bd60bbbba2d10835301f4e05253d 100644 --- a/resources/assets/stylesheets/scss/courseware/blocks/link.scss +++ b/resources/assets/stylesheets/scss/courseware/blocks/link.scss @@ -13,6 +13,11 @@ .cw-link-title { margin-left: 3em; + &.unit { + header { + font-size: 16px; + } + } } &:hover { @@ -69,5 +74,35 @@ } } } + + &.unit { + height: unset; + display: flex; + + .cw-unit-link { + background-repeat: no-repeat; + background-size: 270px 180px; + background-position: center; + min-width: 270px; + height: 180px; + } + + .cw-link-title { + p { + color: var(--black); + } + } + + &:hover { + background-color: unset; + border: solid thin var(--base-color); + .cw-link-title { + header { + color: var(--active-color); + } + + } + } + } } } diff --git a/resources/assets/stylesheets/scss/courseware/layouts/ribbon.scss b/resources/assets/stylesheets/scss/courseware/layouts/ribbon.scss index 4e489cc073f277151b223420aa504f71e0f62f06..457cd4decd772324de985deb74e0b9ab9be79a27 100644 --- a/resources/assets/stylesheets/scss/courseware/layouts/ribbon.scss +++ b/resources/assets/stylesheets/scss/courseware/layouts/ribbon.scss @@ -306,6 +306,16 @@ $consum_ribbon_width: calc(100% - 58px); overflow: hidden; padding: 0; } + + .cw-tools-contents { + display: flex; + flex-direction: column; + height: 100%; + + .cw-tree { + flex-grow: 1; + } + } } } } diff --git a/resources/assets/stylesheets/scss/courseware/layouts/tree-units.scss b/resources/assets/stylesheets/scss/courseware/layouts/tree-units.scss new file mode 100644 index 0000000000000000000000000000000000000000..63288b7b3c4c40f882dc4fb01176e9c4c52ef4e9 --- /dev/null +++ b/resources/assets/stylesheets/scss/courseware/layouts/tree-units.scss @@ -0,0 +1,83 @@ +.cw-tree-units { + .cw-tree-unit-title { + border-bottom: solid thin var(--content-color-40); + color: var(--black); + font-size: 16px; + } + ol { + list-style: none; + padding-left: 0; + } + .button.trash { + margin: 0; + min-width: unset; + height: 30px; + width: 30px; + border: none; + } +} + +.cw-tree-unit-link { + display: flex; + flex-direction: row; + align-items: start; + justify-content: space-between; + margin: 0.5em 0; + + .cw-tree-units-header { + display: flex; + flex-direction: row; + height: 100px; + margin-top: 8px; + .cw-tree-units-header-image { + height: 100px; + width: 150px; + min-width: 150px; + background-size: 100% auto; + background-repeat: no-repeat; + background-position: center; + background-color: var(--content-color-20); + } + + .cw-tree-units-header-details { + margin: 0 8px; + display: -webkit-box; + overflow: hidden; + height: 100px; + -webkit-line-clamp: 5; + -webkit-box-orient: vertical; + header { + margin: 0 0 6px 0; + font-size: 16px; + line-height: 16px; + } + p { + margin: 0; + color: var(--black); + } + } + } +} + +.cw-tree-units-adder { + .add-element { + border: none; + cursor: pointer; + background-color: transparent; + height: 34px; + } + .cw-tree-units-adder-form { + display: flex; + + label { + flex-grow: 1; + } + .button.cancel { + min-width: unset; + height: 30px; + width: 30px; + margin: 2px 0 0 5px; + border: none; + } + } +} diff --git a/resources/vue/components/courseware/blocks/CoursewareLinkBlock.vue b/resources/vue/components/courseware/blocks/CoursewareLinkBlock.vue index 532c97b1c649b8609c22b42cc85240d73f59acc3..b87b29b2146c8e73522daac1e4a6824beef3de7b 100644 --- a/resources/vue/components/courseware/blocks/CoursewareLinkBlock.vue +++ b/resources/vue/components/courseware/blocks/CoursewareLinkBlock.vue @@ -26,20 +26,44 @@ </div> </router-link> </div> + <div v-if="currentType === 'unit' && inCourseContext"> + <a v-if="currentUnitData" :href="currentUnitData.url"> + <studip-ident-image + v-model="identimage" + :baseColor="currentUnitData.color?.hex ?? '#fff'" + :pattern="currentUnitData.title ?? ''" + /> + <div class="cw-link unit"> + <div class="cw-unit-link" :style="previewImageStyle"></div> + <div class="cw-link-title unit"> + <header>{{ currentUnitData.title }}</header> + <p>{{ currentUnitData.description }}</p> + </div> + </div> + </a> + <courseware-companion-box + v-else + mood="pointing" + :msgCompanion="$gettext('Bitte wählen Sie ein Lernmaterial als Ziel aus.')" + /> + </div> </template> <template v-if="canEdit" #edit> <form class="default" @submit.prevent=""> - <label> - {{ $gettext('Titel') }} - <input type="text" v-model="currentTitle" /> - </label> <label> {{ $gettext('Art des Links') }} <select v-model="currentType"> <option value="external">{{ $gettext('Extern') }}</option> <option value="internal">{{ $gettext('Intern') }}</option> + <option v-if="inCourseContext" value="unit"> + {{ $gettext('Lernmaterial in der Veranstaltung') }} + </option> </select> </label> + <label v-show="currentType !== 'unit'"> + {{ $gettext('Titel') }} + <input type="text" v-model="currentTitle" /> + </label> <label v-show="currentType === 'external'"> {{ $gettext('URL') }} <input type="text" v-model="currentUrl" @change="fixUrl" /> @@ -47,11 +71,19 @@ <label v-show="currentType === 'internal'"> {{ $gettext('Seite') }} <select v-model="currentTarget"> - <option v-for="(el, index) in courseware" :key="index" :value="el.id"> + <option v-for="(el, index) in filteredStructuralElements" :key="index" :value="el.id"> {{ el.attributes.title }} </option> </select> </label> + <label v-show="currentType === 'unit' && inCourseContext"> + {{ $gettext('Lernmaterial') }} + <select v-model="currentUnitTarget"> + <option v-for="(unit, index) in units" :key="index" :value="unit.id"> + {{ unit.title }} + </option> + </select> + </label> </form> </template> <template #info> @@ -62,14 +94,16 @@ </template> <script> +import StudipIdentImage from './../../StudipIdentImage.vue'; import BlockComponents from './block-components.js'; import blockMixin from '@/vue/mixins/courseware/block.js'; +import colorMixin from '@/vue/mixins/courseware/colors.js'; import { mapActions, mapGetters } from 'vuex'; export default { name: 'courseware-link-block', - mixins: [blockMixin], - components: Object.assign(BlockComponents, {}), + mixins: [blockMixin, colorMixin], + components: Object.assign(BlockComponents, { StudipIdentImage }), props: { block: Object, canEdit: Boolean, @@ -79,13 +113,19 @@ export default { return { currentType: '', currentTarget: '', + currentUnitTarget: '', currentUrl: '', currentTitle: '', + identimage: '', }; }, computed: { ...mapGetters({ - courseware: 'courseware-structural-elements/all', + context: 'context', + courseUnits: 'courseware-units/all', + unitById: 'courseware-units/byId', + allStructuralElements: 'courseware-structural-elements/all', + structuralElementById: 'courseware-structural-elements/byId', }), type() { return this.block?.attributes?.payload?.type; @@ -93,28 +133,69 @@ export default { target() { return this.block?.attributes?.payload?.target; }, + unitTarget() { + return this.block?.attributes?.payload?.['unit-target']; + }, url() { return this.block?.attributes?.payload?.url; }, title() { return this.block?.attributes?.payload?.title; }, + units() { + const allUnits = this.courseUnits; + const units = allUnits.filter((unit) => unit.id !== this.context.unit); + + let unitData = []; + for (const unit of units) { + unitData.push(this.getUnitData(unit)); + } + return unitData; + }, + currentUnitData() { + return this.currentType === 'unit' ? this.getUnitData(this.unitById({ id: this.currentUnitTarget })) : null; + }, + headerImageUrl() { + const headerUrl = this.rootElement(this.unitById({ id: this.currentUnitTarget }))?.relationships?.image?.meta?.[ + 'download-url' + ]; + return headerUrl ? headerUrl : null; + }, + previewImageStyle() { + if (this.headerImageUrl) { + return { 'background-image': 'url(' + this.headerImageUrl + ')' }; + } + + return { 'background-image': 'url(' + this.identimage + ')' }; + }, + inCourseContext() { + return this.context.type === 'courses'; + }, + filteredStructuralElements() { + return this.allStructuralElements.filter( + (element) => element.relationships.unit.data.id === this.context.unit + ); + }, }, mounted() { this.initCurrentData(); }, methods: { ...mapActions({ + loadCourseUnits: 'loadCourseUnits', updateBlock: 'updateBlockInContainer', companionWarning: 'companionWarning', }), initCurrentData() { + this.loadCourseUnits(this.context.id); this.currentType = this.type; this.currentTarget = this.target; + this.currentUnitTarget = this.unitTarget; this.currentUrl = this.url; - this.currentTitle = this.title; this.fixUrl(); + this.currentTitle = this.title; }, + fixUrl() { if ( this.currentUrl.indexOf('http://') !== 0 && @@ -125,18 +206,53 @@ export default { } }, storeBlock() { - let attributes = {}; - attributes.payload = {}; - attributes.payload.type = this.currentType; - attributes.payload.target = this.currentTarget; - attributes.payload.url = this.currentUrl; - attributes.payload.title = this.currentTitle; - if (this.currentType === 'internal' && this.currentTarget === '') { - this.companionWarning({ - info: this.$gettext('Bitte wählen Sie eine Seite als Ziel aus.'), - }); + let empty = false; + let info = ''; + let defaultTitle = ''; + + switch (this.currentType) { + case 'external': + info = this.$gettext('Bitte wählen Sie eine URL als Ziel aus.'); + empty = this.currentUrl === ''; + this.currentTarget = ''; + this.currentUnitTarget = ''; + this.currentTitle = this.currentTitle || this.currentUrl; + break; + case 'internal': + info = this.$gettext('Bitte wählen Sie eine Seite als Ziel aus.'); + empty = this.currentTarget === ''; + if (!empty) { + const element = this.filteredStructuralElements.find((el) => el.id === this.currentTarget); + defaultTitle = element.attributes.title; + } + this.currentUrl = ''; + this.currentUnitTarget = ''; + this.currentTitle = this.currentTitle || defaultTitle; + break; + case 'unit': + info = this.$gettext('Bitte wählen Sie ein Lernmaterial als Ziel aus.'); + empty = this.currentUnitTarget === ''; + this.currentTarget = ''; + this.currentUrl = ''; + this.currentTitle = ''; + break; + } + + if (empty) { + this.companionWarning({ info: info }); + return false; } else { + const attributes = { + payload: { + type: this.currentType, + target: this.currentTarget, + 'unit-target': this.currentUnitTarget, + url: this.currentUrl, + title: this.currentTitle + } + }; + this.updateBlock({ attributes: attributes, blockId: this.block.id, @@ -144,7 +260,31 @@ export default { }); } }, - }, + getUnitData(unit) { + if (unit) { + const url = STUDIP.URLHelper.getURL('dispatch.php/course/courseware/courseware/' + unit.id, { + cid: this.context.id, + }); + const element = this.rootElement(unit); + const color = this.mixinColors.find((color) => color.class === element.attributes.payload.color); + return { + id: unit.id, + url: url, + title: element.attributes.title, + description: element.attributes.payload.description, + color: color, + }; + } + return null; + }, + rootElement(unit) { + if (unit && this.context.type === 'courses') { + return this.structuralElementById({ + id: unit.relationships['structural-element'].data.id, + }); + } + }, + } }; </script> <style scoped lang="scss"> diff --git a/resources/vue/components/courseware/structural-element/CoursewareToolsContents.vue b/resources/vue/components/courseware/structural-element/CoursewareToolsContents.vue index 234d9af13a8e9053e0fa1cbcf8383c873d4320f2..6426b7c83100d3d10a396c0b9b04452f745f0137 100644 --- a/resources/vue/components/courseware/structural-element/CoursewareToolsContents.vue +++ b/resources/vue/components/courseware/structural-element/CoursewareToolsContents.vue @@ -14,11 +14,13 @@ </div> </component> <courseware-tree v-if="structuralElements.length" /> + <courseware-tree-units v-if="context.type === 'courses'" /> </div> </template> <script> import CoursewareTree from './CoursewareTree.vue'; +import CoursewareTreeUnits from './CoursewareTreeUnits.vue'; import colorMixin from '@/vue/mixins/courseware/colors.js'; import StudipIdentImage from './../../StudipIdentImage.vue'; import { mapGetters } from 'vuex'; @@ -29,6 +31,7 @@ export default { components: { CoursewareTree, StudipIdentImage, + CoursewareTreeUnits, }, data() { return { @@ -38,6 +41,7 @@ export default { computed: { ...mapGetters({ courseware: 'courseware', + context: 'context', relatedStructuralElement: 'courseware-structural-elements/related', rootLayout: 'rootLayout', structuralElements: 'courseware-structural-elements/all', diff --git a/resources/vue/components/courseware/structural-element/CoursewareTreeUnit.vue b/resources/vue/components/courseware/structural-element/CoursewareTreeUnit.vue new file mode 100644 index 0000000000000000000000000000000000000000..9e5482a22a6ba6d5a4ffaa68d84b4add05874b53 --- /dev/null +++ b/resources/vue/components/courseware/structural-element/CoursewareTreeUnit.vue @@ -0,0 +1,91 @@ +<template> + <li> + <div class="cw-tree-unit-link"> + <a :href="url"> + <div class="cw-tree-units-header"> + <studip-ident-image + v-model="identimage" + :baseColor="color.hex ?? '#fff'" + :pattern="rootElement.title ?? '-'" + /> + <div class="cw-tree-units-header-image" :style="style"></div> + <div class="cw-tree-units-header-details"> + <header> + {{ title }} + </header> + <p>{{ description }}</p> + </div> + </div> + </a> + <button v-if="canEditRoot" class="button trash" :title="$gettext('Link entfernen')" @click.prevent="removeUnitLink"> + </button> + </div> + </li> +</template> + +<script> +import StudipIdentImage from './../../StudipIdentImage.vue'; +import colorMixin from '@/vue/mixins/courseware/colors.js'; +import { mapActions, mapGetters } from 'vuex'; + +export default { + name: 'CoursewareTreeUnit', + mixins: [colorMixin], + components: { + StudipIdentImage, + }, + props: { + unit: { + type: Object, + required: true, + }, + canEditRoot: { + type: Boolean, + default: false + } + }, + data() { + return { + identimage: '', + }; + }, + computed: { + ...mapGetters({ + context: 'context', + structuralElementById: 'courseware-structural-elements/byId', + }), + rootElement() { + return this.structuralElementById({ + id: this.unit.relationships['structural-element'].data.id, + }); + }, + title() { + return this.rootElement.attributes.title; + }, + description() { + return this.rootElement.attributes.payload.description; + }, + url() { + return STUDIP.URLHelper.getURL('dispatch.php/course/courseware/courseware/' + this.unit.id, { + cid: this.context.id, + }); + }, + color() { + return this.mixinColors.find((color) => color.class === this.rootElement.attributes.payload.color); + }, + style() { + const imageUrl = this.rootElement.relationships?.image?.meta?.['download-url']; + if (imageUrl) { + return { 'background-image': 'url(' + imageUrl + ')' }; + } + + return { 'background-image': 'url(' + this.identimage + ')' }; + }, + }, + methods: { + removeUnitLink() { + this.$emit('removeUnitLink', this.unit.id); + }, + }, +}; +</script> diff --git a/resources/vue/components/courseware/structural-element/CoursewareTreeUnits.vue b/resources/vue/components/courseware/structural-element/CoursewareTreeUnits.vue new file mode 100644 index 0000000000000000000000000000000000000000..9023e065315b18bce4dc3b5514310a24d2b8d999 --- /dev/null +++ b/resources/vue/components/courseware/structural-element/CoursewareTreeUnits.vue @@ -0,0 +1,188 @@ +<template> + <div v-if="canEditRoot || linkedUnits.length > 0" class="cw-tree-units"> + <div class="cw-tree-unit-title">{{ $gettext('Weitere Lernmaterialien') }}</div> + <div v-if="!processing"> + <ol v-if="linkedUnits.length > 0"> + <courseware-tree-unit + v-for="unit in linkedUnits" + :unit="unit" + :canEditRoot="canEditRoot" + :key="unit.id" + @removeUnitLink="removeUnitLink" + ></courseware-tree-unit> + </ol> + <div v-if="canEditRoot && units.length > 0" class="cw-tree-units-adder"> + <form v-if="showForm" class="default cw-tree-units-adder-form" @submit.prevent=""> + <label> + <span class="sr-only">{{ $gettext('Lernmaterial') }}</span> + <select v-model="selectedUnit" name="addUnit" @change="addUnitLink"> + <option v-show="false" value="" disabled> + {{ $gettext('Link zum Lernmaterial auswählen') }} + </option> + <option v-for="(unit, index) in units" :key="index" :value="unit.id"> + {{ getUnitTitle(unit) }} + </option> + </select> + </label> + <button + v-if="canEditRoot" + class="button cancel" + :title="$gettext('Auswahl abbrechen')" + @click.prevent="showForm = false" + ></button> + </form> + <button + v-else + class="add-element" + :title="$gettext('Link zum Lernmaterial hinzufügen')" + @click="showForm = true" + > + <studip-icon shape="add" /> + </button> + </div> + </div> + <studip-progress-indicator v-else :description="$gettext('Vorgang wird bearbeitet...')" /> + </div> +</template> + +<script> +import CoursewareTreeUnit from './CoursewareTreeUnit.vue'; +import StudipProgressIndicator from '../../StudipProgressIndicator.vue'; +import colorMixin from '@/vue/mixins/courseware/colors.js'; +import { mapActions, mapGetters } from 'vuex'; + +export default { + name: 'CoursewareTreeUnits', + mixins: [colorMixin], + components: { + CoursewareTreeUnit, + StudipProgressIndicator, + }, + data() { + return { + processing: false, + showForm: false, + selectedUnit: '', + currentInstance: null, + identimage: '', + }; + }, + computed: { + ...mapGetters({ + context: 'context', + courseUnits: 'courseware-units/all', + currentRootElement: 'currentRootElement', + unitById: 'courseware-units/byId', + instanceById: 'courseware-instances/byId', + structuralElementById: 'courseware-structural-elements/byId', + }), + + instance() { + if (this.context.type === 'courses') { + return this.instanceById({ id: 'course_' + this.context.id + '_' + this.context.unit }); + } else { + return this.instanceById({ id: 'user_' + this.context.id + '_' + this.context.unit }); + } + }, + + canEditRoot() { + return this.currentRootElement?.attributes['can-edit']; + }, + + units() { + // returns all course units that are not already linked + const units = this.courseUnits; + const unitsWithoutSelf = units.filter((unit) => unit.id !== this.context.unit); + const linkedUnits = this.currentInstance?.attributes['linked-units']; + if (linkedUnits) { + return unitsWithoutSelf.filter((unit) => !this.instance.attributes['linked-units'].includes(unit.id)); + } else { + return unitsWithoutSelf; + } + }, + + linkedUnits() { + // returns the required unit data of all linked units + const units = this.courseUnits; + const linkedUnitIds = this.currentInstance?.attributes['linked-units']; + + if (linkedUnitIds) { + // filter out not linked units + const filteredUnits = units.filter((unit) => + this.instance.attributes['linked-units'].includes(unit.id) + ); + // map units to their unit ids instead of array keys to return the correct order + const mappedUnits = new Map(filteredUnits.map((unit) => [unit.id, unit])); + + return linkedUnitIds.map((unit) => mappedUnits.get(unit)); + } + return []; + }, + }, + + mounted() { + this.initData(); + }, + + methods: { + ...mapActions({ + loadCourseUnits: 'loadCourseUnits', + storeCoursewareLinkedUnits: 'storeCoursewareLinkedUnits', + }), + + async initData() { + if (this.context.type === 'courses') { + this.currentInstance = this.instance; + const linkedUnits = this.currentInstance?.attributes['linked-units']; + if (this.canEditRoot || linkedUnits.length > 0) { + this.processing = true; + await this.loadCourseUnits(this.context.id); + this.processing = false; + } + } + }, + + getUnitTitle(unit) { + const rootElement = this.structuralElementById({ + id: unit.relationships['structural-element'].data.id, + }); + return rootElement.attributes.title; + }, + + async addUnitLink() { + this.showForm = false; + this.processing = true; + const linkedUnits = this.currentInstance.attributes['linked-units']; + if (!linkedUnits) { + await this.storeCoursewareLinkedUnits({ + instance: this.currentInstance, + linkedUnits: [this.selectedUnit], + }); + } else if (!linkedUnits.includes(this.selectedUnit)) { + this.currentInstance.attributes['linked-units'].push(this.selectedUnit); + await this.storeCoursewareLinkedUnits({ + instance: this.currentInstance, + linkedUnits: linkedUnits, + }); + } + this.processing = false; + }, + + async removeUnitLink(id) { + let linkedUnits = this.currentInstance.attributes['linked-units'].filter((unitId) => unitId !== id); + await this.storeCoursewareLinkedUnits({ + instance: this.currentInstance, + linkedUnits: linkedUnits, + }); + this.selectedUnit = ''; + }, + }, +}; +</script> +<style lang="scss"> +.cw-tree-units { + .progress-indicator-wrapper { + margin-top: 15px; + } +} +</style> \ No newline at end of file diff --git a/resources/vue/courseware-index-app.js b/resources/vue/courseware-index-app.js index 6be42295dba114cd725c230491e5e4f8575a09f4..76f8034b675df7940155ec50e93c19e65131191b 100644 --- a/resources/vue/courseware-index-app.js +++ b/resources/vue/courseware-index-app.js @@ -156,6 +156,7 @@ const mountApp = async (STUDIP, createApp, element) => { if (entry_type === 'courses') { store.dispatch('loadProgresses'); await store.dispatch('setFeedbackSettings', feedbackSettings); + await store.dispatch('courseware-units/loadById', { id: unit_id, options: {include: 'structural-element'} }); } store.dispatch('coursewareCurrentElement', elem_id); diff --git a/resources/vue/store/courseware/courseware.module.js b/resources/vue/store/courseware/courseware.module.js index 7004b1628c3eda5d25d97d713f4b121d7423f455..b6a383b7ed9d1cc054bf4f7349566f94effc1615 100644 --- a/resources/vue/store/courseware/courseware.module.js +++ b/resources/vue/store/courseware/courseware.module.js @@ -96,6 +96,10 @@ const getters = { rootId(state, getters) { return getters.courseware?.relationships?.root?.data?.id; }, + currentRootElement(state, getters, rootState, rootGetters) { + const id = getters.rootId; + return rootGetters['courseware-structural-elements/byId']({ id }); + }, currentElement(state) { return state.currentElement; }, @@ -711,7 +715,7 @@ export const actions = { return updatedBlock; }, - + async storeCoursewareSettings({ dispatch, getters }, { permission, progression, certificateSettings, reminderSettings, resetProgressSettings }) { @@ -725,6 +729,13 @@ export const actions = { return dispatch('courseware-instances/update', courseware, { root: true }); }, + async storeCoursewareLinkedUnits({ dispatch, getters }, { linkedUnits }) { + const courseware = getters.courseware; + courseware.attributes['linked-units'] = linkedUnits; + + return dispatch('courseware-instances/update', courseware, { root: true }); + }, + sortChildrenInStructualElements({ dispatch }, { parent, children }) { const childrenResourceIdentifiers = children.map(({ type, id }) => ({ type, id }));