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

EventFilter using combobox doesn't work as expected in timeAxisHeaderMenu #6477

Closed
taauntik opened this issue Mar 28, 2023 · 4 comments
Closed
Labels
bug Something isn't working forum Issues from forum invalid This doesn't seem right

Comments

@taauntik
Copy link

taauntik commented Mar 28, 2023

Forum post

We have the following TimeAxisHeaderMenu on a Scheduler component that works flawlessly in v5.1.3:

timeAxisHeaderMenu: {
    items: {
        currentTimeLine: {
            text: 'Highlight today'
        },
        eventsFilter: {
            text: 'Filter absence types',
            menu: {
                items: {
                    nameFilter: {
                        clearable: true,
                        cls: "b-eventfilter b-last-row",
                        label: 'By type',
                        type: 'combo',
                        store: { data : [ { id : 1, absenceType : 'A1' }, { id : 2, absenceType : 'A2' }, { id : 3, absenceType : 'A3' } ] },
                        valueField: 'id',
                        displayField: 'absenceType',
                        editable: false,
                        listItemTpl: (item) => {
                            return `<div><div style="font-size: 80%">${item.absenceCategory}</div><div>${item.absenceType}</div></div>`;
                        },
                        width: 250
                    }
                }
            }
        }
    }
}

After updating to v5.3.0, this produces the correct UI elements, but upon selecting an option from the combo box, all events in the Scheduler suddenly disappear, regardless of the option chosen. Opening the context menu again and clearing the filter does not fix this, but if the user then attempts to set another filter, the events magically reappear again when hovering the mouse over the "Filter absence types" menu item!

I have attached a screen recording of all of this in action for your greater understanding.

Many thanks in advance for any help with this


Here is a video of reproduction

Recording.2023-03-28.172405.mp4
@taauntik taauntik added bug Something isn't working forum Issues from forum labels Mar 28, 2023
@matsbryntse
Copy link
Member

Correct version


eventsFilter : {
                    text : 'Filter absence types',
                    menu : {
                        items : {
                            nameFilter     : false,
                            categoryFilter : {
                                clearable    : true,
                                label        : 'By type',
                                type         : 'combo',
                                store        : { data : [{ id : 1, absenceType : 'A1' }, { id : 2, absenceType : 'A2' }, { id : 3, absenceType : 'A3' }] },
                                valueField   : 'id',
                                displayField : 'absenceType',
                                editable     : false,
                                listItemTpl  : (item) => {
                                    return `<div><div style="font-size: 80%">${item.absenceCategory}</div><div>${item.absenceType}</div></div>`;
                                },
                                listeners : {
                                    change({ value }) {
                                        const { eventStore } = this.up('menu').owner;
                                        if (value) {
                                            eventStore.filter('absenceCategory', value);
                                        }
                                        else {
                                            eventStore.clearFilters();
                                        }
                                    }
                                }
                            }
                        }
                    }
                }

@matsbryntse matsbryntse added the invalid This doesn't seem right label May 13, 2023
@kenny1983
Copy link

Thanks for the reply @matsbryntse, but that still doesn't work for me. Same issue as before persists. Please let me know if you need any further information to reproduce this. Thanks :-)

@matsbryntse
Copy link
Member

Works for me, can you please post a full test case?

@kenny1983
Copy link

@matsbryntse Sure, here's the entire scheduler:

