diff --git a/frontend/src/assets/scss/form/dropdown.scss b/frontend/src/assets/scss/form/dropdown.scss index e0d5d69fda..acc3212faf 100644 --- a/frontend/src/assets/scss/form/dropdown.scss +++ b/frontend/src/assets/scss/form/dropdown.scss @@ -36,7 +36,7 @@ // Selected state &.is-selected, &:focus.is-selected { - @apply relative bg-brand-50; + @apply relative bg-brand-50 text-gray-900 cursor-default; i { @apply mr-3 text-brand-600; } diff --git a/frontend/src/modules/activity/config/filters/activityType/config.ts b/frontend/src/modules/activity/config/filters/activityType/config.ts index 97774f5564..76e461d0be 100644 --- a/frontend/src/modules/activity/config/filters/activityType/config.ts +++ b/frontend/src/modules/activity/config/filters/activityType/config.ts @@ -2,6 +2,7 @@ import { FilterConfigType } from '@/shared/modules/filters/types/FilterConfig'; import { CustomFilterConfig } from '@/shared/modules/filters/types/filterTypes/CustomFilterConfig'; import ActivityTypeFilter from '@/modules/activity/config/filters/activityType/ActivityTypeFilter.vue'; import { SelectFilterValue } from '@/shared/modules/filters/types/filterTypes/SelectFilterConfig'; +import { queryUrlParserByType } from '@/shared/modules/filters/config/queryUrlParserByType'; const activityType: CustomFilterConfig = { id: 'activityType', @@ -10,12 +11,13 @@ const activityType: CustomFilterConfig = { component: ActivityTypeFilter, options: { }, + queryUrlParser: queryUrlParserByType[FilterConfigType.SELECT], itemLabelRenderer(value: SelectFilterValue): string { return `Activity type ${value || '...'}`; }, - queryRenderer(value: SelectFilterValue): string { + apiFilterRenderer(value): any[] { console.log(value); - return ''; + return []; }, }; diff --git a/frontend/src/modules/activity/config/filters/channel/config.ts b/frontend/src/modules/activity/config/filters/channel/config.ts index 9a2b0a137a..91488c8a7a 100644 --- a/frontend/src/modules/activity/config/filters/channel/config.ts +++ b/frontend/src/modules/activity/config/filters/channel/config.ts @@ -2,6 +2,7 @@ import { FilterConfigType } from '@/shared/modules/filters/types/FilterConfig'; import { CustomFilterConfig } from '@/shared/modules/filters/types/filterTypes/CustomFilterConfig'; import { MultiSelectFilterValue } from '@/shared/modules/filters/types/filterTypes/MultiSelectFilterConfig'; import ChannelFilter from '@/modules/activity/config/filters/channel/ChannelFilter.vue'; +import { queryUrlParserByType } from '@/shared/modules/filters/config/queryUrlParserByType'; const channel: CustomFilterConfig = { id: 'channel', @@ -10,12 +11,13 @@ const channel: CustomFilterConfig = { component: ChannelFilter, options: { }, + queryUrlParser: queryUrlParserByType[FilterConfigType.MULTISELECT], itemLabelRenderer(value: MultiSelectFilterValue): string { return `Channel ${value || '...'}`; }, - queryRenderer(value: MultiSelectFilterValue): string { + apiFilterRenderer(value): any[] { console.log(value); - return ''; + return []; }, }; diff --git a/frontend/src/modules/activity/config/filters/date/config.ts b/frontend/src/modules/activity/config/filters/date/config.ts index dc27352d26..41d7e16cd8 100644 --- a/frontend/src/modules/activity/config/filters/date/config.ts +++ b/frontend/src/modules/activity/config/filters/date/config.ts @@ -12,9 +12,9 @@ const date: SelectFilterConfig = { itemLabelRenderer(value: SelectFilterValue): string { return `Date ${value || '...'}`; }, - queryRenderer(value: SelectFilterValue): string { + apiFilterRenderer(value): any[] { console.log(value); - return ''; + return []; }, }; diff --git a/frontend/src/modules/activity/config/filters/member/config.ts b/frontend/src/modules/activity/config/filters/member/config.ts index 8267546510..d5c74bbd3f 100644 --- a/frontend/src/modules/activity/config/filters/member/config.ts +++ b/frontend/src/modules/activity/config/filters/member/config.ts @@ -12,9 +12,9 @@ const member: MultiSelectFilterConfig = { itemLabelRenderer(value: MultiSelectFilterValue): string { return `Member ${value?.value.join(',') || '...'}`; }, - queryRenderer(value: MultiSelectFilterValue): string { + apiFilterRenderer(value): any[] { console.log(value); - return ''; + return []; }, }; diff --git a/frontend/src/modules/activity/config/filters/platform/config.ts b/frontend/src/modules/activity/config/filters/platform/config.ts index 0b8cf4d09c..e2f9a8a9af 100644 --- a/frontend/src/modules/activity/config/filters/platform/config.ts +++ b/frontend/src/modules/activity/config/filters/platform/config.ts @@ -26,9 +26,9 @@ const platform: MultiSelectFilterConfig = { itemLabelRenderer(value: MultiSelectFilterValue): string { return `Platform ${value?.value.join(',') || '...'}`; }, - queryRenderer(value: MultiSelectFilterValue): string { + apiFilterRenderer(value): any[] { console.log(value); - return ''; + return []; }, }; diff --git a/frontend/src/modules/activity/config/filters/sentiment/config.ts b/frontend/src/modules/activity/config/filters/sentiment/config.ts index 44c07d2bf2..31173f0843 100644 --- a/frontend/src/modules/activity/config/filters/sentiment/config.ts +++ b/frontend/src/modules/activity/config/filters/sentiment/config.ts @@ -12,9 +12,9 @@ const sentiment: MultiSelectFilterConfig = { itemLabelRenderer(value: MultiSelectFilterValue): string { return `Sentiment ${value?.value.join(',') || '...'}`; }, - queryRenderer(value: MultiSelectFilterValue): string { + apiFilterRenderer(value): any[] { console.log(value); - return ''; + return []; }, }; diff --git a/frontend/src/modules/member/config/filters/activeOn/config.ts b/frontend/src/modules/member/config/filters/activeOn/config.ts index a33cc1a181..33f2833e23 100644 --- a/frontend/src/modules/member/config/filters/activeOn/config.ts +++ b/frontend/src/modules/member/config/filters/activeOn/config.ts @@ -20,11 +20,11 @@ const activeOn: MultiSelectFilterConfig = { ], }, itemLabelRenderer(value: MultiSelectFilterValue): string { - return `Active On ${value?.value.join(',') || '...'}`; + return `Active On ${value?.value.join(',') || '...'}`; }, - queryRenderer(value: MultiSelectFilterValue): string { + apiFilterRenderer(value): any[] { console.log(value); - return ''; + return []; }, }; diff --git a/frontend/src/modules/member/config/filters/activityType/config.ts b/frontend/src/modules/member/config/filters/activityType/config.ts index dad9246e45..63c0c95f2b 100644 --- a/frontend/src/modules/member/config/filters/activityType/config.ts +++ b/frontend/src/modules/member/config/filters/activityType/config.ts @@ -2,6 +2,7 @@ import { FilterConfigType } from '@/shared/modules/filters/types/FilterConfig'; import { CustomFilterConfig } from '@/shared/modules/filters/types/filterTypes/CustomFilterConfig'; import ActivityTypeFilter from '@/modules/activity/config/filters/activityType/ActivityTypeFilter.vue'; import { SelectFilterValue } from '@/shared/modules/filters/types/filterTypes/SelectFilterConfig'; +import { queryUrlParserByType } from '@/shared/modules/filters/config/queryUrlParserByType'; const activityType: CustomFilterConfig = { id: 'activityType', @@ -10,12 +11,13 @@ const activityType: CustomFilterConfig = { component: ActivityTypeFilter, options: { }, + queryUrlParser: queryUrlParserByType[FilterConfigType.SELECT], itemLabelRenderer(value: SelectFilterValue): string { - return `Active on ${value || '...'}`; + return `Active on ${value || '...'}`; }, - queryRenderer(value: SelectFilterValue): string { + apiFilterRenderer(value): any[] { console.log(value); - return ''; + return []; }, }; diff --git a/frontend/src/modules/member/config/filters/avgSentiment/config.ts b/frontend/src/modules/member/config/filters/avgSentiment/config.ts index af132c7bf1..96c17d23f5 100644 --- a/frontend/src/modules/member/config/filters/avgSentiment/config.ts +++ b/frontend/src/modules/member/config/filters/avgSentiment/config.ts @@ -10,11 +10,11 @@ const avgSentiment: MultiSelectFilterConfig = { options, }, itemLabelRenderer(value: MultiSelectFilterValue): string { - return `Avg. sentiment ${value?.value.join(',') || '...'}`; + return `Avg. sentiment ${value?.value.join(',') || '...'}`; }, - queryRenderer(value: MultiSelectFilterValue): string { + apiFilterRenderer(value): any[] { console.log(value); - return ''; + return []; }, }; diff --git a/frontend/src/modules/member/config/filters/engagementLevel/config.ts b/frontend/src/modules/member/config/filters/engagementLevel/config.ts index e961766d8a..e5ec26b1f9 100644 --- a/frontend/src/modules/member/config/filters/engagementLevel/config.ts +++ b/frontend/src/modules/member/config/filters/engagementLevel/config.ts @@ -10,11 +10,11 @@ const engagementLevel: MultiSelectFilterConfig = { options, }, itemLabelRenderer(value: MultiSelectFilterValue): string { - return `Engagement level ${value?.value.join(',') || '...'}`; + return `Engagement level ${value?.value.join(',') || '...'}`; }, - queryRenderer(value: MultiSelectFilterValue): string { + apiFilterRenderer(value): any[] { console.log(value); - return ''; + return []; }, }; diff --git a/frontend/src/modules/member/config/filters/enrichedMember/config.ts b/frontend/src/modules/member/config/filters/enrichedMember/config.ts index da8515acc5..a798a0bc19 100644 --- a/frontend/src/modules/member/config/filters/enrichedMember/config.ts +++ b/frontend/src/modules/member/config/filters/enrichedMember/config.ts @@ -7,12 +7,11 @@ const enrichedMember: BooleanFilterConfig = { type: FilterConfigType.BOOLEAN, options: {}, itemLabelRenderer(value): string { - return `Enriched member ${value?.value ? 'True' : 'False'}`; + return `Enriched member ${value?.value ? 'True' : 'False'}`; }, - queryRenderer(value) { - return { - activityCount: value, - }; + apiFilterRenderer(value): any[] { + console.log(value); + return []; }, }; diff --git a/frontend/src/modules/member/config/filters/identities/config.ts b/frontend/src/modules/member/config/filters/identities/config.ts index a103a9033b..cb569d9ee8 100644 --- a/frontend/src/modules/member/config/filters/identities/config.ts +++ b/frontend/src/modules/member/config/filters/identities/config.ts @@ -20,11 +20,11 @@ const identities: MultiSelectFilterConfig = { ], }, itemLabelRenderer(value: MultiSelectFilterValue): string { - return `Identities ${value?.value.join(',') || '...'}`; + return `Identities ${value?.value.join(',') || '...'}`; }, - queryRenderer(value: MultiSelectFilterValue): string { + apiFilterRenderer(value): any[] { console.log(value); - return ''; + return []; }, }; diff --git a/frontend/src/modules/member/config/filters/joinedDate/config.ts b/frontend/src/modules/member/config/filters/joinedDate/config.ts index dcc8250a65..5b83a2918d 100644 --- a/frontend/src/modules/member/config/filters/joinedDate/config.ts +++ b/frontend/src/modules/member/config/filters/joinedDate/config.ts @@ -7,11 +7,11 @@ const joinedDate: DateFilterConfig = { type: FilterConfigType.DATE, options: {}, itemLabelRenderer(value): string { - return `Joined date ${value.value || '...'}`; + return `Joined date ${value.value || '...'}`; }, - queryRenderer(value): string { + apiFilterRenderer(value): any[] { console.log(value); - return ''; + return []; }, }; diff --git a/frontend/src/modules/member/config/filters/lastActivityDate/config.ts b/frontend/src/modules/member/config/filters/lastActivityDate/config.ts index 7f1d44d2dc..8ea5600954 100644 --- a/frontend/src/modules/member/config/filters/lastActivityDate/config.ts +++ b/frontend/src/modules/member/config/filters/lastActivityDate/config.ts @@ -7,11 +7,11 @@ const lastActivityDate: DateFilterConfig = { type: FilterConfigType.DATE, options: {}, itemLabelRenderer(value): string { - return `Last activity date ${value.value || '...'}`; + return `Last activity date ${value.value || '...'}`; }, - queryRenderer(value): string { + apiFilterRenderer(value): any[] { console.log(value); - return ''; + return []; }, }; diff --git a/frontend/src/modules/member/config/filters/main.ts b/frontend/src/modules/member/config/filters/main.ts index 3f7e53f5cd..4c1d249051 100644 --- a/frontend/src/modules/member/config/filters/main.ts +++ b/frontend/src/modules/member/config/filters/main.ts @@ -1,4 +1,5 @@ import { FilterConfig } from '@/shared/modules/filters/types/FilterConfig'; +import { SearchFilterConfig } from '@/shared/modules/filters/types/filterTypes/SearchFilterConfig'; import noOfActivities from './noOfActivities/config'; import noOfOSSContributions from './noOfOSSContributions/config'; import activeOn from './activeOn/config'; @@ -26,3 +27,17 @@ export const memberFilters: Record = { reach, tags, }; + +export const memberSearchFilter: SearchFilterConfig = { + placeholder: 'Search members', + apiFilterRenderer(value: string): any[] { + return [ + { + or: [ + { displayName: { textContains: value } }, + { emails: { contains: [value] } }, + ], + }, + ]; + }, +}; diff --git a/frontend/src/modules/member/config/filters/noOfActivities/config.ts b/frontend/src/modules/member/config/filters/noOfActivities/config.ts index 3408fc038d..4e9715ed57 100644 --- a/frontend/src/modules/member/config/filters/noOfActivities/config.ts +++ b/frontend/src/modules/member/config/filters/noOfActivities/config.ts @@ -7,12 +7,12 @@ const noOfActivities: NumberFilterConfig = { type: FilterConfigType.NUMBER, options: {}, itemLabelRenderer(value): string { - return `# of activities ${value?.value || '...'}`; + return `# of activities ${value?.value || '...'}`; }, - queryRenderer(value) { - return { - activityCount: value, - }; + apiFilterRenderer({ value }): any[] { + return [ + { activityCount: { eq: value } }, + ]; }, }; diff --git a/frontend/src/modules/member/config/filters/noOfOSSContributions/config.ts b/frontend/src/modules/member/config/filters/noOfOSSContributions/config.ts index 593aeb2a30..e2c1adb685 100644 --- a/frontend/src/modules/member/config/filters/noOfOSSContributions/config.ts +++ b/frontend/src/modules/member/config/filters/noOfOSSContributions/config.ts @@ -7,12 +7,13 @@ const noOfOSSContributions: NumberFilterConfig = { type: FilterConfigType.NUMBER, options: {}, itemLabelRenderer(value): string { - return `# of open source contributions ${value?.value || '...'}`; + return `# of open source contributions ${value?.value || '...'}`; }, - queryRenderer(value) { - return { - activityCount: value, - }; + apiFilterRenderer({ value }): any[] { + console.log(value); + return [ + { numberOfOpenSourceContributions: { eq: value } }, + ]; }, }; diff --git a/frontend/src/modules/member/config/filters/reach/config.ts b/frontend/src/modules/member/config/filters/reach/config.ts index fe48a032b0..5965612c18 100644 --- a/frontend/src/modules/member/config/filters/reach/config.ts +++ b/frontend/src/modules/member/config/filters/reach/config.ts @@ -7,12 +7,11 @@ const reach: NumberFilterConfig = { type: FilterConfigType.NUMBER, options: {}, itemLabelRenderer(value): string { - return `Reach ${value?.value || '...'}`; + return `Reach ${value?.value || '...'}`; }, - queryRenderer(value) { - return { - activityCount: value, - }; + apiFilterRenderer(value): any[] { + console.log(value); + return []; }, }; diff --git a/frontend/src/modules/member/config/filters/tags/config.ts b/frontend/src/modules/member/config/filters/tags/config.ts index c7069369e0..39054483cc 100644 --- a/frontend/src/modules/member/config/filters/tags/config.ts +++ b/frontend/src/modules/member/config/filters/tags/config.ts @@ -10,11 +10,11 @@ const tags: MultiSelectFilterConfig = { options: [], }, itemLabelRenderer(value: MultiSelectFilterValue): string { - return `Tags ${value?.value.join(',') || '...'}`; + return `Tags ${value?.value.join(',') || '...'}`; }, - queryRenderer(value: MultiSelectFilterValue): string { + apiFilterRenderer(value): any[] { console.log(value); - return ''; + return []; }, }; diff --git a/frontend/src/modules/member/member-service.js b/frontend/src/modules/member/member-service.js index 81b113aee1..0e248ed5d9 100644 --- a/frontend/src/modules/member/member-service.js +++ b/frontend/src/modules/member/member-service.js @@ -252,7 +252,7 @@ export class MemberService { offset, }; - const response = await authAxios.get( + return authAxios.get( `/tenant/${tenantId}/membersToMerge`, { params, @@ -260,9 +260,8 @@ export class MemberService { Authorization: sampleTenant?.token, }, }, - ); - - return response.data; + ) + .then(({ data }) => Promise.resolve(data)); } static async getCustomAttribute(id) { diff --git a/frontend/src/modules/member/pages/member-list-page.vue b/frontend/src/modules/member/pages/member-list-page.vue index fd2a19af4c..b46bfec76d 100644 --- a/frontend/src/modules/member/pages/member-list-page.vue +++ b/frontend/src/modules/member/pages/member-list-page.vue @@ -27,7 +27,7 @@ - --> + + + + + + - diff --git a/frontend/src/modules/member/store/pinia/actions.ts b/frontend/src/modules/member/store/pinia/actions.ts new file mode 100644 index 0000000000..02a61bed5e --- /dev/null +++ b/frontend/src/modules/member/store/pinia/actions.ts @@ -0,0 +1,2 @@ +export default { +}; diff --git a/frontend/src/modules/member/store/pinia/getters.ts b/frontend/src/modules/member/store/pinia/getters.ts new file mode 100644 index 0000000000..02a61bed5e --- /dev/null +++ b/frontend/src/modules/member/store/pinia/getters.ts @@ -0,0 +1,2 @@ +export default { +}; diff --git a/frontend/src/modules/member/store/pinia/index.ts b/frontend/src/modules/member/store/pinia/index.ts new file mode 100644 index 0000000000..a7e2af13fa --- /dev/null +++ b/frontend/src/modules/member/store/pinia/index.ts @@ -0,0 +1,13 @@ +import { defineStore } from 'pinia'; +import state from './state'; +import getters from './getters'; +import actions from './actions'; + +export const useMemberStore = defineStore( + 'member', + { + state, + getters, + actions, + }, +); diff --git a/frontend/src/modules/member/store/pinia/state.ts b/frontend/src/modules/member/store/pinia/state.ts new file mode 100644 index 0000000000..254eb23aeb --- /dev/null +++ b/frontend/src/modules/member/store/pinia/state.ts @@ -0,0 +1,20 @@ +import { Filter } from '@/shared/modules/filters/types/FilterConfig'; + +interface MemberState { + filters: Filter +} + +export default () => ({ + filters: { + search: '', + relation: 'and', + pagination: { + page: 1, + perPage: 20, + }, + order: { + prop: 'createdBy', + order: 'descending', + }, + } as Filter, +} as MemberState); diff --git a/frontend/src/modules/organization/config/filters/activeOn/config.ts b/frontend/src/modules/organization/config/filters/activeOn/config.ts index bd52612a2e..6cbefda175 100644 --- a/frontend/src/modules/organization/config/filters/activeOn/config.ts +++ b/frontend/src/modules/organization/config/filters/activeOn/config.ts @@ -12,9 +12,9 @@ const activeOn: MultiSelectFilterConfig = { itemLabelRenderer(value): string { return `Last activity date ${value.value.join(', ') || '...'}`; }, - queryRenderer(value): string { + apiFilterRenderer(value): any[] { console.log(value); - return ''; + return []; }, }; diff --git a/frontend/src/modules/organization/config/filters/joinedDate/config.ts b/frontend/src/modules/organization/config/filters/joinedDate/config.ts index dcc8250a65..992793bb3e 100644 --- a/frontend/src/modules/organization/config/filters/joinedDate/config.ts +++ b/frontend/src/modules/organization/config/filters/joinedDate/config.ts @@ -9,9 +9,9 @@ const joinedDate: DateFilterConfig = { itemLabelRenderer(value): string { return `Joined date ${value.value || '...'}`; }, - queryRenderer(value): string { + apiFilterRenderer(value): any[] { console.log(value); - return ''; + return []; }, }; diff --git a/frontend/src/modules/organization/config/filters/lastActivityDate/config.ts b/frontend/src/modules/organization/config/filters/lastActivityDate/config.ts index 7f1d44d2dc..a14070a752 100644 --- a/frontend/src/modules/organization/config/filters/lastActivityDate/config.ts +++ b/frontend/src/modules/organization/config/filters/lastActivityDate/config.ts @@ -9,9 +9,9 @@ const lastActivityDate: DateFilterConfig = { itemLabelRenderer(value): string { return `Last activity date ${value.value || '...'}`; }, - queryRenderer(value): string { + apiFilterRenderer(value): any[] { console.log(value); - return ''; + return []; }, }; diff --git a/frontend/src/modules/organization/config/filters/noOfActivities/config.ts b/frontend/src/modules/organization/config/filters/noOfActivities/config.ts index 3408fc038d..1f856f56d2 100644 --- a/frontend/src/modules/organization/config/filters/noOfActivities/config.ts +++ b/frontend/src/modules/organization/config/filters/noOfActivities/config.ts @@ -9,10 +9,9 @@ const noOfActivities: NumberFilterConfig = { itemLabelRenderer(value): string { return `# of activities ${value?.value || '...'}`; }, - queryRenderer(value) { - return { - activityCount: value, - }; + apiFilterRenderer(value): any[] { + console.log(value); + return []; }, }; diff --git a/frontend/src/modules/organization/config/filters/noOfMembers/config.ts b/frontend/src/modules/organization/config/filters/noOfMembers/config.ts index 3b4b0f6c05..bc8c5a93de 100644 --- a/frontend/src/modules/organization/config/filters/noOfMembers/config.ts +++ b/frontend/src/modules/organization/config/filters/noOfMembers/config.ts @@ -9,9 +9,9 @@ const noOfMembers: NumberFilterConfig = { itemLabelRenderer(value): string { return `# of members ${value.value || '...'}`; }, - queryRenderer(value): string { + apiFilterRenderer(value): any[] { console.log(value); - return ''; + return []; }, }; diff --git a/frontend/src/modules/organization/pages/organization-list-page.vue b/frontend/src/modules/organization/pages/organization-list-page.vue index d43e76c994..9a740ec260 100644 --- a/frontend/src/modules/organization/pages/organization-list-page.vue +++ b/frontend/src/modules/organization/pages/organization-list-page.vue @@ -34,7 +34,7 @@ - +
-
- +
+ + +
@@ -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[]; }