<template> <div id="filters-container"> <div class="ui styled accordion" @click="displayFilters = !displayFilters" > <div id="filters" class="title collapsible-filters" > FILTRES <i :class="['ui icon customcaret', { 'collapsed': !displayFilters }]" aria-hidden="true" /> </div> </div> <!-- TODO: FIX TRANSITION --> <div :class="['full-width', { 'hidden': displayFilters }]"> <div class="ui menu filters"> <div class="item"> <label> Niveau d'autorisation requis </label> <DropdownMenuItem :options="accessLevelOptions" v-on="$listeners" /> </div> <div class="item"> <label> Mon niveau d'autorisation </label> <DropdownMenuItem :options="userAccessLevelOptions" v-on="$listeners" /> </div> <div class="item"> <label> Modération </label> <DropdownMenuItem :options="moderationOptions" v-on="$listeners" /> </div> <div class="item"> <label> Recherche par nom </label> <search-projects :search-function="SEARCH_PROJECTS" v-on="$listeners" /> </div> </div> <!-- TODO: make several rows if more than 4 project attributes --> <!-- (create a computed seperating into groups to loop over) --> <div class="ui menu filters"> <div v-for="attribute in displayedAttributeFilters" :key="attribute.id" class="item" > <label> {{ attribute.label }} </label> <DropdownMenuItem :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" /> </div> </div> </div> </div> </template> <script> import { mapState, mapActions } from 'vuex'; import DropdownMenuItem from '@/components/Projects/DropdownMenuItem.vue'; import SearchProjects from '@/components/Projects/SearchProjects.vue'; export default { name: 'ProjectsMenu', components: { DropdownMenuItem, SearchProjects, }, data() { return { displayFilters: false, moderationOptions: [ { label: 'Tous', filter: 'moderation', value: null }, { label: 'Projet modéré', filter: 'moderation', value: 'true' }, { label: 'Projet non modéré', filter: 'moderation', value: 'false' }, ], accessLevelOptions: [ { label: 'Tous', filter: 'access_level', value: null }, { label: 'Utilisateur anonyme', filter: 'access_level', value: 'anonymous' }, { label: 'Utilisateur connecté', filter: 'access_level', value: 'logged_user' }, { label: 'Contributeur', filter: 'access_level', value: 'contributor' }, ], userAccessLevelOptions: [ { label: 'Tous', filter: 'user_access_level', value: null }, { label: 'Utilisateur connecté', filter: 'user_access_level', value: '1' }, { label: 'Contributeur', filter: 'user_access_level', value: '2' }, { label: 'Super contributeur', filter: 'user_access_level', value: '3' }, { label: 'Modérateur', filter: 'user_access_level', value: '4' }, { label: 'Administrateur projet', filter: 'user_access_level', value: '5' }, ], attributesFilter: {}, }; }, computed: { ...mapState(['user', 'projectAttributes']), /** * 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. * * @returns {Array} An array of project attributes with modified options for display. */ displayedAttributeFilters() { // Filter out attributes that should not be displayed based on `display_filter` flag return this.projectAttributes.filter(attribute => attribute.display_filter) .map(attribute => { // Generate options based on the attribute's field type const options = this.generateOptionsForAttribute(attribute); // Add a global selector option at the beginning of the options list options.unshift({ label: 'Tous', filter: attribute.id, value: null, }); // Return the attribute with the updated options return { ...attribute, options }; }); }, }, created() { if (!this.user) { this.userAccessLevelOptions.splice(1, 0, { label: 'Utilisateur anonyme', filter: 'user_access_level', value: '0' }); } }, methods: { ...mapActions('projects', [ 'SEARCH_PROJECTS' ]), /** * Generates options for a given attribute based on its field type. * 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. */ generateOptionsForAttribute(attribute) { // Handle boolean attributes specially by creating true/false options if (attribute.field_type === 'boolean') { return [ { filter: attribute.id, label: 'true', value: 'true' }, { filter: attribute.id, label: 'false', value: 'false' }, ]; } // For other attribute types, map each option to the expected format return attribute.options.map(option => ({ filter: attribute.id, label: option, value: option, })); }, /** * 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({ value, filter }) { // 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 === 'multi_choices_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]; } // 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({ value, filter }) { // Retrieve attribute information to determine if it's a multi-choice attribute const attribute = this.getProjectAttribute(filter); const isMultiChoice = attribute.field_type === 'multi_choices_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(); }, } }; </script> <style lang="less" scoped> .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; .accordion { 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; min-height: 0; max-height:75px; opacity: 1; border: none; box-shadow: none; .transition-properties(all 0.2s ease-out;); .item { display: flex; flex-direction: column; align-items: flex-start !important; padding: 0.5em; label { margin-bottom: 0.2em; font-size: 0.9em; font-weight: 600; } } .item { width: 25%; } .item::before { width: 0; } #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; } } } @media screen and (max-width: 700px) { #filters-container { .filters { display: flex; flex-direction: column; max-height: 275px; .transition-properties(all 0.2s ease-out;); .item { width: 100%; padding-right: 0; padding-left: 0; } } } } </style>