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 {
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);
}
}
}
};
......
......@@ -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 => {
......
......@@ -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;
......@@ -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;
})
......
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