Select menu freezes and does not generate event

Tags: #<Tag:0x00007efc61b95438>

I am rather perplexed as to why whenever trying to use a select box, it simply freezes and does not generate the event which should trigger the function making a call to the server.
Here is the code:

<div>
    <div class="grid">
        <div class="sub-navigation">
          <div class="filters">
            <div class="form-row">
                <h3>Filter Interactions</h3>
            </div>
            <div class="form-row">
                <label>Search</label><input type="text" v-model.lazy="filterText" @input="debounceFilterText" placeholder="Search" id="filterText" />
            </div>
            <div class="form-row">
                <select v-bind:value="semesterID" @change="setSemester($event.currentTarget.value)">
                    <option value="">Select a Semester</option>
                    <option v-for="s in availableSemesters" :value="s.SemesterID">{{semesterNames[s.SemesterID]}}</option>
                </select>
            </div>
            <div class="form-row">
                <input type="checkbox" v-model="onlyMyInteractions" /> <small>Only show my interactions</small>
            </div>
            <hr />
            <div class="form-row">
                <new-interaction v-bind="{ semesterID, semesterIds, currentEducatorUserID, schoolID: sessionSchool }"></new-interaction>
            </div>
            <div class="form-row">
                <button class="btn btn--full" @click.prevent="bulkAddInteraction">Bulk add Students/Interactions</button>
            </div>
            <div class="form-row">
              <button :disabled="!previousSemester" class="btn btn--full" @click.prevent="toggleAddInteractionFromLastSemesterModal">Add from last semester</button>
                <Modal :show="show" :requestClose="() => show = false" title="Add From Last Semester" width="800px">
                   <template slot-scope="{requestClose}">
                        <div>
                            <AddFromLastSemester 
                             :currentSemester="semesterID"
                             :previousSemester="previousSemester"
                             :semesterIds="semesterIds"
                             @submitAddFromLastSemester="submitAddFromLastSemester"
                             ></AddFromLastSemester>
                        </div>
                   </template>
                </Modal>
            </div>
          <hr />
          <div class="form-row scrolls">
              <new-semester :schoolID="sessionSchool"></new-semester>
          </div>
          <hr />
          <div class="form-row">
              <button class="btn btn--full" @click.prevent="exportInteractions">Export Interactions</button>
          </div>
          <hr />
          <div class="form-row">
              <button :disabled="!canDeleteInteractions" class="btn btn--full" @click.prevent="deleteInteractions">Delete Selected Interactions</button>
          </div>
        </div>
      </div>
      <div class="interactions-container">
          <Spinner v-if="loading"></Spinner>
          <div class="interactions-container-inner">
              <h1>Interactions <small>{{semesterString}}</small></h1>
              <Handsontable :data="gridData" :settings="settings" ref="interactionsTable"></Handsontable>
          </div>
      </div>
  </div>
</div>
</template>

<script>
import Handsontable from '@/shared/components/ui/Handsontable';
import map from 'lodash/map';
import each from 'lodash/each';
import orderBy from 'lodash/orderBy';
import filter from 'lodash/filter';
import some from 'lodash/some';
import Modal from '@/shared/components/ui/modal';
import NewSemester from '@/pages/Educators/Interactions/NewSemester';
import NewInteraction from './NewInteraction';
import AddFromLastSemester from './AddFromLastSemester';
import debounce from 'debounce';
import lang from '@/shared/lib/lang';
import { sqlDate } from '@/shared/lib/formatters';
import { normalize, schema } from 'normalizr';
import { eSeason, eLearnOrProg, eRelationType, eRelationWith } from '@/shared/lib/enums';
import { Interaction, StudentView } from '@/shared//lib/valueObjects';
import Spinner from '@/shared/components/ui/Spinner';
import isEqual from 'lodash/isEqual';
import { ApolloClient } from '@/main';

let _this;

