-
-
+
+
+
+
+
+
-
- {{ operator }}
+
+ {{ filters.relation }}
-
+
@@ -19,40 +27,85 @@ import {
computed,
defineProps, ref, watch,
} from 'vue';
-import { FilterConfig } from '@/shared/modules/filters/types/FilterConfig';
+import { Filter, FilterConfig } from '@/shared/modules/filters/types/FilterConfig';
import CrFilterDropdown from '@/shared/modules/filters/components/FilterDropdown.vue';
import CrFilterItem from '@/shared/modules/filters/components/FilterItem.vue';
+import CrFilterSearch from '@/shared/modules/filters/components/FilterSearch.vue';
+import { filterQueryService } from '@/shared/modules/filters/services/filter-query.service';
+import { SearchFilterConfig } from '@/shared/modules/filters/types/filterTypes/SearchFilterConfig';
+import { useRoute, useRouter } from 'vue-router';
+import { filterApiService } from '@/shared/modules/filters/services/filter-api.service';
+import { FilterQuery } from '@/shared/modules/filters/types/FilterQuery';
const props = defineProps<{
+ modelValue: Filter,
config: Record
,
+ customConfig?: Record,
+ searchConfig: SearchFilterConfig,
}>();
-const filters = ref({});
+const emit = defineEmits<{(e: 'update:modelValue', value: Filter), (e: 'fetch', value: FilterQuery),}>();
-const operator = ref<'AND' | 'OR'>('AND');
-const filterList = ref([]);
+const router = useRouter();
+const route = useRoute();
-const relation = computed(() => filterList.value.join(`-${operator.value}-`));
+const open = ref('');
-const filtersObject = computed(() => ({
- ...filters.value,
- relation: relation.value,
-}));
+const filters = computed({
+ get() {
+ return props.modelValue;
+ },
+ set(value: Filter) {
+ const {
+ config, search, relation, order, pagination, ...filterValues
+ } = value;
+ filterList.value = Object.keys(filterValues);
+ emit('update:modelValue', value);
+ },
+});
+
+const filterList = ref([]);
const switchOperator = () => {
- operator.value = operator.value === 'AND' ? 'OR' : 'AND';
+ filters.value.relation = filters.value.relation === 'and' ? 'or' : 'and';
};
const removeFilter = (key) => {
+ open.value = '';
filterList.value = filterList.value.filter((el) => el !== key);
filters.value[key] = undefined;
};
-watch(() => filtersObject.value, (value) => {
- // TODO: sync with store
- // TODO: sync with query
- console.log(value);
-});
+const { setQuery, parseQuery } = filterQueryService();
+const { buildApiFilter } = filterApiService();
+
+const fetch = (value: Filter) => {
+ const data = buildApiFilter(value, { ...props.config, ...props.customConfig }, props.searchConfig);
+ emit('fetch', data);
+ console.log('fetch', data);
+};
+
+watch(() => filters.value, (value: Filter) => {
+ fetch(value);
+ const query = setQuery(value);
+ router.push({ query });
+}, { deep: true });
+
+// Watch for query change
+watch(() => route.query, (query) => {
+ const parsed = parseQuery(query, {
+ ...props.config,
+ ...props.customConfig,
+ });
+ if (!parsed || Object.keys(parsed).length === 0) {
+ const query = setQuery(props.modelValue);
+ router.push({ query });
+ return;
+ }
+ if (JSON.stringify(parsed) !== JSON.stringify(filters.value)) {
+ filters.value = parsed as Filter;
+ }
+}, { immediate: true });
diff --git a/frontend/src/shared/modules/filters/components/FilterSearch.vue b/frontend/src/shared/modules/filters/components/FilterSearch.vue
new file mode 100644
index 0000000000..fa55ca28f9
--- /dev/null
+++ b/frontend/src/shared/modules/filters/components/FilterSearch.vue
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/shared/modules/filters/components/filterTypes/BooleanFilter.vue b/frontend/src/shared/modules/filters/components/filterTypes/BooleanFilter.vue
index ef134fc5ad..899cf3c727 100644
--- a/frontend/src/shared/modules/filters/components/filterTypes/BooleanFilter.vue
+++ b/frontend/src/shared/modules/filters/components/filterTypes/BooleanFilter.vue
@@ -38,7 +38,7 @@ const rules: any = {
useVuelidate(rules, form);
onMounted(() => {
- if (!form.value) {
+ if (!form.value || Object.keys(form.value).length === 0) {
form.value = defaultForm;
}
});
diff --git a/frontend/src/shared/modules/filters/components/filterTypes/DateFilter.vue b/frontend/src/shared/modules/filters/components/filterTypes/DateFilter.vue
index 177379ae81..62363cad9d 100644
--- a/frontend/src/shared/modules/filters/components/filterTypes/DateFilter.vue
+++ b/frontend/src/shared/modules/filters/components/filterTypes/DateFilter.vue
@@ -42,7 +42,7 @@ const rules: any = {
useVuelidate(rules, form);
onMounted(() => {
- if (!form.value) {
+ if (!form.value || Object.keys(form.value).length === 0) {
form.value = defaultForm;
}
});
diff --git a/frontend/src/shared/modules/filters/components/filterTypes/MultiSelectFilter.vue b/frontend/src/shared/modules/filters/components/filterTypes/MultiSelectFilter.vue
index bba6f4c1a4..e7bb096fdb 100644
--- a/frontend/src/shared/modules/filters/components/filterTypes/MultiSelectFilter.vue
+++ b/frontend/src/shared/modules/filters/components/filterTypes/MultiSelectFilter.vue
@@ -41,7 +41,7 @@ const rules: any = {
useVuelidate(rules, form);
onMounted(() => {
- if (!form.value) {
+ if (!form.value || Object.keys(form.value).length === 0) {
form.value = defaultForm;
}
});
diff --git a/frontend/src/shared/modules/filters/components/filterTypes/NumberFilter.vue b/frontend/src/shared/modules/filters/components/filterTypes/NumberFilter.vue
index 175014a596..b5bb2829b8 100644
--- a/frontend/src/shared/modules/filters/components/filterTypes/NumberFilter.vue
+++ b/frontend/src/shared/modules/filters/components/filterTypes/NumberFilter.vue
@@ -1,6 +1,6 @@
-
+
@@ -41,7 +41,7 @@ const rules: any = {
useVuelidate(rules, form);
onMounted(() => {
- if (!form.value) {
+ if (!form.value || Object.keys(form.value).length === 0) {
form.value = defaultForm;
}
});
diff --git a/frontend/src/shared/modules/filters/components/filterTypes/SelectFilter.vue b/frontend/src/shared/modules/filters/components/filterTypes/SelectFilter.vue
index b4727bc915..801d0fc588 100644
--- a/frontend/src/shared/modules/filters/components/filterTypes/SelectFilter.vue
+++ b/frontend/src/shared/modules/filters/components/filterTypes/SelectFilter.vue
@@ -41,7 +41,7 @@ const rules: any = {
useVuelidate(rules, form);
onMounted(() => {
- if (!form.value) {
+ if (!form.value || Object.keys(form.value).length === 0) {
form.value = defaultForm;
}
});
diff --git a/frontend/src/shared/modules/filters/config/queryUrlParser/boolean.parser.ts b/frontend/src/shared/modules/filters/config/queryUrlParser/boolean.parser.ts
new file mode 100644
index 0000000000..d968529ac0
--- /dev/null
+++ b/frontend/src/shared/modules/filters/config/queryUrlParser/boolean.parser.ts
@@ -0,0 +1,11 @@
+import { BooleanFilterValue } from '@/shared/modules/filters/types/filterTypes/BooleanFilterConfig';
+
+interface QueryUrlBooleanValue {
+ value: string,
+ exclude: string,
+}
+
+export const booleanQueryUrlParser = (query: QueryUrlBooleanValue): BooleanFilterValue => ({
+ exclude: query.exclude === 'true',
+ value: query.value === 'true',
+});
diff --git a/frontend/src/shared/modules/filters/config/queryUrlParser/date.parser.ts b/frontend/src/shared/modules/filters/config/queryUrlParser/date.parser.ts
new file mode 100644
index 0000000000..64b140a92e
--- /dev/null
+++ b/frontend/src/shared/modules/filters/config/queryUrlParser/date.parser.ts
@@ -0,0 +1,12 @@
+import { DateFilterValue } from '@/shared/modules/filters/types/filterTypes/DateFilterConfig';
+
+interface QueryUrlDateValue {
+ operator: string,
+ value: string,
+ exclude: string,
+}
+
+export const dateQueryUrlParser = (query: QueryUrlDateValue): DateFilterValue => ({
+ ...query,
+ exclude: query.exclude === 'true',
+});
diff --git a/frontend/src/shared/modules/filters/config/queryUrlParser/multiselect.parser.ts b/frontend/src/shared/modules/filters/config/queryUrlParser/multiselect.parser.ts
new file mode 100644
index 0000000000..16d93f72f0
--- /dev/null
+++ b/frontend/src/shared/modules/filters/config/queryUrlParser/multiselect.parser.ts
@@ -0,0 +1,12 @@
+import { MultiSelectFilterValue } from '@/shared/modules/filters/types/filterTypes/MultiSelectFilterConfig';
+
+interface QueryUrlMultiSelectValue {
+ value: string,
+ exclude: string,
+}
+
+export const multiSelectQueryUrlParser = (query: QueryUrlMultiSelectValue): MultiSelectFilterValue => ({
+ ...query,
+ value: query.value.split(','),
+ exclude: query.exclude === 'true',
+});
diff --git a/frontend/src/shared/modules/filters/config/queryUrlParser/number.parser.ts b/frontend/src/shared/modules/filters/config/queryUrlParser/number.parser.ts
new file mode 100644
index 0000000000..319c791b77
--- /dev/null
+++ b/frontend/src/shared/modules/filters/config/queryUrlParser/number.parser.ts
@@ -0,0 +1,13 @@
+import { NumberFilterValue } from '@/shared/modules/filters/types/filterTypes/NumberFilterConfig';
+
+interface QueryUrlNumberValue {
+ operator: string,
+ value: string,
+ exclude: string,
+}
+
+export const numberQueryUrlParser = (query: QueryUrlNumberValue): NumberFilterValue => ({
+ ...query,
+ exclude: query.exclude === 'true',
+ value: +query.value,
+});
diff --git a/frontend/src/shared/modules/filters/config/queryUrlParser/select.parser.ts b/frontend/src/shared/modules/filters/config/queryUrlParser/select.parser.ts
new file mode 100644
index 0000000000..af7f30022f
--- /dev/null
+++ b/frontend/src/shared/modules/filters/config/queryUrlParser/select.parser.ts
@@ -0,0 +1,11 @@
+import { SelectFilterValue } from '@/shared/modules/filters/types/filterTypes/SelectFilterConfig';
+
+interface QueryUrlSelectValue {
+ value: string,
+ exclude: string,
+}
+
+export const selectQueryUrlParser = (query: QueryUrlSelectValue): SelectFilterValue => ({
+ ...query,
+ exclude: query.exclude === 'true',
+});
diff --git a/frontend/src/shared/modules/filters/config/queryUrlParserByType.ts b/frontend/src/shared/modules/filters/config/queryUrlParserByType.ts
new file mode 100644
index 0000000000..b9446611dd
--- /dev/null
+++ b/frontend/src/shared/modules/filters/config/queryUrlParserByType.ts
@@ -0,0 +1,15 @@
+import { FilterConfigType } from '@/shared/modules/filters/types/FilterConfig';
+import { booleanQueryUrlParser } from '@/shared/modules/filters/config/queryUrlParser/boolean.parser';
+import { numberQueryUrlParser } from '@/shared/modules/filters/config/queryUrlParser/number.parser';
+import { dateQueryUrlParser } from '@/shared/modules/filters/config/queryUrlParser/date.parser';
+import { selectQueryUrlParser } from '@/shared/modules/filters/config/queryUrlParser/select.parser';
+import { multiSelectQueryUrlParser } from '@/shared/modules/filters/config/queryUrlParser/multiselect.parser';
+
+export const queryUrlParserByType: Record any) | null> = {
+ [FilterConfigType.BOOLEAN]: booleanQueryUrlParser,
+ [FilterConfigType.NUMBER]: numberQueryUrlParser,
+ [FilterConfigType.DATE]: dateQueryUrlParser,
+ [FilterConfigType.SELECT]: selectQueryUrlParser,
+ [FilterConfigType.MULTISELECT]: multiSelectQueryUrlParser,
+ [FilterConfigType.CUSTOM]: null,
+};
diff --git a/frontend/src/shared/modules/filters/services/filter-api.service.ts b/frontend/src/shared/modules/filters/services/filter-api.service.ts
new file mode 100644
index 0000000000..49e3df897d
--- /dev/null
+++ b/frontend/src/shared/modules/filters/services/filter-api.service.ts
@@ -0,0 +1,66 @@
+import { Filter, FilterConfig } from '@/shared/modules/filters/types/FilterConfig';
+import { SearchFilterConfig } from '@/shared/modules/filters/types/filterTypes/SearchFilterConfig';
+import { FilterQuery } from '@/shared/modules/filters/types/FilterQuery';
+
+export const filterApiService = () => {
+ function buildApiFilter(values: Filter, configuration: Record, searchConfig: SearchFilterConfig): FilterQuery {
+ const {
+ search,
+ relation,
+ order,
+ pagination,
+ config,
+ ...filterValues
+ } = values;
+
+ // Remove when saved views done
+ console.log(config);
+
+ let baseFilters: any[] = [];
+ let filters: any[] = [];
+
+ if (search.length > 0) {
+ baseFilters = [
+ ...baseFilters,
+ ...searchConfig.apiFilterRenderer(search),
+ ];
+ }
+
+ // TODO: config filter parsing
+
+ Object.entries(filterValues).forEach(([key, values]) => {
+ const filter = configuration[key]?.apiFilterRenderer(values);
+ if (filter && filter.length > 0) {
+ filters = [
+ ...filters,
+ ...filter,
+ ];
+ }
+ });
+
+ const filter = {
+ and: [
+ ...baseFilters,
+ {
+ [relation]: filters.length > 0 ? filters : undefined,
+ },
+ ],
+ };
+
+ const orderBy = `${order.prop}_${order.order === 'descending' ? 'DESC' : 'ASC'}`;
+
+ const limit = pagination.perPage;
+ const offset = (pagination.page - 1) * limit;
+
+ return {
+ filter,
+ limit,
+ offset,
+ orderBy,
+ };
+ }
+
+ return {
+ buildApiFilter,
+ };
+};
diff --git a/frontend/src/shared/modules/filters/services/filter-query.service.ts b/frontend/src/shared/modules/filters/services/filter-query.service.ts
new file mode 100644
index 0000000000..162c09dfa5
--- /dev/null
+++ b/frontend/src/shared/modules/filters/services/filter-query.service.ts
@@ -0,0 +1,62 @@
+import { Filter, FilterConfig } from '@/shared/modules/filters/types/FilterConfig';
+import { queryUrlParserByType } from '@/shared/modules/filters/config/queryUrlParserByType';
+import { CustomFilterConfig } from '@/shared/modules/filters/types/filterTypes/CustomFilterConfig';
+
+export const filterQueryService = () => {
+ // Parses url query params and puts them in nested object format
+ function parseQuery(query: Record, config: Record) {
+ const object: Record = {};
+ Object.entries(query).forEach(([key, value]) => {
+ const [mainKey, subKey] = key.split('.');
+ if (subKey) {
+ // If nested value something.test=123 --> {something: {test: 123}}
+ if (!(mainKey in object)) {
+ object[mainKey] = {};
+ }
+ object[mainKey][subKey] = value;
+ } else {
+ // If value not nested something=123 --> {something: 123}
+ object[mainKey] = value;
+ }
+ });
+ // Url params come out as strings so we need to transform them to boolean, number or array
+ Object.keys(object).forEach((key) => {
+ if (key in config) {
+ const { type } = config[key];
+ const queryUrlParser = queryUrlParserByType[type] ?? (config[key] as CustomFilterConfig).queryUrlParser;
+ if (queryUrlParser) {
+ object[key] = queryUrlParser(object[key]);
+ }
+ }
+ });
+ return object;
+ }
+
+ // Transforms value to be used in url query. Transforms array to string
+ function setQueryValue(value: any) {
+ if (Array.isArray(value)) {
+ return value.join(',');
+ }
+ return value;
+ }
+
+ // Prepares query object to be only one level nested for query params to be more readable
+ function setQuery(value: Filter) {
+ const query: Record = {};
+ Object.entries(value).forEach(([key, filterValue]) => {
+ if (typeof filterValue === 'object') {
+ Object.entries(filterValue).forEach(([subKey, subFilterValue]) => {
+ query[`${key}.${subKey}`] = setQueryValue(subFilterValue);
+ });
+ } else {
+ query[key] = setQueryValue(filterValue);
+ }
+ });
+ return query;
+ }
+
+ return {
+ parseQuery,
+ setQuery,
+ };
+};
diff --git a/frontend/src/shared/modules/filters/types/FilterConfig.ts b/frontend/src/shared/modules/filters/types/FilterConfig.ts
index 0ff111a3e7..39034ee465 100644
--- a/frontend/src/shared/modules/filters/types/FilterConfig.ts
+++ b/frontend/src/shared/modules/filters/types/FilterConfig.ts
@@ -25,3 +25,19 @@ export type FilterConfig = NumberFilterConfig
| BooleanFilterConfig
| DateFilterConfig
| CustomFilterConfig
+
+interface FilterObject {
+ search: string;
+ relation: 'and' | 'or',
+ order: {
+ prop: string,
+ order: 'descending' | 'ascending'
+ },
+ pagination: {
+ page: number,
+ perPage: number
+ },
+ config: Record
+}
+
+export type Filter = FilterObject & Record
diff --git a/frontend/src/shared/modules/filters/types/FilterQuery.ts b/frontend/src/shared/modules/filters/types/FilterQuery.ts
new file mode 100644
index 0000000000..7b9449d1dc
--- /dev/null
+++ b/frontend/src/shared/modules/filters/types/FilterQuery.ts
@@ -0,0 +1,6 @@
+export interface FilterQuery {
+ filter: any,
+ orderBy: string,
+ limit: number,
+ offset: number,
+}
diff --git a/frontend/src/shared/modules/filters/types/filterTypes/BooleanFilterConfig.ts b/frontend/src/shared/modules/filters/types/filterTypes/BooleanFilterConfig.ts
index 5c6897bde7..46d38e377f 100644
--- a/frontend/src/shared/modules/filters/types/filterTypes/BooleanFilterConfig.ts
+++ b/frontend/src/shared/modules/filters/types/filterTypes/BooleanFilterConfig.ts
@@ -13,5 +13,5 @@ export interface BooleanFilterConfig extends BaseFilterConfig {
type: FilterConfigType.BOOLEAN;
options: BooleanFilterOptions;
itemLabelRenderer: (value: BooleanFilterValue) => string;
- queryRenderer: (value: BooleanFilterValue) => any;
+ apiFilterRenderer: (value: BooleanFilterValue) => any[];
}
diff --git a/frontend/src/shared/modules/filters/types/filterTypes/CustomFilterConfig.ts b/frontend/src/shared/modules/filters/types/filterTypes/CustomFilterConfig.ts
index 2038681c5c..065be188f7 100644
--- a/frontend/src/shared/modules/filters/types/filterTypes/CustomFilterConfig.ts
+++ b/frontend/src/shared/modules/filters/types/filterTypes/CustomFilterConfig.ts
@@ -5,6 +5,7 @@ export interface CustomFilterConfig extends BaseFilterConfig {
type: FilterConfigType.CUSTOM;
component: any;
options: any;
+ queryUrlParser: ((value: any) => Record) | null;
itemLabelRenderer: (value: any) => string;
- queryRenderer: (value: any) => any;
+ apiFilterRenderer: (value: any) => any[];
}
diff --git a/frontend/src/shared/modules/filters/types/filterTypes/DateFilterConfig.ts b/frontend/src/shared/modules/filters/types/filterTypes/DateFilterConfig.ts
index 17f2d22c51..2859680dde 100644
--- a/frontend/src/shared/modules/filters/types/filterTypes/DateFilterConfig.ts
+++ b/frontend/src/shared/modules/filters/types/filterTypes/DateFilterConfig.ts
@@ -14,5 +14,5 @@ export interface DateFilterConfig extends BaseFilterConfig {
type: FilterConfigType.DATE;
options: DateFilterOptions;
itemLabelRenderer: (value: DateFilterValue) => string;
- queryRenderer: (value: DateFilterValue) => any;
+ apiFilterRenderer: (value: DateFilterValue) => any[];
}
diff --git a/frontend/src/shared/modules/filters/types/filterTypes/MultiSelectFilterConfig.ts b/frontend/src/shared/modules/filters/types/filterTypes/MultiSelectFilterConfig.ts
index 252caa467f..9a73ade59f 100644
--- a/frontend/src/shared/modules/filters/types/filterTypes/MultiSelectFilterConfig.ts
+++ b/frontend/src/shared/modules/filters/types/filterTypes/MultiSelectFilterConfig.ts
@@ -22,5 +22,5 @@ export interface MultiSelectFilterConfig extends BaseFilterConfig {
type: FilterConfigType.MULTISELECT;
options: MultiSelectFilterOptions;
itemLabelRenderer: (value: MultiSelectFilterValue) => string;
- queryRenderer: (value: MultiSelectFilterValue) => any;
+ apiFilterRenderer: (value: MultiSelectFilterValue) => any[];
}
diff --git a/frontend/src/shared/modules/filters/types/filterTypes/NumberFilterConfig.ts b/frontend/src/shared/modules/filters/types/filterTypes/NumberFilterConfig.ts
index 4a83ef6995..b22085ee95 100644
--- a/frontend/src/shared/modules/filters/types/filterTypes/NumberFilterConfig.ts
+++ b/frontend/src/shared/modules/filters/types/filterTypes/NumberFilterConfig.ts
@@ -13,5 +13,5 @@ export interface NumberFilterConfig extends BaseFilterConfig {
type: FilterConfigType.NUMBER;
options: NumberFilterOptions;
itemLabelRenderer: (value: NumberFilterValue) => string;
- queryRenderer: (value: NumberFilterValue) => any;
+ apiFilterRenderer: (value: NumberFilterValue) => any[];
}
diff --git a/frontend/src/shared/modules/filters/types/filterTypes/SearchFilterConfig.ts b/frontend/src/shared/modules/filters/types/filterTypes/SearchFilterConfig.ts
new file mode 100644
index 0000000000..e1bb1a1236
--- /dev/null
+++ b/frontend/src/shared/modules/filters/types/filterTypes/SearchFilterConfig.ts
@@ -0,0 +1,5 @@
+/* eslint-disable no-unused-vars */
+export interface SearchFilterConfig {
+ placeholder?: string;
+ apiFilterRenderer: (value: string) => any[];
+}
diff --git a/frontend/src/shared/modules/filters/types/filterTypes/SelectFilterConfig.ts b/frontend/src/shared/modules/filters/types/filterTypes/SelectFilterConfig.ts
index fe6ce6d146..1a97b7e07d 100644
--- a/frontend/src/shared/modules/filters/types/filterTypes/SelectFilterConfig.ts
+++ b/frontend/src/shared/modules/filters/types/filterTypes/SelectFilterConfig.ts
@@ -23,5 +23,5 @@ export interface SelectFilterConfig extends BaseFilterConfig {
type: FilterConfigType.SELECT;
options: SelectFilterOptions;
itemLabelRenderer: (value: SelectFilterValue) => string;
- queryRenderer: (value: SelectFilterValue) => any;
+ apiFilterRenderer: (value: SelectFilterValue) => any[];
}