Newer
Older

Timothee P
committed
v-if="chunkedNsortedFilters.length > 0"
id="filters-container"
<div
class="ui styled accordion"
@click="displayFilters = !displayFilters"
>
:class="['ui icon customcaret', { 'collapsed': !displayFilters }]"
<div :class="['full-width', 'filters', { 'hidden': displayFilters }]">
v-for="(chunkedFilters, index) in chunkedNsortedFilters"
:key="index"
class="ui menu filter-row"
>
<div
v-for="filter in chunkedFilters"
:key="filter.name"
class="item"
>
<label>
{{ filter.label }}
</label>
<search-projects
v-if="filter.name === 'search'"
v-on="$listeners"
/>
<DropdownMenuItem
v-else-if="!filter.id"
:options="filter.options"

Timothee P
committed
:loading="loading"
v-on="$listeners"
/>
<DropdownMenuItem
v-else
:options="filter.options"

Timothee P
committed
:loading="loading"
:multiple="isMultiple(filter)"
:current-selection="attributesFilter[filter.id]"
:default-filter="filter.default_filter_enabled ? filter.default_filter_value : null"
@filter="updateAttributeFilter"
@remove="removeAttributeFilter"
/>
</div>
import { mapState, mapMutations } from 'vuex';
import DropdownMenuItem from '@/components/Projects/DropdownMenuItem.vue';
import SearchProjects from '@/components/Projects/SearchProjects.vue';

Timothee P
committed
props: {
loading: {
type: Boolean,
default: false
},
},
displayFilters: false,
name: 'access_level',
label: 'Niveau d\'autorisation requis',
options: [
{
label: 'Utilisateur anonyme',
value: 'anonymous'
},
{
label: 'Utilisateur connecté',
value: 'logged_user'
},
{
label: 'Contributeur',
value: 'contributor'
},
],
name: 'user_access_level',
label: 'Mon niveau d\'autorisation',
options: [
{
label: 'Utilisateur connecté',
value: '1'
},
{
label: 'Contributeur',
value: '2'
},
{
label: 'Super contributeur',
value: '3'
},
{
label: 'Modérateur',
value: '4'
},
{
label: 'Administrateur projet',
value: '5'
},
],
name: 'moderation',
label: 'Modération',
options: [
{
label: 'Projet modéré',
value: 'true'
},
{
label: 'Projet non modéré',
value: 'false'
},
]
name: 'search',
label: 'Recherche par nom',
}
],
attributesFilter: {},
...mapState([
'user',
'configuration',
'projectAttributes'
]),
...mapState('projects', [
'filters',
]),
/**
* Processes project filters to prepare them for display.
* It also adds a global 'Tous' (All) option to each attribute's options for filtering purposes.
*
* @returns {Array} An array of filter objects with modified options for display.
*/
displayedClassicFilters() {
if (!this.configuration.VUE_APP_PROJECT_FILTERS) return [];
const projectFilters = this.configuration.VUE_APP_PROJECT_FILTERS.split(',');
// Filter filters to be displayed according to configuration and process filters
return this.classicFilters.filter(filter => projectFilters.includes(filter.name))
.map(filter => {
if (filter.options) {
// if user is not connected display its user access level corresponding to anonymous user
if (!this.user && filter.name ==='user_access_level') {
filter.options.unshift({
label: 'Utilisateur anonyme',
value: '0'
});
}
// Format the options to be displayed by dropdowns
const options = this.generateFilterOptions(filter);
// Add the global option at beginning
options.unshift({
label: 'Tous',
filter: filter.name,
value: null,
});
return { ...filter, options };
} else { // Search input field doesn't take options
return filter;
}
});
},
/**
* Processes project attributes to prepare them for display, adjusting the options based on the attribute type.
* For boolean attributes, it creates specific options for true and false values.
* It also adds a global 'Tous' (All) option to each attribute's options for filtering purposes.
* Finally, it chunks the array of attributes into multiple arrays, each containing up to 4 elements.
*
* @returns {Array} An array of arrays, where each sub-array contains up to 4 project attributes with modified options for display.
*/

