diff --git a/lib/classes/JsonApi/Schemas/Folder.php b/lib/classes/JsonApi/Schemas/Folder.php index 1cd5ba55a5f7f98e943f94d7bc9ce32a440ace36..2c61cae4ba2b0459658615c2f7260a2d075e1bc1 100644 --- a/lib/classes/JsonApi/Schemas/Folder.php +++ b/lib/classes/JsonApi/Schemas/Folder.php @@ -32,10 +32,11 @@ class Folder extends SchemaProvider 'mkdate' => date('c', $resource->mkdate), 'chdate' => date('c', $resource->chdate), - 'is-visible' => (bool) $resource->isVisible($user->id), + 'is-visible' => (bool) $resource->isVisible($user->id), 'is-readable' => (bool) $resource->isReadable($user->id), 'is-writable' => (bool) $resource->isWritable($user->id), 'is-editable' => (bool) $resource->isEditable($user->id), + 'is-empty' => (bool) $resource->is_empty, 'is-subfolder-allowed' => (bool) $resource->isSubfolderAllowed($user->id), ]; @@ -76,6 +77,9 @@ class Folder extends SchemaProvider self::RELATIONSHIP_LINKS => [ Link::RELATED => $this->createLinkToResource($resource->owner), ], + self::RELATIONSHIP_META => [ + 'name' => $resource->owner->getFullName('no_title_rev'), + ] ]; } @@ -157,6 +161,9 @@ class Folder extends SchemaProvider self::RELATIONSHIP_LINKS => [ Link::RELATED => $this->getRelationshipRelatedLink($resource, self::REL_FOLDERS), ], + self::RELATIONSHIP_META => [ + 'count' => count($resource->subfolders) + ], ]; return $relationships; @@ -168,6 +175,9 @@ class Folder extends SchemaProvider self::RELATIONSHIP_LINKS => [ Link::RELATED => $this->getRelationshipRelatedLink($resource, self::REL_FILE_REFS), ], + self::RELATIONSHIP_META => [ + 'count' => count($resource->file_refs) + ], ]; return $relationships; diff --git a/resources/vue/components/StudipFileChooser.vue b/resources/vue/components/StudipFileChooser.vue new file mode 100644 index 0000000000000000000000000000000000000000..e021aec3cad110db990c82e4acf4163e358d0614 --- /dev/null +++ b/resources/vue/components/StudipFileChooser.vue @@ -0,0 +1,149 @@ +<template> + <div class="file-chooser"> + <button class="button" @click="openDialog">{{ buttonTitle }}</button><span>{{ selectedName }}</span> + <file-chooser-dialog v-if="showDialog" v-bind="$props" @close="closeDialog" @selected="select" /> + </div> +</template> + +<script> +import FileChooserDialog from './file-chooser/FileChooserDialog.vue'; +import { mapActions, mapGetters } from 'vuex'; + +export default { + name: 'studip-file-chooser', + components: { + FileChooserDialog, + }, + + props: { + selectable: { + type: String, + default: 'file', + validator: (value) => { + return ['file', 'folder'].includes(value); + }, + }, + selectedId: { + type: String, + required: false, + }, + courseId: { + type: String, + validator: (value) => { + return value !== ''; + }, + required: false, + }, + userId: { + type: String, + validator: (value) => { + return value !== ''; + }, + required: false, + }, + isImage: { type: Boolean, default: false }, + isVideo: { type: Boolean, default: false }, + isAudio: { type: Boolean, default: false }, + isDocument: { type: Boolean, default: false }, + excludedCourseFolderTypes: { type: Array, default: () => [] }, + excludedUserFolderTypes: { type: Array, default: () => [] }, + }, + model: { + prop: 'selectedId', + event: 'select', + }, + data() { + return { + showDialog: false, + selectedFile: null, + selectedFolder: null, + }; + }, + computed: { + ...mapGetters({ + fileById: 'file-refs/byId', + folderById: 'folders/byId', + }), + buttonTitle() { + if (this.selectable === 'folder') { + return this.$gettext('Ordner auswählen'); + } + + return this.$gettext('Datei auswählen'); + }, + selectedName() { + if (this.selectable === 'folder') { + if (this.selectedId === '') { + return this.$gettext('Kein Ordner ausgewählt'); + } + return this.$gettextInterpolate(this.$gettext('Ordner "%{folderName}" ausgewählt'), { + folderName: this.folderById({ id: this.selectedId })?.attributes?.name ?? '-', + }); + } + + if (this.selectedId === '') { + return this.$gettext('Keine Datei ausgewählt'); + } + return this.$gettextInterpolate(this.$gettext('Datei "%{fileName}" ausgewählt'), { + fileName: this.fileById({ id: this.selectedId })?.attributes?.name ?? '-', + }); + }, + }, + methods: { + ...mapActions({ + loadFile: 'file-refs/loadById', + loadFolder: 'folders/loadById', + }), + openDialog() { + this.showDialog = true; + }, + closeDialog() { + this.showDialog = false; + }, + select(id) { + this.closeDialog(); + this.$emit('select', id); + }, + loadSelection() { + if (this.selectable === 'folder') { + if (this.selectedId !== '') { + this.loadFolder({ id: this.selectedId }); + } + } else { + if (this.selectedId !== '') { + this.loadFile({ id: this.selectedId }); + } + } + } + }, + mounted() { + this.loadSelection(); + }, +}; +</script> + +<style lang="scss" scoped> +.file-chooser { + text-indent: 0; + max-width: 48em; + button { + margin: 0.5ex 0 0.5ex 0; + min-width: 140px; + } + span { + box-sizing: border-box; + border: solid thin var(--content-color-40); + border-left: none; + display: inline-block; + font-size: 14px; + line-height: 130%; + min-width: 100px; + width: calc(100% - 140px); + overflow: hidden; + text-overflow: ellipsis; + padding: 5px 15px; + vertical-align: middle; + white-space: nowrap; + } +} +</style> diff --git a/resources/vue/components/courseware/blocks/CoursewareAudioBlock.vue b/resources/vue/components/courseware/blocks/CoursewareAudioBlock.vue index 9ca52b04dfaa6c514f304c11174dbc861a52e6f1..4789c9fa1096d9eee6dcca0a1fe5193c056cf966 100644 --- a/resources/vue/components/courseware/blocks/CoursewareAudioBlock.vue +++ b/resources/vue/components/courseware/blocks/CoursewareAudioBlock.vue @@ -288,18 +288,14 @@ </label> <label v-show="currentSource === 'studip_file'"> {{ $gettext('Datei') }} - <courseware-file-chooser - v-model="currentFileId" - :isAudio="true" - @selectFile="updateCurrentFile" - /> + <studip-file-chooser v-model="currentFileId" selectable="file" :courseId="context.id" :userId="userId" :isAudio="true" :excludedCourseFolderTypes="excludedCourseFolderTypes" /> </label> <label v-show="currentSource === 'studip_folder'"> {{ $gettext('Ordner') }} - <courseware-folder-chooser v-model="currentFolderId" allowUserFolders /> + <studip-file-chooser v-model="currentFolderId" selectable="folder" :courseId="context.id" :userId="userId" :excludedCourseFolderTypes="excludedCourseFolderTypes" /> </label> <label v-show="currentSource === 'studip_folder'"> - {{ $gettext('Audio Aufnahmen zulassen') }} + {{ $gettext('Audio-Aufnahmen zulassen') }} <span class="tooltip tooltip-icon" :data-tooltip="$gettext('Um Aufnahmen zu ermöglichen, muss ein Ordner ausgewählt werden.')" @@ -377,7 +373,6 @@ export default { fileRefById: 'file-refs/byId', relatedFileRefs: 'file-refs/related', urlHelper: 'urlHelper', - userId: 'userId', usersById: 'users/byId', relatedTermOfUse: 'terms-of-use/related', }), diff --git a/resources/vue/components/courseware/blocks/CoursewareBeforeAfterBlock.vue b/resources/vue/components/courseware/blocks/CoursewareBeforeAfterBlock.vue index 8d4e496403b5090352f3bf82a76d0701a47a2f81..65df25c4bcb5d79f0cbc86c61e40844f344aafee 100644 --- a/resources/vue/components/courseware/blocks/CoursewareBeforeAfterBlock.vue +++ b/resources/vue/components/courseware/blocks/CoursewareBeforeAfterBlock.vue @@ -34,11 +34,7 @@ </label> <label v-if="currentBeforeSource === 'studip'"> {{ $gettext('Bilddatei') }} - <courseware-file-chooser - v-model="currentBeforeFileId" - :isImage="true" - @selectFile="updateCurrentBeforeFile" - /> + <studip-file-chooser v-model="currentBeforeFileId" selectable="file" :courseId="context.id" :userId="userId" :isImage="true" :excludedCourseFolderTypes="excludedCourseFolderTypes"/> </label> </form> </courseware-tab> @@ -57,11 +53,7 @@ </label> <label v-if="currentAfterSource === 'studip'"> {{ $gettext('Bilddatei') }} - <courseware-file-chooser - v-model="currentAfterFileId" - :isImage="true" - @selectFile="updateCurrentAfterFile" - /> + <studip-file-chooser v-model="currentAfterFileId" selectable="file" :courseId="context.id" :userId="userId" :isImage="true"/> </label> </form> </courseware-tab> @@ -106,6 +98,7 @@ export default { }, computed: { ...mapGetters({ + fileRefById: 'file-refs/byId', viewMode: 'viewMode', }), beforeSource() { @@ -204,14 +197,6 @@ export default { this.currentAfterFileId = this.afterFileId; this.currentAfterWebUrl = this.afterWebUrl; }, - updateCurrentBeforeFile(file) { - this.currentBeforeFile = file; - this.currentBeforeFileId = file.id; - }, - updateCurrentAfterFile(file) { - this.currentAfterFile = file; - this.currentAfterFileId = file.id; - }, storeBlock() { let cmpInfo = false; let cmpInfoBefore = this.$gettext('Bitte wählen Sie ein Vorherbild aus.'); @@ -269,6 +254,18 @@ export default { } }, }, + watch: { + currentBeforeFileId(newId) { + if (newId) { + this.currentBeforeFile = this.fileRefById({ id: newId }); + } + }, + currentAfterFileId(newId) { + if (newId) { + this.currentAfterFile = this.fileRefById({ id: newId }); + } + } + } }; </script> <style scoped lang="scss"> diff --git a/resources/vue/components/courseware/blocks/CoursewareCanvasBlock.vue b/resources/vue/components/courseware/blocks/CoursewareCanvasBlock.vue index d26446825d9ddf4b5ed8f34eeb2d25df57c9cf7a..669a5b8ade0df0b8b96ab8cb697384356162da54 100644 --- a/resources/vue/components/courseware/blocks/CoursewareCanvasBlock.vue +++ b/resources/vue/components/courseware/blocks/CoursewareCanvasBlock.vue @@ -124,10 +124,13 @@ </label> <label v-if="currentImage === 'true'"> {{ $gettext('Bilddatei') }} - <courseware-file-chooser + <studip-file-chooser v-model="currentFileId" + selectable="file" + :courseId="studipContext.id" + :userId="userId" :isImage="true" - @selectFile="updateCurrentFile" + :excludedCourseFolderTypes="excludedCourseFolderTypes" /> </label> </form> @@ -218,10 +221,11 @@ export default { }, computed: { ...mapGetters({ - userId: 'userId', + studipContext: 'context', + fileRefById: 'file-refs/byId', getUserDataById: 'courseware-user-data-fields/byId', + relatedUserData: 'user-data-field/related', usersById: 'users/byId', - relatedUserData: 'user-data-field/related' }), userData() { return this.getUserDataById({ id: this.block.relationships['user-data-field'].data.id }); @@ -366,11 +370,6 @@ export default { this.buildCanvas(); }); }, - updateCurrentFile(file) { - this.currentFile = file; - this.currentFileId = file.id; - this.buildCanvas(); - }, setColor(color) { if (this.write) { return; @@ -700,6 +699,12 @@ export default { watch: { currentUserView() { this.redraw(); + }, + currentFileId(newId) { + if (newId) { + this.currentFile = this.fileRefById({ id: newId }); + this.buildCanvas(); + } } }, }; diff --git a/resources/vue/components/courseware/blocks/CoursewareConfirmBlock.vue b/resources/vue/components/courseware/blocks/CoursewareConfirmBlock.vue index 040d27f9b5e73f2d361c466f4e0dd61c49fc5e8e..60d9ae154c46d9a531a8f9a69b7d3af0bed7b2d8 100644 --- a/resources/vue/components/courseware/blocks/CoursewareConfirmBlock.vue +++ b/resources/vue/components/courseware/blocks/CoursewareConfirmBlock.vue @@ -56,7 +56,6 @@ export default { }, computed: { ...mapGetters({ - userId: 'userId', getUserDataById: 'courseware-user-data-fields/byId', }), text() { diff --git a/resources/vue/components/courseware/blocks/CoursewareDialogCardsBlock.vue b/resources/vue/components/courseware/blocks/CoursewareDialogCardsBlock.vue index 5bfc2f78c3124624de3e84972e953339455e92b4..1847c6d86f5f026410f10a23e017467b99d3ba9f 100644 --- a/resources/vue/components/courseware/blocks/CoursewareDialogCardsBlock.vue +++ b/resources/vue/components/courseware/blocks/CoursewareDialogCardsBlock.vue @@ -70,29 +70,35 @@ > <form class="default" @submit.prevent=""> <label> - {{ $gettext('Bild Vorderseite') }}: - <courseware-file-chooser + {{ $gettext('Bild Vorderseite') }} + <studip-file-chooser v-model="card.front_file_id" + selectable="file" + :courseId="context.id" + :userId="userId" :isImage="true" - :canBeEmpty="true" - @selectFile="updateFile(index, 'front', $event)" + :excludedCourseFolderTypes="excludedCourseFolderTypes" + @select="updateFile(index, 'front', $event)" /> - </label> + </label> <label> - {{ $gettext('Text Vorderseite') }}: + {{ $gettext('Text Vorderseite') }} <input type="text" v-model="card.front_text" /> </label> <label> - {{ $gettext('Bild Rückseite') }}: - <courseware-file-chooser + {{ $gettext('Bild Rückseite') }} + <studip-file-chooser v-model="card.back_file_id" + selectable="file" + :courseId="context.id" + :userId="userId" :isImage="true" - :canBeEmpty="true" - @selectFile="updateFile(index, 'back', $event)" + :excludedCourseFolderTypes="excludedCourseFolderTypes" + @select="updateFile(index, 'back', $event)" /> </label> <label> - {{ $gettext('Text Rückseite') }}: + {{ $gettext('Text Rückseite') }} <input type="text" v-model="card.back_text" /> </label> <label v-if="!onlyCard"> @@ -114,7 +120,7 @@ <script> import BlockComponents from './block-components.js'; import blockMixin from '@/vue/mixins/courseware/block.js'; -import { mapActions } from 'vuex'; +import { mapActions, mapGetters } from 'vuex'; export default { name: 'courseware-dialog-cards-block', @@ -132,6 +138,9 @@ export default { }; }, computed: { + ...mapGetters({ + fileRefById: 'file-refs/byId', + }), cards() { return this.block?.attributes?.payload?.cards; }, @@ -191,20 +200,22 @@ export default { containerId: this.block.relationships.container.data.id, }); }, - updateFile(cardIndex, side, file) { + updateFile(cardIndex, side, fileId) { if (side === 'front') { - if (file) { - this.currentCards[cardIndex].front_file_id = file.id; - this.currentCards[cardIndex].front_file = file; + if (fileId) { + this.currentCards[cardIndex].front_file_id = fileId; + this.currentCards[cardIndex].front_file = this.fileRefById({ id: fileId }); + this.currentCards[cardIndex].front_file.download_url = this.currentCards[cardIndex].front_file.meta['download-url']; } else { this.currentCards[cardIndex].front_file_id = ''; this.currentCards[cardIndex].front_file = []; } } if (side === 'back') { - if (file) { - this.currentCards[cardIndex].back_file_id = file.id; - this.currentCards[cardIndex].back_file = file; + if (fileId) { + this.currentCards[cardIndex].back_file_id = fileId; + this.currentCards[cardIndex].back_file = this.fileRefById({ id: fileId }); + this.currentCards[cardIndex].back_file.download_url = this.currentCards[cardIndex].back_file.meta['download-url']; } else { this.currentCards[cardIndex].back_file_id = ''; this.currentCards[cardIndex].back_file = []; diff --git a/resources/vue/components/courseware/blocks/CoursewareDocumentBlock.vue b/resources/vue/components/courseware/blocks/CoursewareDocumentBlock.vue index 0af177e27c52cca228a5898c30d2113070133b23..72cfeb3ed480f94fd9ddb1d5dd238c3dd456337e 100644 --- a/resources/vue/components/courseware/blocks/CoursewareDocumentBlock.vue +++ b/resources/vue/components/courseware/blocks/CoursewareDocumentBlock.vue @@ -221,10 +221,13 @@ </label> <label> {{ $gettext('Datei') }} - <courseware-file-chooser + <studip-file-chooser v-model="currentFileId" + selectable="file" + :courseId="context.id" + :userId="userId" :isDocument="true" - @selectFile="updateCurrentFile" + :excludedCourseFolderTypes="excludedCourseFolderTypes" /> </label> <label> @@ -269,7 +272,7 @@ import { import pdfjsWorker from 'pdfjs-dist/build/pdf.worker.entry'; import { dragscroll } from 'vue-dragscroll'; -import { mapActions } from 'vuex'; +import { mapActions, mapGetters } from 'vuex'; import 'pdfjs-dist/web/pdf_viewer.css'; export default { @@ -327,6 +330,9 @@ export default { }; }, computed: { + ...mapGetters({ + fileRefById: 'file-refs/byId', + }), title() { return this.block?.attributes?.payload?.title; }, @@ -369,6 +375,11 @@ export default { showPdfSearchBox() { this.resetPdfSearch(); }, + currentFileId(newId) { + if (newId) { + this.currentFile = this.fileRefById({ id: newId }); + } + } }, mounted() { if (this.block.id) { @@ -392,10 +403,6 @@ export default { this.currentFileId = this.fileId; this.currentDocType = this.docType; }, - updateCurrentFile(file) { - this.currentFile = file; - this.currentFileId = file.id; - }, initPdfTask() { if (this.currentUrl) { let view = this; diff --git a/resources/vue/components/courseware/blocks/CoursewareDownloadBlock.vue b/resources/vue/components/courseware/blocks/CoursewareDownloadBlock.vue index 2897b085568befd5da8baa6b17cfc323cddc0f68..ff01d78a2638252e0206f149c6ba250880f4c48c 100644 --- a/resources/vue/components/courseware/blocks/CoursewareDownloadBlock.vue +++ b/resources/vue/components/courseware/blocks/CoursewareDownloadBlock.vue @@ -51,7 +51,7 @@ </label> <label> {{ $gettext('Datei') }} - <courseware-file-chooser v-model="currentFileId" @selectFile="updateCurrentFile" /> + <studip-file-chooser v-model="currentFileId" selectable="file" :courseId="context.id" :userId="userId" :excludedCourseFolderTypes="excludedCourseFolderTypes" /> </label> </form> </courseware-tab> diff --git a/resources/vue/components/courseware/blocks/CoursewareFolderBlock.vue b/resources/vue/components/courseware/blocks/CoursewareFolderBlock.vue index 6bbc48d00a8455edd368a47c5ecac5ff10ea69d4..0eefa6cc040e727fe86f0cecd073d0ed29121478 100644 --- a/resources/vue/components/courseware/blocks/CoursewareFolderBlock.vue +++ b/resources/vue/components/courseware/blocks/CoursewareFolderBlock.vue @@ -140,7 +140,7 @@ </label> <label> {{ $gettext('Ordner') }} - <courseware-folder-chooser v-model="currentFolderId" allowUserFolders allowHomeworkFolders /> + <studip-file-chooser v-model="currentFolderId" selectable="folder" :courseId="context.id" :userId="userId" /> </label> </form> </template> diff --git a/resources/vue/components/courseware/blocks/CoursewareGalleryBlock.vue b/resources/vue/components/courseware/blocks/CoursewareGalleryBlock.vue index 40bc33c5dad2cc1030055226053856b2aef91793..816447edda216c0004efc446ca94ea1462de0c14 100644 --- a/resources/vue/components/courseware/blocks/CoursewareGalleryBlock.vue +++ b/resources/vue/components/courseware/blocks/CoursewareGalleryBlock.vue @@ -86,7 +86,7 @@ </label> <label> {{ $gettext('Ordner') }} - <courseware-folder-chooser v-model="currentFolderId" allowUserFolders /> + <studip-file-chooser v-model="currentFolderId" selectable="folder" :courseId="context.id" :userId="userId" :excludedCourseFolderTypes="excludedCourseFolderTypes" /> </label> <label v-if="currentLayout === 'carousel'"> {{ $gettext('Höhe') }} diff --git a/resources/vue/components/courseware/blocks/CoursewareHeadlineBlock.vue b/resources/vue/components/courseware/blocks/CoursewareHeadlineBlock.vue index c7dd6b52ab426e2f0290b9903f3a5ea4a78728f1..c9c2ce0ac7fa0f66b94040efaf471944d5b657ee 100644 --- a/resources/vue/components/courseware/blocks/CoursewareHeadlineBlock.vue +++ b/resources/vue/components/courseware/blocks/CoursewareHeadlineBlock.vue @@ -242,11 +242,15 @@ </template> <template v-if="!currentBackgroundImageId"> - <courseware-file-chooser + <studip-file-chooser v-model="currentBackgroundImageId" + selectable="file" + :courseId="context.id" + :userId="userId" :isImage="true" - @selectFile="onSelectFile" - /> + :excludedCourseFolderTypes="excludedCourseFolderTypes" + @select="onSelectFile" + /> <div style="margin-block-start: 1em">{{ $gettext('oder') }}</div> <button class="button" type="button" @click="showStockImageSelector = true"> {{ $gettext('Aus dem Bilderpool auswählen') }} @@ -340,11 +344,11 @@ export default { }, computed: { ...mapGetters({ + currentStructuralElementImageURL: 'currentStructuralElementImageURL', fileRefById: 'file-refs/byId', + relatedTermOfUse: 'terms-of-use/related', stockImageById: 'stock-images/byId', urlHelper: 'urlHelper', - relatedTermOfUse: 'terms-of-use/related', - currentStructuralElementImageURL: 'currentStructuralElementImageURL' }), title() { return this.block?.attributes?.payload?.title; @@ -531,8 +535,10 @@ export default { this.currentBackgroundImageType = file.type; this.currentBackgroundURL = file.download_url; }, - onSelectFile(file) { - this.updateCurrentBackgroundImage({ ...file, type: 'file-refs' }); + onSelectFile(fileId) { + let file = this.fileRefById({ id: fileId }); + file.download_url = file.meta['download-url']; + this.updateCurrentBackgroundImage(file); }, storeText() { let attributes = {}; diff --git a/resources/vue/components/courseware/blocks/CoursewareIframeBlock.vue b/resources/vue/components/courseware/blocks/CoursewareIframeBlock.vue index 90df3f66e0970660bbcc3f4b50ba36b7da804ab3..d628b12e78c24e99b855d64d6694e2b18643b7b0 100644 --- a/resources/vue/components/courseware/blocks/CoursewareIframeBlock.vue +++ b/resources/vue/components/courseware/blocks/CoursewareIframeBlock.vue @@ -123,7 +123,7 @@ <script> import BlockComponents from './block-components.js'; import blockMixin from '@/vue/mixins/courseware/block.js'; -import { mapActions, mapGetters } from 'vuex'; +import { mapActions } from 'vuex'; import md5 from 'md5'; export default { @@ -150,7 +150,6 @@ export default { }; }, computed: { - ...mapGetters(['userId']), url() { return this.block?.attributes?.payload?.url; }, diff --git a/resources/vue/components/courseware/blocks/CoursewareImageMapBlock.vue b/resources/vue/components/courseware/blocks/CoursewareImageMapBlock.vue index 41b39d2cb395d2f27139e81f785a7b3b8191903e..6d129801ef5da65ccea2ae89b96845014f90fb43 100644 --- a/resources/vue/components/courseware/blocks/CoursewareImageMapBlock.vue +++ b/resources/vue/components/courseware/blocks/CoursewareImageMapBlock.vue @@ -78,10 +78,13 @@ <form class="default" @submit.prevent=""> <label> {{ $gettext('Bilddatei') }} - <courseware-file-chooser + <studip-file-chooser v-model="currentFileId" + selectable="file" + :courseId="studipContext.id" + :userId="userId" :isImage="true" - @selectFile="updateCurrentFile" + :excludedCourseFolderTypes="excludedCourseFolderTypes" /> </label> <label> @@ -307,6 +310,7 @@ export default { }, computed: { ...mapGetters({ + studipContext: 'context', courseware: 'courseware-structural-elements/all', fileRefById: 'file-refs/byId', urlHelper: 'urlHelper', @@ -343,6 +347,9 @@ export default { }, async loadFile() { const id = this.currentFileId; + if (id === '') { + return; + } await this.loadFileRef({ id }); const fileRef = this.fileRefById({ id }); @@ -797,6 +804,13 @@ export default { } }, }, + watch: { + currentFileId(newId) { + if (newId) { + this.loadFile(); + } + } + } }; </script> <style scoped lang="scss"> diff --git a/resources/vue/components/courseware/blocks/CoursewareTableOfContentsBlock.vue b/resources/vue/components/courseware/blocks/CoursewareTableOfContentsBlock.vue index dd619dd1d8c2e03d64278fcb5c99d0b311c3844c..99d42647df1c77f0d63edd662671d49103cda175 100644 --- a/resources/vue/components/courseware/blocks/CoursewareTableOfContentsBlock.vue +++ b/resources/vue/components/courseware/blocks/CoursewareTableOfContentsBlock.vue @@ -100,7 +100,6 @@ export default { ...mapGetters({ childrenById: 'courseware-structure/children', structuralElementById: 'courseware-structural-elements/byId', - context: 'context', taskById: 'courseware-tasks/byId', userById: 'users/byId', groupById: 'status-groups/byId', diff --git a/resources/vue/components/courseware/blocks/CoursewareVideoBlock.vue b/resources/vue/components/courseware/blocks/CoursewareVideoBlock.vue index 2ed3074426222f17d14b4a7a9abcf9f83f4ea761..7febfbc0a6160c3321c73c0eb79b76aee2496846 100644 --- a/resources/vue/components/courseware/blocks/CoursewareVideoBlock.vue +++ b/resources/vue/components/courseware/blocks/CoursewareVideoBlock.vue @@ -45,10 +45,13 @@ </label> <label v-show="currentSource === 'studip'"> {{ $gettext('Datei') }} - <courseware-file-chooser + <studip-file-chooser v-model="currentFileId" + selectable="file" + :courseId="context.id" + :userId="userId" :isVideo="true" - @selectFile="updateCurrentFile" + :excludedCourseFolderTypes="excludedCourseFolderTypes" /> </label> @@ -230,6 +233,13 @@ export default { } }, }, + watch: { + currentFileId(newId) { + if (newId) { + this.currentFile = this.fileRefById({ id: newId }); + } + } + } }; </script> <style scoped lang="scss"> diff --git a/resources/vue/components/courseware/blocks/block-components.js b/resources/vue/components/courseware/blocks/block-components.js index a79ed4a30493de0632b116253bab80a8c2969b5f..b0950595bd1cd7a9e447edd7d61e57315149d701 100644 --- a/resources/vue/components/courseware/blocks/block-components.js +++ b/resources/vue/components/courseware/blocks/block-components.js @@ -5,6 +5,8 @@ import CoursewareTabs from '../layouts/CoursewareTabs.vue'; import CoursewareTab from '../layouts/CoursewareTab.vue'; import CoursewareCompanionBox from '../layouts/CoursewareCompanionBox.vue' +import StudipFileChooser from './../../StudipFileChooser.vue'; + const BlockComponents = { CoursewareDefaultBlock, CoursewareFileChooser, @@ -13,6 +15,8 @@ const BlockComponents = { CoursewareTabs, CoursewareTab, CoursewareCompanionBox, + + StudipFileChooser, } export default BlockComponents; \ No newline at end of file diff --git a/resources/vue/components/file-chooser/FileChooserBox.vue b/resources/vue/components/file-chooser/FileChooserBox.vue new file mode 100644 index 0000000000000000000000000000000000000000..cc5a1bd7d2cb6f2bed578d04478e756195502297 --- /dev/null +++ b/resources/vue/components/file-chooser/FileChooserBox.vue @@ -0,0 +1,249 @@ +<template> + <div class="file-chooser-box"> + <header> + <file-chooser-breadcrumb :folders="folders" /> + <button class="toggle-view" :title="$gettext('Rasteransicht umschalten')" @click="toggleGrid"> + <studip-icon :shape="showGrid ? 'view-list' : 'view-wall'" /> + </button> + </header> + <div v-if="showMessageBox" class="messagebox messagebox_success"> + <div class="messagebox_buttons"> + <a + href="#" + :title="$gettext('Nachrichtenbox schließen')" + class="close" + @click.prevent="showMessageBox = false" + ></a> + </div> + {{ successMessage }} + </div> + <div v-if="contentForbidden" class="messagebox messagebox_error"> + {{ $gettext('Sie sind nicht berechtigt, den Inhalt dieses Ordners anzuzeigen.') }} + </div> + <div v-else class="file-chooser-box-content"> + <div v-if="showGrid" class="file-chooser-items"> + <template v-if="!isEmpty"> + <file-chooser-folder-item v-for="folder in subFolders" :key="folder.id" :folder="folder" /> + <file-chooser-file-item + v-for="file in currentFolderFiles" + :key="file.id" + :file="file" + @selectId="$emit('selectId')" + /> + <studip-progress-indicator v-if="loadingFiles" :description="$gettext('Lade Dateien…')" /> + </template> + <file-chooser-empty v-if="isEmpty" /> + </div> + <file-chooser-table + v-else + :files="currentFolderFiles" + :subfolders="subFolders" + :empty="isEmpty" + @selectId="$emit('selectId')" + /> + <file-chooser-toolbar + :class="{ 'with-table': !showGrid }" + @fileAdded="fileAdded" + @folderAdded="folderAdded" + /> + </div> + </div> +</template> + +<script> +import FileChooserBreadcrumb from './FileChooserBreadcrumb.vue'; +import FileChooserEmpty from './FileChooserEmpty.vue'; +import FileChooserFileItem from './FileChooserFileItem.vue'; +import FileChooserFolderItem from './FileChooserFolderItem.vue'; +import FileChooserTable from './FileChooserTable.vue'; +import FileChooserToolbar from './FileChooserToolbar.vue'; +import StudipProgressIndicator from '../StudipProgressIndicator.vue'; + +import { mapActions, mapGetters } from 'vuex'; + +export default { + name: 'file-chooser-box', + components: { + FileChooserBreadcrumb, + FileChooserEmpty, + FileChooserFileItem, + FileChooserFolderItem, + FileChooserTable, + FileChooserToolbar, + StudipProgressIndicator, + }, + props: { + excludedFolderTypes: { type: Array, default: () => [] }, + }, + data() { + return { + loadingFiles: false, + showGrid: true, + showMessageBox: false, + successMessage: '', + contentForbidden: false, + }; + }, + computed: { + ...mapGetters({ + activeFolder: 'file-chooser/activeFolder', + activeFolderId: 'file-chooser/activeFolderId', + currentFolderFiles: 'file-chooser/currentFolderFiles', + relatedUsersFolders: 'file-chooser/relatedUsersFolders', + relatedCoursesFolders: 'file-chooser/relatedCoursesFolders', + rangeType: 'file-chooser/activeFolderRangeType', + }), + rootFolder() { + if (this.folders.length > 0) { + return this.folders.find((folder) => { + return folder.attributes['folder-type'] === 'RootFolder'; + }); + } + return null; + }, + folders() { + if (this.rangeType === 'courses') { + return this.relatedCoursesFolders; + } + if (this.rangeType === 'users') { + return this.relatedUsersFolders; + } + + return []; + }, + subFolders() { + const excludedFolderTypes = ['InboxFolder', 'OutboxFolder'].concat(this.excludedFolderTypes); + return this.folders.filter((folder) => { + return ( + folder.relationships?.parent?.data?.id === this.activeFolderId + && !excludedFolderTypes.includes(folder.attributes['folder-type']) + ); + }); + }, + isEmpty() { + return this.activeFolder?.attributes['is-empty'] ?? false; + }, + foldersCounter() { + return this.activeFolder?.relationships?.folders?.meta?.count ?? 0; + }, + filesCounter() { + return this.activeFolder?.relationships?.['file-refs']?.meta?.count ?? 0; + }, + }, + methods: { + ...mapActions({ + loadFolderFiles: 'file-chooser/loadFolderFiles', + }), + async loadFiles(folderId) { + this.contentForbidden = false; + if (this.filesCounter === 0) { + return; + } + setTimeout(() => { + if (loading) { + this.loadingFiles = true; + } + }, 100); + let loading = true; + try { + await this.loadFolderFiles({ folderId }); + } catch(response) { + if (response.data?.errors[0].status === '403') { + this.contentForbidden = true; + } + } + loading = false; + this.loadingFiles = false; + }, + toggleGrid() { + this.showGrid = !this.showGrid; + }, + fileAdded() { + this.showMessageBox = true; + this.successMessage = this.$gettext('Es wurde eine Datei hochgeladen.'); + }, + folderAdded() { + this.showMessageBox = true; + this.successMessage = this.$gettext('Der Ordner wurde angelegt.'); + }, + }, + watch: { + activeFolderId(newId) { + this.showMessageBox = false; + this.loadFiles(newId); + }, + }, +}; +</script> +<style lang="scss"> +.file-chooser-box { + flex-grow: 1; + + header { + display: flex; + flex-direction: row; + position: sticky; + top: 0; + background-color: var(--content-color-20); + padding: 0.5em 1em; + border: solid thin var(--content-color-40); + margin-bottom: 1em; + + .file-chooser-breadcrumb { + flex-grow: 1; + } + .toggle-view { + width: 20px; + height: 20px; + border: none; + background-color: transparent; + cursor: pointer; + } + } + .file-chooser-box-content { + display: flex; + flex-direction: column; + justify-content: space-between; + overflow-y: scroll; + height: calc(100% - 36px); + } +} +.file-chooser-items { + display: flex; + flex-direction: row; + flex-wrap: wrap; + overflow-y: auto; + + .file-chooser-item { + display: flex; + flex-direction: column; + width: 104px; + min-height: 104px; + border: solid thin transparent; + background-color: transparent; + word-break: break-word; + margin: 0 4px 4px 4px; + padding: 4px; + cursor: pointer; + + &.selected { + background-color: var(--activity-color-20); + border: solid thin var(--base-color); + font-weight: 700; + } + &.disabled { + cursor: default; + } + img { + margin: 0 auto; + } + span { + width: 100%; + overflow: hidden; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + } + } +} +</style> diff --git a/resources/vue/components/file-chooser/FileChooserBreadcrumb.vue b/resources/vue/components/file-chooser/FileChooserBreadcrumb.vue new file mode 100644 index 0000000000000000000000000000000000000000..5f0ffa1a1d83049e3770b3d4314ac8ba7f2632c1 --- /dev/null +++ b/resources/vue/components/file-chooser/FileChooserBreadcrumb.vue @@ -0,0 +1,87 @@ +<template> + <ol class="file-chooser-breadcrumb"> + <li v-for="(folder, index) in breadcrumbItems" :key="folder.id"> + <a href="#" @click.prevent="selectFolder(folder)"> + <template v-if="rootId !== folder.id"> + {{ folder.attributes.name }} + </template> + <studip-icon v-else shape="home" :title="homeTitle"/> + </a> + <span v-if="breadcrumbItems.length > 1 && index !== breadcrumbItems.length - 1">/</span> + </li> + </ol> +</template> + +<script> +import { mapActions, mapGetters } from 'vuex'; +export default { + name: 'file-chooser-breadcrumb', + props: { + folders: Array, + }, + data() { + return { + breadcrumbItems: [], + rootId: '' + }; + }, + computed: { + ...mapGetters({ + activeFolderId: 'file-chooser/activeFolderId', + activeFolder: 'file-chooser/activeFolder', + folderById: 'folders/byId', + }), + homeTitle() { + return this.activeFolderRangeType === 'users' ? this.$gettext('Arbeitsplatz') : this.$gettext('Diese Veranstaltung'); + } + }, + methods: { + ...mapActions({ + setActiveFolderId: 'file-chooser/setActiveFolderId', + setSelectedFolderId: 'file-chooser/setSelectedFolderId' + }), + selectFolder(folder) { + this.setActiveFolderId(folder.id); + }, + updateBreadcrumb() { + this.breadcrumbItems = []; + this.addBreadcrumbItem(this.activeFolder); + this.breadcrumbItems = this.breadcrumbItems.reverse(); + }, + addBreadcrumbItem(folder) { + this.breadcrumbItems.push(folder); + if (folder.relationships.parent) { + const id = folder.relationships.parent.data.id; + const parent = this.folderById({ id }); + this.addBreadcrumbItem(parent); + } else { + this.rootId = folder.id; + } + }, + }, + watch: { + activeFolderId(newId) { + this.updateBreadcrumb(); + this.setSelectedFolderId(''); + }, + }, +}; +</script> + +<style lang="scss"> +.file-chooser-breadcrumb { + display: flex; + flex-direction: row; + padding: 0; + margin: 0; + li { + list-style: none; + a img { + vertical-align: text-bottom; + } + span { + padding: 0 4px 0 0; + } + } +} +</style> diff --git a/resources/vue/components/file-chooser/FileChooserDialog.vue b/resources/vue/components/file-chooser/FileChooserDialog.vue new file mode 100644 index 0000000000000000000000000000000000000000..2102d1e3b0d416f4c53c676f54c51e6ff3f1722f --- /dev/null +++ b/resources/vue/components/file-chooser/FileChooserDialog.vue @@ -0,0 +1,295 @@ +<template> + <studip-dialog + :title="dialogTitle" + :confirmText="$gettext('Auswählen')" + confirmClass="accept" + :closeText="$gettext('Abbrechen')" + closeClass="cancel" + @close="$emit('close')" + @confirm="selectId" + :confirmDisabled="!hasSelection" + :height="height" + :width="width" + > + <template v-slot:dialogContent> + <div class="file-chooser-content"> + <ul class="file-chooser-folder-selector"> + <li v-if="courseId && allowCourseFolders" class="file-chooser-tree-item"> + <a + href="#" + @click.prevent="setFolder('courses')" + :class="{ selected: coursesRootFolderSelected }" + > + <studip-icon shape="seminar" /> + <span>{{ $gettext('Diese Veranstaltung') }}</span> + </a> + <ul class="file-chooser-tree file-chooser-tree-first-level"> + <file-chooser-tree v-for="child in coursesTree.children" :key="child.id" :folder="child" /> + </ul> + </li> + <li v-if="userId && allowUserFolders" class="file-chooser-tree-item"> + <a href="#" @click.prevent="setFolder('users')" :class="{ selected: usersRootFolderSelected }"> + <studip-icon shape="content" /> + <span>{{ $gettext('Arbeitsplatz') }}</span> + </a> + <ul class="file-chooser-tree file-chooser-tree-first-level"> + <file-chooser-tree v-for="child in usersTree.children" :key="child.id" :folder="child" /> + </ul> + </li> + </ul> + <file-chooser-box + :excludedFolderTypes="[...excludedCourseFolderTypes, ...excludedUserFolderTypes]" + @selectId="selectId" + /> + </div> + </template> + </studip-dialog> +</template> +<script> +import FileChooserBox from './FileChooserBox.vue'; +import FileChooserTree from './FileChooserTree.vue'; +import { mapActions, mapGetters } from 'vuex'; + +export default { + name: 'file-chooser-dialog', + components: { + FileChooserBox, + FileChooserTree, + }, + props: { + selectable: { + type: String, + default: 'file', + validator: (value) => { + return ['file', 'folder'].includes(value); + }, + }, + selectedId: { + type: String, + required: false, + }, + courseId: { + type: String, + validator: (value) => { + return value !== ''; + }, + required: false, + }, + userId: { + type: String, + validator: (value) => { + return value !== ''; + }, + required: false, + }, + isImage: { type: Boolean, default: false }, + isVideo: { type: Boolean, default: false }, + isAudio: { type: Boolean, default: false }, + isDocument: { type: Boolean, default: false }, + excludedCourseFolderTypes: { type: Array, default: () => [] }, + excludedUserFolderTypes: { type: Array, default: () => [] }, + allowUserFolders: { type: Boolean, default: true }, + allowCourseFolders: { type: Boolean, default: true }, + }, + data() { + return { + height: 600, + width: 1000, + scope: 'courses', + coursesTree: [], + usersTree: [], + }; + }, + computed: { + ...mapGetters({ + activeFolderId: 'file-chooser/activeFolderId', + relatedUsersFolders: 'file-chooser/relatedUsersFolders', + relatedCoursesFolders: 'file-chooser/relatedCoursesFolders', + isFolderChooser: 'file-chooser/isFolderChooser', + selectedFileId: 'file-chooser/selectedFileId', + selectedFolderId: 'file-chooser/selectedFolderId', + fileById: 'file-refs/byId', + folderById: 'folders/byId', + }), + dialogTitle() { + if (this.isFolderChooser) { + return this.$gettext('Ordner auswählen'); + } + + return this.$gettext('Datei auswählen'); + }, + showCourse() { + return this.scope === 'courses'; + }, + showUser() { + return this.scope === 'users'; + }, + hasSelection() { + if (this.isFolderChooser) { + return this.selectedFolderId !== ''; + } + + return this.selectedFileId !== ''; + }, + coursesRootFolder() { + return this.getRootFolder(this.relatedCoursesFolders); + }, + usersRootFolder() { + return this.getRootFolder(this.relatedUsersFolders); + }, + coursesRootFolderSelected() { + return this.coursesRootFolder?.id === this.activeFolderId; + }, + usersRootFolderSelected() { + return this.usersRootFolder?.id === this.activeFolderId; + }, + }, + methods: { + ...mapActions({ + setCourseId: 'file-chooser/setCourseId', + setUserId: 'file-chooser/setUserId', + setSelectable: 'file-chooser/setSelectable', + setIsAudio: 'file-chooser/setIsAudio', + setIsDocument: 'file-chooser/setIsDocument', + setIsImage: 'file-chooser/setIsImage', + setIsVideo: 'file-chooser/setIsVideo', + + setSelectedFileId: 'file-chooser/setSelectedFileId', + setSelectedFolderId: 'file-chooser/setSelectedFolderId', + setActiveFolderId: 'file-chooser/setActiveFolderId', + loadRangeFolders: 'file-chooser/loadRangeFolders', + }), + selectId() { + if (this.isFolderChooser) { + this.$emit('selected', this.selectedFolderId); + } else { + this.$emit('selected', this.selectedFileId); + } + }, + setDimensions() { + this.height = (window.innerHeight * 0.8).toFixed(0); + this.width = Math.min((window.innerWidth * 0.9).toFixed(0), 1200).toFixed(0); + }, + setFolder(range) { + if (range === 'courses') { + this.setActiveFolderId(this.coursesRootFolder.id); + } + if (range === 'users') { + this.setActiveFolderId(this.usersRootFolder.id); + } + }, + getRootFolder(folders) { + if (folders?.length > 0) { + return folders.filter((folder) => { + return folder.attributes['folder-type'] === 'RootFolder'; + })[0]; + } + return null; + }, + getFolderTree(rootFolder, folders, excludedFolderTypes) { + if (rootFolder) { + rootFolder.children = this.getSubfolders(rootFolder, folders, excludedFolderTypes); + + return rootFolder; + } + + return []; + }, + getSubfolders(parent, folders, excludedFolderTypes) { + const children = folders.filter((folder) => { + return ( + folder.relationships?.parent?.data?.id === parent.id && + !excludedFolderTypes.includes(folder.attributes['folder-type']) + ); + }); + children.forEach((child) => { + child.children = this.getSubfolders(child, folders, excludedFolderTypes); + }); + + return children; + }, + }, + async mounted() { + this.setDimensions(); + + this.setSelectable(this.selectable); + this.setCourseId(this.courseId); + this.setUserId(this.userId); + if (this.selectable === 'file') { + this.setIsAudio(this.isAudio); + this.setIsDocument(this.isDocument); + this.setIsImage(this.isImage); + this.setIsVideo(this.isVideo); + } + + if (this.userId && this.allowUserFolders) { + await this.loadRangeFolders({ rangeType: 'users', rangeId: this.userId }); + const excludedFolderTypes = ['InboxFolder', 'OutboxFolder']; + this.usersTree = this.getFolderTree(this.usersRootFolder, this.relatedUsersFolders, [ + ...excludedFolderTypes, + ...this.excludedUserFolderTypes, + ]); + if (!this.courseId && !this.selectedId) { + this.setActiveFolderId(this.usersRootFolder.id); + } + } + + if (this.courseId && this.allowCourseFolders) { + await this.loadRangeFolders({ rangeType: 'courses', rangeId: this.courseId }); + this.coursesTree = this.getFolderTree( + this.coursesRootFolder, + this.relatedCoursesFolders, + this.excludedCourseFolderTypes + ); + if (!this.selectedId) { + this.setActiveFolderId(this.coursesRootFolder.id); + } + } + + if (this.selectedId) { + if (this.isFolderChooser) { + const folder = this.folderById({ id: this.selectedId }); + this.setActiveFolderId(folder.relationships.parent.data.id); + this.$nextTick(() => { + this.setSelectedFolderId(this.selectedId); + }); + } else { + const file = this.fileById({ id: this.selectedId }); + this.setActiveFolderId(file.relationships.parent.data.id); + this.setSelectedFileId(file.id); + } + } + }, +}; +</script> + +<style lang="scss" scoped> +.file-chooser-content { + display: flex; + flex-direction: row; + height: 100%; + .file-chooser-folder-selector { + min-width: 270px; + max-width: 270px; + list-style: none; + margin: 0 1em 0 0; + padding: 0 1em 0 0; + border-right: solid thin var(--content-color-40); + overflow-y: auto; + } +} + + +@media (max-width: 580px) { + .file-chooser-content .file-chooser-folder-selector { + display: none; + } +} +@media (max-width: 768px) { + .file-chooser-content .file-chooser-folder-selector { + min-width: 130px; + max-width: 130px; + } +} + +</style> diff --git a/resources/vue/components/file-chooser/FileChooserEmpty.vue b/resources/vue/components/file-chooser/FileChooserEmpty.vue new file mode 100644 index 0000000000000000000000000000000000000000..fcadae0199f88571352c362e395d39faa91feecf --- /dev/null +++ b/resources/vue/components/file-chooser/FileChooserEmpty.vue @@ -0,0 +1,19 @@ +<template> + <div class="file-chooser-empty"> + <studip-icon shape="folder-empty" :size="128" role="inactive" /> + {{ $gettext('Dieser Ordner ist leer') }} + </div> +</template> + +<script> +export default { + name: 'file-chooser-empty', +}; +</script> +<style lang="scss"> +.file-chooser-empty { + display: flex; + flex-direction: column; + margin: 1em auto; +} +</style> diff --git a/resources/vue/components/file-chooser/FileChooserFileItem.vue b/resources/vue/components/file-chooser/FileChooserFileItem.vue new file mode 100644 index 0000000000000000000000000000000000000000..90c2f397d46e432c4640ce1d65b5324732a0f04f --- /dev/null +++ b/resources/vue/components/file-chooser/FileChooserFileItem.vue @@ -0,0 +1,142 @@ +<template v-if="isReadable"> + <button + v-if="isButton" + class="file-chooser-item" + :class="{ selected: isSelected, disabled: isFolderChooser }" + :title="fileName" + @click="selectFile" + v-on:dblclick="instantSelectFile" + > + <studip-icon :shape="fileIcon" :size="48" /> + <span>{{ fileName }}</span> + </button> + <tr v-else :class="{ selected: isSelected }"> + <td class="document-icon"> + <a href="#" @click.prevent="selectFile" v-on:dblclick.prevent="instantSelectFile"> + <studip-icon :shape="fileIcon" :size="24" /> + </a> + </td> + <td> + <a href="#" @click.prevent="selectFile" v-on:dblclick.prevent="instantSelectFile">{{ fileName }}</a> + </td> + <td>{{ fileSize }}</td> + <td class="responsive-hidden">{{ fileOwner }}</td> + <td>{{ fileMkdate }}</td> + </tr> +</template> + +<script> +import { mapActions, mapGetters } from 'vuex'; +export default { + name: 'file-chooser-file-item', + props: { + file: { + type: Object, + required: true, + }, + tag: { + type: String, + default: 'button', + validator: (tag) => { + return ['button', 'tr'].includes(tag); + }, + }, + }, + computed: { + ...mapGetters({ + selectedFileId: 'file-chooser/selectedFileId', + isFolderChooser: 'file-chooser/isFolderChooser', + }), + isReadable() { + return this.file.attributes['is-readable']; + }, + isSelected() { + return this.selectedFileId === this.file.id; + }, + isButton() { + return this.tag === 'button'; + }, + isTableRow() { + return this.tag === 'tr'; + }, + fileName() { + return this.file.attributes.name; + }, + fileMimeType() { + return this.file.attributes['mime-type']; + }, + fileIcon() { + if (this.fileMimeType.includes('audio')) { + return 'file-audio2'; + } + if (this.fileMimeType.includes('video')) { + return 'file-video'; + } + if (this.fileMimeType.includes('image')) { + return 'file-pic2'; + } + if (this.fileMimeType.includes('pdf')) { + return 'file-pdf'; + } + if (this.fileMimeType.includes('zip')) { + return 'file-archive'; + } + if (this.fileMimeType.includes(['msexcel', 'spreadsheetml.sheet'])) { + return 'file-excel'; + } + if (this.fileMimeType.includes(['opendocument.spreadsheet'])) { + return 'file-spreadsheet'; + } + if (this.fileMimeType.includes(['msword', 'wordprocessingml.document'])) { + return 'file-word'; + } + if (this.fileMimeType.includes(['opendocument.text'])) { + return 'file-text'; + } + if (this.fileMimeType.includes(['mspowerpoint', 'presentationml.presentation'])) { + return 'file-ppt '; + } + if (this.fileMimeType.includes(['opendocument.presentation'])) { + return 'file-presentation'; + } + + return 'file'; + }, + fileSize() { + let i = -1; + let size = this.file.attributes.filesize; + const units = ['KB', 'MB', 'GB', 'TB']; + do { + size = size / 1024; + i++; + } while (size > 1000); + + return Math.max(size, 0.1).toFixed(1) + ' ' + units[i]; + }, + fileOwner() { + return this.file.relationships.owner?.meta?.name; + }, + fileMkdate() { + const date = new Date(this.file.attributes.mkdate); + const options = { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' }; + + return date.toLocaleDateString('de-DE', options); + }, + }, + methods: { + ...mapActions({ + setSelectedFileId: 'file-chooser/setSelectedFileId', + }), + selectFile() { + if (this.isFolderChooser) { + return; + } + this.setSelectedFileId(this.file.id); + }, + instantSelectFile() { + this.selectFile(); + this.$emit('selectId'); + }, + }, +}; +</script> diff --git a/resources/vue/components/file-chooser/FileChooserFolderItem.vue b/resources/vue/components/file-chooser/FileChooserFolderItem.vue new file mode 100644 index 0000000000000000000000000000000000000000..116f582458637ac511e8141a23466a0f8e05da6f --- /dev/null +++ b/resources/vue/components/file-chooser/FileChooserFolderItem.vue @@ -0,0 +1,102 @@ +<template v-if="folderIsReadable"> + <button + v-if="isButton" + class="file-chooser-item" + :class="{ selected: isSelected, disabled: !isFolderChooser }" + :title="folderName" + @click="selectFolder" + v-on:dblclick="openFolder" + > + <studip-icon :shape="folderIcon" :size="48" /> + <span>{{ folderName }}</span> + </button> + <tr v-else :class="{ selected: isSelected }"> + <td class="document-icon"> + <a href="#" @click.prevent="selectFolder" v-on:dblclick.prevent="openFolder" + ><studip-icon :shape="folderIcon" :size="24" + /></a> + </td> + <td> + <a href="#" @click.prevent="selectFolder" v-on:dblclick.prevent="openFolder">{{ folderName }}</a> + </td> + <td> + <template v-if="!folderIsEmpty">{{ folderSize }}</template> + </td> + <td class="responsive-hidden">{{ folderOwner }}</td> + <td>{{ folderMkdate }}</td> + </tr> +</template> + +<script> +import folderIconMixin from '@/vue/mixins/file-chooser/folder-icon.js'; +import { mapActions, mapGetters } from 'vuex'; +export default { + name: 'file-chooser-folder-item', + mixins: [folderIconMixin], + props: { + folder: { + type: Object, + required: true, + }, + tag: { + type: String, + default: 'button', + validator: (tag) => { + return ['button', 'tr'].includes(tag); + }, + }, + }, + computed: { + ...mapGetters({ + selectedFolderId: 'file-chooser/selectedFolderId', + isFolderChooser: 'file-chooser/isFolderChooser', + }), + isSelected() { + return this.selectedFolderId === this.folder.id; + }, + isButton() { + return this.tag === 'button'; + }, + isTableRow() { + return this.tag === 'tr'; + }, + folderOwner() { + return this.folder.relationships.owner?.meta?.name; + }, + folderMkdate() { + const date = new Date(this.folder.attributes.mkdate); + const options = { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' }; + + return date.toLocaleDateString('de-DE', options); + }, + folderSubfolderCounter() { + return this.folder.relationships?.folders?.meta?.count ?? 0; + }, + folderFilesCounter() { + return this.folder.relationships?.['file-refs']?.meta?.count ?? 0; + }, + folderSize() { + const length = this.folderSubfolderCounter + this.folderFilesCounter; + return this.$gettextInterpolate(this.$ngettext('%{length} Objekt', '%{length} Objekte', length), { + length: length, + }); + }, + }, + methods: { + ...mapActions({ + setSelectedFolderId: 'file-chooser/setSelectedFolderId', + setActiveFolderId: 'file-chooser/setActiveFolderId', + }), + selectFolder() { + if (!this.isFolderChooser) { + return; + } + this.setSelectedFolderId(this.folder.id); + }, + openFolder() { + this.setActiveFolderId(this.folder.id); + this.setSelectedFolderId(''); + }, + }, +}; +</script> diff --git a/resources/vue/components/file-chooser/FileChooserTable.vue b/resources/vue/components/file-chooser/FileChooserTable.vue new file mode 100644 index 0000000000000000000000000000000000000000..ad7363dc9e6d5e5e03d3bd28c39381471daca632 --- /dev/null +++ b/resources/vue/components/file-chooser/FileChooserTable.vue @@ -0,0 +1,200 @@ +<template> + <table class="default"> + <colgroup> + <col style="width: 36px" /> + <col /> + <col style="width: 100px" /> + <col class="responsive-hidden" style="width: 150px" /> + <col style="width: 126px" /> + </colgroup> + <thead> + <tr class="sortable"> + <th>{{ $gettext('Typ') }}</th> + <th :class="getSortClass('name')" @click="sort('name')"> + <a href="#">{{ $gettext('Name') }}</a> + </th> + <th :class="getSortClass('size')" @click="sort('size')"> + <a href="#">{{ $gettext('Größe') }}</a> + </th> + <th class="responsive-hidden" :class="getSortClass('owner')" @click="sort('owner')"> + <a href="#">{{ $gettext('Autor/-in') }}</a> + </th> + <th :class="getSortClass('mkdate')" @click="sort('mkdate')"> + <a href="#">{{ $gettext('Datum') }}</a> + </th> + </tr> + </thead> + <tbody v-if="empty"> + <tr class="empty"> + <td colspan="5">{{ $gettext('Dieser Ordner ist leer') }}</td> + </tr> + </tbody> + <template v-else> + <tbody class="subfolders"> + <file-chooser-folder-item v-for="folder in sortedFolders" :key="folder.id" :folder="folder" tag="tr" /> + </tbody> + <tbody class="files"> + <file-chooser-file-item + v-for="file in sortedFiles" + :key="file.id" + :file="file" + tag="tr" + @selectId="$emit('selectId')" + /> + </tbody> + </template> + </table> +</template> + +<script> +import folderIconMixin from '@/vue/mixins/file-chooser/folder-icon.js'; +import fileChooserFileItem from './FileChooserFileItem.vue'; +import fileChooserFolderItem from './FileChooserFolderItem.vue'; + +export default { + name: 'file-chooser-table', + mixins: [folderIconMixin], + components: { + fileChooserFileItem, + fileChooserFolderItem, + }, + props: { + files: { + type: Array, + required: false, + }, + subfolders: { + type: Array, + required: false, + }, + empty: { + type: Boolean, + default: false, + }, + }, + data() { + return { + sortBy: 'name', + sortASC: true, + }; + }, + computed: { + sortedFiles() { + let files = this.files; + switch (this.sortBy) { + case 'name': + files = files.sort((a, b) => { + const aName = a.attributes.name.toUpperCase(); + const bName = b.attributes.name.toUpperCase(); + if (this.sortASC) { + return aName.localeCompare(bName, 'de', { sensitivity: 'base' }); + } else { + return bName.localeCompare(aName, 'de', { sensitivity: 'base' }); + } + }); + break; + case 'size': + files = files.sort((a, b) => { + if (this.sortASC) { + return a.attributes.filesize < b.attributes.filesize ? -1 : 1; + } else { + return a.attributes.filesize > b.attributes.filesize ? -1 : 1; + } + }); + break; + case 'owner': + files = files.sort((a, b) => { + const aName = (a.relationships.owner?.meta?.name ?? '').toUpperCase(); + const bName = (b.relationships.owner?.meta?.name ?? '').toUpperCase(); + if (this.sortASC) { + return aName.localeCompare(bName, 'de', { + sensitivity: 'base', + }); + } else { + return bName.localeCompare(aName, 'de', { + sensitivity: 'base', + }); + } + }); + break; + case 'mkdate': + files = files.sort((a, b) => { + if (this.sortASC) { + return new Date(a.attributes.mkdate) < new Date(b.attributes.mkdate) ? -1 : 1; + } else { + return new Date(a.attributes.mkdate) > new Date(b.attributes.mkdate) ? -1 : 1; + } + }); + break; + } + return files; + }, + sortedFolders() { + let folders = this.subfolders; + switch (this.sortBy) { + case 'name': + folders = folders.sort((a, b) => { + const aName = a.attributes.name.toUpperCase(); + const bName = b.attributes.name.toUpperCase(); + if (this.sortASC) { + return aName.localeCompare(bName, 'de', { sensitivity: 'base' }); + } else { + return bName.localeCompare(aName, 'de', { sensitivity: 'base' }); + } + }); + break; + case 'size': + folders = folders.sort((a, b) => { + const aObjects = a.relationships['file-refs'].meta.count + a.relationships.folders.meta.count; + const bObjects = b.relationships['file-refs'].meta.count + b.relationships.folders.meta.count; + if (this.sortASC) { + return aObjects < bObjects ? -1 : 1; + } else { + return aObjects > bObjects ? -1 : 1; + } + }); + break; + case 'owner': + folders = folders.sort((a, b) => { + const aName = (a.relationships.owner?.meta?.name ?? '').toUpperCase(); + const bName = (b.relationships.owner?.meta?.name ?? '').toUpperCase(); + if (this.sortASC) { + return aName.localeCompare(bName, 'de', { + sensitivity: 'base', + }); + } else { + return bName.localeCompare(aName, 'de', { + sensitivity: 'base', + }); + } + }); + break; + case 'mkdate': + folders = folders.sort((a, b) => { + if (this.sortASC) { + return new Date(a.attributes.mkdate) < new Date(b.attributes.mkdate) ? -1 : 1; + } else { + return new Date(a.attributes.mkdate) > new Date(b.attributes.mkdate) ? -1 : 1; + } + }); + break; + } + return folders; + }, + }, + methods: { + sort(sortBy) { + if (this.sortBy === sortBy) { + this.sortASC = !this.sortASC; + } else { + this.sortBy = sortBy; + } + }, + getSortClass(col) { + if (col === this.sortBy) { + return this.sortASC ? 'sortasc' : 'sortdesc'; + } + }, + }, +}; +</script> diff --git a/resources/vue/components/file-chooser/FileChooserToolbar.vue b/resources/vue/components/file-chooser/FileChooserToolbar.vue new file mode 100644 index 0000000000000000000000000000000000000000..4f3053e94288876d5177c8052fa333d6dbc70e2a --- /dev/null +++ b/resources/vue/components/file-chooser/FileChooserToolbar.vue @@ -0,0 +1,224 @@ +<template> + <div class="file-chooser-toolbar"> + <button v-if="showButtons" class="button" :disabled="!canAddFolder" @click="addFolder"> + {{ $gettext('Ordner hinzufügen') }} + </button> + <form v-if="showFolderAdder" class="inline-form" @submit.prevent=""> + <label for="file-chooser-add-folder">{{ $gettext('Ordner hinzufügen') }}:</label> + <input + id="file-chooser-add-folder" + type="text" + v-model="newFolderName" + :placeholder="$gettext('Ordnername')" + /> + <div class="inline-buttons"> + <button :title="$gettext('Ordner anlegen')" @click="createFolder"> + <studip-icon shape="accept" /> + </button> + <button :title="$gettext('Abbrechen')" @click="closeAddFolder"><studip-icon shape="decline" /></button> + </div> + </form> + <button v-if="showButtons && !isFolderChooser" class="button" @click="$refs.fileInput.click()"> + {{ $gettext('Datei hinzufügen') }} + </button> + <input v-show="false" type="file" ref="fileInput" :disabled="!canAddFile" @change="updateUpload" /> + <form v-if="showUpload" class="inline-form" @submit.prevent=""> + <label for="file-chooser-add-file">{{ $gettext('Datei hinzufügen') }}:</label> + <input + id="file-chooser-add-file" + :title="$gettext('Datei auswählen')" + type="text" + :value="uploadFileName" + readonly + @click="$refs.fileInput.click()" + /> + <div class="inline-buttons"> + <button :title="$gettext('Datei hochladen')" @click="createFile"><studip-icon shape="accept" /></button> + <button :title="$gettext('Abbrechen')" @click="closeAddFile"><studip-icon shape="decline" /></button> + </div> + </form> + </div> +</template> + +<script> +import axios from 'axios'; +import { mapActions, mapGetters } from 'vuex'; + +export default { + name: 'file-chooser-toolbar', + data() { + return { + showFolderAdder: false, + newFolderName: '', + showUpload: false, + uploadFile: null, + }; + }, + computed: { + ...mapGetters({ + activeFolderId: 'file-chooser/activeFolderId', + activeFolder: 'file-chooser/activeFolder', + activeFolderRangeType: 'file-chooser/activeFolderRangeType', + courseId: 'file-chooser/courseId', + isFolderChooser: 'file-chooser/isFolderChooser', + userId: 'file-chooser/userId', + }), + showButtons() { + return !this.showUpload && !this.showFolderAdder; + }, + canAddFolder() { + if (this.activeFolder) { + return ( + this.activeFolder.attributes['is-writable'] && this.activeFolder.attributes['is-subfolder-allowed'] + ); + } + return false; + }, + canAddFile() { + if (this.activeFolder) { + return this.activeFolder.attributes['is-writable']; + } + return false; + }, + uploadFileName() { + return this.uploadFile.name; + }, + }, + methods: { + ...mapActions({ + loadRangeFolders: 'file-chooser/loadRangeFolders', + loadFolderFiles: 'file-chooser/loadFolderFiles', + }), + addFolder() { + this.showFolderAdder = true; + }, + closeAddFolder() { + this.showFolderAdder = false; + this.newFolderName = ''; + }, + async createFolder() { + if (this.newFolderName === '') { + this.closeAddFolder(); + } + this.showFolderAdder = false; + const httpClient = await this.getHttpClient(); + const newFolder = { + data: { + type: 'folders', + attributes: { + name: this.newFolderName, + 'folder-type': 'StandardFolder', + }, + relationships: { + parent: { + data: { + id: this.activeFolderId, + type: 'folders', + }, + }, + }, + }, + }; + const context = { + type: this.activeFolderRangeType, + id: this.activeFolderRangeType === 'users' ? this.userId : this.courseId, + }; + await httpClient.post(`${context.type}/${context.id}/folders`, newFolder); + this.$emit('folderAdded'); + this.newFolderName = ''; + this.loadRangeFolders({ rangeType: context.type, rangeId: context.id }); + }, + getHttpClient() { + return axios.create({ + baseURL: STUDIP.URLHelper.getURL(`jsonapi.php/v1`, {}, true), + headers: { + 'Content-Type': 'application/vnd.api+json', + }, + }); + }, + updateUpload() { + this.showUpload = true; + this.uploadFile = this.$refs.fileInput.files[0]; + }, + closeAddFile() { + this.showUpload = false; + this.$refs.fileInput.value = null; + }, + async createFile() { + this.showUpload = false; + const httpClient = await this.getHttpClient(); + const formData = new FormData(); + formData.append('file', this.uploadFile, this.uploadFileName); + const url = `folders/${this.activeFolderId}/file-refs`; + let request = await httpClient.post(url, formData, { + headers: { + 'Content-Type': 'multipart/form-data', + }, + }); + let response = null; + try { + response = await httpClient.get(request.headers.location); + } catch (e) { + console.debug(e); + response = null; + } + + await this.loadFolderFiles({ folderId: this.activeFolderId }); + this.$emit('fileAdded'); + this.$refs.fileInput.value = null; + }, + }, + watch: { + activeFolderId(newId) { + this.closeAddFolder(); + this.closeAddFile(); + }, + }, +}; +</script> + +<style lang="scss"> +.file-chooser-toolbar { + display: flex; + flex-direction: row; + flex-wrap: wrap; + border-top: solid thin var(--content-color-40); + + &.with-table { + border: none; + margin-top: -16px; + } + + .inline-form { + display: flex; + flex-direction: row; + flex-wrap: wrap; + justify-content: start; + gap: 4px; + width: 100%; + margin: 0.8em 0.6em 0.8em 0; + + label { + line-height: 30px; + } + + input { + flex-grow: 1; + padding: 4px; + border: solid thin var(--content-color-40); + border-radius: 0; + } + button { + border: solid thin var(--base-color); + background-color: transparent; + height: 30px; + width: 30px; + cursor: pointer; + + img { + vertical-align: middle; + } + } + } +} +</style> diff --git a/resources/vue/components/file-chooser/FileChooserTree.vue b/resources/vue/components/file-chooser/FileChooserTree.vue new file mode 100644 index 0000000000000000000000000000000000000000..b3e2d525be3b736a3a0bc023e67e69e4f4dbca44 --- /dev/null +++ b/resources/vue/components/file-chooser/FileChooserTree.vue @@ -0,0 +1,98 @@ +<template v-if="folderIsReadable"> + <li class="file-chooser-tree-item"> + <span class="folder-toggle"> + <a + v-if="hasSubfolders" + herf="#" + @click.prevent="toggleSubfolders" + :title="unfold ? $gettext('Ordner zuklappen') : $gettext('Ordner aufklappen')" + > + <studip-icon :shape="unfold ? 'arr_1down' : 'arr_1right'" /> + </a> + </span> + <a href="#" @click.prevent="selectFolder" :class="{ selected: isSelected }"> + <studip-icon :shape="folderIcon" /> + <span>{{ folder.attributes.name }}</span> + </a> + <ul v-if="unfold" class="file-chooser-tree"> + <li v-for="child in folder.children" :key="child.id" class="file-chooser-tree-item"> + <file-chooser-tree :folder="child" /> + </li> + </ul> + </li> +</template> + +<script> +import folderIconMixin from '@/vue/mixins/file-chooser/folder-icon.js'; +import { mapActions, mapGetters } from 'vuex'; +export default { + name: 'file-chooser-tree', + mixins: [folderIconMixin], + props: { + folder: Object, + }, + data() { + return { + unfold: false, + }; + }, + computed: { + ...mapGetters({ + activeFolderId: 'file-chooser/activeFolderId', + }), + isSelected() { + return this.folder.id === this.activeFolderId; + }, + hasSubfolders() { + const counter = this.folder.relationships?.folders?.meta?.count ?? 0; + + return counter > 0; + }, + }, + methods: { + ...mapActions({ + setActiveFolderId: 'file-chooser/setActiveFolderId', + setSelectedFolderId: 'file-chooser/setSelectedFolderId', + }), + selectFolder() { + this.setActiveFolderId(this.folder.id); + this.setSelectedFolderId(''); + this.unfold = true; + }, + toggleSubfolders() { + this.unfold = !this.unfold; + }, + }, +}; +</script> + +<style lang="scss"> +.file-chooser-tree { + padding-left: 18px; + &.file-chooser-tree-first-level { + padding-left: 0; + } +} +.file-chooser-tree-item { + list-style: none; + padding: 2px 0 0 0; + .folder-toggle { + width: 16px; + } + a.selected { + font-weight: 700; + } + img { + vertical-align: middle; + } + span { + width: calc(100% - 46px); + display: inline-block; + overflow: hidden; + text-overflow: ellipsis; + height: 16px; + white-space: nowrap; + vertical-align: sub; + } +} +</style> diff --git a/resources/vue/courseware-index-app.js b/resources/vue/courseware-index-app.js index 3309b55ce47f16e98983da45e904cab1d112523e..e32baed7d259150b1d0876c32e3d3e1bcd294016 100644 --- a/resources/vue/courseware-index-app.js +++ b/resources/vue/courseware-index-app.js @@ -1,5 +1,6 @@ import CoursewareModule from './store/courseware/courseware.module'; import CoursewareStructureModule from './store/courseware/structure.module'; +import FileChooserStore from './store/file-chooser.js'; import CoursewareStructuralElement from './components/courseware/structural-element/CoursewareStructuralElement.vue'; import IndexApp from './components/courseware/IndexApp.vue'; import PluginManager from './components/courseware/plugin-manager.js'; @@ -82,6 +83,7 @@ const mountApp = async (STUDIP, createApp, element) => { modules: { courseware: CoursewareModule, 'courseware-structure': CoursewareStructureModule, + 'file-chooser': FileChooserStore, ...mapResourceModules({ names: [ 'courses', diff --git a/resources/vue/mixins/courseware/block.js b/resources/vue/mixins/courseware/block.js index 0a83906e742831004a01894866fe4a89c08de885..c49a69789b19175a8dec774b4685a746b5a511d7 100644 --- a/resources/vue/mixins/courseware/block.js +++ b/resources/vue/mixins/courseware/block.js @@ -4,6 +4,8 @@ const blockMixin = { computed: { ...mapGetters({ getUserProgress: 'courseware-user-progresses/related', + context: 'context', + userId: 'userId', }), userProgress: { get: function () { @@ -15,6 +17,9 @@ const blockMixin = { return this.updateUserProgress(this.userProgress); }, }, + excludedCourseFolderTypes() { + return ['HomeworkFolder']; + } }, methods: { ...mapActions({ diff --git a/resources/vue/mixins/file-chooser/folder-icon.js b/resources/vue/mixins/file-chooser/folder-icon.js new file mode 100644 index 0000000000000000000000000000000000000000..be23dfa3a52a4d2047e922772732d028d7d58134 --- /dev/null +++ b/resources/vue/mixins/file-chooser/folder-icon.js @@ -0,0 +1,56 @@ +const folderIconMixin = { + computed: { + folderName() { + return this.folder.attributes.name; + }, + folderType() { + return this.folder.attributes['folder-type']; + }, + folderIsEmpty() { + return this.folder.attributes['is-empty']; + }, + folderIsReadable() { + return this.folder.attributes['is-readable']; + }, + folderIcon() { + let shape = 'folder'; + + switch (this.folderType) { + case 'HomeworkFolder': + case 'HiddenFolder': + shape = 'folder-lock'; + break; + case 'CourseGroupFolder': + shape = 'folder-group'; + break; + case 'TimedFolder': + shape = 'folder-date'; + break; + case 'CourseDateFolder': + shape = 'folder-topic'; + break; + case 'MaterialFolder': + return 'download'; + case 'PublicFolder': + case 'CoursePublicFolder': + shape = 'folder-public'; + break; + case 'InboxFolder': + case 'InboxOutboxFolder': + shape = 'folder-inbox'; + break; + } + + if (this.folderIsEmpty) { + shape += '-empty'; + } else { + shape += '-full'; + } + + return shape; + } + }, + +}; + +export default folderIconMixin; \ No newline at end of file diff --git a/resources/vue/store/file-chooser.js b/resources/vue/store/file-chooser.js new file mode 100644 index 0000000000000000000000000000000000000000..e8554b3b01bb9346fdcf999bb80c2a066efb1997 --- /dev/null +++ b/resources/vue/store/file-chooser.js @@ -0,0 +1,248 @@ +const getDefaultState = () => { + return { + selectable: 'file', + selectedFileId: '', + selectedFolderId: '', + activeFolderId: '', + userId: '', + courseId: '', + isAudio: false, + isDocument: false, + isImage: false, + isVideo: false, + }; +}; + +const initialState = getDefaultState(); +const state = { ...initialState }; + +const getters = { + selectable(state) { + return state.selectable; + }, + selectedFileId(state) { + return state.selectedFileId; + }, + selectedFolderId(state) { + return state.selectedFolderId; + }, + activeFolderId(state) { + return state.activeFolderId; + }, + userId(state) { + return state.userId; + }, + courseId(state) { + return state.courseId; + }, + isAudio(state) { + return state.isAudio; + }, + isDocument(state) { + return state.isDocument; + }, + isImage(state) { + return state.isImage; + }, + isVideo(state) { + return state.isVideo; + }, + + activeFolder(state, getters, rootState, rootGetters) { + const id = state.activeFolderId; + if (id) { + return rootGetters['folders/byId']({ id }); + } + + return null; + }, + + activeFolderRangeType(state, getters) { + return getters.activeFolder?.relationships?.range?.data?.type; + }, + + relatedUsersFolders(state, getters, rootState, rootGetters) { + const parent = { type: 'users', id: getters.userId }; + const relationship = 'folders'; + return rootGetters['folders/related']({ parent, relationship }); + }, + + relatedCoursesFolders(state, getters, rootState, rootGetters) { + const parent = { type: 'courses', id: getters.courseId }; + const relationship = 'folders'; + return rootGetters['folders/related']({ parent, relationship }); + }, + filterActive(state, getters) { + return getters.isAudio || getters.isDocument || getters.isImage || getters.isVideo; + }, + currentFolderFiles(state, getters, rootState, rootGetters) { + const id = state.activeFolderId; + if (id === '') { + return []; + } + const parent = { type: 'folders', id: id }; + const relationship = 'file-refs'; + let files = rootGetters['file-refs/related']({ parent, relationship }) ?? []; + + if (!getters.filterActive) { + return files; + } + + files = + files.filter((file) => { + const fileTermsOfUse = rootGetters['terms-of-use/related']({ + parent: file, + relationship: 'terms-of-use', + }); + if (fileTermsOfUse !== null && fileTermsOfUse.attributes['download-condition'] !== 0) { + return false; + } + if (getters.isImage && !file.attributes['mime-type'].includes('image')) { + return false; + } + const videoConditions = ['video/mp4', 'video/ogg', 'video/webm']; + if ( + getters.isVideo && + !videoConditions.some((condition) => file.attributes['mime-type'].includes(condition)) + ) { + return false; + } + const audioConditions = [ + 'audio/wav', + 'audio/ogg', + 'audio/webm', + 'audio/flac', + 'audio/mpeg', + 'audio/x-m4a', + 'audio/mp4', + ]; + if ( + getters.isAudio + && !audioConditions.some((condition) => file.attributes['mime-type'].includes(condition)) + ) { + return false; + } + const officeConditions = ['application/pdf']; //TODO enable more mime types + if ( + getters.isDocument + && !officeConditions.some((condition) => file.attributes['mime-type'].includes(condition)) + ) { + return false; + } + + return true; + }) ?? []; + + return files; + }, + isFolderChooser(state, getters) { + return getters.selectable === 'folder'; + }, +}; + +const actions = { + //setters + setSelectable({ commit }, value) { + commit('setSelectable', value); + }, + setSelectedFileId({ commit }, id) { + commit('setSelectedFileId', id); + }, + setSelectedFolderId({ commit }, id) { + commit('setSelectedFolderId', id); + }, + setActiveFolderId({ commit }, id) { + commit('setActiveFolderId', id); + }, + setCourseId({ commit }, id) { + commit('setCourseId', id); + }, + setUserId({ commit }, id) { + commit('setUserId', id); + }, + setIsAudio({ commit }, id) { + commit('setIsAudio', id); + }, + setIsDocument({ commit }, id) { + commit('setIsDocument', id); + }, + setIsImage({ commit }, id) { + commit('setIsImage', id); + }, + setIsVideo({ commit }, id) { + commit('setIsVideo', id); + }, + // custom action + async loadRangeFolders({ dispatch }, { rangeType, rangeId }) { + const parent = { type: rangeType, id: rangeId }; + const relationship = 'folders'; + const options = { 'page[limit]': 10000 }; + + return dispatch( + 'folders/loadRelated', + { + parent, + relationship, + options, + }, + { root: true } + ); + }, + + loadFolderFiles({ dispatch }, { folderId }) { + const parent = { type: 'folders', id: folderId }; + const relationship = 'file-refs'; + const options = { include: 'terms-of-use', 'page[limit]': 10000 }; + return dispatch( + 'file-refs/loadRelated', + { + parent, + relationship, + options, + }, + { root: true } + ); + }, +}; + +export const mutations = { + setSelectable(state, data) { + state.selectable = data; + }, + setSelectedFileId(state, data) { + state.selectedFileId = data; + }, + setSelectedFolderId(state, data) { + state.selectedFolderId = data; + }, + setActiveFolderId(state, data) { + state.activeFolderId = data; + state.selectedFileId = ''; + }, + setCourseId(state, data) { + state.courseId = data; + }, + setUserId(state, data) { + state.userId = data; + }, + setIsAudio(state, data) { + state.isAudio = data; + }, + setIsDocument(state, data) { + state.isDocument = data; + }, + setIsImage(state, data) { + state.isImage = data; + }, + setIsVideo(state, data) { + state.isVideo = data; + }, +}; + +export default { + namespaced: true, + actions, + getters, + mutations, + state, +};