Skip to content
Snippets Groups Projects
Commit 80d2966c authored by Timothee P's avatar Timothee P :sunflower:
Browse files

use default attribute filter at load & use cancel search to avoid multiple response asynchronously

parent 685f97c4
No related branches found
No related tags found
1 merge request!756REDMINE_ISSUE-19722 | Créer des filtres pour les attributs projet sur l'accueil de l'application
...@@ -55,6 +55,10 @@ export default { ...@@ -55,6 +55,10 @@ export default {
currentSelection: { currentSelection: {
type: [String, Array, Boolean], type: [String, Array, Boolean],
default: null, default: null,
},
defaultFilter: {
type: [String, Array, Boolean],
default: null,
} }
}, },
...@@ -69,38 +73,31 @@ export default { ...@@ -69,38 +73,31 @@ export default {
deep: true, deep: true,
handler(newValue) { handler(newValue) {
if (!newValue) { if (!newValue) {
// TODO: use current selection if defined
this.selection = this.options[0]; this.selection = this.options[0];
this.$emit('filter', this.selection); 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. currentSelection(newValue) {
this.selection = normalizedValues.map(value => this.updateSelection(newValue);
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);
}
}, },
}, },
created() { created() {
this.selection = this.options[0]; 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: { methods: {
...@@ -123,6 +120,29 @@ export default { ...@@ -123,6 +120,29 @@ export default {
normalizeValues(value) { normalizeValues(value) {
// If the value is a string and contains commas, split it into an array; otherwise, wrap it in an array. // 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; 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);
}
} }
} }
}; };
......
...@@ -59,7 +59,7 @@ ...@@ -59,7 +59,7 @@
<!-- (create a computed seperating into groups to loop over) --> <!-- (create a computed seperating into groups to loop over) -->
<div class="ui menu filters"> <div class="ui menu filters">
<div <div
v-for="attribute in displayedAttributes" v-for="attribute in displayedAttributeFilters"
:key="attribute.id" :key="attribute.id"
class="item" class="item"
> >
...@@ -70,6 +70,7 @@ ...@@ -70,6 +70,7 @@
:options="attribute.options" :options="attribute.options"
:multiple="attribute.field_type === 'multi_choices_list'" :multiple="attribute.field_type === 'multi_choices_list'"
:current-selection="attributesFilter[attribute.id]" :current-selection="attributesFilter[attribute.id]"
:default-filter="attribute.default_filter_enabled ? attribute.default_filter_value : null"
@filter="updateAttributeFilter" @filter="updateAttributeFilter"
@remove="removeAttributeFilter" @remove="removeAttributeFilter"
/> />
...@@ -180,7 +181,7 @@ export default { ...@@ -180,7 +181,7 @@ export default {
* *
* @returns {Array} An array of project attributes with modified options for display. * @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 // Filter out attributes that should not be displayed based on `display_filter` flag
return this.projectAttributes.filter(attribute => attribute.display_filter) return this.projectAttributes.filter(attribute => attribute.display_filter)
.map(attribute => { .map(attribute => {
......
...@@ -8,6 +8,48 @@ const initialFilters = { ...@@ -8,6 +8,48 @@ const initialFilters = {
accessible: null 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 = { const projectsStore = {
namespaced: true, namespaced: true,
...@@ -78,9 +120,18 @@ const projectsStore = { ...@@ -78,9 +120,18 @@ const projectsStore = {
return; 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) { async SEARCH_PROJECTS({ commit, dispatch }, text) {
if (text) { if (text) {
await dispatch('HANDLE_PROJECTS_SEARCH_REQUEST', text); await dispatch('HANDLE_PROJECTS_SEARCH_REQUEST', { text });
} else { } else {
commit('SET_PROJECTS_SEARCH_STATE', { commit('SET_PROJECTS_SEARCH_STATE', {
isSearched: false, isSearched: false,
...@@ -122,47 +173,45 @@ const projectsStore = { ...@@ -122,47 +173,45 @@ const projectsStore = {
}); });
}, },
async HANDLE_PROJECTS_SEARCH_REQUEST({ state, rootState, commit }, text) { /**
* Asynchronously handles the search request for projects, incorporating search text and applied filters.
if (rootState.cancellableSearchRequest.length > 0) { * Cancels any ongoing search request to ensure that only the latest request is processed,
const currentRequestCancelToken = * which enhances the responsiveness of search functionality.
rootState.cancellableSearchRequest[rootState.cancellableSearchRequest.length - 1]; *
currentRequestCancelToken.cancel(); * @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(); const cancelToken = axios.CancelToken.source();
commit('SET_CANCELLABLE_SEARCH_REQUEST', cancelToken, { root: true }); commit('SET_CANCELLABLE_SEARCH_REQUEST', cancelToken, { root: true });
const url = `${rootState.configuration.VUE_APP_DJANGO_API_BASE}v2/projects/?search=${text}`; // Construct the search URL with any applied filters.
let filteredUrl; const searchUrl = constructSearchUrl({
if (Object.values(state.filters).some(el => el && el.length > 0)) { baseUrl: rootState.configuration.VUE_APP_DJANGO_API_BASE,
filteredUrl = url; filters: state.filters,
for (const filter in state.filters) { text,
if (state.filters[filter]) { page
filteredUrl = filteredUrl.concat('', `&${filter}=${state.filters[filter]}`); });
}
}
}
const response = await axios.get( try {
filteredUrl ? filteredUrl : url, // Perform the search request.
{ const response = await axios.get(searchUrl, { cancelToken: cancelToken.token });
cancelToken: cancelToken.token,
} // Process successful response.
); if (response.status === 200 && response.data) {
if (response.status === 200) { commit('SET_PROJECTS', response.data);
const projects = response.data; commit('SET_PROJECTS_SEARCH_STATE', { isSearched: true, text });
if (projects) {
commit('SET_PROJECTS', projects);
commit('SET_PROJECTS_SEARCH_STATE', {
isSearched: true,
text: text
});
} }
} catch (error) {
// Handle potential errors, such as request cancellation.
console.error('Search request canceled or failed', error);
} }
}, }
} }
}; };
export default projectsStore; export default projectsStore;
...@@ -145,12 +145,12 @@ export default { ...@@ -145,12 +145,12 @@ export default {
'SET_PROJECTS_FILTER' 'SET_PROJECTS_FILTER'
]), ]),
...mapActions('projects', [ ...mapActions('projects', [
'GET_PROJECTS' 'FILTER_PROJECTS'
]), ]),
getData(page) { getData(page) {
this.loading = true; this.loading = true;
this.GET_PROJECTS({ page }) this.FILTER_PROJECTS({ page })
.then(() => { .then(() => {
this.loading = false; this.loading = false;
}) })
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment