Skip to content
Snippets Groups Projects
Forked from Stud.IP / Stud.IP
3778 commits behind the upstream repository.
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
CoursewareManagerElement.vue 24.21 KiB
<template>
    <div class="cw-manager-element">
        <div v-if="currentElement">
            <courseware-companion-box v-if="insertingInProgress" :msgCompanion="text.inProgress" mood="pointing" />
            <courseware-companion-box v-if="copyingFailed && !insertingInProgress" :msgCompanion="copyProcessFailedMessage" mood="sad" />
            <div class="cw-manager-element-title">
                <nav aria-label="Breadcrumb" class="cw-manager-element-breadcrumb">
                    <a
                        v-for="element in breadcrumb"
                        :key="element.id"
                        :title="element.attributes.title"
                        href="#"
                        class="cw-manager-element-breadcrumb-item"
                        @click="selectChapter(element.id)"
                    >
                        {{ element.attributes.title }}
                    </a>
                </nav>
                <header>
                    <a
                        v-if="elementInserterActive && moveSelfPossible && canEdit"
                        href="#"
                        :title="$gettextInterpolate('%{ elementTitle } verschieben', {elementTitle: elementTitle})"
                        @click="insertElement({element: currentElement, source: type})"
                    >
                        <studip-icon shape="arr_2left" size="24" role="clickable" />
                    </a>
                    {{ elementTitle }}
                </header>
            </div>
            <courseware-collapsible-box
                :open="true"
                :title="$gettext('Abschnitt')"
                class="cw-manager-element-containers"
            >
                <div v-if="canSortContainers">
                    <button v-show="!sortContainersActive && isCurrent" class="button sort" @click="sortContainers">
                        <translate>Abschnitte sortieren</translate>
                    </button>
                    <button v-show="sortContainersActive && isCurrent" class="button accept" @click="storeContainersSort">
                        <translate>Sortieren beenden</translate>
                    </button>
                    <button v-show="sortContainersActive && isCurrent" class="button cancel" @click="resetContainersSort">
                        <translate>Sortieren abbrechen</translate>
                    </button>
                </div>
                <p v-if="!hasContainers">
                    <translate>Dieses Element enthält keine Abschnitte.</translate>
                </p>
                <transition-group name="cw-sort-ease" tag="div">
                    <courseware-manager-container
                        v-for="(container, index) in sortArrayContainers"
                        :key="container.id"
                        :container="container"
                        :isCurrent="isCurrent"
                        :sortContainers="sortContainersActive"
                        :inserter="containerInserterActive && moveSelfChildPossible"
                        :elementType="type"
                        :blockInserter="blockInserterActive"
                        :canMoveUp="index !== 0"
                        :canMoveDown="index+1 !== sortArrayContainers.length"
                        @insertContainer="insertContainer"
                        @insertBlock="insertBlock"
                        @moveUp="moveUpContainer"
                        @moveDown="moveDownContainer"
                    />
                </transition-group>
                <courseware-manager-filing
                    v-if="isCurrent && !sortContainersActive && canEdit"
                    :parentId="currentElement.id"
                    :parentItem="currentElement"
                    itemType="container"
                />
            </courseware-collapsible-box>
            <courseware-collapsible-box :open="true" :title="$gettext('Unterseiten')" class="cw-manager-element-subchapters">
                <div v-if="canSortChildren">
                    <button v-show="!sortChildrenActive && isCurrent" class="button sort" @click="sortChildren">
                        <translate>Unterseiten sortieren</translate>
                    </button>
                    <button v-show="sortChildrenActive && isCurrent" class="button accept" @click="storeChildrenSort">
                        <translate>Sortieren beenden</translate>
                    </button>
                    <button v-show="sortChildrenActive && isCurrent" class="button cancel" @click="resetChildrenSort">
                        <translate>Sortieren abbrechen</translate>
                    </button>
                </div>
                <p v-if="!hasChildren">
                    <translate>Dieses Element enthält keine Unterseiten.</translate>
                </p>
                <transition-group name="cw-sort-ease" tag="div">
                    <courseware-manager-element-item
                        v-for="(child, index) in sortArrayChildren"
                        :key="child.id"
                        :element="child"
                        :sortChapters="sortChildrenActive"
                        :inserter="elementInserterActive && moveSelfChildPossible && filingData.parentItem.id !== child.id"
                        :type="type"
                        :canMoveUp="index !== 0"
                        :canMoveDown="index+1 !== sortArrayChildren.length"
                        @selectChapter="selectChapter"
                        @insertElement="insertElement"
                        @moveUp="moveUpChild"
                        @moveDown="moveDownChild"
                    />
                </transition-group>
                <courseware-manager-filing
                    v-if="isCurrent && !sortChildrenActive && canEdit"
                    :parentId="currentElement.id"
                    :parentItem="currentElement"
                    itemType="element"
                />
            </courseware-collapsible-box>
        </div>
    </div>
