Merge pull request #913 from tainacan/feature/912

Creates select-type filter for taxonomies. #912
This commit is contained in:
Mateus Machado Luna 2024-08-06 10:42:39 -03:00 committed by GitHub
commit 800409fe29
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 345 additions and 69 deletions

View File

@ -587,6 +587,7 @@ class Item extends Entity {
'metadatum_index' => null 'metadatum_index' => null
); );
$args = wp_parse_args($args, $defaults); $args = wp_parse_args($args, $defaults);
$item_metadata = array(); $item_metadata = array();
// If a single metadata is passed, we use it instead of fetching more // If a single metadata is passed, we use it instead of fetching more
@ -749,6 +750,13 @@ class Item extends Entity {
); );
$args = wp_parse_args($args, $defaults); $args = wp_parse_args($args, $defaults);
/**
* Filter the arguments passed to the get_item_metadatum_as_html function
* @param array $args The arguments passed to the function
* @param object $metadata_section The Item Metadatum object
*/
$args = apply_filters( 'tainacan-get-item-metadatum-as-html-filter-args', $args, $item_metadatum );
if ($item_metadatum->has_value() || !$args['hide_empty']) { if ($item_metadatum->has_value() || !$args['hide_empty']) {
// Gets the metadata type object to use it if we need the slug // Gets the metadata type object to use it if we need the slug
@ -1230,6 +1238,13 @@ class Item extends Entity {
); );
$args = wp_parse_args($args, $defaults); $args = wp_parse_args($args, $defaults);
/**
* Filter the arguments passed to the get_metadata_section_as_html function
* @param array $args The arguments passed to the function
* @param object $metadata_section The Metadata Section object
*/
$args = apply_filters( 'tainacan-get-metadata-section-as-html-filter-args', $args, $metadata_section );
// Gets the metadata section inner metadata list // Gets the metadata section inner metadata list
$metadata_section_metadata_list = $metadata_section->get_metadata_object_list(); $metadata_section_metadata_list = $metadata_section->get_metadata_object_list();
$has_metadata_list = (is_array($metadata_section_metadata_list) && count($metadata_section_metadata_list) > 0 ); $has_metadata_list = (is_array($metadata_section_metadata_list) && count($metadata_section_metadata_list) > 0 );

View File

@ -107,7 +107,7 @@ class Filters extends Repository {
'map' => 'meta', 'map' => 'meta',
'title' => __( 'Max of options', 'tainacan' ), 'title' => __( 'Max of options', 'tainacan' ),
'type' => ['integer', 'string'], 'type' => ['integer', 'string'],
'description' => __( 'The max number of options to be showed in filter sidebar.', 'tainacan' ), 'description' => __( 'The maximum number of options to be loaded by default on the filter.', 'tainacan' ),
'validation' => '', 'validation' => '',
'default' => 4 'default' => 4
], ],

View File