this._scheduler = new Scheduler({
    appendTo: 'scheduler-panel',
    minHeight: '20em',
    rowHeight: 70,
    emptyText: 'No personnel specified',
    enableRecurringEvents: true,
    eventColor: 'orange',
    //set start and end dates to the past, seems to fix recurring event rendering issue - #361
    startDate: DateHelper.add(currentDate, -6, 'month'),
    endDate: DateHelper.add(DateHelper.add(currentDate, -6, 'month'), 1, 'day'),
    viewPreset: 'monthAndYear',
    readOnly: this.configuration.readOnly,
    features: {
        eventDrag: {
            disabled: true
        },
        eventResize: {
            disabled: true
        },
        timeRanges: {
            enableResizing: false,
            showCurrentTimeLine: true,
            showHeaderElements: false
        },
        sort: 'name',
        labels: {
            top: {
                field: 'absenceType'
            }
        },
        eventEdit: {
            editorConfig: {
                title: 'Absence',
                autoClose: false,
                cls: 'absence-editor'
            },
            dateFormat: this.dateFormat,
            timeFormat: 'HH:mm',
            showRecurringUI: true,
            items: {
                nameField: {
                    hidden: true
                },
                resourceField: {
                    hidden: true
                },
                startDateField: {
                    format: this.dateFormat,
                    editable: false,
                    listeners: {
                        change: () => {
                            // noop
                        }
                    }
                },
                startTimeField: {
                    step: '15min',
                    editable: false,
                    listeners: {
                        change: () => {
                            // noop
                        }
                    }
                },
                endDateField: {
                    format: this.dateFormat,
                    editable: false,
                    listeners: {
                        change: (e) => {
                            const newValue = e.userAction ? e.value : (e.source.owner.widgetMap.endDateField.value !== undefined ? e.source.owner.widgetMap.endDateField.value : e.source.owner.widgetMap.startDateField.value);  //new Date(e.source.owner.record.originalData.endDate);
                            //calculate duration
                            const durationField = e.source.owner.widgetMap.durationField;
                            durationField.unit === undefined ? 'hour' : durationField.unit;
                            const newDuration = this.calculateDuration(e.source.owner.widgetMap.startDateField.value, e.source.owner.widgetMap.startTimeField.value, newValue, e.source.owner.widgetMap.endTimeField.value, durationField.unit);
                            this.validateDuration(newDuration, e.source.owner.widgetMap);
                            durationField.value = { magnitude: newDuration, unit: durationField.unit };
                        }
                    }
                },
                endTimeField: {
                    step: '15min',
                    editable: false,
                    listeners: {
                        change: (e) => {
                            const newValue = e.userAction ? e.value : (e.source.owner.widgetMap.endDateField.value !== undefined ? e.source.owner.widgetMap.endDateField.value : e.source.owner.widgetMap.startDateField.value); //e.source.owner.widgetMap.endTimeField.value;
                            //calculate duration
                            const durationField = e.source.owner.widgetMap.durationField;
                            durationField.unit === undefined ? 'hour' : durationField.unit;
                            const newDuration = this.calculateDuration(e.source.owner.widgetMap.startDateField.value, e.source.owner.widgetMap.startTimeField.value, e.source.owner.widgetMap.endDateField.value, newValue, durationField.unit);
                            this.validateDuration(newDuration, e.source.owner.widgetMap);
                            durationField.value = { magnitude: newDuration, unit: durationField.unit };
                        }
                    }
                },
                absenceTypeIdField: {
                    type: 'combo',
                    name: 'absenceTypeId',
                    label: 'Type',
                    store: this._eventTypeStore,
                    valueField: 'id',
                    displayField: 'absenceType',
                    editable: false,
                    listItemTpl: (item) => {
                        return `<div><div style="font-size: 80%">${item.absenceCategory}</div><div>${item.absenceType}</div></div>`;
                    },
                    weight: -100,
                    required: true,
                    listeners: {
                        action: (e) => {
                            const combo = e.source;
                            if (e.record) {
                                if (!e.record.showTime) { //clear the existing time
                                    combo.owner.widgetMap.startTimeField.value = '00:00:00';
                                    combo.owner.widgetMap.endTimeField.value = '00:00:00';
                                }

                                combo.owner.widgetMap.startTimeField.disabled = !e.record.showTime;
                                combo.owner.widgetMap.endTimeField.disabled = !e.record.showTime;
                                //make sure we update the showTime flag
                                combo.owner.record.showTime = e.record.showTime;
                            }
                        }
                    }
                },
                durationField: new DurationField({
                    name: 'duration',
                    label: 'Duration',
                    unit: 'hour',
                    allowedUnits: 'hour',
                    weight: 650,
                    required: true,
                    disabled: true
                }),
                commentField: new TextAreaField({
                    name: 'comment',
                    label: 'Comment',
                    height: 70,
                    weight: 675,
                    required: false,
                    placeholder: 'Please enter a comment',
                    cls: 'b-comment-field'
                })
            }
        },
        eventTooltip: {
            template: (data) => {
                const isPMKeySFlag = data.eventRecord.readonly && data.eventRecord.absenceTypeId !== 19 ? `<span class="label label-info" style="position:absolute; right:10px; width:60px">PMKeyS</span>` : ``;

                const occuranceIndicator = data.eventRecord.isRecurring ? `<span class="b-fa b-fa-star" style="position:absolute; right:10px;"></span>` : data.eventRecord.isOccurrence ? `<span class="b-fa b-fa-sync" style="position:absolute; right:10px;"></span>` : '';

                const startDateFormatted = data.eventRecord.showTime ? `${DateHelper.format(data.startDate, this.dateTimeFormatNoSeconds)}` : `${DateHelper.format(data.startDate, this.dateFormat)}`;
                const endDateFormatted = data.eventRecord.showTime ? `${DateHelper.format(data.endDate, this.dateTimeFormatNoSeconds)}` : `${DateHelper.format(data.endDate, this.dateFormat)}`;

                return `
					<div style="overflow:hidden;" class="b-sch-event-tooltip">
						<div style="float:left;min-width:250px;" class="b-sch-event-title">${data.eventRecord.absenceType}</div>
                        <div style="float:left;">
							${occuranceIndicator}
							${isPMKeySFlag}
                        </div>
						<div style="float:left; min-width:250px;" class="b-sch-clockwrap b-sch-clock-day b-sch-tooltip-startdate">
							<div class="b-sch-clock">
								<div class="b-sch-hour-indicator" style="transform: none;">${DateHelper.format(data.startDate, 'MMM')}</div>
								<div class="b-sch-minute-indicator" style="transform: none;">${DateHelper.format(data.startDate, 'D')}</div>
								<div class="b-sch-clock-dot"></div>
							</div>
							<span class="b-sch-clock-text">${startDateFormatted}</span>
						</div>
						<div style="float:left;min-width:250px;" class="b-sch-clockwrap b-sch-clock-day b-sch-tooltip-enddate">
							<div class="b-sch-clock">
								<div class="b-sch-hour-indicator" style="transform: none;">${DateHelper.format(data.endDate, 'MMM')}</div>
								<div class="b-sch-minute-indicator" style="transform: none;">${DateHelper.format(data.endDate, 'D')}</div>
								<div class="b-sch-clock-dot"></div>
							</div>
							<span class="b-sch-clock-text">${endDateFormatted}</span>
						</div>
						<div style="float:left;margin-top:5px; font-size:0.85em">${StringHelperExtensions.trimWithEllipse(data.eventRecord.comment, 100)}</div>
					</div>`;
            }
        },
        eventMenu: {
            items: {
                unassignEvent: false,
                copyEvent: false,
                cutEvent: false
            },
            processItems({ eventRecord, items }) {
                if (items.editEvent) {
                    items.editEvent.text = "Edit Absence";
                }

                if (items.deleteEvent) {
                    items.deleteEvent.text = "Delete Absence";
                }

                return !eventRecord.readonly;
            }
        },
        scheduleMenu: {
            items: {
                addEvent: {
                    text: 'Add Absence'
                },
                pasteEvent: false
            }
        },
        timeAxisHeaderMenu: {
            items: {
                currentTimeLine: {
                    text: 'Highlight today'
                },
                eventsFilter: {
                    text : 'Filter absence types',
                    menu : {
                        items : {
                            nameFilter     : false,
                            categoryFilter : {
                                clearable    : true,
                                label        : 'By type',
                                type         : 'combo',
                                store        : this._eventTypeStore,
                                valueField   : 'id',
                                displayField : 'absenceType',
                                editable     : false,
                                listItemTpl  : (item) => {
                                    return `<div><div style="font-size: 80%">${item.absenceCategory}</div><div>${item.absenceType}</div></div>`;
                                },
                                listeners : {
                                    change({ value }) {
                                        const { eventStore } = this.up('menu').owner;
                                        if (value) {
                                            eventStore.filter('absenceCategory', value);
                                        } else {
                                            eventStore.clearFilters();
                                        }
                                    }
                                }
                            }
                        } as Partial<MenuItemConfig> // <---- This explicit cast is necessary otherwise I get a TS2322 error in Visual Studio
                    }
                },
                dateRange: {
                    menu: {
                        items: {
                            startDateField: {
                                format: this.dateFormat
                            },
                            endDateField: {
                                format: this.dateFormat
                            }
                        } as Partial<MenuItemConfig>
                    }
                },
            },
        }
    },
    columns: [
        {
            field: 'name', text: 'Resource', minWidth: 135, hidden: false, enableCellContextMenu: false, htmlEncode: false, editor: false, renderer: (e) => {
                const surname = this.getSurnameFromFullname(e.record.name);
                const firstInitial = this.getFirstNameInitialFromFullname(e.record.name);

                return `<div>
                    <div style="float:left;padding-right:3px;"><img class="b-resource-avatar"src="${this.imgFolderPath}${e.record.rankIcon?.toLowerCase() || 'none.png'}"></div>
					<div>${surname}, ${firstInitial}<div>
					<div style="font-size:0.8em">${e.record.employeeId}</div>
				</div>`;
            },
            tooltipRenderer: (e) => {
                const item = e.record;

                return `
				<table border="0" cellspacing="0" cellpadding="0" class="b-gantt-task-tooltip resource-tooltip" style="width:100%">
					<tr>
						<td colspan=2>
							<div class="avatar-container"><img title="${item.rank}" class="b-resource-avatar-lg" src="${this.imgFolderPath}${e.record.rankIcon?.toLowerCase() || 'none.png'}"></div>

							<div class="resource-name">${item.name.toProperCase()}</div>
							<div class="emp-id">${item.employeeId}</div>
						</td>
					</tr>
				</table>`;
            },
        },
    ],
    crudManager: {
        supportShortSyncResponse: false,
        resourceStore: {
            fields: [
                { name: 'id', dataSource: 'id' },
                { name: 'name', dataSource: 'name' },
                { name: 'employeeId', dataSource: 'employeeId' },
            ]
        },
        eventStore: {
            modelClass: AbsenceModel,
        },
        stores: [this._eventTypeStore],
        transport: {
            load: {
                url: `${this.baseUrl}load`,
                method: 'GET',
                paramName: 'q',
                // get rid of cache-buster parametero
                disableCaching: false,
                params: additionalRequestParams
            },
            sync: {
                requestConfig: {
                    url: `${this.baseUrl}sync`,
                    method: 'POST',
                    // get rid of cache-buster parameter
                    disableCaching: false,
                },
            }
        },
        listeners: {
            beforeloadapply: (e) => {
                this.processLoadResponse(e.response);
            },
            beforesync: (e) => {
                AjaxSyncHelper.preProcess(e);
            },
            loadFail: (e) => {
                Toast.show({ html: `Failed to load, please try again. <br/> if problem persist call IMPPACT Support Team and quote: ${e.response.requestId}`, timeout: 5000 });
            },
            sync: () => {
                Mask.unmask();
                this._absenceGrid.instance.onDataChange(this._scheduler.crudManager.eventStore, this._scheduler.crudManager.resourceStore);
                Toast.show('Saved successfully');
            },
            syncFail: (e) => {
                Mask.unmask();
                Toast.show({ html: `Failed to save changes, please try again. <br/> if problem persist call IMPPACT Support Team and quote: ${e.response.requestId}`, timeout: 5000 });
            },
            syncCanceled: () => { Toast.show('Saving changes was canceled'); }
        },
        autoSync: true
    },
    eventRenderer({ renderData, eventRecord }) {
        renderData.iconCls = eventRecord.isRecurring ? 'b-fa b-fa-star' : (eventRecord.isOccurrence ? 'b-fa b-fa-sync' : 'b-fa b-fa-calendar');
        return eventRecord.name;
    },
    listeners: {
        beforeEventEdit: e => {
            console.log(" before event edit");
            const record = e.eventRecord;

            if (!record.eventStore) {
                record.setDuration(null, 'hour');
            }

            return !record.data.readonly;
        },
        beforeEventEditShow: ({ eventRecord, editor, eventEdit }) => {
            console.log("before event edit show");
            const defaultDurationUnit = "hour";
            let duration = DateHelper.getDurationInUnit(eventRecord.startDate, eventRecord.endDate, defaultDurationUnit);
            if (eventRecord.absenceType !== "New event" && eventRecord.absenceType !== "") {
                editor.title = "Modify Absence Details";
                console.log(eventEdit);
                eventEdit.items.durationField.unit = defaultDurationUnit;
                eventEdit.durationField.defaultUnit = defaultDurationUnit;
                eventEdit.endDateField.value = eventRecord.endDate = (eventRecord.data.originalEndDate !== undefined ? new Date(eventRecord.data.originalEndDate) : eventRecord.endDate);
                editor.widgetMap.endDateField.value = eventRecord.endDate;
                editor.widgetMap.endTimeField.value = eventRecord.endDate;
                duration = this.calculateDuration(eventRecord.startDate, eventRecord.startDate, eventRecord.endDate, eventRecord.endDate, defaultDurationUnit);
                eventEdit.durationField.value = { magnitude: duration, unit: defaultDurationUnit };
            } else {
                editor.title = "New Absence Details";
                eventEdit.items.durationField.unit = defaultDurationUnit;
                eventEdit.durationField.value = { magnitude: duration, unit: defaultDurationUnit };
                eventEdit.startDateField.value = eventRecord.startDate = DateHelper.clearTime(eventRecord.startDate);
                eventEdit.endDateField.value = eventRecord.endDate = DateHelper.clearTime(eventRecord.endDate);
            }
            eventEdit.items.durationField.disabled = true;
            //remove the "YEARLY" option #250
            eventEdit.recurrenceCombo.items = eventEdit.recurrenceCombo.items.filter(i => i.id !== "YEARLY");
        },
        beforeEventSave: e => {
            console.log('beforeEventSave');

            // duration validation...
            if (e.values.duration <= 0) {
                Toast.show({ html: `Duration must be greater than zero`, color: 'b-red', style: 'overflow-y: auto;' });
                return false;
            }

            // do validation on comment and start / end dates here.
            if ((e.values && e.values.absenceTypeId === 20 && e.values.comment.length === 0) || e.values.comment === null) {
                Toast.show({ html: `Comment is required`, color: 'b-red', style: 'overflow-y: auto;' });
                return false;
            }

            if (e.values.comment.length > 0 && e.values.comment.length < this.minimumCommentLength) {
                Toast.show({ html: `Please provide a more detailed comment (minimum ${this.minimumCommentLength} characters)`, color: 'b-red', style: 'overflow-y: auto;' });
                return false;
            }

            if (e.values.endDate <= e.values.startDate || e.values.duration === 0) {
                Toast.show({ html: `Please provide valid dates, start and end times CANNOT be the same`, color: 'b-red', style: 'overflow-y: auto;' });
                return false;
            }

            if (e.values && e.values.absenceTypeId) {
                e.eventRecord.absenceType = (this._eventTypeStore.getById(e.values.absenceTypeId) as any).absenceType;
                e.eventRecord.name = e.eventRecord.absenceType;
            }
        }
    }
});

Let me know if you still need anything else and thanks again! :-)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working forum Issues from forum invalid This doesn't seem right
Projects
None yet
Development

No branches or pull requests

3 participants