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>