@ -111,7 +111,7 @@ function tainacan_autoload($class_name) {
} }
if( in_array('Metadata_Types', $class_path) || in_array('Filter_Types', $class_path) ){ if( in_array('Metadata_Types', $class_path) || in_array('Filter_Types', $class_path) ){
$exceptions = ['taxonomytaginput','taxonomycheckbox']; $exceptions = ['taxonomytaginput','taxonomycheckbox','taxonomyselectbox'];
if( in_array( strtolower( $class_name ), $exceptions) ){ if( in_array( strtolower( $class_name ), $exceptions) ){
$dir.= 'taxonomy/'; $dir.= 'taxonomy/';
}else{ }else{

View File

@ -179,7 +179,8 @@
v-model="form.max_options" v-model="form.max_options"
name="max_options" name="max_options"
type="number" type="number"
step="1" /> step="1"
:max="maxOptionsLimit" />
<button <button
class="button is-white is-pulled-right" class="button is-white is-pulled-right"
@click.prevent="showEditMaxOptions = false"> @click.prevent="showEditMaxOptions = false">
@ -327,7 +328,8 @@ export default {
closedByForm: false, closedByForm: false,
showEditMaxOptions: false, showEditMaxOptions: false,
entityName: 'filter', entityName: 'filter',
isLoading: false isLoading: false,
maxOptionsLimit: tainacan_plugin.api_max_items_per_page && !isNaN(tainacan_plugin.api_max_items_per_page) ? Number(tainacan_plugin.api_max_items_per_page) : 96
} }
}, },
created() { created() {

View File

@ -416,10 +416,10 @@
:message="metadataSection.description" /> :message="metadataSection.description" />
</span> </span>
</div> </div>
<transition name="filter-item">
<div <div
v-show="(metadataSectionCollapses[sectionIndex] || isMetadataNavigation) && !isSectionHidden(metadataSection.id)" v-show="((metadataSectionCollapses[sectionIndex] || isMetadataNavigation) && !isSectionHidden(metadataSection.id))"
class="metadata-section-metadata-list"> class="metadata-section-metadata-list"
:class="((metadataSectionCollapses[sectionIndex] || isMetadataNavigation) && !isSectionHidden(metadataSection.id)) ? '' : 'is-section-content-hidden'">
<p <p
v-if="metadataSection.description && metadataSection.description_bellow_name == 'yes'" v-if="metadataSection.description && metadataSection.description_bellow_name == 'yes'"
class="metadata-section-description-help-info metadatum-description-help-info"> class="metadata-section-description-help-info metadatum-description-help-info">
@ -454,7 +454,7 @@
@mobile-special-focus="setMetadatumFocus({ index: index, scrollIntoView: true })" /> @mobile-special-focus="setMetadatumFocus({ index: index, scrollIntoView: true })" />
</template> </template>
</div> </div>
</transition>
</div> </div>
<!-- Hook for extra Form options --> <!-- Hook for extra Form options -->
@ -1696,9 +1696,6 @@ export default {
}, },
onChangeCollapse(event, index) { onChangeCollapse(event, index) {
if (event && !this.metadataCollapses[index] && this.itemMetadata[index].metadatum && this.itemMetadata[index].metadatum['metadata_type'] === "Tainacan\\Metadata_Types\\GeoCoordinate")
this.$emitter.emit('itemEditionFormResize');
this.metadataCollapses.splice(index, 1, event); this.metadataCollapses.splice(index, 1, event);
}, },
toggleMetadataSectionCollapse(sectionIndex) { toggleMetadataSectionCollapse(sectionIndex) {

View File

@ -42,6 +42,7 @@ class Filter_Type_Helper {
$this->Tainacan_Filters->register_filter_type('Tainacan\Filter_Types\Numeric_Interval'); $this->Tainacan_Filters->register_filter_type('Tainacan\Filter_Types\Numeric_Interval');
$this->Tainacan_Filters->register_filter_type('Tainacan\Filter_Types\TaxonomyTaginput'); $this->Tainacan_Filters->register_filter_type('Tainacan\Filter_Types\TaxonomyTaginput');
$this->Tainacan_Filters->register_filter_type('Tainacan\Filter_Types\TaxonomyCheckbox'); $this->Tainacan_Filters->register_filter_type('Tainacan\Filter_Types\TaxonomyCheckbox');
$this->Tainacan_Filters->register_filter_type('Tainacan\Filter_Types\TaxonomySelectbox');
$this->Tainacan_Filters->register_filter_type('Tainacan\Filter_Types\Numeric_List_Interval'); $this->Tainacan_Filters->register_filter_type('Tainacan\Filter_Types\Numeric_List_Interval');
$this->Tainacan_Filters->register_filter_type('Tainacan\Filter_Types\Numerics_Intersection'); $this->Tainacan_Filters->register_filter_type('Tainacan\Filter_Types\Numerics_Intersection');

View File

@ -1,21 +1,20 @@
<template> <template>
<div <div class="block">
:class="{ 'skeleton': isLoadingOptions }"
class="block">
<b-select <b-select
v-if="!isLoadingOptions" :loading="isLoadingOptions"
:disabled="!isLoadingOptions && options.length <= 0"
:model-value="selected" :model-value="selected"
:aria-labelledby="'filter-label-id-' + filter.id" :aria-labelledby="'filter-label-id-' + filter.id"
:placeholder="filter.placeholder ? filter.placeholder : $i18n.get('label_selectbox_init')" :placeholder="filter.placeholder ? filter.placeholder : $i18n.get('label_selectbox_init')"
expanded expanded
@update:model-value="($event) => { resetPage(); onSelect($event) }"> @update:model-value="($event) => { resetPage(); onSelect($event) }">
<option value=""> <option value="">
{{ $i18n.get('label_selectbox_init') }}... {{ filter.placeholder ? filter.placeholder : $i18n.get('label_selectbox_init') }}
</option> </option>
<option <option
v-for="(option, index) in options" v-for="(option, index) in options"
:key="index" :key="index"
:label="option.label" :label="option.label + ( option.total_items ? (' (' + option.total_items + ')') : '' )"
:value="option.value"> :value="option.value">
{{ option.label }} {{ option.label }}
<span <span
@ -76,7 +75,9 @@
promise = this.getValuesPlainText({ promise = this.getValuesPlainText({
metadatumId: this.metadatumId, metadatumId: this.metadatumId,
search: null, search: null,
isRepositoryLevel: this.isRepositoryLevel isRepositoryLevel: this.isRepositoryLevel,
number: this.filter.max_options,
offset: 0
}); });
promise.request promise.request
.then((res) => { .then((res) => {

View File

@ -12,7 +12,7 @@ class Selectbox extends Filter_Type {
$this->set_name( __('Select Box', 'tainacan') ); $this->set_name( __('Select Box', 'tainacan') );
$this->set_supported_types(['string', 'long_string']); $this->set_supported_types(['string', 'long_string']);
$this->set_component('tainacan-filter-selectbox'); $this->set_component('tainacan-filter-selectbox');
$this->set_use_max_options(false); $this->set_use_max_options(true);
$this->set_preview_template(' $this->set_preview_template('
<div> <div>
<div class="control is-expanded"> <div class="control is-expanded">

View File

@ -138,6 +138,7 @@
TainacanFilterTaginput: defineAsyncComponent(() => import('./taginput/TainacanFilterTaginput.vue')), TainacanFilterTaginput: defineAsyncComponent(() => import('./taginput/TainacanFilterTaginput.vue')),
TainacanFilterTaxonomyCheckbox: defineAsyncComponent(() => import('./taxonomy/TainacanFilterCheckbox.vue')), TainacanFilterTaxonomyCheckbox: defineAsyncComponent(() => import('./taxonomy/TainacanFilterCheckbox.vue')),
TainacanFilterTaxonomyTaginput: defineAsyncComponent(() => import('./taxonomy/TainacanFilterTaginput.vue')), TainacanFilterTaxonomyTaginput: defineAsyncComponent(() => import('./taxonomy/TainacanFilterTaginput.vue')),
TainacanFilterTaxonomySelectbox: defineAsyncComponent(() => import('./taxonomy/TainacanFilterSelectbox.vue')),
TainacanFilterDateInterval: defineAsyncComponent(() => import('./date-interval/TainacanFilterDateInterval.vue')), TainacanFilterDateInterval: defineAsyncComponent(() => import('./date-interval/TainacanFilterDateInterval.vue')),
TainacanFilterDatesIntersection: defineAsyncComponent(() => import('./dates-intersection/TainacanFilterDatesIntersection.vue')), TainacanFilterDatesIntersection: defineAsyncComponent(() => import('./dates-intersection/TainacanFilterDatesIntersection.vue')),
TainacanFilterNumericInterval: defineAsyncComponent(() => import('./numeric-interval/TainacanFilterNumericInterval.vue')), TainacanFilterNumericInterval: defineAsyncComponent(() => import('./numeric-interval/TainacanFilterNumericInterval.vue')),

View File

@ -0,0 +1,210 @@
<template>
<div class="block">
<b-select
:loading="isLoadingOptions"
:disabled="!isLoadingOptions && options.length <= 0"
:model-value="selected"
:aria-labelledby="'filter-label-id-' + filter.id"
:placeholder="filter.placeholder ? filter.placeholder : $i18n.get('label_selectbox_init')"
expanded
@update:model-value="($event) => { resetPage(); onSelect($event) }">
<option value="">
{{ filter.placeholder ? filter.placeholder : $i18n.get('label_selectbox_init') }}
</option>
<option
v-for="(option, index) in options"
:key="index"
:label="option.label + ( option.total_items ? (' (' + option.total_items + ')') : '' )"
:value="option.value">
<span
v-if="option.total_items != undefined"
class="has-text-gray">{{ "(" + option.total_items + ")" }}</span>
</option>
</b-select>
</div>
</template>
<script>
import qs from 'qs';
import { tainacanApi, CancelToken, isCancel } from '../../../js/axios';
import { mapGetters } from 'vuex';
import { filterTypeMixin } from '../../../js/filter-types-mixin';
export default {
mixins: [ filterTypeMixin ],
emits: [
'input',
'update-parent-collapse'
],
data(){
return {
isLoadingOptions: false,
getOptionsValuesCancel: undefined,
selected: '',
options: [],
taxonomy: '',
taxonomyId: ''
}
},
computed: {
...mapGetters('search', {
'facetsFromItemSearch': 'getFacets'
}),
},
watch: {
facetsFromItemSearch: {
handler() {
if (this.isUsingElasticSearch)
this.loadOptions();
},
immediate: true,
deep:true
},
isLoadingItems: {
handler() {
if ( this.isUsingElasticSearch )
this.isLoadingOptions = this.isLoadingItems;
},
immediate: true
},
'query': {
handler() {
this.updateSelectedValues();
},
deep: true
}
},
mounted() {
if (!this.isUsingElasticSearch)
this.loadOptions();
},
created() {
if (this.filter.metadatum &&
this.filter.metadatum.metadata_type_object &&
this.filter.metadatum.metadata_type_object.options &&
this.filter.metadatum.metadata_type_object.options.taxonomy &&
this.filter.metadatum.metadata_type_object.options.taxonomy_id
) {
this.taxonomyId = this.filter.metadatum.metadata_type_object.options.taxonomy_id;
this.taxonomy = this.filter.metadatum.metadata_type_object.options.taxonomy;
}
this.$eventBusSearchEmitter.on('hasToReloadFacets', this.reloadOptions);
},
beforeUnmount() {
// Cancels previous Request
if (this.getOptionsValuesCancel != undefined)
this.getOptionsValuesCancel.cancel('Facet search Canceled.');
this.$eventBusSearchEmitter.off('hasToReloadFacets', this.reloadOptions);
},
methods: {
reloadOptions(shouldReload) {
if ( !this.isUsingElasticSearch && shouldReload )
this.loadOptions();
},
loadOptions() {
if (!this.isUsingElasticSearch) {
let promise = null;
const source = CancelToken.source();
// Cancels previous Request
if (this.getOptionsValuesCancel != undefined)
this.getOptionsValuesCancel.cancel('Facet search Canceled.');
this.isLoadingOptions = true;
let query_items = { 'current_query': this.query };
let route = '';
if (this.isRepositoryLevel)
route = `/facets/${this.metadatumId}?getSelected=1&order=asc&parent=0&number=${this.filter.max_options}&` + qs.stringify(query_items);
else {
if (this.filter.collection_id == 'default' && this.currentCollectionId)
route = `/collection/${this.currentCollectionId}/facets/${this.metadatumId}?getSelected=1&order=asc&number=${this.filter.max_options}&` + qs.stringify(query_items);
else
route = `/collection/${this.filter.collection_id}/facets/${this.metadatumId}?getSelected=1&order=asc&number=${this.filter.max_options}&` + qs.stringify(query_items);
}
this.options = [];
promise = new Object({
request:
new Promise((resolve, reject) => {
tainacanApi.get(route, { cancelToken: source.token})
.then( res => {
resolve(res)
})
.catch(error => {
reject(error)
});
}),
source: source
});
promise.request
.then((res) => {
this.isLoadingOptions = false;
this.prepareOptionsForTaxonomy(res.data.values ? res.data.values : res.data);
if (res && res.data && res.data.values)
this.$emit('update-parent-collapse', res.data.values.length > 0 );
})
.catch( error => {
if (isCancel(error)) {
this.$console.log('Request canceled: ' + error.message);
} else {
this.$console.log('Error on facets request: ', error);
this.isLoadingOptions = false;
}
});
// Search Request Token for cancelling
this.getOptionsValuesCancel = promise.source;
} else {
for (const facet in this.facetsFromItemSearch) {
if (facet == this.filter.id) {
if (Array.isArray(this.facetsFromItemSearch[facet])) {
this.prepareOptionsForTaxonomy(this.facetsFromItemSearch[facet]);
this.$emit('update-parent-collapse', this.facetsFromItemSearch[facet].length > 0 );
} else {
this.prepareOptionsForTaxonomy(Object.values(this.facetsFromItemSearch[facet]));
this.$emit('update-parent-collapse', Object.values(this.facetsFromItemSearch[facet]).length > 0 );
}
}
}
}
},
updateSelectedValues() {
if ( !this.query || !this.query.taxquery || !Array.isArray( this.query.taxquery ) )
return false;
// Cleared either way, we might be coming from a situation where all the filters were removed.
this.selected = '';
const index = this.query.taxquery.findIndex(newMetadatum => newMetadatum.taxonomy == this.taxonomy);
if (index >= 0) {
const metadata = this.query.taxquery[ index ];
if (this.selected != metadata.terms)
this.selected = metadata.terms;
}
},
onSelect(selection) {
this.$emit('input', {
filter: 'selectbox',
taxonomy: this.taxonomy,
metadatum_id: this.metadatumId,
collection_id: this.collectionId,
terms: selection
});
},
prepareOptionsForTaxonomy(items) {
this.options = [];
this.options = items.slice(); // copy array.
this.updateSelectedValues();
}
}
}
</script>

View File

@ -0,0 +1,27 @@
<?php
namespace Tainacan\Filter_Types;
defined( 'ABSPATH' ) or die( 'No script kiddies please!' );
/**
* Class TaxonomySelectbox
*/
class TaxonomySelectbox extends Filter_Type {
function __construct(){
$this->set_name( __('Selectbox', 'tainacan') );
$this->set_supported_types(['term']);
$this->set_component('tainacan-filter-taxonomy-selectbox');
$this->set_preview_template('
<div>
<div class="control is-expanded">
<span class="select is-fullwidth">
<select>
<option value="someValue">' . __('Select here...') . '</option>
</select>
</span>
</div>
</div>
');
}
}

View File

@ -124,6 +124,7 @@
latitude: -14.4086569, latitude: -14.4086569,
longitude: -51.31668, longitude: -51.31668,
selected: [], selected: [],
mapIntersectionObserver: null
} }
}, },
computed: { computed: {
@ -195,22 +196,23 @@
created() { created() {
if ( this.value && this.value != "" ) if ( this.value && this.value != "" )
this.selected = Array.isArray(this.value) ? (this.value.length == 1 && this.value[0] == "" ? [] : this.value) : [this.value]; this.selected = Array.isArray(this.value) ? (this.value.length == 1 && this.value[0] == "" ? [] : this.value) : [this.value];
// Listens to window resize event to update map bounds
// We need to pass mapComponentRef here instead of creating it inside the function
// otherwise the listener would conflict when multiple geo metadata are inserted.
const mapComponentRef = 'map--' + this.itemMetadatumIdentifier;
this.$emitter.on('itemEditionFormResize', () => this.handleWindowResize(mapComponentRef));
}, },
mounted() { mounted() {
nextTick(() => { nextTick(() => {
const mapComponentRef = 'map--' + this.itemMetadatumIdentifier; const mapComponentRef = 'map--' + this.itemMetadatumIdentifier;
this.handleWindowResize(mapComponentRef); this.handleWindowResize(mapComponentRef);
// Intersection Observer to handle map resize
if ( this.$refs[mapComponentRef] && this.$refs[mapComponentRef]['$el'] ) {
this.mapIntersectionObserver = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting)
this.handleWindowResize(mapComponentRef);
});
}, { threshold: 0.1 });
this.mapIntersectionObserver.observe(this.$refs[mapComponentRef]['$el']);
}
}); });
},
beforeUnmount() {
const mapComponentRef = 'map--' + this.itemMetadatumIdentifier;
this.$emitter.off('itemEditionFormResize', () => this.handleWindowResize(mapComponentRef));
}, },
methods: { methods: {
onUpdateFromLatitudeInput: _.debounce( function(value) { onUpdateFromLatitudeInput: _.debounce( function(value) {

View File

@ -258,6 +258,7 @@
}); });
} }
// Presets second tab as active to display selected items
if ( this.itemMetadatum.value.length > 0 && this.itemMetadatum.metadatum.multiple != 'yes' ) if ( this.itemMetadatum.value.length > 0 && this.itemMetadatum.metadatum.multiple != 'yes' )
this.activeTab = 1; this.activeTab = 1;
} }

View File

@ -16,7 +16,7 @@
@input="($event) => getMask ? null : onInput($event.target.value)" @input="($event) => getMask ? null : onInput($event.target.value)"
@blur="onBlur"> @blur="onBlur">
<small <small
v-if="getMaxlength" v-if="value && getMaxlength"
class="help counter" class="help counter"
:class="{ 'is-invisible': !isInputFocused }"> :class="{ 'is-invisible': !isInputFocused }">
{{ value.length }} / {{ getMaxlength }} {{ value.length }} / {{ getMaxlength }}

View File

@ -443,6 +443,7 @@
this.initializeValues(); this.initializeValues();
} }
}); });
}, },
beforeUnmount() { beforeUnmount() {
// Cancels previous Request // Cancels previous Request
@ -795,6 +796,24 @@
this.createColumn(res, key, option ? option.label : null); this.createColumn(res, key, option ? option.label : null);
this.isColumnLoading = false; this.isColumnLoading = false;
// If this is the first time loading, these will be undefined
if (
option === undefined &&
key === undefined &&
index === undefined
) {
// Here we already have a value for the hasToDisplaySearchBar. Thus we can decide if we should
// Preset the second tab as active to display selected values
if (
( Array.isArray(this.selected) ? (this.selected.length) : this.selected ) &&
this.metadatum.multiple != 'yes' &&
this.hasToDisplaySearchBar
) {
this.fetchSelectedLabels();
this.activeTab = 1;
}
}
}) })
.catch(error => { .catch(error => {
this.$console.log(error); this.$console.log(error);

View File

@ -45,7 +45,7 @@ export const fetchItemMetadata = ({ commit }, item_id) => {
export const fetchCompoundFirstParentMetaId = ({ commit }, { item_id, metadatum_id }) => { export const fetchCompoundFirstParentMetaId = ({ commit }, { item_id, metadatum_id }) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
axios.tainacanApi.put(`/item/${item_id}/metadata/${metadatum_id}`, { value: [] }) axios.tainacanApi.put(`/item/${item_id}/metadata/${metadatum_id}`, { values: [] })
.then( res => { .then( res => {
const parentMetaId = res.data.parent_meta_id; const parentMetaId = res.data.parent_meta_id;
resolve(parentMetaId); resolve(parentMetaId);

View File

@ -97,7 +97,7 @@ class Filters extends TAINACAN_UnitTestCase {
$Tainacan_Filters = \Tainacan\Repositories\Filters::get_instance(); $Tainacan_Filters = \Tainacan\Repositories\Filters::get_instance();
$all_filter_types = $Tainacan_Filters->fetch_filter_types(); $all_filter_types = $Tainacan_Filters->fetch_filter_types();
$this->assertEquals( 13, count( $all_filter_types ) ); $this->assertEquals( 14, count( $all_filter_types ) );
$float_filters = $Tainacan_Filters->fetch_supported_filter_types('float'); $float_filters = $Tainacan_Filters->fetch_supported_filter_types('float');
$this->assertTrue( count( $float_filters ) > 0 ); $this->assertTrue( count( $float_filters ) > 0 );