Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions frontend/src/assets/scss/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
@import 'badge';
@import 'content';
@import 'popover';
@import 'tooltip';
@import 'html';
@import 'form/date-picker';
@import 'form/radio';
Expand Down
21 changes: 21 additions & 0 deletions frontend/src/assets/scss/tooltip.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
[data-tooltip]{
position:relative;

&:before{
content: attr(data-tooltip);
position:absolute;

bottom: calc(100% + 0.75rem);
left:50%;
transform:translateX(-50%);

width: max-content;
max-width: 20rem;
display: block;
@apply bg-gray-900 text-white rounded-md text-xs leading-5 py-0.5 px-1.5 opacity-0 transition invisible;
}

&:hover:before {
@apply opacity-100 visible;
}
}
35 changes: 25 additions & 10 deletions frontend/src/modules/activity/config/filters/member/config.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,36 @@
import { FilterConfigType } from '@/shared/modules/filters/types/FilterConfig';
import { MultiSelectFilterConfig, MultiSelectFilterValue } from '@/shared/modules/filters/types/filterTypes/MultiSelectFilterConfig';
import { itemLabelRendererByType } from '@/shared/modules/filters/config/itemLabelRendererByType';
import { apiFilterRendererByType } from '@/shared/modules/filters/config/apiFilterRendererByType';
import {
MultiSelectAsyncFilterConfig,
MultiSelectAsyncFilterOptions, MultiSelectAsyncFilterValue,
} from '@/shared/modules/filters/types/filterTypes/MultiSelectAsyncFilterConfig';
import { MemberService } from '@/modules/member/member-service';

