-
Notifications
You must be signed in to change notification settings - Fork 471
Open
Labels
Description
Affected Projects
Vue.JS
Library Version: x.y.z
"@appbaseio/reactivesearch-vue": "3.1.0-alpha.1",
Describe the bug
I am migrating our app from vue 2 to vue 3, so also need to migrate appbase. there is is migration docs which we following. everything seems work but when we filter it gives this error
bugsnag.js:2688 {"settings":{"took":6,"searchTook":2},"SearchResults":{"error":{"root_cause":[{"type":"illegal_argument_exception","reason":"Text fields are not optimised for operations that require per-document field data like aggregations and sorting, so these operations are disabled by default. Please use a keyword field instead. Alternatively, set fielddata=true on [email] in order to load field data by uninverting the inverted index. Note that this can use significant memory."}],"type":"search_phase_execution_exception","reason":"all shards failed","phase":"query","grouped":true,"failed_shards":[{"shard":0,"index":"4760d3c8-ab04-450d-b83c-83ff9bdc1d0d","node":"Fa2quZrSSlCuIwgA2TZRVg","reason":{"type":"illegal_argument_exception","reason":"Text fields are not optimised for operations that require per-document field data like aggregations and sorting, so these operations are disabled by default. Please use a keyword field instead. Alternatively, set fielddata=true on [email] in order to load field data by uninverting the inverted index. Note that this can use significant memory."}}],"caused_by":{"type":"illegal_argument_exception","reason":"Text fields are not optimised for operations that require per-document field data like aggregations and sorting, so these operations are disabled by default. Please use a keyword field instead. Alternatively, set fielddata=true on [email] in order to load field data by uninverting the inverted index. Note that this can use significant memory.","caused_by":{"type":"illegal_argument_exception","reason":"Text fields are not optimised for operations that require per-document field data like aggregations and sorting, so these operations are disabled by default. Please use a keyword field instead. Alternatively, set fielddata=true on [email] in order to load field data by uninverting the inverted index. Note that this can use significant memory."}}},"status":400}}
console.<computed> @ bugsnag.js:2688
handleError @ utils.js:1
eval @ query.js:1
Desktop (please complete the following information):
- Browser [e.g. chrome]
here is my component code
<template>
<!-- use v-show here because the popup can still stay open while the page is loading -->
<tr>
<th
v-for="header of headers"
:key="header.text"
class="tw-border-r tw-border-r-slate-200 !tw-border-b-slate-200 !tw-bg-slate-100 !tw-min-w-[100px]"
>
<div class="tw-flex tw-items-center tw-justify-between">
<div class="tw-mr-1 tw-text-sm tw-font-semibold tw-text-slate-600" :class="['text', { hidden: !header.text }]">
{{ header.text }}
</div>
<div v-if="!header.text" class="tw-flex tw-flex-1 tw-items-center tw-justify-center">
<input
type="checkbox"
class="tw-h-4 tw-w-4 tw-rounded tw-border-solid tw-border-slate-500 tw-text-indigo-600 focus:tw-ring-indigo-500 dark:tw-bg-black"
:checked="allSelected"
@change="toggleColumnSelected"
/>
</div>
<div v-else-if="header.value == 'user__magic_link' || header.value == 'date_registered'" />
<v-menu v-else :modelValue="headerPopups[header.value]" :closeOnContentClick="false">
<template #activator="{ props }">
<button class="tw-h-full tw-mt-1" v-bind="props">
<MagnifyingGlass v-if="header.value == 'full_name' || header.value == 'email'" class="!tw-h-5 !tw-w-5" />
<Funnel v-else class="!tw-h-5 !tw-w-5" />
</button>
</template>
<div class="tw-bg-white tw-rounded tw-flex tw-items-center !tw-p-2">
<multi-list
v-if="header.value === 'role'"
componentId="role"
dataField="role"
:innerClass="{
list: '!tw-p-0'
}"
>
<template #renderItem="{ label }">
<div class="tw-flex tw-text-sm tw-space-2">
{{ $store.state.registration.roleNames[label] }}
<img
v-if="$store.state.registration.roleIcons[label]"
:src="$store.state.registration.roleIcons[label]"
style="width: 13px"
/>
</div>
</template>
</multi-list>
<data-search
v-else-if="header.value === 'full_name'"
queryFormat="and"
componentId="full_name"
className="inlineSearch"
:dataField="['full_name']"
/>
<data-search
v-else-if="header.value === 'email'"
queryFormat="and"
componentId="email"
className="inlineSearch"
:dataField="['email']"
/>
<multi-list
v-else
:componentId="header.value"
:dataField="`${header.value}.keyword`"
title=""
:showCount="false"
:size="500"
:innerClass="{
list: '!tw-p-0'
}"
>
<template #renderItem="{ label }">
<div class="tw-text-sm">
{{ label }}
</div>
</template>
</multi-list>
</div>
</v-menu>
</div>
</th>
</tr>
</template>
<script>
import MagnifyingGlass from '@/studioComponents/Icons/MagnifyingGlass.vue';
import Funnel from '@/studioComponents/Icons/Funnel.vue';
import { mapGetters, mapMutations, mapState } from 'vuex';
export default {
name: 'StudioRegistrantsTableHeader',
components: { MagnifyingGlass, Funnel },
props: {
headers: {
type: Array,
default: () => []
}
},
data() {
const headerPopups = this.headers
.map(it => it.value)
.reduce((acuum, item) => {
acuum[item] = false;
return acuum;
}, {});
return {
headerPopups
};
},
computed: {
...mapState('registration', ['allSelected', 'lastSearchedQueryResultSize']),
...mapGetters('registration', ['numSelected'])
},
methods: {
...mapMutations('registration', ['toggleSelectAll']),
toggleColumnSelected() {
this.toggleSelectAll(!this.allSelected);
}
}
};
</script>
<style lang="scss">
.inlineSearch {
#full_name-downshift {
height: 160px;
}
#email-downshift {
height: 160px;
}
}
.v-menu__content {
background: white;
}
</style>
Parent component
<template>
<div class="tw-flex tw-flex-col tw-px-4">
<StudioPageIntro title="Registrants">
Register attendees for your event and control access for each user. Registrants will automatically be sent email
notifications once added, so confirm your email schedule before uploading registrants.
</StudioPageIntro>
<div class="tw-flex tw-flex-1 tw-flex-col tw-overflow-scroll tw-w-full">
<!--
This is a very frustrating component because of reactive-base. If you make any chnages,
you can start having problems with the components rendered by appbase, such as filters
not updating or the pagination changing back to page 1 randomly.
It seems reactive-base observes all its direct children and on any change that will
re-render the virtual dom of its direct descendants it will refresh. It will also
stop refreshing when it is supposed to refresh if its not a direct descendant.
A rule of thumb, if you want it to make the data update, make it a direct child. If you
want it to not make the reactive-base store refresh, put it as a descendant of a descendant
and then provide all the data it needs through the store instead of through props.
-->
<StudioSpinner v-if="loadingColumns" overlay />
<reactive-base v-else :app="eventId" :credentials="appbaseCredentials" :url="appBaseUrl">
<StudioSpinner v-show="loadingRegistration" overlay />
<div>
<div class="tw-flex tw-flex-row tw-py-3 tw-align-top">
<div class="tw-flex tw-flex-row tw-flex-1 tw-justify-start tw-items-start tw-mt-1">
<button class="hover:tw-opacity-80 tw-mr-2 tw-mt-[2px]" @click="refreshList">
<Refresh class="tw-w-5 tw-h-5 tw-text-indigo-600 tw-transition-transform" />
</button>
<div v-if="summary" class="tw-mr-2">
{{ summary }}
</div>
</div>
<div class="tw-flex tw-flex-row tw-justify-end tw-items-start tw-px-2">
<search-box
placeholder="Search Registrants..."
className="appbase-search"
:innerClass="{
list: 'search-bar-list'
}"
queryFormat="and"
filterLabel="Search"
componentId="SearchUsers"
:dataField="['full_name']"
/>
<StudioAddRegistrant @onRegistrantAdd="searchRegistrants" />
</div>
</div>
<selected-filters :showClearAll="true">
<template #default="{ selectedValues, setValue, clearValues }">
<div class="tw-flex tw-flex-row tw-flex-wrap">
<Chip
v-for="componentId in Object.keys(getFilteredValues(selectedValues))"
:key="componentId"
class="tw-mr-2"
:borderRadius="16"
:label="`${filteredText(componentId)}: ${selectedFilterValue(componentId, selectedValues)}`"
allowClose
@closeChip="() => setValue(componentId, null)"
/>
<StudioButton
v-if="Object.keys(getFilteredValues(selectedValues)).length"
variant="text"
trackingId="registrants-clear-all-filters-button"
@click="clearValues"
>
Clear All Filters
</StudioButton>
</div>
</template>
</selected-filters>
<div class="content tw-w-full tw-mb-8 tw-relative">
<div class="tw-absolute tw-right-[168px] tw-top-[12px]">
<StudioRegistrantsColumnSelectionPopup v-if="$store.state.registration.lastSearchedQueryResultSize > 0" />
</div>
<reactive-list
ref="datalist"
dataField="email"
componentId="SearchResults"
prevLabel="Previous"
nextLabel="Next"
:react="{ and: oredFilters }"
:pagination="true"
:defaultQuery="defaultQuery"
:sortOptions="sortOptions"
:size="25"
:showEndPage="true"
:showResultStats="false"
:innerClass="{
sortOptions: 'appbase-sort-options',
pagination: 'appbase-paginator'
}"
:renderNoResults="() => null"
@data="onResult"
@queryChange="onQueryChange"
>
<template #render="{ data }">
<div>
<div class="tw-flex tw-flex-row tw-flex-1 tw-justify-between tw-py-3">
<StudioRegistrantsActions
:react="{ and: oredFilters }"
:defaultQuery="defaultQuery"
@banfinished="handleBanFinished"
/>
</div>
<v-data-table
:headers="visibleColumns"
:items="data"
showSelect
fixedHeader
class="tw-border tw-border-slate-200 tw-overflow-hidden"
>
<!--
Do not bind props to this component. Route them through the store.
If you bind props to this element other than items then everytime
this component changes the reactive-list component above switches
back to page 1. If you route the changes through the store it cannot
tell.
-->
<template #headers="{ columns }">
<!-- Do not use v-if here. It will break filters. -->
<StudioRegistrantsTableHeader v-show="data.length > 0" :headers="columns" />
</template>
<template #bottom></template>
<template #body></template>
<template #tbody="{ items }">
<div v-if="items.length === 0" class="tw-py-12 tw-text-center">
<h5
class="tw-font-medium tw-leading-tight tw-text-md lg:tw-text-lg lg:tw-leading-tight tw-text-slate-400 dark:tw-text-slate-500"
>
No registrants yet!
</h5>
</div>
<StudioRegistrantsTableRow
:rows="items"
@singleItemSelectionHandler="singleItemSelectionHandler"
/>
</template>
</v-data-table>
</div>
</template>
</reactive-list>
</div>
</div>
</reactive-base>
</div>
</div>
</template>
<script>
import { mapState, mapActions, mapGetters, mapMutations } from 'vuex';
import StudioRegistrantsTableRow from './StudioRegistrantsTableRow.vue';
import StudioRegistrantsTableHeader from './StudioRegistrantsTableHeader.vue';
import StudioRegistrantsActions from './StudioRegistrantsActions.vue';
import StudioRegistrantsColumnSelectionPopup from './StudioRegistrantsColumnSelectionPopup.vue';
import StudioAddRegistrant from './StudioAddRegistrant.vue';
import { snakeCaseToCapitalized } from '@/store/modules/registration';
import LogRocket from 'logrocket';
import StudioButton from '@/studioComponents/StudioButton.vue';
import StudioPageIntro from '@/studioComponents/StudioPageIntro.vue';
import StudioSpinner from '@/studioComponents/StudioSpinner.vue';
import Chip from '@/commonComponents/Chip.vue';
import throttle from 'lodash.throttle';
import Refresh from '@/studioComponents/Icons/Refresh.vue';
import { VDataTable } from 'vuetify/labs/VDataTable';
export default {
name: 'StudioRegistrantsWrapper',
components: {
StudioRegistrantsTableHeader,
StudioRegistrantsTableRow,
StudioRegistrantsActions,
StudioRegistrantsColumnSelectionPopup,
StudioAddRegistrant,
Chip,
StudioButton,
StudioSpinner,
StudioPageIntro,
Refresh,
VDataTable
},
computed: {
...mapState('registration', {
appBaseUrl: 'appBaseUrl',
loadingRegistration: 'loadingRegistration',
appbaseError: 'appbaseError',
columns: 'columns',
roleNames: 'roleNames',
lastSearchedQueryResultSize: 'lastSearchedQueryResultSize'
}),
...mapGetters('registration', ['visibleColumns']),
...mapGetters('studioEventList', {
event: 'getCurrentEvent'
}),
appbaseCredentials() {
return this.event?.appbase_credentials;
},
selectedFilterValue() {
return (componentId, selectedValues) => {
if (componentId === 'role') {
return selectedValues[componentId].value
.map(role => {
return this.roleNames[role];
})
.join(',');
}
const value = selectedValues[componentId].value;
if (Array.isArray(value)) {
return value.join(',');
}
return value;
};
},
oredFilters() {
const ored = this.columns
.map(it => it.value)
.filter(it => {
return !['user__magic_link', 'id', 'event_id', 'profile_picture_url'].includes(it);
});
return ored.concat('SearchUsers').concat('role');
},
eventId() {
return this.$route.params.eventId;
},
summary() {
if (this.loadingRegistration) {
return '';
}
if (this.lastSearchedQueryResultSize <= 0) {
return '';
}
if (this.lastSearchedQueryResultSize === 1) {
return '1 Registrant';
}
return `${this.lastSearchedQueryResultSize} Registrants`;
}
},
data() {
return {
loadingColumns: true,
currentPage: 1,
sortOptions: [
{
label: 'Full Name ↑',
dataField: 'full_name.keyword',
sortBy: 'asc'
},
{
label: 'Full Name ↓',
dataField: 'full_name.keyword',
sortBy: 'desc'
},
{
label: 'Email ↑',
dataField: 'email.keyword',
sortBy: 'asc'
},
{
label: 'Email ↓',
dataField: 'email.keyword',
sortBy: 'desc'
},
{
label: 'Date Registered ↑',
dataField: 'date_registered.keyword',
sortBy: 'asc'
},
{
label: 'Date Registered ↓',
dataField: 'date_registered.keyword',
sortBy: 'desc'
}
]
};
},
mounted() {
this.fetchColumns().then(() => {
this.loadingColumns = false;
});
this.fetchEventMembers({ eventId: this.eventId });
},
beforeRouteLeave(to, from, next) {
this.toggleSelectAll(false);
this.toggleColumnPopup(false);
this.setSelectedQuery(null);
next();
},
methods: {
...mapActions('registration', ['fetchColumns']),
...mapActions('eventList', ['getEventMembers', 'blockEventBulkUser']),
...mapActions('studioEventList', ['fetchEventMembers']),
...mapMutations('registration', [
'toggleSelectAll',
'toggleColumnPopup',
'setSelectedQuery',
'setLoadingRegistration',
'storeAppbaseQueryDSL',
'toggleSelection'
]),
// This cannot be a computed property because it is inside the render
// function of appbase and hence cannot know when to recompute unless
// its passed as a method
handleBanFinished({ timeout }) {
this.toggleSelectAll(false);
this.searchRegistrants();
// The banned data doesn't reflect in appbase immediately so wait to refresh
setTimeout(() => {
this.refreshList();
}, timeout);
},
refreshList: throttle(function () {
const datalist = this.$refs.datalist;
if (datalist) {
this.setLoadingRegistration(true);
if (!this.columns?.length) {
this.fetchColumns();
}
const currentPage = datalist.$el.__vue__.currentPageState;
datalist.$el.__vue__.setPage(currentPage + 1);
setTimeout(() => {
datalist.$el.__vue__.setPage(currentPage);
this.setLoadingRegistration(false);
}, 100);
} else {
LogRocket.info('Could not refresh by page change because reactivebase was not found!');
}
}, 2000),
filteredText(componentId) {
return this.snakeCaseToCapitalized(componentId === 'blocked' ? 'banned' : componentId);
},
onQueryChange(prev, next) {
LogRocket.info('Query change: ', prev, next);
this.storeAppbaseQueryDSL({
next,
numResults: -1
});
},
onResult(payload) {
const numResults = payload.resultStats.numberOfResults;
LogRocket.info('Number of results in appbase query: ' + numResults);
this.storeAppbaseQueryDSL({
numResults
});
},
searchRegistrants() {
if (this.currentPage == 1) {
this.fetchEventMembers({ eventId: this.event.id });
} else {
this.currentPage = 1;
}
},
snakeCaseToCapitalized,
getFilteredValues(values = {}) {
const filteredValues = {};
Object.keys(values).forEach(componentId => {
if (
values[componentId].showFilter &&
(Array.isArray(values[componentId].value) ? values[componentId].value.length : !!values[componentId].value)
) {
filteredValues[componentId] = values[componentId];
}
});
return filteredValues;
},
defaultQuery() {
return {
track_total_hits: true,
query: { match_all: {} }
};
},
singleItemSelectionHandler(row) {
this.toggleSelection(row);
}
}
};
</script>
<style lang="scss">
// This block is for modifying appbase css
// Do not scope it
.appbase-search {
& #SearchUsers-input {
font-size: 14px;
min-height: 0;
border-radius: 999px;
border-color: rgb(209 213 219);
background-color: white;
&:focus {
border-color: rgb(135 128 253);
}
}
& svg.search-icon[alt='Search'] {
fill: #222;
height: 12px;
width: 12px;
}
}
.search-bar-list {
border: 1px solid rgb(209 213 219) !important;
max-height: 160px !important;
margin-top: 2px !important;
border-radius: 0.25rem !important;
}
select.appbase-sort-options {
position: absolute;
right: 2px;
top: 8px;
width: 160px;
border-radius: 0.25rem;
cursor: pointer;
&:hover {
border-color: rgb(135 128 253);
outline-color: rgb(135 128 253);
}
&:focus {
border-color: rgb(135 128 253);
outline: 2px solid rgb(135 128 253);
outline-offset: 0px;
}
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e");
background-position: right 0.5rem center;
background-repeat: no-repeat;
background-size: 1.5em 1.5em;
padding-right: 2.5rem;
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
.content {
& .appbase-paginator {
text-align: center;
& > button {
font-family: 'Inter var', sans-serif;
&.active {
background-color: rgb(135 128 253);
color: white;
}
}
& > a {
color: rgba(28, 30, 31, 0.6);
background-color: transparent;
&.active {
color: white;
background: rgba(28, 30, 31, 0.6);
}
&:focus {
outline: none;
}
}
}
& .appbase-noresults {
display: none;
}
}
</style>