diff --git a/resources/assets/stylesheets/less/pagination.less b/resources/assets/stylesheets/less/pagination.less index c6b3498b868bd966fa883420f105107b360b5de8..6e1bf9fff3d87f3a1c1649d792563c37dd827630 100644 --- a/resources/assets/stylesheets/less/pagination.less +++ b/resources/assets/stylesheets/less/pagination.less @@ -12,7 +12,9 @@ .pagination { li { display: inline-block; + } + li:not(.no-divider) { &:not(:first-of-type) { &::before { content: ' | '; @@ -57,3 +59,13 @@ .background-icon('arr_1right'); } } + +.pagination-wrapper-flex { + display: flex; + justify-content: space-between; + align-items: center; + .pagination { + margin-left: auto; + } +} + diff --git a/resources/vue/components/StudipPagination.vue b/resources/vue/components/StudipPagination.vue new file mode 100644 index 0000000000000000000000000000000000000000..8b82f989460b82149c1c4a20ba12925a607421ba --- /dev/null +++ b/resources/vue/components/StudipPagination.vue @@ -0,0 +1,101 @@ +<template> + <div class="pagination-wrapper-flex"> + <p :id="pagination_id" class="audible"> + {{ $gettext('Blättern') }} + </p> + <ul class="pagination" role="navigation" :aria-labelledby="pagination_id"> + <li class="prev" v-if="currentOffset > 0"> + <button class="pagination--link" @click.prevent="goBack" rel="prev" :title="$gettext('Zurück')"> + <span class="audible">{{ $gettext('Eine Seite') }}</span> + {{ $gettext('zurück') }} + </button> + </li> + <template v-for="offset of offsets"> + <li :key="'end-dots-' + offset" class="divider" + v-if="offset === (total_offsets - 1) && currentOffset < (total_offsets - 1) - (range + 1)"> + … + </li> + <li :key="'offset-' + offset" :class="{'current': offset === currentOffset, 'no-divider': offset === 0}"> + <button class="pagination--link" @click.prevent="goTo(offset)"> + <span class="audible">{{ $gettext('Seite') }}</span> + {{ offset + 1 }} + </button> + </li> + <li :key="'start-dots' + offset" class="divider" + v-if="offset === 0 && currentOffset > range + 1"> + … + </li> + </template> + <li class="next no-divider" v-if="currentOffset < total_offsets - 1"> + <button class="pagination--link" @click.prevent="goNext" rel="next" :title="$gettext('Weiter')"> + <span class="audible">{{ $gettext('Eine Seite') }}</span> + {{ $gettext('weiter') }} + </button> + </li> + </ul> + </div> +</template> + +<script> +export default { + name: 'studip-pagination', + props: { + currentOffset: { + type: Number, + required: true + }, + totalItems: { + type: Number, + required: true + }, + itemsPerPage: { + type: Number, + required: true + }, + range: { + type: Number, + default: 2, + min: 1 + } + }, + computed: { + pagination_id() { + return 'pagination-label-' + this._uid; + }, + total_offsets() { + let total = Math.ceil(this.totalItems / this.itemsPerPage); + return total; + }, + offsets() { + let offsets = [0, this.currentOffset, (this.total_offsets - 1)]; + for (let i = 1; i <= this.range; i++) { + offsets.push(this.currentOffset - i); + offsets.push(this.currentOffset + i); + } + offsets = offsets.map(item => parseInt(item, 10)); + offsets = [...new Set(offsets)]; + offsets = offsets.filter(item => item >= 0 && item < this.total_offsets); + offsets.sort((a, b) => a - b); + return offsets; + }, + + }, + methods: { + goBack() { + this.updateOffset(this.currentOffset - 1); + }, + goNext() { + this.updateOffset(this.currentOffset + 1); + }, + goTo(selected) { + if (selected === this.currentOffset) { + return; + } + this.updateOffset(selected); + }, + updateOffset(offset) { + this.$emit('updateOffset', parseInt(offset, 10)); + } + } +} +</script> diff --git a/resources/vue/components/courseware/CoursewareStructuralElementPermissions.vue b/resources/vue/components/courseware/CoursewareStructuralElementPermissions.vue index c061ba3763c7fa85ba3c8edcbcbfc9af1d3d0a1b..473b82f530ceb5a338602edc69a167899c7aaeb3 100644 --- a/resources/vue/components/courseware/CoursewareStructuralElementPermissions.vue +++ b/resources/vue/components/courseware/CoursewareStructuralElementPermissions.vue @@ -14,20 +14,24 @@ <translate>Studierende</translate> </caption> <colgroup> - <col style="width:20%" /> - <col style="width:35%" /> - <col style="width:45%" /> + <col style="width:1%" /> + <col style="width:19%" /> + <col style="width:1%" /> + <col style="width:29%" /> + <col style="width:50%" /> </colgroup> <thead> <tr> + <th><input type="checkbox" v-model="bulkSelectAutorRead" @click="handleBulkSelectRead($event, 'autor')"/></th> <th><translate>Lesen</translate></th> + <th><input type="checkbox" v-model="bulkSelectAutorWrite" @click="handleBulkSelectWrite($event)"/></th> <th><translate>Lesen und Schreiben</translate></th> <th><translate>Name</translate></th> </tr> </thead> <tbody> - <tr v-for="user in autor_members" :key="user.user_id"> - <td class="perm"> + <tr v-for="user in autor_members_filtered" :key="user.user_id"> + <td class="perm" colspan="2"> <input type="checkbox" :id="user.user_id + `_read`" @@ -35,7 +39,7 @@ v-model="userPermsReadUsers" /> </td> - <td class="perm"> + <td class="perm" colspan="2"> <input type="checkbox" :id="user.user_id + `_write`" @@ -52,6 +56,17 @@ </td> </tr> </tbody> + <tfoot v-if="can_paginate && autor_members.length > entries_per_page"> + <tr> + <td colspan="5"> + <studip-pagination + :currentOffset="autorOffset" + :totalItems="autor_members.length" + :itemsPerPage="entries_per_page" + @updateOffset="updateAutorOffset" /> + </td> + </tr> + </tfoot> </table> <table class="default" v-if="user_members.length"> @@ -59,22 +74,24 @@ <translate>Leser/-innen</translate> </caption> <colgroup> - <col style="width:55%" /> - <col style="width:45%" /> + <col style="width:1%" /> + <col style="width:39%" /> + <col style="width:50%" /> </colgroup> <thead> <tr> + <th><input type="checkbox" v-model="bulkSelectUserRead" @click="handleBulkSelectRead($event, 'user')"/></th> <th><translate>Lesen</translate></th> <th><translate>Name</translate></th> </tr> </thead> <tbody> - <tr v-for="user in user_members" :key="user.user_id"> - <td> + <tr v-for="user in user_members_filtered" :key="user.user_id"> + <td colspan="2"> <input type="checkbox" :id="user.user_id + `_read`" - :value="user.id" + :value="user.user_id" v-model="userPermsReadUsers" /> </td> @@ -87,6 +104,17 @@ </td> </tr> </tbody> + <tfoot v-if="can_paginate && user_members.length > entries_per_page"> + <tr> + <td colspan="3"> + <studip-pagination + :currentOffset="userOffset" + :totalItems="user_members.length" + :itemsPerPage="entries_per_page" + @updateOffset="updateUserOffset"/> + </td> + </tr> + </tfoot> </table> <table class="default" v-if="groups.length"> @@ -135,6 +163,8 @@ </div> </template> <script> +import StudipPagination from './../StudipPagination.vue'; + import { mapActions, mapGetters } from 'vuex'; export default { @@ -142,6 +172,9 @@ export default { props: { element: Object, }, + components: { + StudipPagination + }, data() { return { user_perms: {}, @@ -151,6 +184,11 @@ export default { userPermsWriteUsers: [], userPermsWriteGroups: [], userPermsWriteAll: Boolean, + bulkSelectAutorRead: false, + bulkSelectUserRead: false, + bulkSelectAutorWrite: false, + userOffset: 0, + autorOffset: 0, }; }, @@ -181,11 +219,21 @@ export default { // load memberships for coursewares in a course context if (this.context.type === 'courses') { const parent = { type: 'courses', id: this.context.id }; - this.loadCourseMemberships({ parent, relationship: 'memberships', options: { include: 'user' } }); + let options = { + include: 'user', + 'page[limit]': 10000, + } + this.loadCourseMemberships({ parent, relationship: 'memberships', options: options }); this.loadCourseStatusGroups({ parent, relationship: 'status-groups' }); } }, + updated () { + this.handleBulkSelectReadPassive('autor'); + this.handleBulkSelectReadPassive('user'); + this.handleBulkSelectWritePassive(); + }, + computed: { ...mapGetters({ context: 'context', @@ -240,6 +288,15 @@ export default { return members; }, + autor_members_filtered() { + if (this.autor_members.length === 0) { + return []; + } + let start = this.autorOffset * this.entries_per_page; + let end = ((this.autorOffset + 1) * this.entries_per_page); + return this.autor_members.slice(start, end); + }, + user_members() { if (Object.keys(this.users).length === 0 && this.users.constructor === Object) { return []; @@ -252,6 +309,23 @@ export default { return members; }, + user_members_filtered() { + if (this.user_members.length === 0) { + return []; + } + let start = this.userOffset * this.entries_per_page; + let end = ((this.userOffset + 1) * this.entries_per_page); + return this.user_members.slice(start, end); + }, + + entries_per_page() { + return STUDIP?.config?.ENTRIES_PER_PAGE ?? 0; + }, + + can_paginate() { + return this.entries_per_page > 0; + }, + readApproval() { return { all: this.userPermsReadAll, @@ -274,10 +348,93 @@ export default { loadCourseMemberships: 'course-memberships/loadRelated', loadCourseStatusGroups: 'status-groups/loadRelated', }), + + updateAutorOffset(offset) { + this.autorOffset = parseInt(offset); + }, + + updateUserOffset(offset) { + this.userOffset = parseInt(offset); + }, + + handleBulkSelectRead(event, type) { + let state = event.target.checked; + let list = type === 'autor' ? this.autor_members_filtered : this.user_members_filtered; + if (list.length == 0) { + return; + } + if (type === 'autor') { + this.bulkSelectAutorRead = state; + } else { + this.bulkSelectUserRead = state; + } + for (let user of list) { + if (state) { // Add + if (this.userPermsReadUsers.includes(user.user_id) === false) { + this.userPermsReadUsers.push(user.user_id); + } + } else { // Remove + if (this.userPermsReadUsers.includes(user.user_id) === true) { + let index = this.userPermsReadUsers.findIndex((perm) => perm == user.user_id); + this.userPermsReadUsers.splice(index, 1); + } + } + } + }, + + handleBulkSelectWrite(event) { + let state = event.target.checked; + let list = this.autor_members_filtered; + if (list.length == 0) { + return; + } + this.bulkSelectAutorWrite = state; + for (let user of list) { + if (state) { // Add + if (this.userPermsWriteUsers.includes(user.user_id) === false) { + this.userPermsWriteUsers.push(user.user_id); + } + } else { // Remove + if (this.userPermsWriteUsers.includes(user.user_id) === true) { + let index = this.userPermsWriteUsers.findIndex((perm) => perm == user.user_id); + this.userPermsWriteUsers.splice(index, 1); + } + } + } + }, + + handleBulkSelectReadPassive(type) { + let bulkState = false; + if (type === 'autor' && this.autor_members_filtered?.length > 0) { + let currentAutorsIds = this.autor_members_filtered.map((autor) => autor.user_id); + if (currentAutorsIds.every((id) => this.userPermsReadUsers.includes(id))) { + bulkState = true; + } + this.bulkSelectAutorRead = bulkState; + } + if (type === 'user' && this.user_members_filtered?.length > 0) { + let currentUsersIds = this.user_members_filtered.map((user) => user.user_id); + if (currentUsersIds.every((id) => this.userPermsReadUsers.includes(id))) { + bulkState = true; + } + this.bulkSelectUserRead = bulkState; + } + }, + + handleBulkSelectWritePassive() { + let bulkState = false; + let currentAutorsIds = this.autor_members_filtered.map((autor) => autor.user_id); + if (currentAutorsIds.every((id) => this.userPermsWriteUsers.includes(id))) { + bulkState = true; + } + this.bulkSelectAutorWrite = bulkState; + } }, watch: { userPermsReadUsers(newVal, oldVal) { + this.handleBulkSelectReadPassive('autor'); + this.handleBulkSelectReadPassive('user'); this.$emit('updateReadApproval', this.readApproval); }, userPermsReadGroups(newVal, oldVal) { @@ -290,6 +447,7 @@ export default { } }, userPermsWriteUsers(newVal, oldVal) { + this.handleBulkSelectWritePassive(); this.$emit('updateWriteApproval', this.writeApproval); }, userPermsWriteGroups(newVal, oldVal) { @@ -301,6 +459,13 @@ export default { this.userPermsReadAll = false; } }, + autorOffset(newVal, oldVal) { + this.handleBulkSelectReadPassive('autor'); + this.handleBulkSelectWritePassive(); + }, + userOffset(newVal, oldVal) { + this.handleBulkSelectReadPassive('user'); + } }, }; </script>