Skip to content
Snippets Groups Projects
ProjectsMenu.vue 14.1 KiB
Newer Older
Florent Lavelle's avatar
dev
Florent Lavelle committed
<template>
    v-if="chunkedNsortedFilters.length > 0"
    <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', '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"
        >
          <search-projects
            v-if="filter.name === 'search'"
            v-else
            :options="filter.options"
            :multiple="filter.field_type.includes('list')"
            :current-selection="attributesFilter[filter.id]"
            :default-filter="filter.default_filter_enabled ? filter.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
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
        {
          name: 'access_level',
          label: 'Niveau d\'autorisation requis',
          options: [
            {
              label: 'Utilisateur anonyme',
              value: 'anonymous'
            },
            {
              label: 'Utilisateur connecté',
              value: 'logged_user'
            },
            {
              label: 'Contributeur',
              value: 'contributor'
            },
          ],
Timothee P's avatar
Timothee P committed
        },
        {
          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'
            },
          ],
Timothee P's avatar
Timothee P committed
        },
        {
          name: 'moderation',
          label: 'Modération',
          options: [
            {
              label: 'Projet modéré',
              value: 'true'
            },
            {
              label: 'Projet non modéré',
              value: 'false'
            },
          ]
Timothee P's avatar
Timothee P committed
        },
          name: 'search',
          label: 'Recherche par nom',
        }
Timothee P's avatar
Timothee P committed
    };
  },
Florent Lavelle's avatar
Florent Lavelle committed
  computed: {
    ...mapState([
      'user',
      'configuration',
      'projectAttributes'
    ]),
    /**
     * 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.
      // 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
          // 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);
Florent Lavelle's avatar
Florent Lavelle committed
  },

Timothee P's avatar
Timothee P committed
  methods: {
    /**
     * 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') {
          { filter: filter.id, label: 'Oui', value: 'true' },
          { filter: filter.id, label: 'Non', value: 'false' },
      } 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,
          label: option.name || option,
          value: option.id || 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({ filter, value }) {
      // 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];
      }

      // 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();
    },
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;
		.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;
      }
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) {
      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>