From 80d2966c070d862e458d55cbe0073cac28b6e1df Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Timoth=C3=A9e=20Poussard?= <tpoussard@neogeo.fr>
Date: Fri, 23 Feb 2024 10:58:44 +0100
Subject: [PATCH] use default attribute filter at load & use cancel search to
 avoid multiple response asynchronously

---
 src/components/Projects/DropdownMenuItem.vue |  62 ++++++----
 src/components/Projects/ProjectsMenu.vue     |   5 +-
 src/store/modules/projects.store.js          | 119 +++++++++++++------
 src/views/Projects/ProjectsList.vue          |   4 +-
 4 files changed, 130 insertions(+), 60 deletions(-)

diff --git a/src/components/Projects/DropdownMenuItem.vue b/src/components/Projects/DropdownMenuItem.vue
index 30f9f63f..358b3c8a 100644
--- a/src/components/Projects/DropdownMenuItem.vue
+++ b/src/components/Projects/DropdownMenuItem.vue
@@ -55,6 +55,10 @@ export default {
     currentSelection: {
       type: [String, Array, Boolean],
       default: null,
+    },
+    defaultFilter: {
+      type: [String, Array, Boolean],
+      default: null,
     }
   },
 
@@ -69,38 +73,31 @@ export default {
       deep: true,
       handler(newValue) {
         if (!newValue) {
-          // TODO: use current selection if defined
           this.selection = this.options[0];
           this.$emit('filter', this.selection);
         }
       }
     },
-    /**
-     * Updates the current selection based on new value, ensuring compatibility with multiselect.
-     * This method processes the new selection value, accommodating both single and multiple selections,
-     * and updates the internal `selection` state with the corresponding option objects from `options`.
-     *
-     * @param {String|Array} newValue - The new selection value(s), can be a string or an array of strings.
-     */
-    currentSelection(newValue) {
-      // Check if the component is in multiple selection mode and the new value is provided.
-      if (this.multiple && newValue) {
-        // Normalize the newValue to an array format, accommodating both single and comma-separated values.
-        const normalizedValues = this.normalizeValues(newValue);
 
-        // Map each value to its corresponding option object based on the 'value' field.
-        this.selection = normalizedValues.map(value => 
-          this.options.find(option => option.value === value)
-        );
-      } else {
-        // For single selection mode or null value, find the option object that matches the newValue.
-        this.selection = this.options.find(option => option.value === newValue);
-      }
+    currentSelection(newValue) {
+      this.updateSelection(newValue);
     },
   },
 
   created() {
     this.selection = this.options[0];
+    if (this.defaultFilter) {
+      const selectFilter = (filter) => this.select(this.options.find(option => option.value === filter));
+      // Specific process if multiple values type and has more than one values
+      if (this.multiple && this.defaultFilter.includes(',')) {
+        // make an array from the string
+        const filtersArray = this.defaultFilter.split(',');
+        // for each value update the filter
+        filtersArray.forEach(val => selectFilter(val));
+      } else { // Process for single value
+        selectFilter(this.defaultFilter);
+      }
+    }
   },
 
   methods: {
@@ -123,6 +120,29 @@ export default {
     normalizeValues(value) {
       // If the value is a string and contains commas, split it into an array; otherwise, wrap it in an array.
       return typeof value === 'string' ? (value.includes(',') ? value.split(',') : [value]) : value;
+    },
+
+    /**
+     * Updates the current selection based on new value, ensuring compatibility with multiselect.
+     * This method processes the new selection value, accommodating both single and multiple selections,
+     * and updates the internal `selection` state with the corresponding option objects from `options`.
+     *
+     * @param {String|Array} value - The new selection value(s), can be a string or an array of strings.
+     */
+    // Check if the component is in multiple selection mode and the new value is provided.
+    updateSelection(value) {
+      if (this.multiple && value) {
+        // Normalize the value to an array format, accommodating both single and comma-separated values.
+        const normalizedValues = this.normalizeValues(value);
+
+        // Map each value to its corresponding option object based on the 'value' field.
+        this.selection = normalizedValues.map(value => 
+          this.options.find(option => option.value === value)
+        );
+      } else {
+        // For single selection mode or null value, find the option object that matches the value.
+        this.selection = this.options.find(option => option.value === value);
+      }
     }
   }
 };
