Skip to content
Snippets Groups Projects
ProjectsMenu.vue 12.4 KiB
Newer Older
Florent Lavelle's avatar
dev
Florent Lavelle committed
<template>
  <div id="filters-container">
    <div
      class="ui styled accordion"
      @click="displayFilters = !displayFilters"
    >
      <div
        class="title collapsible-filters"
      >
Timothee P's avatar
Timothee P committed
        FILTRES
Florent Lavelle's avatar
Florent Lavelle committed
        <i
          :class="['ui icon customcaret', { 'collapsed': !displayFilters }]"
Florent Lavelle's avatar
Florent Lavelle committed
          aria-hidden="true"
Timothee P's avatar
Timothee P committed
        />
      </div>
    </div>
    <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>
Timothee P's avatar
Timothee P committed
      </div>
      <!-- TODO: make several rows if more than 4 project attributes -->
      <!-- (create a computed seperating into groups to loop over) -->
          <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"
          />
Timothee P's avatar
Timothee P committed
      </div>
    </div>
  </div>
Florent Lavelle's avatar
dev
Florent Lavelle committed
</template>

<script>
Florent Lavelle's avatar
Florent Lavelle committed
import { mapState, mapActions } from 'vuex';
Florent Lavelle's avatar
dev
Florent Lavelle committed

Florent Lavelle's avatar
Florent Lavelle committed
import DropdownMenuItem from '@/components/Projects/DropdownMenuItem.vue';
import SearchProjects from '@/components/Projects/SearchProjects.vue';
Florent Lavelle's avatar
dev
Florent Lavelle committed

export default {
Timothee P's avatar
Timothee P committed
  name: 'ProjectsMenu',
Florent Lavelle's avatar
dev
Florent Lavelle committed

Timothee P's avatar
Timothee P committed
  components: {
    DropdownMenuItem,
    SearchProjects,
  },
Florent Lavelle's avatar
Florent Lavelle committed

Timothee P's avatar
Timothee P committed
  data() {
    return {
Timothee P's avatar
Timothee P committed
      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
        },
Florent Lavelle's avatar
Florent Lavelle committed
        {
          label: 'Utilisateur anonyme',
          filter: 'access_level',
          value: 'anonymous'
        },
Timothee P's avatar
Timothee P committed
        {
          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',
Timothee P's avatar
Timothee P committed
          filter: 'user_access_level',
          value: '3'
        },
        {
          label: 'Modérateur',
Timothee P's avatar
Timothee P committed
          filter: 'user_access_level',
          value: '4'
        },
        {
          label: 'Administrateur projet',
          filter: 'user_access_level',
          value: '5'
        },
Timothee P's avatar
Timothee P committed
    };
  },
Florent Lavelle's avatar
Florent Lavelle committed
  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.
     */
      // 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 };
        });
Florent Lavelle's avatar
Florent Lavelle committed
  },

  created() {
    if (!this.user) {
      this.userAccessLevelOptions.splice(1, 0, {
        label: 'Utilisateur anonyme',
        filter: 'user_access_level',
        value: '0'
      });
    }
  },

Timothee P's avatar
Timothee P committed
  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();
    },
Timothee P's avatar
Timothee P committed
  }
};
Florent Lavelle's avatar
dev
Florent Lavelle committed
</script>

<style lang="less" scoped>
.transition-properties(...) {
  -webkit-transition: @arguments;
  -moz-transition: @arguments;
  -o-transition: @arguments;
  transition: @arguments;
}
Florent Lavelle's avatar
Florent Lavelle committed

	width: 100%;
	display: flex;
	flex-direction: column;
	justify-content: flex-end;
	.accordion {
		.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;
    border: none;
    box-shadow: none;
		.transition-properties(all 0.2s ease-out;);
		.item {
			display: flex;
			flex-direction: column;
			align-items: flex-start !important;
Florent Lavelle's avatar
Florent Lavelle committed

			label {
				margin-bottom: 0.2em;
				font-size: 0.9em;
				font-weight: 600;
			}
		}
		.item {
			width: 25%;
		}
    .item::before {
			width: 0;
		}
Florent Lavelle's avatar
Florent Lavelle committed
	}
	.filters.hidden {
		overflow: hidden;
Florent Lavelle's avatar
Florent Lavelle committed
	}
Florent Lavelle's avatar
dev
Florent Lavelle committed
}
@media screen and (min-width: 701px) {
  .item {
    &:first-child {
      padding-left: 0;
    }
    &:last-child {
      padding-right: 0;
    }
  }
}

@media screen and (max-width: 700px) {

    .filters {
      display: flex;
      flex-direction: column;
      max-height: 275px;
      .transition-properties(all 0.2s ease-out;);

      .item {
        width: 100%;
Florent Lavelle's avatar
dev
Florent Lavelle committed
</style>