Skip to content

Commit

Permalink
[24.0] Fix history filters taking up space in GridList
Browse files Browse the repository at this point in the history
Add a popover version of the `FilterMenu` component. This can be enabled via the `isPopover` prop. Fixes galaxyproject#17618
  • Loading branch information
ahmedhamidawan committed Mar 20, 2024
1 parent 7cb1de5 commit 8522a01
Show file tree
Hide file tree
Showing 6 changed files with 123 additions and 89 deletions.
180 changes: 109 additions & 71 deletions client/src/components/Common/FilterMenu.vue
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
<script setup lang="ts">
import { library } from "@fortawesome/fontawesome-svg-core";
import { faAngleDoubleUp, faQuestion, faRedo, faSearch } from "@fortawesome/free-solid-svg-icons";
import { faAngleDoubleUp, faQuestion, faSearch } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { BButton, BModal } from "bootstrap-vue";
import { BButton, BModal, BPopover } from "bootstrap-vue";
import { kebabCase } from "lodash";
import { computed, ref } from "vue";
import type Filtering from "@/utils/filtering";
import { type Alias, getOperatorForAlias } from "@/utils/filtering";
import { type Alias, type ErrorType, getOperatorForAlias, type ValidFilter } from "@/utils/filtering";
import DelayedInput from "@/components/Common/DelayedInput.vue";
import FilterMenuBoolean from "@/components/Common/FilterMenuBoolean.vue";
Expand All @@ -17,7 +17,7 @@ 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, faRedo, faSearch);
library.add(faAngleDoubleUp, faQuestion, faSearch);
interface BackendFilterError {
err_msg: string;
Expand Down Expand Up @@ -54,6 +54,8 @@ interface Props {
searchError?: BackendFilterError;
/** Whether the advanced menu is currently expanded */
showAdvanced?: boolean;
/** Whether to use a popover for the menu options */
isPopover?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
Expand All @@ -64,6 +66,7 @@ const props = withDefaults(defineProps<Props>(), {
menuType: "linked",
showAdvanced: false,
searchError: undefined,
isPopover: false,
});
const emit = defineEmits<{
Expand All @@ -78,10 +81,17 @@ const filters = computed(() => Object.fromEntries(props.filterClass.getFiltersFo
const identifier = kebabCase(props.name);
const advancedMenu = ref<HTMLElement | null>(null);
const delayedInputField = ref<InstanceType<typeof DelayedInput> | null>(null);
const toggleMenuButton = computed(() => {
const element = delayedInputField.value?.$el.querySelector(`[data-description='toggle advanced search']`);
return element;
});
// Boolean for showing the help modal for the whole filter menu (if provided)
const showHelp = ref(false);
const formattedSearchError = computed(() => {
const formattedSearchError = computed<ErrorType | null>(() => {
if (props.searchError) {
const { column, col, operation, op, value, val, err_msg, ValueError } = props.searchError;
const alias = operation || op;
Expand All @@ -99,6 +109,25 @@ const formattedSearchError = computed(() => {
}
});
// computed ref with a get and set function
const localAdvancedToggle = computed({
get: () => props.showAdvanced,
set: (value: boolean) => {
emit("update:show-advanced", value);
},
});
/** Returns the `ValidFilter<any>` for given `filter`
*
* This non-null asserts the output because where it's used, the filter is guaranteed
* to exist because of the `<span v-if="validFilters[filter]?.menuItem">` condition
* @param filter the filter to get
* @returns the _defined_ `ValidFilter<any>` for the given `filter`
*/
function getValidFilter(filter: string): ValidFilter<any> {
return validFilters.value[filter]!;
}
/** Explicitly sets a filter: value
* (also closes help modal for this filter if it exists)
* @param filter the filter to set
Expand All @@ -108,6 +137,14 @@ function onOption(filter: string, value: any) {
filters.value[filter] = value;
}
function onPopoverShown() {
advancedMenu.value?.querySelector("input")?.focus();
}
function onPopoverHidden() {
(toggleMenuButton.value as any)?.focus();
}
function onSearch() {
const newFilterText = props.filterClass.getFilterText(filters.value);
const newBackendFilter = props.filterClass.getFilterText(filters.value, true);
Expand All @@ -133,6 +170,7 @@ function updateFilterText(newFilterText: string) {
<DelayedInput
v-if="props.menuType !== 'standalone'"
v-show="props.menuType == 'linked' || (props.menuType == 'separate' && !props.showAdvanced)"
ref="delayedInputField"
:query="props.filterText"
:delay="props.debounceDelay"
:loading="props.loading"
Expand All @@ -155,66 +193,74 @@ function updateFilterText(newFilterText: string) {
<FontAwesomeIcon fixed-width :icon="faAngleDoubleUp" />
</BButton>

<div
v-if="props.menuType == 'standalone' || props.showAdvanced"
<component
:is="!props.isPopover ? 'div' : BPopover"
v-if="(props.isPopover && toggleMenuButton) || props.menuType == 'standalone' || props.showAdvanced"
class="mt-2"
data-description="advanced filters">
<div v-for="filter in Object.keys(validFilters)" :key="filter">
<span v-if="validFilters[filter]?.menuItem">
<FilterMenuBoolean
v-if="validFilters[filter]?.type == Boolean"
:name="filter"
:filter="validFilters[filter]"
:filters="filters"
@change="onOption"
@on-enter="onSearch"
@on-esc="onToggle" />
<FilterMenuRanged
v-else-if="validFilters[filter]?.isRangeInput"
class="m-0"
:name="filter"
:filter="validFilters[filter]"
:filters="filters"
:error="formattedSearchError"
:identifier="identifier"
@change="onOption"
@on-enter="onSearch"
@on-esc="onToggle" />
<FilterMenuMultiTags
v-else-if="validFilters[filter]?.type == 'MultiTags'"
:name="filter"
:filter="validFilters[filter]"
:filters="filters"
:identifier="identifier"
@change="onOption" />
<FilterMenuObjectStore
v-else-if="validFilters[filter]?.type == 'ObjectStore'"
:name="filter"
:filter="validFilters[filter]"
:filters="filters"
@change="onOption" />
<FilterMenuQuotaSource
v-else-if="validFilters[filter]?.type == 'QuotaSource'"
:name="filter"
:filter="validFilters[filter]"
:filters="filters"
:identifier="identifier"
@change="onOption" />
<FilterMenuInput
v-else
:name="filter"
:filter="validFilters[filter]"
:filters="filters"
:error="formattedSearchError"
:identifier="identifier"
@change="onOption"
@on-enter="onSearch"
@on-esc="onToggle" />
</span>
</div>
:show.sync="localAdvancedToggle"
:target="toggleMenuButton"
placement="bottomleft"
data-description="advanced filters"
@hidden="onPopoverHidden"
@shown="onPopoverShown">
<span ref="advancedMenu">
<div v-for="filter in Object.keys(validFilters)" :key="filter">
<span v-if="validFilters[filter]?.menuItem">
<FilterMenuBoolean
v-if="validFilters[filter]?.type == Boolean"
:name="filter"
:filter="getValidFilter(filter)"
:filters="filters"
@change="onOption"
@on-enter="onSearch"
@on-esc="onToggle" />
<FilterMenuRanged
v-else-if="validFilters[filter]?.isRangeInput"
class="m-0"
:name="filter"
:filter="getValidFilter(filter)"
:filters="filters"
:error="formattedSearchError || undefined"
:identifier="identifier"
@change="onOption"
@on-enter="onSearch"
@on-esc="onToggle" />
<FilterMenuMultiTags
v-else-if="validFilters[filter]?.type == 'MultiTags'"
:name="filter"
:filter="getValidFilter(filter)"
:filters="filters"
:identifier="identifier"
@change="onOption" />
<FilterMenuObjectStore
v-else-if="validFilters[filter]?.type == 'ObjectStore'"
:name="filter"
:filter="getValidFilter(filter)"
:filters="filters"
@change="onOption" />
<FilterMenuQuotaSource
v-else-if="validFilters[filter]?.type == 'QuotaSource'"
:name="filter"
:filter="getValidFilter(filter)"
:filters="filters"
:identifier="identifier"
@change="onOption" />
<FilterMenuInput
v-else
:name="filter"
:filter="getValidFilter(filter)"
:filters="filters"
:error="formattedSearchError || undefined"
:identifier="identifier"
@change="onOption"
@on-enter="onSearch"
@on-esc="onToggle" />
</span>
</div>
</span>

<!-- Perform search or cancel out (or open help modal for whole Menu if exists) -->
<div class="mb-3 mt-1">
<div class="mb-3 mt-2">
<BButton
:id="`${identifier}-advanced-filter-submit`"
class="mr-1"
Expand All @@ -227,23 +273,15 @@ function updateFilterText(newFilterText: string) {
<span v-localize>Search</span>
</BButton>

<BButton v-if="props.menuType !== 'standalone'" size="sm" @click="onToggle">
<FontAwesomeIcon :icon="faRedo" />

<span v-localize>Cancel</span>
</BButton>

<BButton v-if="props.hasHelp" title="Search Help" size="sm" @click="showHelp = true">
<FontAwesomeIcon :icon="faQuestion" />
</BButton>

<span> </span>

<BModal v-if="props.hasHelp" v-model="showHelp" :title="`${props.name} Advanced Search Help`" ok-only>
<!-- Slot for Menu help section -->
<slot name="menu-help-text"></slot>
</BModal>
</div>
</div>
</component>
</div>
</template>
7 changes: 3 additions & 4 deletions client/src/components/Common/FilterMenuBoolean.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,13 @@
import { BFormGroup, BFormRadioGroup } from "bootstrap-vue";
import { computed } from "vue";
import type { ValidFilter } from "@/utils/filtering";
type FilterType = string | boolean | undefined;
interface Props {
name: string;
filter: {
boolType?: string;
placeholder: string;
};
filter: ValidFilter<any>;
filters: {
[k: string]: FilterType;
};
Expand Down
8 changes: 1 addition & 7 deletions client/src/components/Common/FilterMenuInput.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,12 @@ import {
import { capitalize } from "lodash";
import { computed, ref, watch } from "vue";
import { ValidFilter } from "@/utils/filtering";
import { type ErrorType, type ValidFilter } from "@/utils/filtering";
library.add(faQuestion);
type FilterType = string | boolean | undefined;
type ErrorType = {
index: string;
typeError: boolean;
msg: string;
};
interface Props {
name: string;
identifier: any;
Expand Down
8 changes: 1 addition & 7 deletions client/src/components/Common/FilterMenuRanged.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,10 @@
import { BFormDatepicker, BFormInput, BInputGroup, BInputGroupAppend } from "bootstrap-vue";
import { computed, ref, watch } from "vue";
import { ValidFilter } from "@/utils/filtering";
import { type ErrorType, type ValidFilter } from "@/utils/filtering";
type FilterType = string | boolean | undefined;
type ErrorType = {
index: string;
typeError: boolean;
msg: string;
};
interface Props {
name: string;
identifier: any;
Expand Down
1 change: 1 addition & 0 deletions client/src/components/Workflow/WorkflowList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,7 @@ onMounted(() => {
:filter-text.sync="filterText"
:loading="loading || overlay"
has-help
is-popover
:placeholder="searchPlaceHolder"
:show-advanced.sync="showAdvanced">
<template v-slot:menu-help-text>
Expand Down
8 changes: 8 additions & 0 deletions client/src/utils/filtering.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,14 @@ const operatorForAlias = {
eq: ":",
} as const satisfies Record<string, string>;

export type ErrorType = {
filter: string;
index?: string;
value?: string;
typeError?: string;
msg: string;
};

type OperatorForAlias = typeof operatorForAlias;
export type Alias = keyof OperatorForAlias;
type Operator = OperatorForAlias[Alias];
Expand Down

0 comments on commit 8522a01

Please sign in to comment.