</template>

<script>
import StudipIcon from '../StudipIcon.vue';
import CoursewareCollapsibleBox from './CoursewareCollapsibleBox.vue';
import CoursewareManagerContainer from './CoursewareManagerContainer.vue';
import CoursewareManagerElementItem from './CoursewareManagerElementItem.vue';
import CoursewareManagerFiling from './CoursewareManagerFiling.vue';
import CoursewareCompanionBox from './CoursewareCompanionBox.vue';
import { mapActions, mapGetters } from 'vuex';
import { forEach } from 'jszip';

export default {
    name: 'courseware-manager-element',
    components: {
        CoursewareCollapsibleBox,
        CoursewareManagerContainer,
        CoursewareManagerElementItem,
        CoursewareManagerFiling,
        CoursewareCompanionBox,
        StudipIcon,
    },
    props: {
        type: {
            validator(value) {
                return ['current', 'self', 'remote', 'own','import'].includes(value);
            },
        },
        remoteCoursewareRangeId: String,
        currentElement: Object,
        moveSelfPossible: {
            default: true
        },
        moveSelfChildPossible: {
            default: true
        }
    },
    data() {
        return {
            elementInserterActive: false,
            containerInserterActive: false,
            blockInserterActive: false,
            sortChildrenActive: false,
            sortContainersActive: false,
            sortArrayChildren: [],
            discardStateArrayChildren: [],
            sortArrayContainers: [],
            discardStateArrayContainers: [],
            insertingInProgress: false,
            copyingFailed: false,
            text: {
                inProgress: this.$gettext('Vorgang läuft. Bitte warten Sie einen Moment.'),
                copyProcessFailed: [],
            },
        };
    },
    computed: {
        ...mapGetters({
            childrenById: 'courseware-structure/children',
            containerById: 'courseware-containers/byId',
            structuralElementById: 'courseware-structural-elements/byId',
        }),
        isCurrent() {
            return this.type === 'current';
        },
        isSelf() {
            return this.type === 'self';
        },
        isRemote() {
            return this.type === 'remote';
        },
        isImport() {
            return this.type === 'import';
        },
        isOwn() {
            return this.type === 'own';
        },
        isSorting() {
            return this.sortChildrenActive || this.sortContainersActive || this.sortBlocksActive;
        },
        canEdit() {
            if (this.currentElement.attributes) {
                return this.currentElement.attributes['can-edit'];
            } else {
                return false;
            }
        },
        breadcrumb() {
            if (!this.currentElement) {
                return [];
            }

            const finder = (parent) => {
                const parentId = parent.relationships?.parent?.data?.id;
                if (!parentId) {
                    return null;
                }
                const element = this.structuralElementById({id: parentId});
                if (!element) {
                    console.error("CoursewareManagerElement#breadcrumb: Could not find parent by ID.");
                }

                return element;
            };

            const visitAncestors = function* (node) {
                const parent = finder(node);
                if (parent) {
                    yield parent;
                    yield *visitAncestors(parent);
                }
            };

            return [...visitAncestors(this.currentElement)].reverse()
        },
        elementTitle() {
            if (this.currentElement.attributes) {
                return this.currentElement.attributes.title
            } else {
                return '';
            }
        },
        hasChildren() {
            if (this.children === null) {
                return false;
            } else {
                return this.children.length >= 1;
            }
        },
        canSortChildren() {
            if (this.children === null) {
                return false;
            } else {
                return this.children.length > 1 && this.canEdit;
            }
        },
        hasContainers() {
            if (this.containers === null) {
                return false;
            } else {
                return this.containers.length >= 1;
            }
        },
        canSortContainers() {
            if (this.containers === null) {
                return false;
            } else {
                return this.containers.length > 1 && this.canEdit;
            }
        },
        emptyContainers() {
            if (this.containers === null) {
                return true;
            } else {
                return this.containers.length === 0;
            }
        },
        containers() {
            if (!this.currentElement || !this.currentElement.relationships) {
                return [];
            }

            return this.currentElement.relationships.containers.data.map(({id}) => this.containerById({ id }));
        },
        children() {
            if (!this.currentElement) {
                return [];
            }

            return this.childrenById(this.currentElement.id)
                .map((id) => this.structuralElementById({ id }))
                .filter(Boolean);
        },
        filingData() {
            return this.$store.getters.filingData;
        },
        copyProcessFailedMessage() {
            let message = this.$gettext('Der Kopiervorgang ist fehlgeschlagen.');
            if (this.text.copyProcessFailed.length) {
                message = this.text.copyProcessFailed.join('<br>');
            }
            return message;
        }
    },
    methods: {
        ...mapActions({
            createStructuralElement: 'createStructuralElement',
            updateStructuralElement: 'updateStructuralElement',
            deleteStructuralElement: 'deleteStructuralElement',
            copyStructuralElement: 'copyStructuralElement',
            loadStructuralElement: 'loadStructuralElement',
            loadContainer: 'loadContainer',
            updateContainer: 'updateContainer',
            deleteContainer: 'deleteContainer',
            copyContainer: 'copyContainer',
            updateBlock: 'updateBlock',
            deleteBlock: 'deleteBlock',
            copyBlock: 'copyBlock',
            lockObject: 'lockObject',
            unlockObject: 'unlockObject',
            sortContainersInStructualElements: 'sortContainersInStructualElements',
            sortChildrenInStructualElements: 'sortChildrenInStructualElements'
        }),

        selectChapter(target) {
            this.$emit('selectElement', target);
        },

         validateSource(source) {
            return (source === 'self' || source === 'remote' || source === 'own');
        },

        afterInsertCompletion() {
            this.$nextTick(() => {
                // will run after $emit is done
                this.$store.dispatch('cwManagerFilingData', {});
                setTimeout(() => {
                    this.insertingInProgress = false;
                }, 250);
            });
        },

        showFailedCopyProcessCompanion() {
            this.copyingFailed = true;
            this.insertingInProgress = false;
        },

        async insertElement(data) {
            let source = data.source;
            let element = data.element;

            if (!this.validateSource(source)) {
                console.log('unreliable source:');
                console.log(source);
                console.log(element);
                return;
            }
            if(!this.insertingInProgress) {
                this.insertingInProgress = true;
                if (source === 'self') {
                    element.relationships.parent.data.id = this.filingData.parentItem.id;
                    element.attributes.position = this.childrenById(this.filingData.parentItem.id).length + 1;
                    await this.lockObject({ id: element.id, type: 'courseware-structural-elements' });
                    await this.updateStructuralElement({
                        element: element,
                        id: element.id,
                    });
                    await this.unlockObject({ id: element.id, type: 'courseware-structural-elements' });
                    this.loadStructuralElement(this.currentElement.id);
                    this.$emit('reloadElement');
                } else if(source === 'remote' || source === 'own') {
                    //create Element
                    let parentId = this.filingData.parentItem.id;
                    await this.copyStructuralElement({
                        parentId: parentId,
                        element: element,
                    }).catch((error) => {
                        let message = this.$gettextInterpolate('%{ pageTitle } konnte nicht kopiert werden.', {pageTitle: element.attributes.title});
                        this.text.copyProcessFailed.push(message);
                        this.showFailedCopyProcessCompanion();
                    });
                    this.$emit('loadSelf', parentId);
                }
                this.afterInsertCompletion();
            }
        },
        async insertContainer(data) {
            let source = data.source;
            let container = data.container;

            if (!this.validateSource(source)) {
                console.log('unreliable source:');
                console.log(source);
                console.log(container);
                return;
            }
            if(!this.insertingInProgress) {
                this.insertingInProgress = true;
                if (source === 'self') {
                    container.relationships['structural-element'].data.id = this.filingData.parentItem.id;
                    container.attributes.position = this.filingData.parentItem.relationships.containers.data.length + 1;
                    await this.lockObject({id: container.id, type: 'courseware-containers'});
                    await this.updateContainer({
                        container: container,
                        structuralElementId: this.currentElement.id
                    });
                    await this.unlockObject({id: container.id, type: 'courseware-containers'});
                    this.$emit('reloadElement');
                } else if (source === 'remote' || source === 'own') {
                    let parentId = this.filingData.parentItem.id;
                    await this.copyContainer({
                        parentId: parentId,
                        container: container,
                    }).catch((error) => {
                        let message = this.$gettextInterpolate('Abschnitt "%{ containerTitle }" konnte nicht kopiert werden', {containerTitle: container.attributes.title});
                        this.text.copyProcessFailed.push(message);
                        this.showFailedCopyProcessCompanion();
                    });
                    this.$emit('loadSelf', parentId);
                }
                this.afterInsertCompletion();
            }

        },
        async insertBlock(data) {
            let source = data.source;
            let block = data.block;

            if (!this.validateSource(source)) {
                console.debug('unreliable source:', source, block);
                return;
            }

            if(!this.insertingInProgress) {
                this.insertingInProgress = true;
                if (source === 'self') {
                    let sourceContainer = await this.containerById({id: block.relationships.container.data.id});
                    sourceContainer.attributes.payload.sections.forEach(section => {
                        let index = section.blocks.indexOf(block.id);
                        if(index !== -1) {
                            section.blocks.splice(index, 1);
                        }
                    });
                    await this.lockObject({id: sourceContainer.id, type: 'courseware-containers'});
                    await this.updateContainer({
                        container: sourceContainer,
                        structuralElementId: sourceContainer.relationships['structural-element'].data.id
                    });
                    await this.unlockObject({id: sourceContainer.id, type: 'courseware-containers'});

                    let destinationContainer = await this.containerById({id: this.filingData.parentItem.id});
                    destinationContainer.attributes.payload.sections[destinationContainer.attributes.payload.sections.length-1].blocks.push(block.id);
                    await this.lockObject({id: destinationContainer.id, type: 'courseware-containers'});
                    await this.updateContainer({
                        container: destinationContainer,
                        structuralElementId: destinationContainer.relationships['structural-element'].data.id
                    });
                    await this.unlockObject({id: destinationContainer.id, type: 'courseware-containers'});

                    block.relationships.container.data.id = this.filingData.parentItem.id;
                    block.attributes.position = this.filingData.parentItem.relationships.blocks.data.length + 1;
                    await this.lockObject({id: block.id, type: 'courseware-blocks'});
                    await this.updateBlock({
                        block: block,
                        containerId: this.filingData.parentItem.id
                    });
                    await this.unlockObject({id: block.id, type: 'courseware-blocks'});
                    await this.loadContainer(sourceContainer.id);
                    await this.loadContainer(destinationContainer.id);
                    this.$emit('reloadElement');
                } else if (source === 'remote' || source === 'own') {
                    let parentId = this.filingData.parentItem.id;
                    await this.copyBlock({
                        parentId: parentId,
                        block: block,
                    }).catch((error) => {
                        let message = this.$gettextInterpolate('Block "%{ blockTitle }" konnte nicht kopiert werden', {blockTitle: block.attributes.title});
                        this.text.copyProcessFailed.push(message);
                        this.showFailedCopyProcessCompanion();
                    });
                    await this.loadContainer(parentId);
                    this.$emit('loadSelf',this.filingData.parentItem.relationships['structural-element'].data.id);
                }
                this.afterInsertCompletion();
            }
        },

        sortChildren() {
            this.discardStateArrayChildren = [...this.children]; //copy array because of watcher?
            this.sortChildrenActive = true;
        },
        sortContainers() {
            this.discardStateArrayContainers = [...this.containers];
            this.sortContainersActive = true;
        },

        storeChildrenSort() {
            this.sortChildrenInStructualElements({parent: this.currentElement, children: this.sortArrayChildren});

            this.discardStateArrayChildren = [];
            this.sortChildrenActive = false;
        },
        resetChildrenSort() {
            this.sortArrayChildren = this.discardStateArrayChildren;
            this.sortChildrenActive = false;
        },

        storeContainersSort() {
            this.sortContainersInStructualElements({structuralElement: this.currentElement, containers: this.sortArrayContainers});

            this.discardStateArrayContainers = [];
            this.sortContainersActive = false;
        },
        resetContainersSort() {
            this.sortArrayContainers = this.discardStateArrayContainers;
            this.sortContainersActive = false;
        },

        moveUpChild(childId) {
            this.moveUp(childId, this.sortArrayChildren);
        },
        moveDownChild(childId) {
            this.moveDown(childId, this.sortArrayChildren);
        },
        moveUpContainer(containerId) {
            this.moveUp(containerId, this.sortArrayContainers);
        },
        moveDownContainer(containerId) {
            this.moveDown(containerId, this.sortArrayContainers);
        },

        moveUp(itemId, sortArray) {
            sortArray.every((item, index) => {
                if (item.id === itemId) {
                    if (index === 0) {
                        return false;
                    }
                    sortArray.splice(index - 1, 0, sortArray.splice(index, 1)[0]);
                    return false;
                } else {
                    return true;
                }
            });
        },
        moveDown(itemId, sortArray) {
            sortArray.every((item, index) => {
                if (item.id === itemId) {
                    if (index === sortArray.length - 1) {
                        return false;
                    }
                    sortArray.splice(index + 1, 0, sortArray.splice(index, 1)[0]);
                    return false;
                } else {
                    return true;
                }
            });
        },
        updateFilingData(data) {
            if (Object.keys(data).length !== 0) {
                switch (data.itemType) {
                    case 'element':
                        this.elementInserterActive = true;
                        break;
                    case 'container':
                        this.containerInserterActive = true;
                        break;
                    case 'block':
                        this.blockInserterActive = true;
                        break;
                }
                this.copyingFailed = false;
                this.text.copyProcessFailed = [];
            } else {
                this.elementInserterActive = false;
                this.containerInserterActive = false;
                this.blockInserterActive = false;
            }
        }
    },
    mounted() {
        this.updateFilingData(this.filingData);
    },
    watch: {
        filingData(newValue) {
            if (!['self', 'remote', 'own', 'import'].includes(this.type)) {
                return false;
            }
            this.updateFilingData(newValue);
        },
        containers(newContainers) {
            this.sortArrayContainers = newContainers;
        },
        children(newChildren) {
            this.sortArrayChildren = newChildren;
        }
    },
};
</script>