export default {
    'name': 'Interactions',
    components: {
        Handsontable,
        Modal,
        NewSemester,
        NewInteraction,
        AddFromLastSemester,
        Spinner
    },
    props: ['currentUser', 'semestersData', 'modals', 'toggleInteractionModal', 'addingInteraction', 'sessionSchool', 'updateInteraction', 'isAdminMode', 'getInteractionsBySemesters', 'createInteraction', 'deleteInteraction'],
    data () {
        return {
            show: false,
            loading: false,
            interactions: [],
            sessionSemester: null,
            savedEntity: {},
            settings: {
                colHeaders: ['InteractionID', '', 'First Name', 'Last Name', 'Student ID', 'Learning Type', 'Program Type', 'High Impact Trip', '# Shabbat Meals', '# Halachic Counsel', '# Personal Counsel', 'Leadership Development', 'Relationship Type', 'Relationship With', 'Chavruta', 'Notes'],
                columns: [
                    { data: 'InteractionID', type: 'text', readOnly: true },
                    { data: 'Selected', type: 'checkbox' },
                    { data: 'StudentView.FirstName', type: 'text', readOnly: true },
                    { data: 'StudentView.LastName', type: 'text', readOnly: true },
                    { data: 'StudentID', readOnly: true },
                    {
                        data: 'LearningType',
                        type: 'dropdown',
                        source: [ '', 'None', 'Regular', 'Occasional' ]
                    },
                    {
                        data: 'ProgramType',
                        type: 'dropdown',
                        source: [ '', 'None', 'Regular', 'Occasional' ]
                    },
                    { data: 'IsHighImpactTrip', type: 'checkbox' },
                    { data: 'ShabbatMeals', type: 'numeric' },
                    { data: 'HalachicCounsel', type: 'numeric' },
                    { data: 'PersonalCounsel', type: 'numeric' },
                    { data: 'IsLeadershipDevelopment', type: 'checkbox' },
                    {
                        data: 'RelationshipType',
                        type: 'dropdown',
                        source: [ 'Casual', 'Significant', 'Regular' ]
                    },
                    {
                        data: 'RelationshipWith',
                        type: 'dropdown',
                        source: [ 'None', 'Male', 'Female', 'Both' ]
                    },
                    { data: 'IsChavruta', type: 'checkbox' },
                    { data: 'Notes', type: 'text' }
                ],
                afterChange: function (change, source) {
                    if (source === 'loadData') {
                        return; // don't save this change
                    }
                    _this.handleChange(change);
                },
                hiddenColumns: {
                    columns: [0, 4],
                    indicators: false
                },
                fixedColumnsLeft: 4,
                cells (row, col, prop) {
                    if (_this) {
                        var cellProperties = {};
                        // check if the row has a educator unit id not equal to what we have
                        var educatorUnitID = (_this.gridData[row] || {}).EducatorUnitID;
                        cellProperties.readOnly = false;
                        if (_this.currentEducatorUserID !== educatorUnitID) cellProperties.readOnly = true;
                        return cellProperties;
                    }
                },
                columnSorting: true
            },
            filterText: '',
            filterTextDebounced: '',
            onlyMyInteractions: true,
            selectedInteractionIds: []
        };
    },
    mounted () {
        _this = this;
        if (this.currentSemesterIndex === -1 && this.availableSemesterIDs.length > 0) {
            var newSemester = (orderBy(this.availableSemesters, ['EndDate', 'Season'], ['desc', 'desc']) || []).shift();
            this.sessionSemester = newSemester.SemesterID
        }
        this.filterText = '';
        this.filterTextDebounced = '';
        this.onlyMyInteractions = true;
        this.selectedInteractionIds = [];
    },
    computed: {
        semesterIds () { 
            return (this.semestersData || []).map(x => x.SemesterID) 
        },
        semesterID () {
            if (this.sessionSemester) return this.sessionSemester;
            var newSemester = (orderBy(this.availableSemesters, ['EndDate', 'Season'], ['desc', 'desc']) || []).shift();
            return (newSemester || {}).SemesterID || -1;
        },
        currentEducatorUserID () {
            return this.currentUser.educatorUnitId
        },
        seasons () {
            return eSeason.enums;
        },
        learnOrProg () {
            return eLearnOrProg.enums;
        },
        relationType () {
            return eRelationType.enums;
        },
        relationWith () {
            return eRelationWith.enums;
        },
        interactionsViewModel () {
            return this.interactions =  this.normalizeInteractions((this.getInteractionsBySemesters || [] ).map(i => ({ ...i, StudentID: i.StudentView.StudentID, LearningType: this.learnOrProg[i.LearningType], ProgramType: this.learnOrProg[i.ProgramType] , RelationshipType: this.relationType[i.RelationshipType], RelationshipWith: this.relationWith[i.RelationshipWith]}))); 
        },
        semesters () {
            return this.normalizeSemesters((this.semestersData || []).map(semester => ({ ...semester, StartDate: sqlDate(semester['StartDate']), EndDate: sqlDate(semester['StartDate']), Season: this.seasons[semester.Season]})))
        },
        gridData () {
            var interactions = this.filteredInteractions;
            
            each(interactions, i => {
                i['Selected'] = this.selectedInteractionIds.indexOf(i.InteractionID) >= 0;
            });
            if (this.onlyMyInteractions) {
                interactions = filter(interactions, i => this.currentEducatorUserID == i.EducatorUnitID);
            }
            if (this.filterTextDebounced) {
                var ft = this.filterTextDebounced.toUpperCase();
                interactions = filter(interactions, i => {
                    return (
                        i.StudentView && (
                            (i.StudentView.FirstName.toUpperCase().indexOf(ft) !== -1) ||
                            (i.StudentView.LastName.toUpperCase().indexOf(ft) !== -1)
                        )
                    );
                });
            }
            return interactions;
        },
        filteredInteractions () {
            if (!this.interactionsViewModel) return [];
            return filter(this.interactionsViewModel, i => parseInt(i.SemesterID) === parseInt(this.semesterID));
        },
        semesterString () {
            return this.semesterNames[`${this.semesterID}`];
        },
        availableSemesters () {
            if (!this.semesters) return [];
            return filter(this.semesters, s => s.SchoolID === this.sessionSchool).sort((a, b) => {
                            var dateA = new Date(a.StartDate);
                            var dateB = new Date(b.StartDate);
                            return dateB - dateA;
            });
        },
        semesterNames () {
            if (!this.availableSemesters) return {};
            let semesters = {};
            each(this.availableSemesters, s => {
                let sYear = s.StartDate.slice(0, 4);
                semesters[s.SemesterID] = `${s.Season} ${sYear}`;
            });
            return semesters;
        },
        semesterID () {
            if (this.sessionSemester) return this.sessionSemester;
            var newSemester = (orderBy(this.availableSemesters, ['EndDate', 'Season'], ['desc', 'desc']) || []).shift();
            return (newSemester || {}).SemesterID || -1;
        },
        availableSemesterIDs () {
            return map(this.availableSemesters, s => s.SemesterID);
        },
        currentSemesterIndex () {
            return this.availableSemesterIDs.indexOf(parseInt(this.sessionSemester));
        },
        previousSemester () {
            if (this.currentSemesterIndex < 1) return null;
            return this.availableSemesterIDs[this.currentSemesterIndex - 1];
        },
        canDeleteInteractions () {
            return this.gridData.some(x => x.Selected);
        }
    },
    methods: {
        setSemester (semesterID) {
            this.sessionSemester = semesterID;
        },
        normalizeInteractions(interactions) {
            const interactionsEntity = new schema.Entity('interactions', {}, { idAttribute: 'InteractionID' });
            const userListSchema = new schema.Array(interactionsEntity);
            const normalizedData = normalize(interactions, userListSchema);
            return normalizedData.entities.interactions
        },
        normalizeSemesters(semesters) {
            const semestersEntity = new schema.Entity('semesters', {}, { idAttribute: 'SemesterID' });
            const userListSchema = new schema.Array(semestersEntity);
            const normalizedData = normalize(semesters, userListSchema);
            return normalizedData.entities.semesters
        },
        setSemester (semesterID) {
            this.sessionSemester = semesterID;
        },
        bulkAddInteraction () {
            this.$router.push('interactions/bulkadd');
        },
        exportInteractions () {
            this.$refs.interactionsTable.export();
        },
        toggleAddInteractionFromLastSemesterModal () {
            this.show = !this.show
        },
        handleChange (change) {
            var idx = change[0][0];
            if (!this.gridData[idx]) return;

            var field = change[0][1];
            var oldVal = change[0][2];
            var val = change[0][3];
            if (val === oldVal) return;

            var iid = this.gridData[idx].InteractionID;
            var entity = {...this.interactions[iid]};
            entity[field] = val;
            if (field === 'Selected') {
                if (val) {
                    this.selectedInteractionIds.push(iid);
                }
                else {
                    var index = this.selectedInteractionIds.indexOf(iid);
                    this.selectedInteractionIds.splice(index, 1);
                }
                return;
            }
            this.updateInteraction({ ...new Interaction(entity) });
        },
        submitAddFromLastSemester (selectedStudents) {

            var studentIDs = selectedStudents.map(x => x.StudentView.StudentID);
            if (studentIDs.length < 1) return;
            var interactions = [];
            const entities = selectedStudents.map(x => ({
                optimisticResponse: {
                    ...x,
                    InteractionID: Math.floor((Math.random() * -100) + 1),
                    SemesterID: this.semesterID,
                    IsHighImpactTrip: false,
                    IsLeadershipDevelopment: false,
                    IsChavruta: false,
                    HalachicCounsel: '0',
                    PersonalCounsel: '0',
                    ShabbatMeals: 0,
                    EducatorUnitID: this.currentEducatorUserID
                },
                interaction: {
                    StudentID: x.StudentID,
                    SemesterID: this.semesterID,
                    IsHighImpactTrip: false,
                    IsLeadershipDevelopment: false,
                    IsChavruta: false,
                    HalachicCounsel: '0',
                    PersonalCounsel: '0',
                    ShabbatMeals: 0,
                    EducatorUnitID: this.currentEducatorUserID
                }
            }))

            entities.map(i => this.createInteraction({ interaction: i.interaction, optimisticResponse: i.optimisticResponse }))
            this.toggleAddInteractionFromLastSemesterModal();
        },
        deleteInteractions () {
            if (!this.canDeleteInteractions) return;
            // only allow deletion of the interactions with the users edu unit
            let selected = map(this.selectedInteractionIds, i => this.interactions[i]).filter(Boolean);

            if (some(selected, x => x.EducatorUnitID !== this.currentEducatorUserID)) {
                window.alert('You can only delete interactions for your own educator unit.');
                return;
            }
            if (!window.confirm('Delete selected interactions?')) {
                return;
            }
            if (this.selectedInteractionIds.length) this.selectedInteractionIds.map(x => this.deleteInteraction(x, this.semesterID));
        },
        debounceFilterText: debounce(e => {
            _this.filterTextDebounced = e.target.value;
            _this.filterText = _this.filterTextDebounced;
        }, 300)
    }
};
</script>

