diff --git a/resources/vue/components/SearchWidget.vue b/resources/vue/components/SearchWidget.vue index 4b17820b0bb55299799140af747e237b340c7bbd..b583d834242786100bda06da5d7fe58df36cbc1c 100644 --- a/resources/vue/components/SearchWidget.vue +++ b/resources/vue/components/SearchWidget.vue @@ -5,7 +5,7 @@ <ul class="needles"> <li> <div class="input-group files-search"> - <input type="text" id="searchterm" name="searchterm" v-model="searchterm" + <input type="text" id="searchterm" name="searchterm" v-model.trim="searchterm" :placeholder="$gettext('Veranstaltung suchen')" :aria-label="$gettext('Veranstaltung suchen')"> <a v-if="isActive" @click.prevent="cancelSearch" class="reset-search"> @@ -13,7 +13,10 @@ </a> <button type="submit" class="submit-search" :title="$gettext('Suchen')" @click.prevent="doSearch"> - <studip-icon shape="search" :size="20"></studip-icon> + <studip-icon shape="search" + :role="maySearch ? 'clickable' : 'inactive'" + :size="20" + ></studip-icon> </button> </div> </li> @@ -33,17 +36,34 @@ export default { StudipIcon, SidebarWidget }, + props: { + minLength: { + type: Number, + default: 0, + } + }, data() { return { searchterm: '', isActive: false }; }, + computed: { + maySearch() { + return this.searchterm.length >= this.minLength; + } + }, methods: { doSearch() { + if (!this.maySearch) { + return; + } + if (this.searchterm !== '') { this.isActive = true; STUDIP.eventBus.emit('do-search', this.searchterm); + } else { + this.cancelSearch(); } }, cancelSearch() { diff --git a/resources/vue/components/tree/StudipTree.vue b/resources/vue/components/tree/StudipTree.vue index ff493739f9823a121fc00fd54550fab639070c5c..be57f16ddf116cbeaeded0c6def14d8d666ba1f8 100644 --- a/resources/vue/components/tree/StudipTree.vue +++ b/resources/vue/components/tree/StudipTree.vue @@ -31,7 +31,7 @@ <tree-search-result :search-config="searchConfig"></tree-search-result> </div> <MountingPortal v-if="withSearch" mountTo="#search-widget" name="sidebar-search"> - <search-widget></search-widget> + <search-widget :min-length="3"></search-widget> </MountingPortal> </div> </template> @@ -199,9 +199,12 @@ export default { axios.interceptors.request.eject(loadingIndicator); this.globalOn('do-search', searchterm => { - this.searchConfig.searchterm = searchterm; - this.searchConfig.semester = this.semester; - this.searchConfig.classname = this.startNode.attributes.classname; + this.searchConfig = { + searchterm, + semester: this.semester, + classname: this.startNode.attributes.classname, + startId: this.currentNode.id, + }; this.isSearching = true; }); diff --git a/resources/vue/components/tree/StudipTreeList.vue b/resources/vue/components/tree/StudipTreeList.vue index 0ba4b550224feaf1673af9140d6c890a2c2b4eb6..b607e02197a1bff868fe4a3ce73a0866055b7e56 100644 --- a/resources/vue/components/tree/StudipTreeList.vue +++ b/resources/vue/components/tree/StudipTreeList.vue @@ -99,7 +99,7 @@ </tr> </tbody> </table> - <MountingPortal v-if="withExport" 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('Veranstaltungen exportieren')" :url="exportUrl()" :export-data="courses"></tree-export-widget> @@ -202,6 +202,11 @@ export default { showingAllCourses: false } }, + computed: { + showExport() { + return this.withExport && document.getElementById('export-widget'); + } + }, methods: { openNode(node, pushState = true) { this.currentNode = node; diff --git a/resources/vue/components/tree/StudipTreeTable.vue b/resources/vue/components/tree/StudipTreeTable.vue index 0bfc244fb7554d8b48fdec1d034795a1d32a6226..585acd426046c1cda90ac862d5354bf105c36a96 100644 --- a/resources/vue/components/tree/StudipTreeTable.vue +++ b/resources/vue/components/tree/StudipTreeTable.vue @@ -115,7 +115,7 @@ </tr> </draggable> </table> - <MountingPortal v-if="withExport" 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()" :export-data="courses"></tree-export-widget> </MountingPortal> @@ -218,6 +218,11 @@ export default { showingAllCourses: false } }, + computed: { + showExport() { + return this.withExport && document.getElementById('export-widget'); + } + }, methods: { openNode(node, pushState = true) { this.currentNode = node; diff --git a/resources/vue/components/tree/TreeSearchResult.vue b/resources/vue/components/tree/TreeSearchResult.vue index 19bc6b927a4c56e57d76f6b6a0c59add4642bbab..c4e0f4155020220e1c1b86feebdd4f31d8855628 100644 --- a/resources/vue/components/tree/TreeSearchResult.vue +++ b/resources/vue/components/tree/TreeSearchResult.vue @@ -1,6 +1,8 @@ <template> <div v-if="isLoading"> - <studip-progress-indicator></studip-progress-indicator> + <studip-progress-indicator v-if="showLoadingAnimation" + :description="searchDescription" + ></studip-progress-indicator> </div> <article v-else class="studip-tree-table"> <table v-if="courses.length > 0" class="default studip-tree-table"> @@ -45,6 +47,10 @@ </tr> </tbody> </table> + <studip-message-box v-else type="info"> + {{ $gettextInterpolate($gettext('Es wurden keine Ergebnisse zu Ihrem Suchbegriff "%{term}" gefunden.'), + { term: searchConfig.searchterm }) }} + </studip-message-box> </article> </template> @@ -54,10 +60,11 @@ import StudipProgressIndicator from '../StudipProgressIndicator.vue'; import StudipIcon from '../StudipIcon.vue'; import TreeNodeCoursePath from './TreeNodeCoursePath.vue'; import TreeCourseDetails from './TreeCourseDetails.vue'; +import StudipMessageBox from "../StudipMessageBox.vue"; export default { name: 'TreeSearchResult', - components: { StudipIcon, StudipProgressIndicator, TreeNodeCoursePath, TreeCourseDetails }, + components: {StudipMessageBox, StudipIcon, StudipProgressIndicator, TreeNodeCoursePath, TreeCourseDetails }, mixins: [ TreeMixin ], props: { searchConfig: { @@ -67,19 +74,47 @@ export default { }, data() { return { + courses: [], + isLoading: true, node: null, - isLoading: false, - isLoaded: false, - courses: [] + showLoadingAnimation: false, + timeout: null, } }, - mounted() { - this.getNode(this.searchConfig.classname + '_root').then(response => { - this.getNodeCourses(response.data.data, this.searchConfig.semester,0, this.searchConfig.searchterm, true) - .then(courses => { - this.courses = courses.data.data; - }); - }); + computed: { + searchDescription() { + return this.$gettextInterpolate( + this.$gettext('Suche nach dem Begriff "%{ term }"'), + {term: this.searchConfig.searchterm} + ); + } + }, + watch: { + searchConfig: { + handler(current, previous) { + if (current.searchterm !== previous?.searchterm) { + this.isLoading = true; + + this.timeout = setTimeout( + () => this.showLoadingAnimation = true, + this.showProgressIndicatorTimeout + ); + + this.getNode(this.searchConfig.startId).then(response => { + return this.getNodeCourses(response.data.data, this.searchConfig.semester, 0, this.searchConfig.searchterm, true); + }).then(courses => { + this.courses = courses.data.data; + + clearTimeout(this.timeout); + + this.isLoading = false; + this.showLoadingAnimation = false; + }); + } + }, + immediate: true, + deep: true + } } } </script> diff --git a/resources/vue/mixins/TreeMixin.js b/resources/vue/mixins/TreeMixin.js index 1e72bbedfbc6815671b4f642873b36db2bf37d8f..9c4c859d7b092aba832c53186e52bb669b4c6a7b 100644 --- a/resources/vue/mixins/TreeMixin.js +++ b/resources/vue/mixins/TreeMixin.js @@ -37,7 +37,7 @@ export const TreeMixin = { parameters['filter[semclass]'] = semClass; } - if (recursive) { + if (node.attributes['has-children'] && recursive) { parameters['filter[recursive]'] = true; }