Skip to content
Snippets Groups Projects
Commit f39148d2 authored by Thomas Hackl's avatar Thomas Hackl
Browse files

Resolve "VVZ: Text läuft aus Kachel"

Closes #3574, #3638, and #3639

Merge request studip/studip!2521
parent ce27c274
No related branches found
No related tags found
No related merge requests found
Showing
with 156 additions and 56 deletions
...@@ -28,6 +28,10 @@ class Search_CoursesController extends AuthenticatedController ...@@ -28,6 +28,10 @@ class Search_CoursesController extends AuthenticatedController
PageLayout::setHelpKeyword('Basis.VeranstaltungenAbonnieren'); PageLayout::setHelpKeyword('Basis.VeranstaltungenAbonnieren');
$this->type = Request::option('type', 'semtree'); $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->semester = Request::option('semester', Semester::findCurrent()->id);
$this->semClass = Request::int('semclass', 0); $this->semClass = Request::int('semclass', 0);
$this->search = Request::get('search', ''); $this->search = Request::get('search', '');
...@@ -107,5 +111,16 @@ class Search_CoursesController extends AuthenticatedController ...@@ -107,5 +111,16 @@ class Search_CoursesController extends AuthenticatedController
$sidebar->addWidget(new VueWidget('search-widget')); $sidebar->addWidget(new VueWidget('search-widget'));
$sidebar->addWidget(new VueWidget('export-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);
} }
} }
<?php <?php
/** /**
* @var String $startId * @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> <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) ?>" title="<?= htmlReady($treeTitle) ?>" breadcrumb-icon="<?= htmlReady($breadcrumbIcon) ?>"
:with-search="true" :with-export="true" :with-courses="true" semester="<?= htmlReady($semester) ?>" :with-search="true" :with-export="true" :with-courses="true" semester="<?= htmlReady($semester) ?>"
:sem-class="<?= htmlReady($semClass) ?>" :with-export="true"></studip-tree> :sem-class="<?= htmlReady($semClass) ?>" :with-export="true"></studip-tree>
......
...@@ -32,8 +32,7 @@ class CourseinfoOfTreeNode extends NonJsonApiController ...@@ -32,8 +32,7 @@ class CourseinfoOfTreeNode extends NonJsonApiController
$filters = $this->getContextFilters($request); $filters = $this->getContextFilters($request);
$info = [ $info = [
'courses' => (int) $node->countCourses($filters['semester'], $filters['semclass']), 'courses' => (int) $node->countCourses($filters['semester'], $filters['semclass'], true)
'allCourses' => (int) $node->countCourses($filters['semester'], $filters['semclass'], true)
]; ];
$response->getBody()->write(json_encode($info)); $response->getBody()->write(json_encode($info));
......
...@@ -494,7 +494,7 @@ class StudipStudyArea extends SimpleORMap implements StudipTreeNode ...@@ -494,7 +494,7 @@ class StudipStudyArea extends SimpleORMap implements StudipTreeNode
OR sc.`semester_id` IS NULL OR sc.`semester_id` IS NULL
)"; )";
$parameters = [ $parameters = [
'ids' => $with_children ? $this->getDescendantIds() : [$this->id], 'ids' => $with_children ? array_merge([$this->id], $this->getDescendantIds()) : [$this->id],
'semester' => $semester_id 'semester' => $semester_id
]; ];
} else { } else {
...@@ -502,7 +502,7 @@ class StudipStudyArea extends SimpleORMap implements StudipTreeNode ...@@ -502,7 +502,7 @@ class StudipStudyArea extends SimpleORMap implements StudipTreeNode
FROM `seminar_sem_tree` t FROM `seminar_sem_tree` t
JOIN `seminare` s ON (s.`Seminar_id` = t.`seminar_id`) JOIN `seminare` s ON (s.`Seminar_id` = t.`seminar_id`)
WHERE `sem_tree_id` IN (:ids)"; 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)) { if (!$GLOBALS['perm']->have_perm(Config::get()->SEM_VISIBILITY_PERM)) {
......
.studip-loading-skeleton {
background-color: var(--light-gray-color-20);
height: 1em;
width: 100%;
}
...@@ -271,8 +271,8 @@ $tree-outline: 1px solid var(--light-gray-color-40); ...@@ -271,8 +271,8 @@ $tree-outline: 1px solid var(--light-gray-color-40);
background: var(--dark-gray-color-5); background: var(--dark-gray-color-5);
border: solid thin var(--light-gray-color-40); border: solid thin var(--light-gray-color-40);
display: flex; display: flex;
height: 130px; min-height: 130px;
padding: 10px; padding: 5px 10px;
/* Handle for drag&drop */ /* Handle for drag&drop */
.drag-handle { .drag-handle {
...@@ -284,11 +284,17 @@ $tree-outline: 1px solid var(--light-gray-color-40); ...@@ -284,11 +284,17 @@ $tree-outline: 1px solid var(--light-gray-color-40);
flex-direction: column; flex-direction: column;
padding: 10px; padding: 10px;
text-align: left; text-align: left;
width: 100%;
.studip-tree-child-title { .studip-tree-child-title {
font-size: 1.1em; font-size: 1.1em;
font-weight: bold; font-weight: bold;
} }
.studip-tree-child-description {
color: var(--black);
font-size: 0.9em;
}
} }
&:hover { &:hover {
......
...@@ -57,6 +57,7 @@ ...@@ -57,6 +57,7 @@
@import "scss/globalsearch"; @import "scss/globalsearch";
@import "scss/links"; @import "scss/links";
@import "scss/lists"; @import "scss/lists";
@import "scss/loading-skeleton.scss";
@import "scss/messages"; @import "scss/messages";
@import "scss/my_courses"; @import "scss/my_courses";
@import "scss/mvv"; @import "scss/mvv";
......
<template>
<div class="studip-loading-skeleton"></div>
</template>
<script>
export default {
name: 'StudipLoadingSkeleton'
}
</script>
...@@ -62,8 +62,8 @@ ...@@ -62,8 +62,8 @@
</span> </span>
<span v-if="withCourses && subLevelsCourses > 0 && !showingAllCourses"> <span v-if="withCourses && subLevelsCourses > 0 && !showingAllCourses">
<button type="button" @click="showAllCourses(true)" <button type="button" @click="showAllCourses(true)"
:title="$gettext('Veranstaltungen auf allen Unterebenen anzeigen')"> :title="$gettext('Veranstaltungen auf Unterebenen anzeigen')">
{{ $gettext('Veranstaltungen auf allen Unterebenen anzeigen') }} {{ $gettext('Veranstaltungen auf Unterebenen anzeigen') }}
</button> </button>
</span> </span>
</section> </section>
...@@ -74,6 +74,15 @@ ...@@ -74,6 +74,15 @@
<col> <col>
</colgroup> </colgroup>
<thead> <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> <tr>
<th>{{ $gettext('Name') }}</th> <th>{{ $gettext('Name') }}</th>
<th>{{ $gettext('Information') }}</th> <th>{{ $gettext('Information') }}</th>
...@@ -98,6 +107,17 @@ ...@@ -98,6 +107,17 @@
</td> </td>
</tr> </tr>
</tbody> </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> </table>
<MountingPortal v-if="showExport" mountTo="#export-widget" name="sidebar-export"> <MountingPortal v-if="showExport" mountTo="#export-widget" name="sidebar-export">
<tree-export-widget v-if="courses.length > 0" <tree-export-widget v-if="courses.length > 0"
...@@ -118,13 +138,14 @@ import TreeBreadcrumb from './TreeBreadcrumb.vue'; ...@@ -118,13 +138,14 @@ import TreeBreadcrumb from './TreeBreadcrumb.vue';
import TreeNodeTile from './TreeNodeTile.vue'; import TreeNodeTile from './TreeNodeTile.vue';
import StudipProgressIndicator from '../StudipProgressIndicator.vue'; import StudipProgressIndicator from '../StudipProgressIndicator.vue';
import TreeCourseDetails from './TreeCourseDetails.vue'; import TreeCourseDetails from './TreeCourseDetails.vue';
import AssignLinkWidget from "./AssignLinkWidget.vue"; import AssignLinkWidget from './AssignLinkWidget.vue';
import StudipPagination from '../StudipPagination.vue';
export default { export default {
name: 'StudipTreeList', name: 'StudipTreeList',
components: { components: {
draggable, StudipProgressIndicator, TreeExportWidget, TreeBreadcrumb, TreeNodeTile, TreeCourseDetails, draggable, StudipProgressIndicator, TreeExportWidget, TreeBreadcrumb, TreeNodeTile, TreeCourseDetails,
AssignLinkWidget AssignLinkWidget, StudipPagination
}, },
mixins: [ TreeMixin ], mixins: [ TreeMixin ],
props: { props: {
...@@ -225,8 +246,10 @@ export default { ...@@ -225,8 +246,10 @@ export default {
}); });
if (this.withCourses) { if (this.withCourses) {
this.getNodeCourses(node, this.semester, this.semClass, '', false) this.getNodeCourses(node, this.offset, this.semester, this.semClass, '', false)
.then(courses => { .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.courses = courses.data.data;
}); });
} }
...@@ -288,8 +311,10 @@ export default { ...@@ -288,8 +311,10 @@ export default {
} }
}, },
showAllCourses(state) { showAllCourses(state) {
this.getNodeCourses(this.currentNode, this.semester, this.semClass, '', state) this.getNodeCourses(this.currentNode, this.offset, this.semester, this.semClass, '', state)
.then(courses => { .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.courses = courses.data.data;
this.showingAllCourses = state; this.showingAllCourses = state;
}); });
...@@ -309,8 +334,10 @@ export default { ...@@ -309,8 +334,10 @@ export default {
}); });
if (this.withCourses) { if (this.withCourses) {
this.getNodeCourses(this.currentNode, this.semester, this.semClass) this.getNodeCourses(this.currentNode, 0, this.semester, this.semClass)
.then(courses => { .then(courses => {
this.totalCourseCount = courses.data.meta.page.total;
this.offset = 0;
this.courses = courses.data.data; this.courses = courses.data.data;
}); });
} }
......
...@@ -175,7 +175,7 @@ export default { ...@@ -175,7 +175,7 @@ export default {
} }
if (ids.length > 0) { if (ids.length > 0) {
this.getNodeCourses(this.node, 'all', 0, '', false, ids) this.getNodeCourses(this.node, 0, 'all', 0, '', false, ids)
.then(response => { .then(response => {
// None of the given courses are assigned here. // None of the given courses are assigned here.
if (response.data.data.length === 0) { if (response.data.data.length === 0) {
......
...@@ -35,8 +35,8 @@ ...@@ -35,8 +35,8 @@
</template> </template>
<span v-if="withCourses && subLevelsCourses > 0 && !showingAllCourses"> <span v-if="withCourses && subLevelsCourses > 0 && !showingAllCourses">
<button type="button" @click="showAllCourses(true)" <button type="button" @click="showAllCourses(true)"
:title="$gettext('Veranstaltungen auf allen Unterebenen anzeigen')"> :title="$gettext('Veranstaltungen auf Unterebenen anzeigen')">
{{ $gettext('Veranstaltungen auf allen Unterebenen anzeigen') }} {{ $gettext('Veranstaltungen auf Unterebenen anzeigen') }}
</button> </button>
</span> </span>
</section> </section>
...@@ -57,6 +57,15 @@ ...@@ -57,6 +57,15 @@
<col style="width: 40%"> <col style="width: 40%">
</colgroup> </colgroup>
<thead> <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> <tr>
<th></th> <th></th>
<th>{{ $gettext('Typ') }}</th> <th>{{ $gettext('Typ') }}</th>
...@@ -89,8 +98,11 @@ ...@@ -89,8 +98,11 @@
</a> </a>
</td> </td>
<td> <td>
<tree-node-course-info :node="child" :semester="semester" <tree-node-course-info v-if="node.attributes.ancestors.length > 2"
:sem-class="semClass"></tree-node-course-info> :node="child"
:semester="semester"
:sem-class="semClass"
></tree-node-course-info>
</td> </td>
</tr> </tr>
<tr v-for="(course) in courses" :key="course.id" class="studip-tree-child studip-tree-course"> <tr v-for="(course) in courses" :key="course.id" class="studip-tree-child studip-tree-course">
...@@ -114,6 +126,17 @@ ...@@ -114,6 +126,17 @@
</td> </td>
</tr> </tr>
</draggable> </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> </table>
<MountingPortal v-if="showExport" mountTo="#export-widget" name="sidebar-export"> <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()" <tree-export-widget v-if="courses.length > 0" :title="$gettext('Download des Ergebnisses')" :url="exportUrl()"
...@@ -242,8 +265,10 @@ export default { ...@@ -242,8 +265,10 @@ export default {
if (this.withCourses) { if (this.withCourses) {
this.getNodeCourses(node, this.semester, this.semClass, '', false) this.getNodeCourses(node, this.offset, this.semester, this.semClass, '', false)
.then(response => { .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; this.courses = response.data.data;
}); });
} }
...@@ -305,8 +330,10 @@ export default { ...@@ -305,8 +330,10 @@ export default {
} }
}, },
showAllCourses(state) { showAllCourses(state) {
this.getNodeCourses(this.currentNode, this.semester, this.semClass, '', state) this.getNodeCourses(this.currentNode, this.offset, this.semester, this.semClass, '', state)
.then(courses => { .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.courses = courses.data.data;
this.showingAllCourses = state; this.showingAllCourses = state;
}); });
...@@ -326,8 +353,10 @@ export default { ...@@ -326,8 +353,10 @@ export default {
}); });
if (this.withCourses) { if (this.withCourses) {
this.getNodeCourses(this.currentNode, this.semester, this.semClass) this.getNodeCourses(this.currentNode, 0, this.semester, this.semClass)
.then(courses => { .then(courses => {
this.totalCourseCount = courses.data.meta.page.total;
this.offset = 0;
this.courses = courses.data.data; this.courses = courses.data.data;
}); });
} }
......
<template> <template>
<div class="studip-tree-child-description"> <div class="studip-tree-child-description">
<template v-if="showingAllCourses"> <studip-loading-skeleton v-if="isLoading" />
<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>
<div v-else v-translate="{ count: courseCount }" :translate-n="courseCount" <div v-else v-translate="{ count: courseCount }" :translate-n="courseCount"
translate-plural="<strong>%{count}</strong> Veranstaltungen auf dieser Ebene."> translate-plural="<strong>%{count}</strong> Veranstaltungen">
<strong>Eine</strong> Veranstaltung auf dieser Ebene. <strong>Eine</strong> Veranstaltung
</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.
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import { TreeMixin } from '../../mixins/TreeMixin'; import { TreeMixin } from '../../mixins/TreeMixin';
import StudipLoadingSkeleton from '../StudipLoadingSkeleton.vue';
export default { export default {
name: 'TreeNodeCourseInfo', name: 'TreeNodeCourseInfo',
components: { StudipLoadingSkeleton },
mixins: [ TreeMixin ], mixins: [ TreeMixin ],
props: { props: {
node: { node: {
...@@ -45,8 +32,8 @@ export default { ...@@ -45,8 +32,8 @@ export default {
}, },
data() { data() {
return { return {
isLoading: false,
courseCount: 0, courseCount: 0,
allCourseCount: 0,
showingAllCourses: false showingAllCourses: false
} }
}, },
...@@ -54,22 +41,22 @@ export default { ...@@ -54,22 +41,22 @@ export default {
showAllCourses(state) { showAllCourses(state) {
this.showingAllCourses = state; this.showingAllCourses = state;
this.$emit('showAllCourses', 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() { mounted() {
this.getNodeCourseInfo(this.node, this.semester, this.semClass) this.loadNodeInfo(this.node);
.then(info => {
this.courseCount = info?.data.courses;
this.allCourseCount = info?.data.allCourses;
});
}, },
watch: { watch: {
node(newNode) { node(newNode) {
this.getNodeCourseInfo(newNode, this.semester, this.semClass) this.loadNodeInfo(newNode);
.then(info => {
this.courseCount = info?.data.courses;
this.allCourseCount = info?.data.allCourses;
});
} }
} }
} }
......
...@@ -5,8 +5,11 @@ ...@@ -5,8 +5,11 @@
{{ node.attributes.name }} {{ node.attributes.name }}
</p> </p>
<tree-node-course-info :node="node" :semester="semester" <tree-node-course-info v-if="node.attributes.ancestors.length > 2"
:sem-class="semClass"></tree-node-course-info> :node="node"
:semester="semester"
:sem-class="semClass"
></tree-node-course-info>
</a> </a>
</template> </template>
......
...@@ -3,7 +3,10 @@ import axios from 'axios'; ...@@ -3,7 +3,10 @@ import axios from 'axios';
export const TreeMixin = { export const TreeMixin = {
data() { data() {
return { return {
showProgressIndicatorTimeout: 500 showProgressIndicatorTimeout: 500,
totalCourseCount: 0,
offset: 0,
limit: 50
}; };
}, },
methods: { methods: {
...@@ -22,9 +25,12 @@ export const TreeMixin = { ...@@ -22,9 +25,12 @@ export const TreeMixin = {
{ params: parameters } { 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 = {}; let parameters = {};
parameters['page[offset]'] = offset * this.limit;
parameters['page[limit]'] = this.limit;
if (semesterId !== 'all' && semesterId !== '0') { if (semesterId !== 'all' && semesterId !== '0') {
parameters['filter[semester]'] = semesterId; parameters['filter[semester]'] = semesterId;
} }
...@@ -103,6 +109,15 @@ export const TreeMixin = { ...@@ -103,6 +109,15 @@ export const TreeMixin = {
{ headers: { 'Content-Type': 'multipart/form-data' }} { headers: { 'Content-Type': 'multipart/form-data' }}
); );
STUDIP.Vue.emit('sort-tree-children', { parent: parentId, children: children }); 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;
});
} }
} }
} }
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment