Skip to content

Unable to use multi-select and its giving error #2247

@deepak-sisodiya

Description

@deepak-sisodiya

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
Screenshot 2023-09-13 at 3 02 53 PM

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>


Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions