Skip to content

Commit

Permalink
Merge pull request galaxyproject#18039 from ahmedhamidawan/search_col…
Browse files Browse the repository at this point in the history
…lections_in_history

Add datasets/collections filter to history panel filters
  • Loading branch information
dannon authored May 10, 2024
2 parents 1e9205a + 5745a5c commit 40177d3
Show file tree
Hide file tree
Showing 7 changed files with 246 additions and 123 deletions.
20 changes: 16 additions & 4 deletions client/src/components/Common/FilterMenu.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ import { type Alias, type ErrorType, getOperatorForAlias, type ValidFilter } fro
import DelayedInput from "@/components/Common/DelayedInput.vue";
import FilterMenuBoolean from "@/components/Common/FilterMenuBoolean.vue";
import FilterMenuDropdown from "@/components/Common/FilterMenuDropdown.vue";
import FilterMenuInput from "@/components/Common/FilterMenuInput.vue";
import FilterMenuMultiTags from "@/components/Common/FilterMenuMultiTags.vue";
import FilterMenuObjectStore from "@/components/Common/FilterMenuObjectStore.vue";
import FilterMenuQuotaSource from "@/components/Common/FilterMenuQuotaSource.vue";
import FilterMenuRanged from "@/components/Common/FilterMenuRanged.vue";
library.add(faAngleDoubleUp, faQuestion, faSearch);
Expand Down Expand Up @@ -117,6 +117,14 @@ const localAdvancedToggle = computed({
},
});
/** Returns the `typeError` or `msg` for a given `field` */
function errorForField(field: string) {
if (formattedSearchError.value && formattedSearchError.value?.index == field) {
return formattedSearchError.value.typeError || formattedSearchError.value.msg;
}
return "";
}
/** Returns the `ValidFilter<any>` for given `filter`
*
* This non-null asserts the output because where it's used, the filter is guaranteed
Expand Down Expand Up @@ -251,9 +259,13 @@ function updateFilterText(newFilterText: string) {
:filter="getValidFilter(filter)"
:filters="filters"
@change="onOption" />
<FilterMenuQuotaSource
v-else-if="validFilters[filter]?.type == 'QuotaSource'"
<FilterMenuDropdown
v-else-if="
validFilters[filter]?.type == 'Dropdown' || validFilters[filter]?.type == 'QuotaSource'
"
:type="validFilters[filter]?.type"
:name="filter"
:error="errorForField(filter) || undefined"
:filter="getValidFilter(filter)"
:filters="filters"
:identifier="identifier"
Expand All @@ -263,7 +275,7 @@ function updateFilterText(newFilterText: string) {
:name="filter"
:filter="getValidFilter(filter)"
:filters="filters"
:error="formattedSearchError || undefined"
:error="errorForField(filter) || undefined"
:identifier="identifier"
@change="onOption"
@on-enter="onSearch"
Expand Down
200 changes: 200 additions & 0 deletions client/src/components/Common/FilterMenuDropdown.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
<script setup lang="ts">
import { library } from "@fortawesome/fontawesome-svg-core";
import { faQuestion } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { BButton, BDropdown, BDropdownItem, BInputGroup, BInputGroupAppend, BModal } from "bootstrap-vue";
import { capitalize } from "lodash";
import { computed, onMounted, ref, type UnwrapRef, watch } from "vue";
import { QuotaUsage } from "@/components/User/DiskUsage/Quota/model";
import { type FilterType, type ValidFilter } from "@/utils/filtering";
import { errorMessageAsString } from "@/utils/simple-error";
import { fetch } from "../User/DiskUsage/Quota/services";
import QuotaUsageBar from "@/components/User/DiskUsage/Quota/QuotaUsageBar.vue";
library.add(faQuestion);
type QuotaUsageUnwrapped = UnwrapRef<QuotaUsage>;
type FilterValue = QuotaUsageUnwrapped | string | boolean | undefined;
type DatalistItem = { value: string; text: string };
interface Props {
type?: FilterType;
name: string;
error?: string;
filter: ValidFilter<any>;
filters: {
[k: string]: FilterValue;
};
identifier: string;
}
const props = defineProps<Props>();
const emit = defineEmits<{
(e: "change", name: string, value: FilterValue): void;
}>();
const propValue = computed<FilterValue>(() => props.filters[props.name]);
const localValue = ref<FilterValue>(propValue.value);
watch(
() => localValue.value,
() => {
emit("change", props.name, localValue.value);
}
);
watch(
() => propValue.value,
() => {
localValue.value = propValue.value;
}
);
// datalist refs
const datalist = computed<(DatalistItem[] | string[]) | undefined>(() => props.filter.datalist);
const stringDatalist = computed<string[]>(() => {
if (datalist.value && typeof datalist.value[0] === "string") {
return datalist.value as string[];
}
return [];
});
const objectDatalist = computed<DatalistItem[]>(() => {
if (datalist.value && typeof datalist.value[0] !== "string") {
return datalist.value as DatalistItem[];
}
return [];
});
// help modal button refs
const helpToggle = ref(false);
const modalTitle = `${capitalize(props.filter.placeholder)} Help`;
function onHelp(_: string, value: string) {
helpToggle.value = false;
localValue.value = value;
}
// Quota Source refs and operations
const quotaUsages = ref<QuotaUsage[]>([] as QuotaUsage[]);
const errorMessage = ref<string>();
async function loadQuotaUsages() {
try {
quotaUsages.value = await fetch();
// if the propValue is a string, find the corresponding QuotaUsage object and update the localValue
if (propValue.value && typeof propValue.value === "string") {
localValue.value = quotaUsages.value.find(
(quotaUsage) => props.filter.handler.converter!(quotaUsage) === propValue.value
);
}
} catch (e) {
errorMessage.value = errorMessageAsString(e);
}
}
const hasMultipleQuotaSources = computed<boolean>(() => {
return !!(quotaUsages.value && quotaUsages.value.length > 1);
});
onMounted(async () => {
if (props.type === "QuotaSource") {
await loadQuotaUsages();
}
});
function isQuotaUsageVal(value: FilterValue): value is QuotaUsageUnwrapped {
return !!(value && value instanceof Object && "rawSourceLabel" in value);
}
const dropDownText = computed<string>(() => {
if (props.type === "QuotaSource" && isQuotaUsageVal(localValue.value)) {
return localValue.value.sourceLabel;
}
if (localValue.value) {
const stringMatch = stringDatalist.value.find((item) => item === localValue.value);
const objectMatch = objectDatalist.value.find((item) => item.value === localValue.value);
if (stringMatch) {
return stringMatch;
} else if (objectMatch) {
return objectMatch.text;
}
}
return "(any)";
});
function setValue(val: string | QuotaUsage | undefined) {
localValue.value = val;
}
</script>

<template>
<div v-if="datalist || hasMultipleQuotaSources">
<small>Filter by {{ props.filter.placeholder }}:</small>
<BInputGroup :id="`${identifier}-advanced-filter-${props.name}`" class="flex-nowrap">
<BDropdown
:text="dropDownText"
block
class="w-100"
menu-class="w-100"
size="sm"
boundary="window"
:toggle-class="props.error ? 'text-danger' : ''">
<BDropdownItem href="#" @click="setValue(undefined)"><i>(any)</i></BDropdownItem>

<span v-if="stringDatalist.length > 0">
<BDropdownItem
v-for="listItem in stringDatalist"
:key="listItem"
href="#"
@click="setValue(listItem)">
{{ listItem }}
</BDropdownItem>
</span>
<span v-else-if="objectDatalist.length > 0">
<BDropdownItem
v-for="listItem in objectDatalist"
:key="listItem.value"
href="#"
@click="setValue(listItem.value)">
{{ listItem.text }}
</BDropdownItem>
</span>
<span v-else-if="props.type === 'QuotaSource'">
<BDropdownItem
v-for="quotaUsage in quotaUsages"
:key="quotaUsage.id"
href="#"
@click="setValue(quotaUsage)">
{{ quotaUsage.sourceLabel }}
<QuotaUsageBar
:quota-usage="quotaUsage"
class="quota-usage-bar"
:compact="true"
:embedded="true" />
</BDropdownItem>
</span>
</BDropdown>
<BInputGroupAppend>
<!-- append Help Modal toggle for filter if included -->
<BButton v-if="props.filter.helpInfo" :title="modalTitle" size="sm" @click="helpToggle = true">
<FontAwesomeIcon :icon="faQuestion" />
</BButton>
</BInputGroupAppend>
</BInputGroup>

<!-- if a filter has help component, place it within a modal -->
<span v-if="props.filter.helpInfo">
<BModal v-model="helpToggle" :title="modalTitle" ok-only>
<component
:is="props.filter.helpInfo"
v-if="typeof props.filter.helpInfo == 'object'"
@set-filter="onHelp" />
<div v-else-if="typeof props.filter.helpInfo == 'string'">
<p>{{ props.filter.helpInfo }}</p>
</div>
</BModal>
</span>
</div>
</template>
15 changes: 4 additions & 11 deletions client/src/components/Common/FilterMenuInput.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
import { capitalize } from "lodash";
import { computed, ref, watch } from "vue";
import { type ErrorType, type ValidFilter } from "@/utils/filtering";
import { type ValidFilter } from "@/utils/filtering";
library.add(faQuestion);
Expand All @@ -23,7 +23,7 @@ type FilterType = string | boolean | undefined;
interface Props {
name: string;
identifier: any;
error?: ErrorType;
error?: string;
filter: ValidFilter<any>;
filters: {
[k: string]: FilterType;
Expand All @@ -45,13 +45,6 @@ const localValue = ref(propValue.value);
const helpToggle = ref(false);
const modalTitle = `${capitalize(props.filter.placeholder)} Help`;
function hasError(field: string) {
if (props.error && props.error.index == field) {
return props.error.typeError || props.error.msg;
}
return "";
}
function onHelp(_: string, value: string) {
helpToggle.value = false;
localValue.value = value;
Expand Down Expand Up @@ -81,10 +74,10 @@ watch(
:id="`${identifier}-advanced-filter-${props.name}`"
ref="filterMenuInput"
v-model="localValue"
v-b-tooltip.focus.v-danger="hasError(props.name)"
v-b-tooltip.focus.v-danger="props.error"
class="mw-100"
size="sm"
:state="hasError(props.name) ? false : null"
:state="props.error ? false : null"
:placeholder="`any ${props.filter.placeholder}`"
:list="props.filter.datalist ? `${identifier}-${props.name}-selectList` : null"
@keyup.enter="emit('on-enter')"
Expand Down
101 changes: 0 additions & 101 deletions client/src/components/Common/FilterMenuQuotaSource.vue

This file was deleted.

Loading

0 comments on commit 40177d3

Please sign in to comment.