Skip to content

Commit

Permalink
separate FilterMenu elements into separate components
Browse files Browse the repository at this point in the history
  • Loading branch information
ahmedhamidawan committed Oct 10, 2023
1 parent 684a07a commit 1e39d4e
Show file tree
Hide file tree
Showing 5 changed files with 312 additions and 249 deletions.
287 changes: 51 additions & 236 deletions client/src/components/Common/FilterMenu.vue
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
<script setup lang="ts">
import { capitalize, kebabCase } from "lodash";
import { computed, type Ref, ref, watch } from "vue";
import { kebabCase } from "lodash";
import { computed, ref, watch } from "vue";
import type Filtering from "@/utils/filtering";
import { type Alias, getOperatorForAlias } from "@/utils/filtering";
import DelayedInput from "@/components/Common/DelayedInput.vue";
import FilterMenuBoolean from "@/components/Common/FilterMenuBoolean.vue";
import StatelessTags from "@/components/TagsMultiselect/StatelessTags.vue";
import FilterMenuInput from "@/components/Common/FilterMenuInput.vue";
import FilterMenuMultiTags from "@/components/Common/FilterMenuMultiTags.vue";
import FilterMenuRanged from "@/components/Common/FilterMenuRanged.vue";
interface BackendFilterError {
err_msg: string;
Expand Down Expand Up @@ -70,63 +72,9 @@ const filters = computed(() => Object.fromEntries(props.filterClass.getFiltersFo
const identifier = kebabCase(props.name);
// all filters that have help info
const helpKeys = Object.keys(validFilters.value).filter((key) => validFilters.value[key]?.helpInfo !== undefined);
const defaultHelpModalsVal = helpKeys.reduce((acc: { [k: string]: boolean }, item: string) => {
acc[item] = false;
return acc;
}, {});
/**
* An object of booleans that are used to show/hide the help modals for
* each filter that has them
*/
const helpModals: Ref<{ [k: string]: boolean }> = ref({ ...defaultHelpModalsVal });
// Boolean for showing the help modal for the whole filter menu (if provided)
const showHelp = ref(false);
/**
* Reactively storing and getting all filters from validFilters which are `.type == Date`
* and .type == 'MultiTags'
* (This was done to make the datepickers and tags store values in the `filters` object)
*/
const dateFilters: Ref<{ [key: string]: string }> = ref({});
const dateKeys = Object.keys(validFilters.value).filter((key) => validFilters.value[key]?.type == Date);
dateKeys.forEach((key: string) => {
if (validFilters.value[key]?.isRangeInput) {
dateKeys.push(key + "_lt");
dateKeys.push(key + "_gt");
}
});
const multiTagFilters: Ref<{ [key: string]: Ref<string[]> }> = ref({});
const multiTagKeys = Object.keys(validFilters.value).filter((key) => validFilters.value[key]?.type == "MultiTags");
multiTagKeys.forEach((key: string) => {
if (filters.value[key] !== undefined) {
multiTagFilters.value[key] = ref(filters.value[key] as string[]);
} else {
multiTagFilters.value[key] = ref([]);
}
});
watch(
() => filters.value,
(newFilters: { [k: string]: any }) => {
dateKeys.forEach((key: string) => {
if (newFilters[key]) {
dateFilters.value[key] = newFilters[key] as string;
} else {
delete dateFilters.value[key];
}
});
multiTagKeys.forEach((key: string) => {
if (newFilters[key]) {
(multiTagFilters.value[key] as Ref<string[]>).value = newFilters[key];
} else {
(multiTagFilters.value[key] as Ref<string[]>).value = [];
}
});
}
);
const formattedSearchError = computed(() => {
if (props.searchError) {
const { column, col, operation, op, value, val, err_msg, ValueError } = props.searchError;
Expand All @@ -145,32 +93,16 @@ const formattedSearchError = computed(() => {
}
});
function hasError(field: string) {
if (formattedSearchError.value && formattedSearchError.value.index == field) {
return formattedSearchError.value.typeError || formattedSearchError.value.msg;
}
return "";
}
/** Explicitly sets a filter: value
* (also closes help modal for this filter if it exists)
* @param filter the filter to set
* @param value the value to set it to
*/
function onOption(filter: string, value: any) {
filters.value[filter] = value;
if (helpModals.value[filter]) {
helpModals.value[filter] = false;
}
}
function onSearch() {
Object.keys(dateFilters.value).forEach((key) => {
onOption(key, dateFilters.value[key]);
});
Object.keys(multiTagFilters.value).forEach((key) => {
onOption(key, (multiTagFilters.value[key] as Ref<string[]>).value);
});
const newFilterText = props.filterClass.getFilterText(filters.value);
const newBackendFilter = props.filterClass.getFilterText(filters.value, true);
if (props.menuType !== "linked") {
Expand All @@ -181,10 +113,6 @@ function onSearch() {
}
}
function onTags(filter: string, tags: string[]) {
(multiTagFilters.value[filter] as Ref<string[]>).value = tags;
}
function onToggle() {
emit("update:show-advanced", !props.showAdvanced);
}
Expand All @@ -193,11 +121,14 @@ function updateFilterText(newFilterText: string) {
emit("update:filter-text", newFilterText);
}
// as the filterText changes, emit a backend-filter that can be used as a query
// as the filterText changes, emit a backend-filter that can be used as a backend query
watch(
() => props.filterText,
(newFilterText: string) => {
const defaultBackendFilter = props.filterClass.getFilterText(props.filterClass.defaultFilters, true);
const defaultBackendFilter = props.filterClass.getFilterText(
props.filterClass.defaultFilters,
true
);
const currentBackendFilter = props.filterClass.getFilterText(filters.value, true);
const backendFilter =
Expand Down Expand Up @@ -242,168 +173,52 @@ watch(
<div
v-if="props.menuType == 'standalone' || props.showAdvanced"
class="mt-2"
data-description="advanced filters"
@keyup.enter="onSearch"
@keyup.esc="onToggle">
data-description="advanced filters">
<div v-for="filter in Object.keys(validFilters)" :key="filter">
<span v-if="validFilters[filter]?.menuItem">
<!-- is a Boolean filter -->
<span v-if="validFilters[filter]?.type == Boolean">
<small>{{ validFilters[filter]?.placeholder }}:</small>
<FilterMenuBoolean
:name="filter"
:filter="validFilters[filter]"
:settings="filters"
@change="onOption" />
</span>

<!-- is a Ranged filter -->
<span v-else-if="validFilters[filter]?.isRangeInput" class="m-0">
<small>Filter by {{ validFilters[filter]?.placeholder }}:</small>
<b-input-group>
<!---------------------------- GREATER THAN ---------------------------->
<!-- is type: other than Date -->
<b-form-input
v-if="validFilters[filter]?.type != Date"
:id="`${identifier}-advanced-filter-${filter}_gt`"
v-model="filters[`${filter}_gt`]"
v-b-tooltip.focus.v-danger="hasError(`${filter}_gt`)"
size="sm"
:state="hasError(`${filter}_gt`) ? false : null"
:placeholder="`${validFilters[filter]?.placeholder} greater`" />
<!-- is type: Date -->
<b-form-input
v-else-if="validFilters[filter]?.type == Date"
:id="`${identifier}-advanced-filter-${filter}_gt`"
v-model="dateFilters[`${filter}_gt`]"
v-b-tooltip.focus.v-danger="hasError(`${filter}_gt`)"
size="sm"
:state="hasError(`${filter}_gt`) ? false : null"
:placeholder="`${validFilters[filter]?.placeholder} after`" />
<b-input-group-append v-if="validFilters[filter]?.type == Date">
<b-form-datepicker
v-model="dateFilters[`${filter}_gt`]"
reset-button
button-only
size="sm" />
</b-input-group-append>
<!--------------------------------------------------------------------->

<!---------------------------- LESSER THAN ---------------------------->
<!-- is type: other than Date -->
<b-form-input
v-if="validFilters[filter]?.type != Date"
:id="`${identifier}-advanced-filter-${filter}_lt`"
v-model="filters[`${filter}_lt`]"
v-b-tooltip.focus.v-danger="hasError(`${filter}_lt`)"
size="sm"
:state="hasError(`${filter}_lt`) ? false : null"
:placeholder="`${validFilters[filter]?.placeholder} lower`" />
<!-- is type: Date -->
<b-form-input
v-else-if="validFilters[filter]?.type == Date"
:id="`${identifier}-advanced-filter-${filter}_lt`"
v-model="dateFilters[`${filter}_lt`]"
v-b-tooltip.focus.v-danger="hasError(`${filter}_lt`)"
size="sm"
:state="hasError(`${filter}_lt`) ? false : null"
:placeholder="`${validFilters[filter]?.placeholder} before`" />
<b-input-group-append v-if="validFilters[filter]?.type == Date">
<b-form-datepicker
v-model="dateFilters[`${filter}_lt`]"
reset-button
button-only
size="sm" />
</b-input-group-append>
<!--------------------------------------------------------------------->
</b-input-group>
</span>

<!-- is a MultiTags filter -->
<span v-else-if="validFilters[filter]?.type == 'MultiTags'">
<small>Filter by {{ validFilters[filter]?.placeholder }}:</small>
<b-input-group :id="`${identifier}-advanced-filter-${filter}`">
<StatelessTags
:value="multiTagFilters[filter]?.value"
:placeholder="`any ${validFilters[filter]?.placeholder}`"
@input="(tags) => onTags(filter, tags)" />
</b-input-group>
</span>

<!-- is any other non-Ranged/non-Boolean filter -->
<span v-else>
<small>Filter by {{ validFilters[filter]?.placeholder }}:</small>
<b-input-group>
<!-- has a datalist -->
<b-form-input
v-if="validFilters[filter]?.datalist"
:id="`${identifier}-advanced-filter-${filter}`"
v-model="filters[filter]"
v-b-tooltip.focus.v-danger="hasError(filter)"
size="sm"
:state="hasError(filter) ? false : null"
:placeholder="`any ${validFilters[filter]?.placeholder}`"
list="selectList" />
<b-form-datalist
v-if="validFilters[filter]?.datalist"
id="selectList"
:options="validFilters[filter]?.datalist"></b-form-datalist>
<!-- is type: Date -->
<b-form-input
v-else-if="validFilters[filter]?.type == Date"
:id="`${identifier}-advanced-filter-${filter}`"
v-model="dateFilters[filter]"
v-b-tooltip.focus.v-danger="hasError(filter)"
size="sm"
:state="hasError(filter) ? false : null"
:placeholder="`any ${validFilters[filter]?.placeholder}`" />
<!-- is type: other than Date -->
<b-form-input
v-else
:id="`${identifier}-advanced-filter-${filter}`"
v-model="filters[filter]"
v-b-tooltip.focus.v-danger="hasError(filter)"
size="sm"
:state="hasError(filter) ? false : null"
:placeholder="`any ${validFilters[filter]?.placeholder}`" />
<!-- append Help Modal for filter if included or/and datepciker if type: Date -->
<b-input-group-append>
<b-button
v-if="validFilters[filter]?.helpInfo"
:title="`${capitalize(validFilters[filter]?.placeholder)} Help`"
size="sm"
@click="helpModals[filter] = true">
<icon icon="question" />
</b-button>
<b-form-datepicker
v-if="validFilters[filter]?.type == Date"
v-model="dateFilters[filter]"
reset-button
button-only
size="sm" />
</b-input-group-append>
</b-input-group>
</span>

<!-- if a filter has help component, place it within a modal -->
<span v-if="validFilters[filter]?.helpInfo">
<b-modal
v-model="helpModals[filter]"
:title="`${capitalize(validFilters[filter]?.placeholder)} Help`"
ok-only>
<component
:is="validFilters[filter]?.helpInfo"
v-if="typeof validFilters[filter]?.helpInfo == 'object'"
@set-filter="onOption" />
<div v-else-if="typeof validFilters[filter]?.helpInfo == 'string'">
<p>{{ validFilters[filter]?.helpInfo }}</p>
</div>
</b-modal>
</span>
<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" />

<FilterMenuInput
v-else
:name="filter"
:filter="validFilters[filter]"
:filters="filters"
:error="formattedSearchError"
:identifier="identifier"
@change="onOption"
@on-enter="onSearch"
@on-esc="onToggle" />
</span>
</div>

<!-- Perform search or cancel out (or open help modal for Menu if exists) -->
<!-- Perform search or cancel out (or open help modal for whole Menu if exists) -->
<div class="mt-3">
<b-button
:id="`${identifier}-advanced-filter-submit`"
Expand Down
Loading

0 comments on commit 1e39d4e

Please sign in to comment.