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 @@
@@ -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 @@
-
- {{ filters.relation }}
-
+
+ {{ filters.relation }}
+
+
+
@@ -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 @@
-
-
-
- Filters
-
-
-
-
-
-
-
-
-
-
+
+
+
+ Filters
+
+
+
+
+
+
+
+
+
+
+
+
-
- {{ label }}
-
-
-
+ {{ label }}
+
+
+
Custom Attributes
-
-
- {{ label }}
-
-
-
+ {{ label }}
+
+
-
No results
-
-
+
+
-
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 {