Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
deepak-sisodiya opened this issue Sep 13, 2023 · 10 comments
Open

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

deepak-sisodiya opened this issue Sep 13, 2023 · 10 comments
Labels

Comments

@deepak-sisodiya
Copy link

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>


@deepak-sisodiya
Copy link
Author

on searching also, I am getting same error

<search-box
    placeholder="Search Registrants..."
    className="appbase-search"
    :innerClass="{
      list: 'search-bar-list'
    }"
    queryFormat="and"
    filterLabel="Search"
    componentId="SearchUsers"
    :dataField="['full_name']"
  />

@SavvyShah
Copy link
Contributor

It seems something is wrong with dataField type in MultiList. Use role.keyword instead of role and see if the error goes away. Also you should use search-box instead of data-search in v3. Refer migration guide for other changes.

Also can you check by removing sortOptions key passed to ReactiveList, so we can just know the root cause if above doesn't work.

@siddharthlatest
Copy link
Member

@deepak-sisodiya Based on the error message, the references for the use of the email field for either sorting or aggregations on the searchResults componentId. The code snippet you've shared looks fine to me, as I see that you're using email.keyword there. Perhaps narrow it down further based on the error message you see. You can try removing the component in question or sortOptions values to see if they help.

And for the searching use-case error, can you share the exact error message?

@deepak-sisodiya
Copy link
Author

@SavvyShah @siddharthlatest Thanks for the reply. I switched to other things in between.
today I checked it.

Here is the update.

  1. I removed sortOptions from ReactiveList , searching and filter for each column starts working now.
  2. I replaced the data-search with search-box, missed it while doing migration.

New issues

  1. we do not have sorting UI now. If we have sortOptions again, other things do not work.
  2. following code is not working for refresh list - this issues is not due to removing sortOptions,
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),

error is - Cannot read properties of undefined (reading 'currentPageState').

Please help @SavvyShah @siddharthlatest

@siddharthlatest
Copy link
Member

@deepak-sisodiya Thanks for the update, glad to see that you got the search UI working.

On the new issues, would you be able to reproduce these two issues with one of our public demos?

@deepak-sisodiya
Copy link
Author

@siddharthlatest where is public demo link?

@deepak-sisodiya
Copy link
Author

@siddharthlatest @SavvyShah I have refresh button outside of table, how can I set the setPage, is there any way for it.

@deepak-sisodiya
Copy link
Author

on refresh, I want to set page to 1

@siddharthlatest
Copy link
Member

@deepak-sisodiya For the public demo, you can use this: https://codesandbox.io/s/github/appbaseio/vue-quick-start/tree/step-5/?from-embed (It has searchbox and reactive list). See if you can reproduce the issue by including sort options and the page operation

@deepak-sisodiya
Copy link
Author

@siddharthlatest

  1. we do not have sorting UI now. If we have sortOptions again, other things do not work.
    this is fixed by removing dataField and passing sortOptions

  2. following code is not working for refresh list
    I changed the logic for it, seems working

thanks

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

3 participants