From fd79564f43de846e34cfdb8eb5e434471fe47863 Mon Sep 17 00:00:00 2001
From: Thomas Hackl <hackl@data-quest.de>
Date: Tue, 26 Sep 2023 15:20:21 +0000
Subject: [PATCH] =?UTF-8?q?Resolve=20"Vorlesungsverzeichnis:=20Suche=20suc?=
 =?UTF-8?q?ht=20nicht=20(nur)=20im=20ausgew=C3=A4hlen=20Bereich"?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Closes #3041

Merge request studip/studip!2169
---
 resources/vue/components/SearchWidget.vue     | 24 +++++++-
 resources/vue/components/tree/StudipTree.vue  | 11 ++--
 .../vue/components/tree/StudipTreeList.vue    |  7 ++-
 .../vue/components/tree/StudipTreeTable.vue   |  7 ++-
 .../vue/components/tree/TreeSearchResult.vue  | 59 +++++++++++++++----
 resources/vue/mixins/TreeMixin.js             |  2 +-
 6 files changed, 89 insertions(+), 21 deletions(-)

diff --git a/resources/vue/components/SearchWidget.vue b/resources/vue/components/SearchWidget.vue
index 4b17820b0bb..b583d834242 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 ff493739f98..be57f16ddf1 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 0ba4b550224..b607e02197a 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 0bfc244fb75..585acd42604 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 19bc6b927a4..c4e0f415502 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 1e72bbedfbc..9c4c859d7b0 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;
             }
 
-- 
GitLab