From b302dfccb13876c1e6104a607e0e3a187b177e3e Mon Sep 17 00:00:00 2001 From: Ron Lucke Date: Wed, 10 Jan 2024 13:38:03 +0000 Subject: StEP #3262 Merge request studip/studip!2379 --- lib/classes/JsonApi/Schemas/Folder.php | 12 +- resources/vue/components/StudipFileChooser.vue | 149 +++++++++++ .../courseware/blocks/CoursewareAudioBlock.vue | 11 +- .../blocks/CoursewareBeforeAfterBlock.vue | 33 ++- .../courseware/blocks/CoursewareCanvasBlock.vue | 23 +- .../courseware/blocks/CoursewareConfirmBlock.vue | 1 - .../blocks/CoursewareDialogCardsBlock.vue | 49 ++-- .../courseware/blocks/CoursewareDocumentBlock.vue | 21 +- .../courseware/blocks/CoursewareDownloadBlock.vue | 2 +- .../courseware/blocks/CoursewareFolderBlock.vue | 2 +- .../courseware/blocks/CoursewareGalleryBlock.vue | 2 +- .../courseware/blocks/CoursewareHeadlineBlock.vue | 20 +- .../courseware/blocks/CoursewareIframeBlock.vue | 3 +- .../courseware/blocks/CoursewareImageMapBlock.vue | 18 +- .../blocks/CoursewareTableOfContentsBlock.vue | 1 - .../courseware/blocks/CoursewareVideoBlock.vue | 14 +- .../courseware/blocks/block-components.js | 4 + .../vue/components/file-chooser/FileChooserBox.vue | 249 +++++++++++++++++ .../file-chooser/FileChooserBreadcrumb.vue | 87 ++++++ .../components/file-chooser/FileChooserDialog.vue | 295 +++++++++++++++++++++ .../components/file-chooser/FileChooserEmpty.vue | 19 ++ .../file-chooser/FileChooserFileItem.vue | 142 ++++++++++ .../file-chooser/FileChooserFolderItem.vue | 102 +++++++ .../components/file-chooser/FileChooserTable.vue | 200 ++++++++++++++ .../components/file-chooser/FileChooserToolbar.vue | 224 ++++++++++++++++ .../components/file-chooser/FileChooserTree.vue | 98 +++++++ resources/vue/courseware-index-app.js | 2 + resources/vue/mixins/courseware/block.js | 5 + resources/vue/mixins/file-chooser/folder-icon.js | 56 ++++ resources/vue/store/file-chooser.js | 248 +++++++++++++++++ 30 files changed, 2012 insertions(+), 80 deletions(-) create mode 100644 resources/vue/components/StudipFileChooser.vue create mode 100644 resources/vue/components/file-chooser/FileChooserBox.vue create mode 100644 resources/vue/components/file-chooser/FileChooserBreadcrumb.vue create mode 100644 resources/vue/components/file-chooser/FileChooserDialog.vue create mode 100644 resources/vue/components/file-chooser/FileChooserEmpty.vue create mode 100644 resources/vue/components/file-chooser/FileChooserFileItem.vue create mode 100644 resources/vue/components/file-chooser/FileChooserFolderItem.vue create mode 100644 resources/vue/components/file-chooser/FileChooserTable.vue create mode 100644 resources/vue/components/file-chooser/FileChooserToolbar.vue create mode 100644 resources/vue/components/file-chooser/FileChooserTree.vue create mode 100644 resources/vue/mixins/file-chooser/folder-icon.js create mode 100644 resources/vue/store/file-chooser.js diff --git a/lib/classes/JsonApi/Schemas/Folder.php b/lib/classes/JsonApi/Schemas/Folder.php index 1cd5ba5..2c61cae 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 0000000..e021aec --- /dev/null +++ b/resources/vue/components/StudipFileChooser.vue @@ -0,0 +1,149 @@ + + + + + diff --git a/resources/vue/components/courseware/blocks/CoursewareAudioBlock.vue b/resources/vue/components/courseware/blocks/CoursewareAudioBlock.vue index 9ca52b0..4789c9f 100644 --- a/resources/vue/components/courseware/blocks/CoursewareAudioBlock.vue +++ b/resources/vue/components/courseware/blocks/CoursewareAudioBlock.vue @@ -288,18 +288,14 @@ @@ -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 }); + } + } + } }; diff --git a/resources/vue/components/file-chooser/FileChooserBreadcrumb.vue b/resources/vue/components/file-chooser/FileChooserBreadcrumb.vue new file mode 100644 index 0000000..5f0ffa1 --- /dev/null +++ b/resources/vue/components/file-chooser/FileChooserBreadcrumb.vue @@ -0,0 +1,87 @@ + + + + + diff --git a/resources/vue/components/file-chooser/FileChooserDialog.vue b/resources/vue/components/file-chooser/FileChooserDialog.vue new file mode 100644 index 0000000..2102d1e --- /dev/null +++ b/resources/vue/components/file-chooser/FileChooserDialog.vue @@ -0,0 +1,295 @@ + + + + diff --git a/resources/vue/components/file-chooser/FileChooserEmpty.vue b/resources/vue/components/file-chooser/FileChooserEmpty.vue new file mode 100644 index 0000000..fcadae0 --- /dev/null +++ b/resources/vue/components/file-chooser/FileChooserEmpty.vue @@ -0,0 +1,19 @@ + + + + diff --git a/resources/vue/components/file-chooser/FileChooserFileItem.vue b/resources/vue/components/file-chooser/FileChooserFileItem.vue new file mode 100644 index 0000000..90c2f39 --- /dev/null +++ b/resources/vue/components/file-chooser/FileChooserFileItem.vue @@ -0,0 +1,142 @@ + + + diff --git a/resources/vue/components/file-chooser/FileChooserFolderItem.vue b/resources/vue/components/file-chooser/FileChooserFolderItem.vue new file mode 100644 index 0000000..116f582 --- /dev/null +++ b/resources/vue/components/file-chooser/FileChooserFolderItem.vue @@ -0,0 +1,102 @@ + + + diff --git a/resources/vue/components/file-chooser/FileChooserTable.vue b/resources/vue/components/file-chooser/FileChooserTable.vue new file mode 100644 index 0000000..ad7363d --- /dev/null +++ b/resources/vue/components/file-chooser/FileChooserTable.vue @@ -0,0 +1,200 @@ + + + diff --git a/resources/vue/components/file-chooser/FileChooserToolbar.vue b/resources/vue/components/file-chooser/FileChooserToolbar.vue new file mode 100644 index 0000000..4f3053e --- /dev/null +++ b/resources/vue/components/file-chooser/FileChooserToolbar.vue @@ -0,0 +1,224 @@ + + + + + diff --git a/resources/vue/components/file-chooser/FileChooserTree.vue b/resources/vue/components/file-chooser/FileChooserTree.vue new file mode 100644 index 0000000..b3e2d52 --- /dev/null +++ b/resources/vue/components/file-chooser/FileChooserTree.vue @@ -0,0 +1,98 @@ + + + + + diff --git a/resources/vue/courseware-index-app.js b/resources/vue/courseware-index-app.js index 3309b55..e32baed 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 0a83906..c49a697 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 0000000..be23dfa --- /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 0000000..e8554b3 --- /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, +}; -- cgit v1.0