Timothee P
committed
displayedAttributeFilters() {

Timothee P
committed
// Filter displayed filters & filter only attribute of boolean type (no need for option property) or list type with options
return this.projectAttributes.filter(attribute => attribute.display_filter && (attribute.field_type === 'boolean' || attribute.options))
// Process attributes for display
.map(attribute => {
// Format the options to be displayed by dropdowns
const options = this.generateFilterOptions(attribute);
// Add the global option at beginning
options.unshift({
label: 'Tous',
filter: attribute.id,
value: null,
});
return { ...attribute, options };
});
},
/**
* Merge all filters and place the search filters at the end of the array
* Then chunks the array into rows of 4 filters to display each chunk in a row
*/
chunkedNsortedFilters() {
const allFilters = [...this.displayedClassicFilters, ...this.displayedAttributeFilters];
const sortedFilters = [
...allFilters.filter(el => el.name !== 'search'),
...allFilters.filter(el => el.name === 'search'),
];
// Chunk the filters into arrays of up to 4 elements
return this.chunkArray(sortedFilters, 4);
},
created() {
// parse all project attributes to find default value and set filters in store before updating project list results
for (const attribFilter of this.displayedAttributeFilters) {
this.setDefaultFilters(attribFilter);
}
// When all the default filters are set, fetch projects list data
this.$emit('getData');
},
...mapMutations('projects', [
'SET_PROJECTS_FILTER'
]),
/**
* Helper function to chunk an array into smaller arrays of a specified size.
*
* @param {Array} array - The original array to be chunked.
* @param {Number} size - The maximum size of each chunk.
* @returns {Array} An array of chunked arrays.
*/
chunkArray(array, size) {
const chunkedArr = [];
for (let i = 0; i < array.length; i += size) {
chunkedArr.push(array.slice(i, i + size));
}
return chunkedArr;
},
/**
* Generates options for a given filter.
* It handles boolean attributes specially by creating explicit true/false options.
* Other attribute types use their predefined options.
*
* @param {Object} attribute - The project attribute for which to generate options.
* @returns {Array} An array of options for the given attribute.
*/
generateFilterOptions(filter) {
// Handle boolean attributes specially by creating true/false options
if (filter.field_type === 'boolean') {
return [
{ filter: filter.id, label: 'Oui', value: 'true' },
{ filter: filter.id, label: 'Non', value: 'false' },
];

Timothee P
committed
} else if (filter.options) {
// For other filter types, map each option to the expected format
return filter.options.map(option => ({
filter: filter.id || filter.name,

Timothee P
committed
label: option.name || option.label || option,
value: option.id || option.value || option,

Timothee P
committed
}));
}

Timothee P
committed
return [];
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
},
/**
* Retrieves a project attribute by its ID.
* Returns an empty object if not found to prevent errors from undefined access.
*
* @param {Number|String} id - The ID of the attribute to find.
* @returns {Object} The found attribute or an empty object.
*/
getProjectAttribute(id) {
// Search for the attribute by ID, default to an empty object if not found
return this.projectAttributes.find(el => el.id === id) || {};
},
/**
* Emits an updated filter event with the current state of attributesFilter.
* This method serializes the attributesFilter object to a JSON string and emits it,
* allowing the parent component to update the query parameters.
*/
emitUpdatedFilter() {
// Emit an 'filter' event with the updated attributes filter as a JSON string
this.$emit('filter', { filter: 'attributes', value: JSON.stringify(this.attributesFilter) });
},
/**
* Updates or adds a new attribute value to the attributesFilter.
* Handles both single-choice and multi-choice attribute types.
* @param {Object} newFilter - The new filter to be added, containing the attribute key and value.
*/
updateAttributeFilter({ filter, value, noUpdate }) {
// Retrieve the attribute type information to determine how to handle the update
const attribute = this.getProjectAttribute(filter);
// Check if the attribute allows multiple selections
const isMultiChoice = attribute.field_type.includes('list');
if (isMultiChoice) {
// For multi-choice attributes, manage the values as an array to allow multiple selections
let arrayValue = this.attributesFilter[filter] ? this.attributesFilter[filter].split(',') : [];
if (value) {
// If a value is provided, add it to the array, ensuring no duplicates and removing null corresponding to "Tous" default option
arrayValue.push(value);
arrayValue = [...new Set(arrayValue)].filter(el => el !== null);
// Convert the array back to a comma-separated string to store in the filter object
this.attributesFilter[filter] = arrayValue.join(',');
} else {
// If null value is provided "Tous" is selected, it indicates removal of the attribute filter
delete this.attributesFilter[filter];
}
} else {
// For single-choice attributes, directly set or delete the value
value ? this.attributesFilter[filter] = value : delete this.attributesFilter[filter];
}
if (noUpdate) {
this.SET_PROJECTS_FILTER({ filter: 'attributes', value: JSON.stringify(this.attributesFilter) });
} else {
// After updating the filter object, emit the updated filter for application-wide use
this.emitUpdatedFilter();
}
},
/**
* Removes a specified value from a project attribute filter.
* Particularly useful for multi-choice attributes where individual values can be deselected.
* @param {Object} removedFilter - The filter to be removed, containing the attribute key and value.
*/
removeAttributeFilter({ filter, value }) {
// Retrieve attribute information to determine if it's a multi-choice attribute
const attribute = this.getProjectAttribute(filter);
const isMultiChoice = attribute.field_type.includes('list');
if (isMultiChoice) {
// For multi-choice attributes, convert the current filter value to an array for manipulation
let arrayValue = this.attributesFilter[filter] ? this.attributesFilter[filter].split(',') : [];
// Remove the specified value from the array
arrayValue = arrayValue.filter(val => val !== value);
// Update the attributesFilter with the new array, converted back to a string
this.attributesFilter[filter] = arrayValue.join(',');
} else {
// For single-choice attributes, directly update the filter to remove the value
delete this.attributesFilter[filter];
}
// Emit the updated filter after removal
this.emitUpdatedFilter();
},
isMultiple(filter) {
return filter.field_type.includes('list');
},
setDefaultFilters(filter) {
const defaultFilter = filter.default_filter_enabled ? filter.default_filter_value : null;
if (defaultFilter) {
// make an array from the string in case of a list
const filtersArray = defaultFilter.split(',');
// for each value update the filter
filtersArray.forEach(defaultValue => {
const defaultOption = filter.options.find(option => option.value === defaultValue);
if (defaultOption) {
this.updateAttributeFilter({ ...defaultOption, noUpdate: true });
}
});
}
},
.transition-properties(...) {
-webkit-transition: @arguments;
-moz-transition: @arguments;
-o-transition: @arguments;
transition: @arguments;
}
#filters-container {
width: 100%;
display: flex;
flex-direction: column;
justify-content: flex-end;
align-items: flex-end;
width: fit-content;
.collapsible-filters {
font-size: 1.25em;
padding-right: 0;
.customcaret{
transition: transform .2s ease;
&.collapsed {
transform: rotate(180deg);
}
&::before{
position: relative;
right: 0;
top: 65%;
color: #999;
margin-top: 4px;
border-color: #999 transparent transparent;
border-style: solid;
border-width: 5px 5px 0;
content: "";
}
}
}
}
.filters {
width: 100%;
height:auto;
opacity: 1;
.transition-properties(all 0.2s ease;);
.filter-row {
border: none;
box-shadow: none;
}
flex-direction: column;
align-items: flex-start !important;
padding: 0.5em;
&:first-child {
padding-left: 0;
}
&:last-child {
padding-right: 0;
}
label {
margin-bottom: 0.2em;
font-size: 0.9em;
font-weight: 600;
}
}
.item {
width: 25%;
}
#search-projects {
width: 100%;
.filters.hidden {
overflow: hidden;
opacity: 0;
max-height: 0;
@media screen and (min-width: 701px) {
.item {
&:first-child {
padding-left: 0;
}
&:last-child {
padding-right: 0;
}
}
}
#filters-container {
max-height: 275px;
.transition-properties(all 0.2s ease-out;);
padding-right: 0;
padding-left: 0;