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 }));