diff --git a/client/src/components/Common/FilterMenu.vue b/client/src/components/Common/FilterMenu.vue index f33bdb70960c..0f0596e2f422 100644 --- a/client/src/components/Common/FilterMenu.vue +++ b/client/src/components/Common/FilterMenu.vue @@ -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); @@ -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` for given `filter` * * This non-null asserts the output because where it's used, the filter is guaranteed @@ -251,9 +259,13 @@ function updateFilterText(newFilterText: string) { :filter="getValidFilter(filter)" :filters="filters" @change="onOption" /> - +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; + +type FilterValue = QuotaUsageUnwrapped | string | boolean | undefined; + +type DatalistItem = { value: string; text: string }; + +interface Props { + type?: FilterType; + name: string; + error?: string; + filter: ValidFilter; + filters: { + [k: string]: FilterValue; + }; + identifier: string; +} + +const props = defineProps(); + +const emit = defineEmits<{ + (e: "change", name: string, value: FilterValue): void; +}>(); + +const propValue = computed(() => props.filters[props.name]); + +const localValue = ref(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(() => { + if (datalist.value && typeof datalist.value[0] === "string") { + return datalist.value as string[]; + } + return []; +}); +const objectDatalist = computed(() => { + 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([] as QuotaUsage[]); +const errorMessage = ref(); +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(() => { + 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(() => { + 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; +} + + + diff --git a/client/src/components/Common/FilterMenuInput.vue b/client/src/components/Common/FilterMenuInput.vue index 7cf541a4c314..011e12a73ae0 100644 --- a/client/src/components/Common/FilterMenuInput.vue +++ b/client/src/components/Common/FilterMenuInput.vue @@ -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); @@ -23,7 +23,7 @@ type FilterType = string | boolean | undefined; interface Props { name: string; identifier: any; - error?: ErrorType; + error?: string; filter: ValidFilter; filters: { [k: string]: FilterType; @@ -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; @@ -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')" diff --git a/client/src/components/Common/FilterMenuQuotaSource.vue b/client/src/components/Common/FilterMenuQuotaSource.vue deleted file mode 100644 index eaf57766055a..000000000000 --- a/client/src/components/Common/FilterMenuQuotaSource.vue +++ /dev/null @@ -1,101 +0,0 @@ - - - diff --git a/client/src/components/History/HistoryFilters.js b/client/src/components/History/HistoryFilters.js index 2b9788d8145c..1bfcaad32cc8 100644 --- a/client/src/components/History/HistoryFilters.js +++ b/client/src/components/History/HistoryFilters.js @@ -17,10 +17,20 @@ const validFilters = { name: { placeholder: "name", type: String, handler: contains("name"), menuItem: true }, name_eq: { handler: equals("name"), menuItem: false }, extension: { placeholder: "extension", type: String, handler: equals("extension"), menuItem: true }, + history_content_type: { + placeholder: "content type", + type: "Dropdown", + handler: equals("history_content_type"), + datalist: [ + { value: "dataset", text: "Datasets Only" }, + { value: "dataset_collection", text: "Collections Only" }, + ], + menuItem: true, + }, tag: { placeholder: "tag", type: String, handler: contains("tags", "tag", expandNameTag), menuItem: true }, state: { placeholder: "state", - type: String, + type: "Dropdown", handler: equals("state"), datalist: states, helpInfo: StatesInfo, diff --git a/client/src/components/Panels/Common/ToolSearch.vue b/client/src/components/Panels/Common/ToolSearch.vue index f38b48c6a09e..70b776fc7fc9 100644 --- a/client/src/components/Panels/Common/ToolSearch.vue +++ b/client/src/components/Panels/Common/ToolSearch.vue @@ -100,7 +100,7 @@ const validFilters: ComputedRef>> = computed( placeholder: "EDAM ontology", type: String, handler: contains("ontology"), - datalist: ontologyList, + datalist: ontologyList.value, menuItem: true, }, id: { placeholder: "id", type: String, handler: contains("id"), menuItem: true }, @@ -115,9 +115,9 @@ const toolStore = useToolStore(); const { searchWorker } = storeToRefs(toolStore); const sectionNames = toolStore.sectionDatalist("default").map((option: { value: string; text: string }) => option.text); -const ontologyList = toolStore - .sectionDatalist("ontology:edam_topics") - .concat(toolStore.sectionDatalist("ontology:edam_operations")); +const ontologyList = computed(() => + toolStore.sectionDatalist("ontology:edam_topics").concat(toolStore.sectionDatalist("ontology:edam_operations")) +); onMounted(() => { // initialize worker diff --git a/client/src/utils/filtering.ts b/client/src/utils/filtering.ts index 9f6ceeee6ddc..686aa8f7f4f2 100644 --- a/client/src/utils/filtering.ts +++ b/client/src/utils/filtering.ts @@ -40,6 +40,15 @@ export type ErrorType = { type OperatorForAlias = typeof operatorForAlias; export type Alias = keyof OperatorForAlias; type Operator = OperatorForAlias[Alias]; +export type FilterType = + | typeof String + | typeof Number + | typeof Boolean + | typeof Date + | "MultiTags" + | "ObjectStore" + | "QuotaSource" + | "Dropdown"; /** A ValidFilter with a `handler` for the `Filtering` class, * and remaining properties for the `FilterMenu` component @@ -48,7 +57,7 @@ export type ValidFilter = { /** The `FilterMenu` input field/tooltip/label placeholder */ placeholder?: string; /** The data type of the `FilterMenu` input field */ - type?: typeof String | typeof Number | typeof Boolean | typeof Date | "MultiTags" | "ObjectStore" | "QuotaSource"; + type?: FilterType; /** If type: Boolean: * - booleanType: 'default' creates: `filter:true|false|any` * - booleanType: 'is' creates: `is:filter` @@ -60,7 +69,7 @@ export type ValidFilter = { * (if `false` the filter is still valid for the search bar `filterText`) */ menuItem: boolean; - /** Is there a datalist of values for this field */ + /** The datalist of values for this field */ datalist?: | string[] | {