diff --git a/frontend/src/assets/scss/form/dropdown.scss b/frontend/src/assets/scss/form/dropdown.scss index acc3212faf..6e11955d83 100644 --- a/frontend/src/assets/scss/form/dropdown.scss +++ b/frontend/src/assets/scss/form/dropdown.scss @@ -42,6 +42,15 @@ } } + // Active state + &.is-active, + &:focus.is-active { + @apply relative bg-gray-50 text-gray-400 cursor-default; + i { + @apply mr-3 text-gray-400; + } + } + // Focus state &:not(.is-selected):not(.is-disabled):not(:hover):focus { @apply text-black bg-white; diff --git a/frontend/src/shared/form/form-errors.vue b/frontend/src/shared/form/form-errors.vue new file mode 100644 index 0000000000..ff7cc9b5da --- /dev/null +++ b/frontend/src/shared/form/form-errors.vue @@ -0,0 +1,57 @@ + + + + + diff --git a/frontend/src/shared/form/form-item.vue b/frontend/src/shared/form/form-item.vue index 07c4db2502..c631a6cb79 100644 --- a/frontend/src/shared/form/form-item.vue +++ b/frontend/src/shared/form/form-item.vue @@ -23,9 +23,10 @@
- {{ errorMessage(errors[0]) }} + {{ errorMessage(errors[0]) }}
@@ -66,6 +67,16 @@ const props = defineProps({ type: Boolean, default: true, }, + errorIcon: { + required: false, + type: String, + default: '', + }, + errorClass: { + required: false, + type: String, + default: '', + }, }); const errors = computed(() => props.validation?.$errors || []); diff --git a/frontend/src/shared/modules/filters/components/Filter.vue b/frontend/src/shared/modules/filters/components/Filter.vue index 58329d3d27..7cf5a8a61d 100644 --- a/frontend/src/shared/modules/filters/components/Filter.vue +++ b/frontend/src/shared/modules/filters/components/Filter.vue @@ -10,20 +10,27 @@
@@ -67,10 +74,7 @@ const filters = computed({ return props.modelValue; }, set(value: Filter) { - const { - settings, search, relation, order, pagination, ...filterValues - } = value; - filterList.value = Object.keys(filterValues); + alignFilterList(value); emit('update:modelValue', value); }, }); @@ -81,11 +85,23 @@ const configuration = computed(() => ({ })); const filterList = ref([]); +const cachedRelation = ref<'and' | 'or'>('and'); const switchOperator = () => { filters.value.relation = filters.value.relation === 'and' ? 'or' : 'and'; }; +const alignFilterList = (value: Filter) => { + const { + settings, search, relation, order, pagination, ...filterValues + } = value; + if (JSON.stringify(relation) !== JSON.stringify(cachedRelation.value)) { + cachedRelation.value = relation; + return; + } + filterList.value = Object.keys(filterValues); +}; + const removeFilter = (key) => { open.value = ''; filterList.value = filterList.value.filter((el) => el !== key); @@ -102,10 +118,7 @@ const fetch = (value: Filter) => { watch(() => filters.value, (value: Filter) => { fetch(value); - const { - settings, search, relation, order, pagination, ...filterValues - } = value; - filterList.value = Object.keys(filterValues); + alignFilterList(value); const query = setQuery(value); router.push({ query }); }, { deep: true }); diff --git a/frontend/src/shared/modules/filters/components/FilterDropdown.vue b/frontend/src/shared/modules/filters/components/FilterDropdown.vue index ae409b280f..5bac897dee 100644 --- a/frontend/src/shared/modules/filters/components/FilterDropdown.vue +++ b/frontend/src/shared/modules/filters/components/FilterDropdown.vue @@ -1,64 +1,65 @@ - diff --git a/frontend/src/shared/modules/filters/config/apiFilterRenderer/number.filter.renderer.ts b/frontend/src/shared/modules/filters/config/apiFilterRenderer/number.filter.renderer.ts index 0590e0c5ad..23344437b4 100644 --- a/frontend/src/shared/modules/filters/config/apiFilterRenderer/number.filter.renderer.ts +++ b/frontend/src/shared/modules/filters/config/apiFilterRenderer/number.filter.renderer.ts @@ -1,7 +1,21 @@ import { NumberFilterValue } from '@/shared/modules/filters/types/filterTypes/NumberFilterConfig'; +import { FilterNumberOperator } from '@/shared/modules/filters/config/constants/number.constants'; -export const numberApiFilterRenderer = (property: string, { value }: NumberFilterValue): any[] => [ - { - [property]: value, - }, -]; +export const numberApiFilterRenderer = (property: string, { + value, valueTo, operator, include, +}: NumberFilterValue): any[] => { + const filterValue = operator === FilterNumberOperator.BETWEEN ? [value, valueTo] : value; + const filter = { + [operator]: filterValue, + }; + + return [ + { + [property]: (include ? filter : { + not: { + filter, + }, + }), + }, + ]; +}; diff --git a/frontend/src/shared/modules/filters/config/constants/number.constants.ts b/frontend/src/shared/modules/filters/config/constants/number.constants.ts new file mode 100644 index 0000000000..95b4bab731 --- /dev/null +++ b/frontend/src/shared/modules/filters/config/constants/number.constants.ts @@ -0,0 +1,41 @@ +import { FilterOperator } from '@/shared/modules/filters/types/FilterOperator'; + +export enum FilterNumberOperator { + EQ = 'eq', + LT = 'lt', + LTE = 'lte', + GT = 'gt', + GTE = 'gte', + BETWEEN = 'between', +} + +export const numberFilterOperators: FilterOperator[] = [ + { + label: 'is', + value: FilterNumberOperator.EQ, + }, + { + label: 'less than', + subLabel: '<', + value: FilterNumberOperator.LT, + }, + { + label: 'equal or less than', + subLabel: '<=', + value: FilterNumberOperator.LTE, + }, + { + label: 'greater than', + subLabel: '>', + value: FilterNumberOperator.GT, + }, + { + label: 'equal or greater than', + subLabel: '>=', + value: FilterNumberOperator.GTE, + }, + { + label: 'between', + value: FilterNumberOperator.BETWEEN, + }, +]; diff --git a/frontend/src/shared/modules/filters/config/itemLabelRenderer/number.label.renderer.ts b/frontend/src/shared/modules/filters/config/itemLabelRenderer/number.label.renderer.ts index db5ecbae17..ed671cef54 100644 --- a/frontend/src/shared/modules/filters/config/itemLabelRenderer/number.label.renderer.ts +++ b/frontend/src/shared/modules/filters/config/itemLabelRenderer/number.label.renderer.ts @@ -1,3 +1,15 @@ import { NumberFilterValue } from '@/shared/modules/filters/types/filterTypes/NumberFilterConfig'; +import { + FilterNumberOperator, + numberFilterOperators, +} from '@/shared/modules/filters/config/constants/number.constants'; -export const numberItemLabelRenderer = (property: string, { value }: NumberFilterValue): string => `${property}:${value}`; +export const numberItemLabelRenderer = (property: string, { + value, include, operator, valueTo, +}: NumberFilterValue): string => { + const excludeText = !include ? ' (exclude)' : ''; + const operatorObject = numberFilterOperators.find((o) => o.value === operator); + const operandText = operatorObject?.subLabel ? `${operatorObject.subLabel} ` : ''; + const valueText = operator === FilterNumberOperator.BETWEEN ? `${value} - ${valueTo}` : `${operandText}${value}`; + return `${property}${excludeText}:${valueText || '...'}`; +}; diff --git a/frontend/src/shared/modules/filters/config/queryUrlParser/number.parser.ts b/frontend/src/shared/modules/filters/config/queryUrlParser/number.parser.ts index 1c32649faf..af2e5b38b2 100644 --- a/frontend/src/shared/modules/filters/config/queryUrlParser/number.parser.ts +++ b/frontend/src/shared/modules/filters/config/queryUrlParser/number.parser.ts @@ -1,13 +1,16 @@ import { NumberFilterValue } from '@/shared/modules/filters/types/filterTypes/NumberFilterConfig'; +import { FilterNumberOperator } from '@/shared/modules/filters/config/constants/number.constants'; interface QueryUrlNumberValue { operator: string, value: string, + valueTo: string, include: string, } export const numberQueryUrlParser = (query: QueryUrlNumberValue): NumberFilterValue => ({ - ...query, + operator: query.operator as FilterNumberOperator, include: query.include === 'true', value: +query.value, + valueTo: +query.valueTo || '', }); diff --git a/frontend/src/shared/modules/filters/services/filter-api.service.ts b/frontend/src/shared/modules/filters/services/filter-api.service.ts index 87854fb84c..b384a6f18f 100644 --- a/frontend/src/shared/modules/filters/services/filter-api.service.ts +++ b/frontend/src/shared/modules/filters/services/filter-api.service.ts @@ -23,7 +23,7 @@ export const filterApiService = () => { let filters: any[] = []; // Search - if (search.length > 0) { + if (search?.length > 0) { baseFilters = [ ...baseFilters, ...searchConfig.apiFilterRenderer(search), diff --git a/frontend/src/shared/modules/filters/services/filter-query.service.ts b/frontend/src/shared/modules/filters/services/filter-query.service.ts index 69f41f4c1d..51c13490d9 100644 --- a/frontend/src/shared/modules/filters/services/filter-query.service.ts +++ b/frontend/src/shared/modules/filters/services/filter-query.service.ts @@ -55,10 +55,16 @@ export const filterQueryService = () => { Object.entries(value).forEach(([key, filterValue]) => { if (typeof filterValue === 'object') { Object.entries(filterValue).forEach(([subKey, subFilterValue]) => { - query[`${key}.${subKey}`] = setQueryValue(subFilterValue); + const value = setQueryValue(subFilterValue); + if (value) { + query[`${key}.${subKey}`] = value; + } }); } else { - query[key] = setQueryValue(filterValue); + const value = setQueryValue(filterValue); + if (value) { + query[key] = value; + } } }); return query; diff --git a/frontend/src/shared/modules/filters/types/FilterOperator.ts b/frontend/src/shared/modules/filters/types/FilterOperator.ts new file mode 100644 index 0000000000..ca6969f54c --- /dev/null +++ b/frontend/src/shared/modules/filters/types/FilterOperator.ts @@ -0,0 +1,5 @@ +export interface FilterOperator { + value: string; + label: string; + subLabel?: string; +} diff --git a/frontend/src/shared/modules/filters/types/filterTypes/NumberFilterConfig.ts b/frontend/src/shared/modules/filters/types/filterTypes/NumberFilterConfig.ts index ca30d3e450..ad1b5bc567 100644 --- a/frontend/src/shared/modules/filters/types/filterTypes/NumberFilterConfig.ts +++ b/frontend/src/shared/modules/filters/types/filterTypes/NumberFilterConfig.ts @@ -1,12 +1,14 @@ /* eslint-disable no-unused-vars */ import { BaseFilterConfig, FilterConfigType } from '@/shared/modules/filters/types/FilterConfig'; +import { FilterNumberOperator } from '@/shared/modules/filters/config/constants/number.constants'; export interface NumberFilterOptions { hideIncludeSwitch?: boolean; } export interface NumberFilterValue { - operator: string, + operator: FilterNumberOperator, value: number | '', + valueTo?: number | '', include: boolean, } export interface NumberFilterConfig extends BaseFilterConfig {