From 008b652c620450a6d80cce56ddc237d5bcc59f6d Mon Sep 17 00:00:00 2001
From: Ahmed Awan <qe66653@umbc.edu>
Date: Wed, 20 Mar 2024 15:34:54 -0500
Subject: [PATCH] add a `compact` version of `FilterMenu`; no collapse on
 search

It groups `is` type boolean filters on one line, and in checkboxes.
Adding filters applies them immediately, and there is no search button.
---
 client/src/components/Common/FilterMenu.vue   | 49 +++++++++++++++----
 .../components/Common/FilterMenuBoolean.vue   | 15 ++++--
 client/src/components/Grid/GridList.vue       |  4 +-
 .../components/Workflow/WorkflowFilters.js    | 12 ++---
 .../src/components/Workflow/WorkflowList.vue  |  2 +-
 5 files changed, 59 insertions(+), 23 deletions(-)

diff --git a/client/src/components/Common/FilterMenu.vue b/client/src/components/Common/FilterMenu.vue
index cffb6aae40e4..f33bdb70960c 100644
--- a/client/src/components/Common/FilterMenu.vue
+++ b/client/src/components/Common/FilterMenu.vue
@@ -54,8 +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;
+    /** What view to use for the menu */
+    view?: "dropdown" | "popover" | "compact";
 }
 
 const props = withDefaults(defineProps<Props>(), {
@@ -66,7 +66,7 @@ const props = withDefaults(defineProps<Props>(), {
     menuType: "linked",
     showAdvanced: false,
     searchError: undefined,
-    isPopover: false,
+    view: "dropdown",
 });
 
 const emit = defineEmits<{
@@ -135,6 +135,11 @@ function getValidFilter(filter: string): ValidFilter<any> {
  */
 function onOption(filter: string, value: any) {
     filters.value[filter] = value;
+
+    // for the compact view, we want to immediately search
+    if (props.view === "compact") {
+        onSearch();
+    }
 }
 
 function onPopoverShown() {
@@ -152,7 +157,11 @@ function onSearch() {
         emit("on-search", filters.value, newFilterText, newBackendFilter);
     } else {
         updateFilterText(newFilterText);
-        onToggle();
+
+        // for the compact view, we do not want to close the advanced menu
+        if (props.view !== "compact") {
+            onToggle();
+        }
     }
 }
 
@@ -194,8 +203,10 @@ function updateFilterText(newFilterText: string) {
         </BButton>
 
         <component
-            :is="!props.isPopover ? 'div' : BPopover"
-            v-if="(props.isPopover && toggleMenuButton) || props.menuType == 'standalone' || props.showAdvanced"
+            :is="props.view !== 'popover' ? 'div' : BPopover"
+            v-if="
+                (props.view === 'popover' && toggleMenuButton) || props.menuType == 'standalone' || props.showAdvanced
+            "
             class="mt-2"
             :show.sync="localAdvancedToggle"
             :target="toggleMenuButton"
@@ -206,11 +217,13 @@ function updateFilterText(newFilterText: string) {
             <span ref="advancedMenu">
                 <div v-for="filter in Object.keys(validFilters)" :key="filter">
                     <span v-if="validFilters[filter]?.menuItem">
+                        <!-- Boolean filters go in another section in compact view -->
                         <FilterMenuBoolean
-                            v-if="validFilters[filter]?.type == Boolean"
+                            v-if="props.view !== 'compact' && validFilters[filter]?.type == Boolean"
                             :name="filter"
                             :filter="getValidFilter(filter)"
                             :filters="filters"
+                            :view="props.view"
                             @change="onOption"
                             @on-enter="onSearch"
                             @on-esc="onToggle" />
@@ -246,7 +259,7 @@ function updateFilterText(newFilterText: string) {
                             :identifier="identifier"
                             @change="onOption" />
                         <FilterMenuInput
-                            v-else
+                            v-else-if="validFilters[filter]?.type !== Boolean"
                             :name="filter"
                             :filter="getValidFilter(filter)"
                             :filters="filters"
@@ -259,9 +272,26 @@ function updateFilterText(newFilterText: string) {
                 </div>
             </span>
 
+            <!-- Compact view: Boolean filters go side by side -->
+            <div v-if="props.view === 'compact'" class="d-flex">
+                <span v-for="filter in Object.keys(validFilters)" :key="filter">
+                    <FilterMenuBoolean
+                        v-if="validFilters[filter]?.menuItem && validFilters[filter]?.type == Boolean"
+                        class="mr-2 mt-1"
+                        :name="filter"
+                        :filter="getValidFilter(filter)"
+                        :filters="filters"
+                        :view="props.view"
+                        @change="onOption"
+                        @on-enter="onSearch"
+                        @on-esc="onToggle" />
+                </span>
+            </div>
+
             <!-- Perform search or cancel out (or open help modal for whole Menu if exists) -->
-            <div class="mb-3 mt-2">
+            <div class="mt-2">
                 <BButton
+                    v-if="props.view !== 'compact'"
                     :id="`${identifier}-advanced-filter-submit`"
                     class="mr-1"
                     size="sm"
@@ -282,6 +312,7 @@ function updateFilterText(newFilterText: string) {
                     <slot name="menu-help-text"></slot>
                 </BModal>
             </div>
+            <hr v-if="props.showAdvanced" class="w-100" />
         </component>
     </div>
 </template>
diff --git a/client/src/components/Common/FilterMenuBoolean.vue b/client/src/components/Common/FilterMenuBoolean.vue
index eb936d14663b..28f55941f0a6 100644
--- a/client/src/components/Common/FilterMenuBoolean.vue
+++ b/client/src/components/Common/FilterMenuBoolean.vue
@@ -1,5 +1,5 @@
 <script setup lang="ts">
-import { BFormGroup, BFormRadioGroup } from "bootstrap-vue";
+import { BFormCheckbox, BFormGroup, BFormRadioGroup } from "bootstrap-vue";
 import { computed } from "vue";
 
 import type { ValidFilter } from "@/utils/filtering";
@@ -12,11 +12,15 @@ interface Props {
     filters: {
         [k: string]: FilterType;
     };
+    view?: "dropdown" | "popover" | "compact";
 }
 
-const props = defineProps<Props>();
+const props = withDefaults(defineProps<Props>(), {
+    view: "dropdown",
+});
 
 const boolType = computed(() => props.filter.boolType || "default");
+const isCheckbox = computed(() => boolType.value === "is" && props.view === "compact");
 
 const options =
     boolType.value == "default"
@@ -50,10 +54,11 @@ const value = computed({
 
 <template>
     <!-- eslint-disable-next-line vuejs-accessibility/no-static-element-interactions -->
-    <div @keyup.enter="emit('on-enter')" @keyup.esc="emit('on-esc')">
-        <small>{{ props.filter.placeholder }}:</small>
+    <div :class="{ 'd-flex': isCheckbox }" @keyup.enter="emit('on-enter')" @keyup.esc="emit('on-esc')">
+        <small :class="{ 'mr-1': isCheckbox }">{{ props.filter.placeholder }}:</small>
 
-        <BFormGroup class="m-0">
+        <BFormCheckbox v-if="isCheckbox" v-model="value" :data-description="`filter ${props.name}`" />
+        <BFormGroup v-else class="m-0">
             <BFormRadioGroup
                 v-model="value"
                 :options="options"
diff --git a/client/src/components/Grid/GridList.vue b/client/src/components/Grid/GridList.vue
index 8c73ba0e5d44..5669e46481c3 100644
--- a/client/src/components/Grid/GridList.vue
+++ b/client/src/components/Grid/GridList.vue
@@ -292,8 +292,8 @@ watch(operationMessage, () => {
                 :filter-class="filterClass"
                 :filter-text.sync="filterText"
                 :loading="initDataLoading || resultsLoading"
-                :show-advanced.sync="showAdvanced" />
-            <hr v-if="showAdvanced" />
+                :show-advanced.sync="showAdvanced"
+                view="compact" />
         </div>
         <LoadingSpan v-if="initDataLoading" />
         <span v-else-if="!isAvailable || hasInvalidFilters" variant="info" show>
diff --git a/client/src/components/Workflow/WorkflowFilters.js b/client/src/components/Workflow/WorkflowFilters.js
index 1c184ff0d4c8..1280b6e450a5 100644
--- a/client/src/components/Workflow/WorkflowFilters.js
+++ b/client/src/components/Workflow/WorkflowFilters.js
@@ -89,28 +89,28 @@ export function WorkflowFilters(activeList = "my") {
             {
                 ...commonFilters,
                 published: {
-                    placeholder: "Filter on published workflows",
+                    placeholder: "Published",
                     type: Boolean,
                     boolType: "is",
                     handler: equals("published", "published", toBool),
                     menuItem: true,
                 },
                 importable: {
-                    placeholder: "Filter on importable workflows",
+                    placeholder: "Importable",
                     type: Boolean,
                     boolType: "is",
                     handler: equals("importable", "importable", toBool),
                     menuItem: true,
                 },
                 shared_with_me: {
-                    placeholder: "Filter on workflows shared with me",
+                    placeholder: "Shared with me",
                     type: Boolean,
                     boolType: "is",
                     handler: equals("shared_with_me", "shared_with_me", toBool),
                     menuItem: true,
                 },
                 deleted: {
-                    placeholder: "Filter on deleted workflows",
+                    placeholder: "Deleted",
                     type: Boolean,
                     boolType: "is",
                     handler: equals("deleted", "deleted", toBool),
@@ -133,7 +133,7 @@ export function WorkflowFilters(activeList = "my") {
                 },
                 u: { handler: contains("u"), menuItem: false },
                 published: {
-                    placeholder: "Filter on published workflows",
+                    placeholder: "Published",
                     type: Boolean,
                     boolType: "is",
                     handler: equals("published", "published", toBool),
@@ -156,7 +156,7 @@ export function WorkflowFilters(activeList = "my") {
                 },
                 u: { handler: contains("u"), menuItem: false },
                 shared_with_me: {
-                    placeholder: "Filter on workflows shared with me",
+                    placeholder: "Shared with me",
                     type: Boolean,
                     boolType: "is",
                     handler: equals("shared_with_me", "shared_with_me", toBool),
diff --git a/client/src/components/Workflow/WorkflowList.vue b/client/src/components/Workflow/WorkflowList.vue
index 8fb59edb8ea1..8a0f770c88a2 100644
--- a/client/src/components/Workflow/WorkflowList.vue
+++ b/client/src/components/Workflow/WorkflowList.vue
@@ -218,7 +218,7 @@ onMounted(() => {
                 :filter-text.sync="filterText"
                 :loading="loading || overlay"
                 has-help
-                is-popover
+                view="compact"
                 :placeholder="searchPlaceHolder"
                 :show-advanced.sync="showAdvanced">
                 <template v-slot:menu-help-text>