From f39148d2a5525ccf018322168480b4a5d816b6f9 Mon Sep 17 00:00:00 2001 From: Thomas Hackl <hackl@data-quest.de> Date: Thu, 18 Jan 2024 08:33:21 +0000 Subject: [PATCH] =?UTF-8?q?Resolve=20"VVZ:=20Text=20l=C3=A4uft=20aus=20Kac?= =?UTF-8?q?hel"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #3574, #3638, and #3639 Merge request studip/studip!2521 --- app/controllers/search/courses.php | 15 +++++++ app/views/search/courses/index.php | 8 +++- .../Routes/Tree/CourseInfoOfTreeNode.php | 3 +- lib/models/StudipStudyArea.class.php | 4 +- .../stylesheets/scss/loading-skeleton.scss | 5 +++ resources/assets/stylesheets/scss/tree.scss | 10 ++++- resources/assets/stylesheets/studip.scss | 1 + .../vue/components/StudipLoadingSkeleton.vue | 9 ++++ .../vue/components/tree/StudipTreeList.vue | 41 ++++++++++++++--- .../vue/components/tree/StudipTreeNode.vue | 2 +- .../vue/components/tree/StudipTreeTable.vue | 43 +++++++++++++++--- .../components/tree/TreeNodeCourseInfo.vue | 45 +++++++------------ .../vue/components/tree/TreeNodeTile.vue | 7 ++- resources/vue/mixins/TreeMixin.js | 19 +++++++- 14 files changed, 156 insertions(+), 56 deletions(-) create mode 100644 resources/assets/stylesheets/scss/loading-skeleton.scss create mode 100644 resources/vue/components/StudipLoadingSkeleton.vue diff --git a/app/controllers/search/courses.php b/app/controllers/search/courses.php index 50a348066fa..9e0b45d6e31 100644 --- a/app/controllers/search/courses.php +++ b/app/controllers/search/courses.php @@ -28,6 +28,10 @@ class Search_CoursesController extends AuthenticatedController PageLayout::setHelpKeyword('Basis.VeranstaltungenAbonnieren'); $this->type = Request::option('type', 'semtree'); + $this->show_as = Request::option('show_as', 'list'); + if (!in_array($this->show_as, ['list', 'table'])) { + $this->show_as = 'list'; + } $this->semester = Request::option('semester', Semester::findCurrent()->id); $this->semClass = Request::int('semclass', 0); $this->search = Request::get('search', ''); @@ -107,5 +111,16 @@ class Search_CoursesController extends AuthenticatedController $sidebar->addWidget(new VueWidget('search-widget')); $sidebar->addWidget(new VueWidget('export-widget')); + + $views = new ViewsWidget(); + $views->addLink( + _('Als Liste'), + $this->url_for('search/courses', array_merge($params, ['show_as' => 'list'])) + )->setActive($this->show_as === 'list'); + $views->addLink( + _('Als Tabelle'), + $this->url_for('search/courses', array_merge($params, ['show_as' => 'table'])) + )->setActive($this->show_as === 'table'); + $sidebar->addWidget($views); } } diff --git a/app/views/search/courses/index.php b/app/views/search/courses/index.php index c6f49050601..500c31e7f69 100644 --- a/app/views/search/courses/index.php +++ b/app/views/search/courses/index.php @@ -1,11 +1,15 @@ <?php /** * @var String $startId - * @var String $nodeClass + * @var String $show_as + * @var String $treeTitle + * @var String $breadcrumIcon + * @var String $semester + * @var String $semClass */ ?> <div data-studip-tree> - <studip-tree start-id="<?= htmlReady($startId) ?>" view-type="list" :visible-children-only="true" + <studip-tree start-id="<?= htmlReady($startId) ?>" view-type="<?= htmlReady($show_as) ?>" :visible-children-only="true" title="<?= htmlReady($treeTitle) ?>" breadcrumb-icon="<?= htmlReady($breadcrumbIcon) ?>" :with-search="true" :with-export="true" :with-courses="true" semester="<?= htmlReady($semester) ?>" :sem-class="<?= htmlReady($semClass) ?>" :with-export="true"></studip-tree> diff --git a/lib/classes/JsonApi/Routes/Tree/CourseInfoOfTreeNode.php b/lib/classes/JsonApi/Routes/Tree/CourseInfoOfTreeNode.php index 283593150cf..e684c40301b 100644 --- a/lib/classes/JsonApi/Routes/Tree/CourseInfoOfTreeNode.php +++ b/lib/classes/JsonApi/Routes/Tree/CourseInfoOfTreeNode.php @@ -32,8 +32,7 @@ class CourseinfoOfTreeNode extends NonJsonApiController $filters = $this->getContextFilters($request); $info = [ - 'courses' => (int) $node->countCourses($filters['semester'], $filters['semclass']), - 'allCourses' => (int) $node->countCourses($filters['semester'], $filters['semclass'], true) + 'courses' => (int) $node->countCourses($filters['semester'], $filters['semclass'], true) ]; $response->getBody()->write(json_encode($info)); diff --git a/lib/models/StudipStudyArea.class.php b/lib/models/StudipStudyArea.class.php index 444ca212912..2d13b186eff 100644 --- a/lib/models/StudipStudyArea.class.php +++ b/lib/models/StudipStudyArea.class.php @@ -494,7 +494,7 @@ class StudipStudyArea extends SimpleORMap implements StudipTreeNode OR sc.`semester_id` IS NULL )"; $parameters = [ - 'ids' => $with_children ? $this->getDescendantIds() : [$this->id], + 'ids' => $with_children ? array_merge([$this->id], $this->getDescendantIds()) : [$this->id], 'semester' => $semester_id ]; } else { @@ -502,7 +502,7 @@ class StudipStudyArea extends SimpleORMap implements StudipTreeNode FROM `seminar_sem_tree` t JOIN `seminare` s ON (s.`Seminar_id` = t.`seminar_id`) WHERE `sem_tree_id` IN (:ids)"; - $parameters = ['ids' => $with_children ? $this->getDescendantIds() : [$this->id]]; + $parameters = ['ids' => $with_children ? array_merge([$this->id], $this->getDescendantIds()) : [$this->id]]; } if (!$GLOBALS['perm']->have_perm(Config::get()->SEM_VISIBILITY_PERM)) { diff --git a/resources/assets/stylesheets/scss/loading-skeleton.scss b/resources/assets/stylesheets/scss/loading-skeleton.scss new file mode 100644 index 00000000000..480255fe502 --- /dev/null +++ b/resources/assets/stylesheets/scss/loading-skeleton.scss @@ -0,0 +1,5 @@ +.studip-loading-skeleton { + background-color: var(--light-gray-color-20); + height: 1em; + width: 100%; +} diff --git a/resources/assets/stylesheets/scss/tree.scss b/resources/assets/stylesheets/scss/tree.scss index dbad68b1bc5..1d700f92eb5 100644 --- a/resources/assets/stylesheets/scss/tree.scss +++ b/resources/assets/stylesheets/scss/tree.scss @@ -271,8 +271,8 @@ $tree-outline: 1px solid var(--light-gray-color-40); background: var(--dark-gray-color-5); border: solid thin var(--light-gray-color-40); display: flex; - height: 130px; - padding: 10px; + min-height: 130px; + padding: 5px 10px; /* Handle for drag&drop */ .drag-handle { @@ -284,11 +284,17 @@ $tree-outline: 1px solid var(--light-gray-color-40); flex-direction: column; padding: 10px; text-align: left; + width: 100%; .studip-tree-child-title { font-size: 1.1em; font-weight: bold; } + + .studip-tree-child-description { + color: var(--black); + font-size: 0.9em; + } } &:hover { diff --git a/resources/assets/stylesheets/studip.scss b/resources/assets/stylesheets/studip.scss index e45cdb3f63f..b0e768ea172 100644 --- a/resources/assets/stylesheets/studip.scss +++ b/resources/assets/stylesheets/studip.scss @@ -57,6 +57,7 @@ @import "scss/globalsearch"; @import "scss/links"; @import "scss/lists"; +@import "scss/loading-skeleton.scss"; @import "scss/messages"; @import "scss/my_courses"; @import "scss/mvv"; diff --git a/resources/vue/components/StudipLoadingSkeleton.vue b/resources/vue/components/StudipLoadingSkeleton.vue new file mode 100644 index 00000000000..90b97339703 --- /dev/null +++ b/resources/vue/components/StudipLoadingSkeleton.vue @@ -0,0 +1,9 @@ +<template> + <div class="studip-loading-skeleton"></div> +</template> + +<script> +export default { + name: 'StudipLoadingSkeleton' +} +</script> diff --git a/resources/vue/components/tree/StudipTreeList.vue b/resources/vue/components/tree/StudipTreeList.vue index b607e02197a..4ba08bcbb81 100644 --- a/resources/vue/components/tree/StudipTreeList.vue +++ b/resources/vue/components/tree/StudipTreeList.vue @@ -62,8 +62,8 @@ </span> <span v-if="withCourses && subLevelsCourses > 0 && !showingAllCourses"> <button type="button" @click="showAllCourses(true)" - :title="$gettext('Veranstaltungen auf allen Unterebenen anzeigen')"> - {{ $gettext('Veranstaltungen auf allen Unterebenen anzeigen') }} + :title="$gettext('Veranstaltungen auf Unterebenen anzeigen')"> + {{ $gettext('Veranstaltungen auf Unterebenen anzeigen') }} </button> </span> </section> @@ -74,6 +74,15 @@ <col> </colgroup> <thead> + <tr v-if="totalCourseCount > limit"> + <td colspan="2"> + <studip-pagination :items-per-page="limit" + :total-items="totalCourseCount" + :current-offset="offset" + @updateOffset="updateOffset" + /> + </td> + </tr> <tr> <th>{{ $gettext('Name') }}</th> <th>{{ $gettext('Information') }}</th> @@ -98,6 +107,17 @@ </td> </tr> </tbody> + <tfoot v-if="totalCourseCount > limit"> + <tr> + <td colspan="2"> + <studip-pagination :items-per-page="limit" + :total-items="totalCourseCount" + :current-offset="offset" + @updateOffset="updateOffset" + /> + </td> + </tr> + </tfoot> </table> <MountingPortal v-if="showExport" mountTo="#export-widget" name="sidebar-export"> <tree-export-widget v-if="courses.length > 0" @@ -118,13 +138,14 @@ import TreeBreadcrumb from './TreeBreadcrumb.vue'; import TreeNodeTile from './TreeNodeTile.vue'; import StudipProgressIndicator from '../StudipProgressIndicator.vue'; import TreeCourseDetails from './TreeCourseDetails.vue'; -import AssignLinkWidget from "./AssignLinkWidget.vue"; +import AssignLinkWidget from './AssignLinkWidget.vue'; +import StudipPagination from '../StudipPagination.vue'; export default { name: 'StudipTreeList', components: { draggable, StudipProgressIndicator, TreeExportWidget, TreeBreadcrumb, TreeNodeTile, TreeCourseDetails, - AssignLinkWidget + AssignLinkWidget, StudipPagination }, mixins: [ TreeMixin ], props: { @@ -225,8 +246,10 @@ export default { }); if (this.withCourses) { - this.getNodeCourses(node, this.semester, this.semClass, '', false) + this.getNodeCourses(node, this.offset, this.semester, this.semClass, '', false) .then(courses => { + this.totalCourseCount = courses.data.meta.page.total; + this.offset = Math.ceil(courses.data.meta.page.offset / this.limit); this.courses = courses.data.data; }); } @@ -288,8 +311,10 @@ export default { } }, showAllCourses(state) { - this.getNodeCourses(this.currentNode, this.semester, this.semClass, '', state) + this.getNodeCourses(this.currentNode, this.offset, this.semester, this.semClass, '', state) .then(courses => { + this.totalCourseCount = courses.data.meta.page.total; + this.offset = Math.ceil(courses.data.meta.page.offset / this.limit); this.courses = courses.data.data; this.showingAllCourses = state; }); @@ -309,8 +334,10 @@ export default { }); if (this.withCourses) { - this.getNodeCourses(this.currentNode, this.semester, this.semClass) + this.getNodeCourses(this.currentNode, 0, this.semester, this.semClass) .then(courses => { + this.totalCourseCount = courses.data.meta.page.total; + this.offset = 0; this.courses = courses.data.data; }); } diff --git a/resources/vue/components/tree/StudipTreeNode.vue b/resources/vue/components/tree/StudipTreeNode.vue index 39f696ccc14..2b5676e24a3 100644 --- a/resources/vue/components/tree/StudipTreeNode.vue +++ b/resources/vue/components/tree/StudipTreeNode.vue @@ -175,7 +175,7 @@ export default { } if (ids.length > 0) { - this.getNodeCourses(this.node, 'all', 0, '', false, ids) + this.getNodeCourses(this.node, 0, 'all', 0, '', false, ids) .then(response => { // None of the given courses are assigned here. if (response.data.data.length === 0) { diff --git a/resources/vue/components/tree/StudipTreeTable.vue b/resources/vue/components/tree/StudipTreeTable.vue index 585acd42604..03a9d7b60e9 100644 --- a/resources/vue/components/tree/StudipTreeTable.vue +++ b/resources/vue/components/tree/StudipTreeTable.vue @@ -35,8 +35,8 @@ </template> <span v-if="withCourses && subLevelsCourses > 0 && !showingAllCourses"> <button type="button" @click="showAllCourses(true)" - :title="$gettext('Veranstaltungen auf allen Unterebenen anzeigen')"> - {{ $gettext('Veranstaltungen auf allen Unterebenen anzeigen') }} + :title="$gettext('Veranstaltungen auf Unterebenen anzeigen')"> + {{ $gettext('Veranstaltungen auf Unterebenen anzeigen') }} </button> </span> </section> @@ -57,6 +57,15 @@ <col style="width: 40%"> </colgroup> <thead> + <tr v-if="totalCourseCount > limit"> + <td colspan="4"> + <studip-pagination :items-per-page="limit" + :total-items="totalCourseCount" + :current-offset="offset" + @updateOffset="updateOffset" + /> + </td> + </tr> <tr> <th></th> <th>{{ $gettext('Typ') }}</th> @@ -89,8 +98,11 @@ </a> </td> <td> - <tree-node-course-info :node="child" :semester="semester" - :sem-class="semClass"></tree-node-course-info> + <tree-node-course-info v-if="node.attributes.ancestors.length > 2" + :node="child" + :semester="semester" + :sem-class="semClass" + ></tree-node-course-info> </td> </tr> <tr v-for="(course) in courses" :key="course.id" class="studip-tree-child studip-tree-course"> @@ -114,6 +126,17 @@ </td> </tr> </draggable> + <tfoot v-if="totalCourseCount > limit"> + <tr> + <td colspan="4"> + <studip-pagination :items-per-page="limit" + :total-items="totalCourseCount" + :current-offset="offset" + @updateOffset="updateOffset" + /> + </td> + </tr> + </tfoot> </table> <MountingPortal v-if="showExport" mountTo="#export-widget" name="sidebar-export"> <tree-export-widget v-if="courses.length > 0" :title="$gettext('Download des Ergebnisses')" :url="exportUrl()" @@ -242,8 +265,10 @@ export default { if (this.withCourses) { - this.getNodeCourses(node, this.semester, this.semClass, '', false) + this.getNodeCourses(node, this.offset, this.semester, this.semClass, '', false) .then(response => { + this.totalCourseCount = response.data.meta.page.total; + this.offset = Math.ceil(response.data.meta.page.offset / this.limit); this.courses = response.data.data; }); } @@ -305,8 +330,10 @@ export default { } }, showAllCourses(state) { - this.getNodeCourses(this.currentNode, this.semester, this.semClass, '', state) + this.getNodeCourses(this.currentNode, this.offset, this.semester, this.semClass, '', state) .then(courses => { + this.totalCourseCount = courses.data.meta.page.total; + this.offset = Math.ceil(courses.data.meta.page.offset / this.limit); this.courses = courses.data.data; this.showingAllCourses = state; }); @@ -326,8 +353,10 @@ export default { }); if (this.withCourses) { - this.getNodeCourses(this.currentNode, this.semester, this.semClass) + this.getNodeCourses(this.currentNode, 0, this.semester, this.semClass) .then(courses => { + this.totalCourseCount = courses.data.meta.page.total; + this.offset = 0; this.courses = courses.data.data; }); } diff --git a/resources/vue/components/tree/TreeNodeCourseInfo.vue b/resources/vue/components/tree/TreeNodeCourseInfo.vue index db31d148c3f..3f3777c9338 100644 --- a/resources/vue/components/tree/TreeNodeCourseInfo.vue +++ b/resources/vue/components/tree/TreeNodeCourseInfo.vue @@ -1,33 +1,20 @@ <template> <div class="studip-tree-child-description"> - <template v-if="showingAllCourses"> - <div v-translate="{ count: courseCount }" :translate-n="courseCount" - translate-plural="<strong>%{count}</strong> Veranstaltungen auf dieser Ebene."> - <strong>Eine</strong> Veranstaltung auf dieser Ebene. - </div> - </template> + <studip-loading-skeleton v-if="isLoading" /> <div v-else v-translate="{ count: courseCount }" :translate-n="courseCount" - translate-plural="<strong>%{count}</strong> Veranstaltungen auf dieser Ebene."> - <strong>Eine</strong> Veranstaltung auf dieser Ebene. - </div> - <template v-if="!showingAllCourses"> - <div v-translate="{ count: allCourseCount }" :translate-n="allCourseCount" - translate-plural="<strong>%{count}</strong> Veranstaltungen auf allen Unterebenen."> - <strong>Eine</strong> Veranstaltung auf allen Unterebenen. - </div> - </template> - <div v-else v-translate="{ count: allCourseCount }" :translate-n="allCourseCount" - translate-plural="<strong>%{count}</strong> Veranstaltungen auf allen Unterebenen."> - <strong>Eine</strong> Veranstaltung auf allen Unterebenen. + translate-plural="<strong>%{count}</strong> Veranstaltungen"> + <strong>Eine</strong> Veranstaltung </div> </div> </template> <script> import { TreeMixin } from '../../mixins/TreeMixin'; +import StudipLoadingSkeleton from '../StudipLoadingSkeleton.vue'; export default { name: 'TreeNodeCourseInfo', + components: { StudipLoadingSkeleton }, mixins: [ TreeMixin ], props: { node: { @@ -45,8 +32,8 @@ export default { }, data() { return { + isLoading: false, courseCount: 0, - allCourseCount: 0, showingAllCourses: false } }, @@ -54,22 +41,22 @@ export default { showAllCourses(state) { this.showingAllCourses = state; this.$emit('showAllCourses', state); + }, + loadNodeInfo(node) { + this.isLoading = true; + this.getNodeCourseInfo(node, this.semester, this.semClass) + .then(info => { + this.courseCount = info?.data.courses; + this.isLoading = false; + }); } }, mounted() { - this.getNodeCourseInfo(this.node, this.semester, this.semClass) - .then(info => { - this.courseCount = info?.data.courses; - this.allCourseCount = info?.data.allCourses; - }); + this.loadNodeInfo(this.node); }, watch: { node(newNode) { - this.getNodeCourseInfo(newNode, this.semester, this.semClass) - .then(info => { - this.courseCount = info?.data.courses; - this.allCourseCount = info?.data.allCourses; - }); + this.loadNodeInfo(newNode); } } } diff --git a/resources/vue/components/tree/TreeNodeTile.vue b/resources/vue/components/tree/TreeNodeTile.vue index cbb0679eaef..dab2bdf9ff6 100644 --- a/resources/vue/components/tree/TreeNodeTile.vue +++ b/resources/vue/components/tree/TreeNodeTile.vue @@ -5,8 +5,11 @@ {{ node.attributes.name }} </p> - <tree-node-course-info :node="node" :semester="semester" - :sem-class="semClass"></tree-node-course-info> + <tree-node-course-info v-if="node.attributes.ancestors.length > 2" + :node="node" + :semester="semester" + :sem-class="semClass" + ></tree-node-course-info> </a> </template> diff --git a/resources/vue/mixins/TreeMixin.js b/resources/vue/mixins/TreeMixin.js index 9c4c859d7b0..4263eaf938d 100644 --- a/resources/vue/mixins/TreeMixin.js +++ b/resources/vue/mixins/TreeMixin.js @@ -3,7 +3,10 @@ import axios from 'axios'; export const TreeMixin = { data() { return { - showProgressIndicatorTimeout: 500 + showProgressIndicatorTimeout: 500, + totalCourseCount: 0, + offset: 0, + limit: 50 }; }, methods: { @@ -22,9 +25,12 @@ export const TreeMixin = { { params: parameters } ); }, - async getNodeCourses(node, semesterId = 'all', semClass = 0, searchterm = '', recursive = false, ids = []) { + async getNodeCourses(node, offset, semesterId = 'all', semClass = 0, searchterm = '', recursive = false, ids = []) { let parameters = {}; + parameters['page[offset]'] = offset * this.limit; + parameters['page[limit]'] = this.limit; + if (semesterId !== 'all' && semesterId !== '0') { parameters['filter[semester]'] = semesterId; } @@ -103,6 +109,15 @@ export const TreeMixin = { { headers: { 'Content-Type': 'multipart/form-data' }} ); STUDIP.Vue.emit('sort-tree-children', { parent: parentId, children: children }); + }, + updateOffset(newOffset) { + this.getNodeCourses(this.currentNode, newOffset, this.semester, this.semClass, '', this.showingAllCourses) + .then(courses => { + this.courseCount = courses.data.meta.page.total; + this.currentOffset = courses.data.meta.page.offset; + this.offset = newOffset; + this.courses = courses.data.data; + }); } } } -- GitLab