diff --git a/src/components/Projects/ProjectsMenu.vue b/src/components/Projects/ProjectsMenu.vue
index b9858ec0..48ce7732 100644
--- a/src/components/Projects/ProjectsMenu.vue
+++ b/src/components/Projects/ProjectsMenu.vue
@@ -59,7 +59,7 @@
       <!-- (create a computed seperating into groups to loop over) -->
       <div class="ui menu filters">
         <div
-          v-for="attribute in displayedAttributes"
+          v-for="attribute in displayedAttributeFilters"
           :key="attribute.id"
           class="item"
         >
@@ -70,6 +70,7 @@
             :options="attribute.options"
             :multiple="attribute.field_type === 'multi_choices_list'"
             :current-selection="attributesFilter[attribute.id]"
+            :default-filter="attribute.default_filter_enabled ? attribute.default_filter_value : null"
             @filter="updateAttributeFilter"
             @remove="removeAttributeFilter"
           />
@@ -180,7 +181,7 @@ export default {
      *
      * @returns {Array} An array of project attributes with modified options for display.
      */
-    displayedAttributes() {
+    displayedAttributeFilters() {
       // Filter out attributes that should not be displayed based on `display_filter` flag
       return this.projectAttributes.filter(attribute => attribute.display_filter)
         .map(attribute => {
diff --git a/src/store/modules/projects.store.js b/src/store/modules/projects.store.js
index e5810c11..48daf338 100644
--- a/src/store/modules/projects.store.js
+++ b/src/store/modules/projects.store.js
@@ -8,6 +8,48 @@ const initialFilters = {
   accessible: null
 };
 
+
+/**
+ * Cancels the most recent search request if it exists.
+ *
+ * @param {Object} rootState - The root state of the Vuex store to access global states.
+ */
+function cancelPreviousSearchRequest(rootState) {
+  if (rootState.cancellableSearchRequest.length > 0) {
+    const lastRequestToken = rootState.cancellableSearchRequest.pop();
+    lastRequestToken.cancel('New search initiated.');
+  }
+}
+
+/**
+ * Constructs the URL for the search request, appending search text and any active filters.
+ *
+ * @param {Object} rootState - The root state to access global configuration settings.
+ * @param {Object} filters - The current state of filters applied to the search.
+ * @param {String} text - The current search text.
+ * @returns {String} The fully constructed URL for the search request.
+ */
+function constructSearchUrl({ baseUrl, filters, text, page }) {
+  let url = `${baseUrl}v2/projects/?`;
+  // Append page number if provided.
+  if (page) {
+    url += `page=${page}`;
+  }
+  // Append search text if provided.
+  if (text) {
+    url += `&search=${encodeURIComponent(text)}`;
+  }
+
+  // Append each active filter to the URL.
+  Object.entries(filters).forEach(([key, value]) => {
+    if (value) {
+      url += `&${key}=${encodeURIComponent(value)}`;
+    }
+  });
+
+  return url;
+}
+
 const projectsStore = {
 
   namespaced: true,
@@ -78,9 +120,18 @@ const projectsStore = {
       return;
     },
 
+    async FILTER_PROJECTS({ state, dispatch }, { page }) {
+      if (!page) {
+        page = state.currentPage;
+      }
+      console.log('page', page);
+
+      await dispatch('HANDLE_PROJECTS_SEARCH_REQUEST', { page });
+    },
+
     async SEARCH_PROJECTS({ commit, dispatch }, text) {
       if (text) {
-        await dispatch('HANDLE_PROJECTS_SEARCH_REQUEST', text);
+        await dispatch('HANDLE_PROJECTS_SEARCH_REQUEST', { text });
       } else {
         commit('SET_PROJECTS_SEARCH_STATE', {
           isSearched: false,
@@ -122,47 +173,45 @@ const projectsStore = {
         });
     },
 
-    async HANDLE_PROJECTS_SEARCH_REQUEST({ state, rootState, commit }, text) {
-
-      if (rootState.cancellableSearchRequest.length > 0) {
-        const currentRequestCancelToken =
-          rootState.cancellableSearchRequest[rootState.cancellableSearchRequest.length - 1];
-        currentRequestCancelToken.cancel();
-      }
-
+    /**
+     * Asynchronously handles the search request for projects, incorporating search text and applied filters.
+     * Cancels any ongoing search request to ensure that only the latest request is processed,
+     * which enhances the responsiveness of search functionality.
+     *
+     * @param {Object} context - Destructured to gain access to Vuex state, rootState, and commit function.
+     * @param {String} text - The search text used for filtering projects.
+     */
+    async HANDLE_PROJECTS_SEARCH_REQUEST({ state, rootState, commit }, { text, page }) {
+      // Cancel any ongoing search request.
+      cancelPreviousSearchRequest(rootState);
+
+      // Prepare the cancel token for the new request and store it.
       const cancelToken = axios.CancelToken.source();
       commit('SET_CANCELLABLE_SEARCH_REQUEST', cancelToken, { root: true });
 
-      const url = `${rootState.configuration.VUE_APP_DJANGO_API_BASE}v2/projects/?search=${text}`;
-      let filteredUrl;
-      if (Object.values(state.filters).some(el => el && el.length > 0)) {
-        filteredUrl = url;
-        for (const filter in state.filters) {
-          if (state.filters[filter]) {
-            filteredUrl = filteredUrl.concat('', `&${filter}=${state.filters[filter]}`);
-          }
-        }
-      }
+      // Construct the search URL with any applied filters.
+      const searchUrl = constructSearchUrl({
+        baseUrl: rootState.configuration.VUE_APP_DJANGO_API_BASE,
+        filters: state.filters,
+        text,
+        page
+      });
 
-      const response = await axios.get(
-        filteredUrl ? filteredUrl : url,
-        {
-          cancelToken: cancelToken.token,
-        }
-      );
-      if (response.status === 200) {
-        const projects = response.data;
-        if (projects) {
-          commit('SET_PROJECTS', projects);
-          commit('SET_PROJECTS_SEARCH_STATE', {
-            isSearched: true,
-            text: text
-          });
+      try {
+        // Perform the search request.
+        const response = await axios.get(searchUrl, { cancelToken: cancelToken.token });
+
+        // Process successful response.
+        if (response.status === 200 && response.data) {
+          commit('SET_PROJECTS', response.data);
+          commit('SET_PROJECTS_SEARCH_STATE', { isSearched: true, text });
         }
+      } catch (error) {
+        // Handle potential errors, such as request cancellation.
+        console.error('Search request canceled or failed', error);
       }
-    },
+    }
   }
-
 };
 
 export default projectsStore;
diff --git a/src/views/Projects/ProjectsList.vue b/src/views/Projects/ProjectsList.vue
index 618350fe..9682c581 100644
--- a/src/views/Projects/ProjectsList.vue
+++ b/src/views/Projects/ProjectsList.vue
@@ -145,12 +145,12 @@ export default {
       'SET_PROJECTS_FILTER'
     ]),
     ...mapActions('projects', [
-      'GET_PROJECTS'
+      'FILTER_PROJECTS'
     ]),
 
     getData(page) {
       this.loading = true;
-      this.GET_PROJECTS({ page })
+      this.FILTER_PROJECTS({ page })
         .then(() => {
           this.loading = false;
         })
-- 
GitLab