const member: MultiSelectFilterConfig = {
const member: MultiSelectAsyncFilterConfig = {
id: 'member',
label: 'Member',
iconClass: 'ri-account-circle-line',
type: FilterConfigType.MULTISELECT,
type: FilterConfigType.MULTISELECT_ASYNC,
options: {
// TODO: load this options remote
options: [],
remoteMethod: (query) => MemberService.listAutocomplete(query, 10)
.then((data: any[]) => data.map((member) => ({
label: member.label,
value: member.id,
}))),
remotePopulateItems: (ids: string[]) => MemberService.list({
id: { in: ids },
}, null, ids.length, 0, false)
.then(({ rows }: any) => rows.map((member: any) => ({
label: member.displayName,
value: member.id,
}))),
},
itemLabelRenderer(value: MultiSelectFilterValue): string {
return `<b>Member</b> ${value?.value.join(',') || '...'}`;
itemLabelRenderer(value: MultiSelectAsyncFilterValue, options: MultiSelectAsyncFilterOptions, data: any): string {
return itemLabelRendererByType[FilterConfigType.MULTISELECT_ASYNC]('Member', value, options, data);
},
apiFilterRenderer(value): any[] {
console.log(value);
return [];
apiFilterRenderer(value: MultiSelectAsyncFilterValue): any[] {
return apiFilterRendererByType[FilterConfigType.MULTISELECT_ASYNC]('member', value);
},
};

Expand Down
2 changes: 1 addition & 1 deletion frontend/src/plugins/sanitize.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ export const vueSanitizeOptions = {
'loading',
],
input: ['checked', 'disabled', 'type'],
span: ['class', 'style'],
span: ['class', 'style', 'data-tooltip'],
},
selfClosing: [
'img',
Expand Down
9 changes: 4 additions & 5 deletions frontend/src/shared/modules/filters/components/FilterItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,15 @@
<span
class="text-xs text-gray-600 filter-item-text leading-6"
v-html="$sanitize(
(props.modelValue && config.itemLabelRenderer(props.modelValue, props.config.options))
(props.modelValue && config.itemLabelRenderer(props.modelValue, props.config.options, data))
|| `<span class='!text-gray-500'>${config.label}...</span>`,
)"
/>
</div>
</template>

<div>
<component :is="getComponent" v-if="getComponent" v-model="form" :config="props.config" v-bind="props.config.options" />
<component :is="getComponent" v-if="getComponent" v-model="form" v-model:data="data" :config="props.config" v-bind="props.config.options" />
</div>
<div class="flex justify-end items-center border-t py-3 px-4">
<el-button class="btn btn--transparent btn--sm !h-8 mr-2" @click="close">
Expand All @@ -49,9 +49,7 @@
</template>

<script setup lang="ts">
import {
computed, ref, watch,
} from 'vue';
import { computed, ref, watch } from 'vue';
import { FilterConfig, FilterConfigType } from '@/shared/modules/filters/types/FilterConfig';
import { filterComponentByType } from '@/shared/modules/filters/config/filterComponentByType';
import useVuelidate from '@vuelidate/core';
Expand All @@ -65,6 +63,7 @@ const props = defineProps<{
const emit = defineEmits<{(e: 'update:modelValue', value: any): void, (e: 'remove'): void, (e: 'update:open', value: string): void}>();

const form = ref({});
const data = ref({});

const isOpen = computed({
get() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
<template>
<div v-if="props.modelValue">
<cr-filter-include-switch
v-if="!props.hideIncludeSwitch"
v-model="include"
/>
<div class="px-4 pt-3">
<el-select
v-model="data.selected"
multiple
remote
filterable
:reserve-keyword="false"
placeholder="Select options"
:remote-method="searchOptions"
:teleported="false"
class="filter-multiselect"
popper-class="filter-multiselect-popper"
:loading="loading"
>
<el-option
v-for="option of data.selected"
:key="option.value"
:label="option.label"
:value="option"
class="!hidden"
/>
<el-option
v-for="option of filteredOptions"
:key="option.value"
:label="option.label"
:value="option"
>
<el-checkbox
:model-value="props.modelValue.value.includes(option.value)"
class="filter-checkbox h-4"
/>
{{ option.label }}
</el-option>
</el-select>
</div>
</div>
</template>

<script setup lang="ts">
import {
computed, onMounted, ref, watch,
} from 'vue';
import { required } from '@vuelidate/validators';
import useVuelidate from '@vuelidate/core';
import CrFilterIncludeSwitch from '@/shared/modules/filters/components/partials/FilterIncludeSwitch.vue';
import {
MultiSelectAsyncFilterConfig,
MultiSelectAsyncFilterOption,
MultiSelectAsyncFilterOptions,
MultiSelectAsyncFilterValue,
} from '@/shared/modules/filters/types/filterTypes/MultiSelectAsyncFilterConfig';
import { MultiSelectFilterValue } from '@/shared/modules/filters/types/filterTypes/MultiSelectFilterConfig';

const props = defineProps<{
modelValue: MultiSelectAsyncFilterValue,
data: any,
config: MultiSelectAsyncFilterConfig
} & MultiSelectAsyncFilterOptions>();

const emit = defineEmits<{(e: 'update:modelValue', value: MultiSelectAsyncFilterValue): void, (e: 'update:data', value: any): void}>();

const form = computed({
get: () => props.modelValue,
set: (value: MultiSelectFilterValue) => emit('update:modelValue', value),
});

const data = computed({
get: () => props.data,
set: (value: any) => emit('update:data', value),
});

const defaultForm: MultiSelectAsyncFilterValue = {
value: [],
include: true,
};

const rules: any = {
value: {
required,
},
};

useVuelidate(rules, form);

const include = computed<boolean>({
get() {
return props.modelValue.include;
},
set(value: boolean) {
emit('update:modelValue', {
...props.modelValue,
include: value,
});
},
});

watch(() => props.modelValue.value, (value: string[]) => {
if (value.length !== data.value.selected?.length) {
console.log(value);
props.remotePopulateItems(value)
.then((options) => {
data.value.selected = options;
});
}
}, { immediate: true });

watch(() => data.value.selected, (value) => {
emit('update:modelValue', {
...props.modelValue,
value: value.map((v) => v.value),
});
});

const loading = ref<boolean>(false);
const filteredOptions = ref<MultiSelectAsyncFilterOption[]>([]);

const searchOptions = (query: string) => {
loading.value = true;
props.remoteMethod(query)
.then((options) => {
filteredOptions.value = options;
})
.finally(() => {
loading.value = false;
});
};

onMounted(() => {
searchOptions('');
if (!props.modelValue.value || Object.keys(props.modelValue.value).length === 0) {
emit('update:modelValue', defaultForm);
}
if (!data.value.selected) {
data.value.selected = [];
}
});
</script>

<script lang="ts">
export default {
name: 'CrMultiSelectAsyncFilter',
};
</script>

<style lang="scss">
.filter-multiselect {
@apply w-full relative;

.el-select__tags{
@apply top-1.5 transform-none;
}

.el-select-dropdown__item {
@apply px-3 #{!important};

&.selected{
@apply bg-brand-25 font-normal px-3 #{!important};
}

&:after{
@apply hidden;
}
}
.el-input__wrapper,
.el-input__wrapper.is-focus,
.el-input__wrapper:hover {
@apply bg-gray-50 shadow-none rounded-md border border-gray-50 transition #{!important};

input {
&,
&:hover,
&:focus {
border: none !important;
@apply bg-gray-50 shadow-none outline-none h-full min-h-8;
}
}
}

.el-tag{
@apply bg-white #{!important};
}
}
.filter-multiselect-popper {
@apply relative inset-0 block shadow-none h-auto opacity-100 transform-none #{!important};

.el-select-dropdown{
@apply -mx-4 p-0 mt-3 border-t border-gray-100;

.el-scrollbar__view{
@apply py-3 px-3;
}
}
}
</style>
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
<template>
<div v-if="form">
<cr-filter-include-switch v-if="!props.hideIncludeSwitch" v-model="form.include" />
<div class="p-4">
Multi select filter
<!-- TODO: prepare multiselect filter -->
<div v-if="allOptions.length <= 7">
<cr-multi-select-checkbox-filter
v-model="form.value"
:config="props.config"
:options="props.options"
/>
</div>
<div v-else>
<cr-multi-select-tags-filter
v-model="form.value"
:config="props.config"
:options="props.options"
/>
</div>
</div>
</template>
Expand All @@ -18,13 +28,19 @@ import {
import { required } from '@vuelidate/validators';
import useVuelidate from '@vuelidate/core';
import CrFilterIncludeSwitch from '@/shared/modules/filters/components/partials/FilterIncludeSwitch.vue';
import CrMultiSelectCheckboxFilter
from '@/shared/modules/filters/components/filterTypes/multiselect/MultiSelectCheckboxFilter.vue';
import CrMultiSelectTagsFilter
from '@/shared/modules/filters/components/filterTypes/multiselect/MultiSelectTagsFilter.vue';

const props = defineProps<{
modelValue: MultiSelectFilterValue,
config: MultiSelectFilterConfig
} & MultiSelectFilterOptions>();

const emit = defineEmits<{(e: 'update:modelValue', value: MultiSelectFilterValue): void}>();
const emit = defineEmits<{(e: 'update:modelValue', value: MultiSelectFilterValue): void, (e: 'update:data', value: any): void}>();

const allOptions = computed(() => props.options.map((g) => g.options).flat());

const form = computed({
get: () => props.modelValue,
Expand Down
Loading