<style scoped>
.grid {
  display: grid;
  grid-template-columns: 270px 1fr;
}
h1{
  font-size: 28px;
  padding: 10px;
  font-weight: bold;
}
h1 small {
  color: #999999;
  font-size: 20px;
  text-transform: capitalize;
  margin-left: 10px;
}
h3 {
  font-weight: bold;
  border-bottom: 1px solid #efefef;
}
</style>

Hi @dobkins

Could you send a demo and a step-by-step scenario on how to replicate the issue?

That would be very difficult without recreating the whole app. Would it be possible to speak with someone and explain the issue which is still unresolved?

HI @dobkins

I can schedule a private session with a developer but the code is still a requirement. Even when we’ll be able to see that the table freezes it won’t take us further with the explanation.

If the code is too complex to share it via JSFiddle you may try to use some callbacks to collect more information about current state of Handsontable’s task stack. I recommend checking the console for the afterRender and afterRenderer hooks. Usually, the table becomes really heavy when the renderer is called.

If you can share any information about custom renderers, editors or styling that would also help me to search for clues.

I have actually created a JSFiddle the problem is it works fine, the issue seems to be some other component of code that is not working with the HOT. I would appreciate a session.
The incoming data is coming from apollo (server side it’s graphql) it’s then normalized and for some reason does not seem to be read properly by the table.

Hi @dobkins

Can you contact me on support@handsontable.com?

yes

Thank you, I got the email. I am closing this topic.

Our issue was that one of the fields in the data object passed to the HOT, was an eNUm and NOT a string. This took a long time to figure out since there is no error thrown. Beware what type of data you give the HOT, it should be receiving just Strings!

Thank you for sharing your case and investigation @dobkins