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