Our pure JavaScript Scheduler component


Post by gitdev »

Hello,

My application uses the tree view to display many resources. The goal is to 1) fit everything vertically in one screen, without scrollbar and 2) reduce the noise due to useless information for the decision he needs to take at the moment.

Therefore, the user want to be able to choose which resources are displayed. If the resource is not displayed, no line should be there to save space just like MS Excel.

The tree view does not solve my problem because :
1) when clicking on the minus sign, the children lines collapse, but the parent line keep taking space. And 2) It is not possible to collapse only one of the child separately. You can either collapse all of them or none of them.

Is there a functionality in Bryntum Scheduler that behaves like the Excel filter ? The user can select/check each of the resources that he wants to display by clicking on the header at the top. I attached a screenshot of Excel to show what I am talking about.

As always, thank you very much for your support.
Attachments
a5.PNG
a5.PNG (2.79 KiB) Viewed 1567 times
a4.PNG
a4.PNG (2.88 KiB) Viewed 1567 times
2019-02-11_16-40-15.png
2019-02-11_16-40-15.png (117.85 KiB) Viewed 1576 times

Post by pmiklashevich »

Hello,

That's a good question. It's not supported out the box but you can easily implement it yourself. There is a Filter feature in scheduler. For now it has 3 types of widgets under the hood: date, number and string. String filter is used by default. Though it's not documented. But there is a public function called showFilterEditor which you can override to get advanced control over the widget to show and over the filtering process. I've created a simple proof of concept to show you an idea of how to implement this. Basically it creates a TreeGrid, copies resource store, expands all nodes and selectes all non-filtered records. On selectionchange event it applies new filter to the original resource store. Actually there are lots of ways to achieve what you need. The widget could be a simple Grid (if resources store is not a tree store), it could be a combobox with multiselect, or you can even create a separate grid on the page next to the scheduler and use its selection model to filter original resource store. What to choose is only up to you.

Here is the override:
let getResourceFilterStore = function(originalStore) {
    let store = new Store({
        modelClass : originalStore.modelClass,
        data       : originalStore.data,
        tree       : true
    });

    store.traverse(record => {
        record.expanded = true;
    });

    return store;
};

class FilterOverride {
    static get target() {
        return {
            class      : Filter,
            product    : 'scheduler',
            minVersion : '1.2.2',
            maxVersion : '1.2.2'
        };
    }

    showFilterEditor(column, value) {
        let me  = this,
            col = typeof column === 'string' ? me.grid.columns.getById(column) : column;

        if (col.filterable !== false && col.type === 'tree' && col.field === 'name') {
            let filterId       = 'resource-filter',
                filter         = me.grid.resourceStore.filters.get(filterId),
                nonSelectedIds = filter && filter.value || [];

            let resourceFilterStore = getResourceFilterStore(me.grid.resourceStore);

            let resourceFilterGrid = new TreeGrid({
                store           : resourceFilterStore,
                selectedRecords : resourceFilterStore.query(record => !nonSelectedIds.includes(record.id)),
                style           : 'background: #fff;', // move to CSS
                features        : {
                    cellEdit      : false,
                    columnPicker  : false,
                    columnReorder : false,
                    columnResize  : false,
                    contextMenu   : false,
                    group         : false,
                    sort          : false
                },
                selectionMode : {
                    'row'         : true,
                    'cell'        : false,
                    'multiSelect' : true,
                    'checkbox'    : true
                },
                columns : [{
                    type     : 'tree',
                    field    : 'name',
                    text     : '<i class="b-fw-icon b-icon-filter-equal"></i>Select resources...',
                    sortable : false,
                    flex     : 1
                }],
                listeners : {
                    selectionchange({ selection }) {
                        let selectedIds        = selection.map(record => record.id),
                            notSelectedRecords = resourceFilterStore.query(record => !selectedIds.includes(record.id)),
                            notSelectedIds     = notSelectedRecords.map(record => record.id);

                        me.grid.resourceStore.filter({
                            id       : filterId,
                            value    : notSelectedIds,
                            filterBy : record => !notSelectedIds.includes(record.id)
                        });
                    }
                }
            });

            me.filterEditorPopup = WidgetHelper.openPopup(col.element, {
                width  : '30em',
                height : '15em',
                cls    : 'b-filter-popup',
                items  : [resourceFilterGrid]
            });
        }
        else {
            return this._overridden.showFilterEditor.apply(this, arguments);
        }
    }
}

Override.apply(FilterOverride);
And here is the full example code. Please copy it to "scheduler/examples/tree/app.js" to check how it works. Please keep in mind that this is a proof of concept and might need some polishing.
import '../_shared/shared.js'; // not required, our example styling etc.
import Scheduler from '../../lib/Scheduler/view/Scheduler.js';
import '../../lib/Grid/column/TreeColumn.js';
import '../../lib/Grid/column/NumberColumn.js';
import '../../lib/Grid/feature/RegionResize.js';
import '../../lib/Grid/feature/Tree.js';
import '../../lib/Scheduler/feature/TimeRanges.js';
import ResourceModel from '../../lib/Scheduler/model/ResourceModel.js';
import Override from '../../lib/Common/mixin/Override.js';
import WidgetHelper from '../../lib/Common/helper/WidgetHelper.js';
import Store from '../../lib/Common/data/Store.js';
import TreeGrid from '../../lib/Grid/view/TreeGrid.js';
import Filter from '../../lib/Grid/feature/Filter.js';

class Gate extends ResourceModel {
    static get fields() {
        return [
            'capacity'
        ];
    }
}

let getResourceFilterStore = function(originalStore) {
    let store = new Store({
        modelClass : originalStore.modelClass,
        data       : originalStore.data,
        tree       : true
    });

    store.traverse(record => {
        record.expanded = true;
    });

    return store;
};

class FilterOverride {
    static get target() {
        return {
            class      : Filter,
            product    : 'scheduler',
            minVersion : '1.2.2',
            maxVersion : '1.2.2'
        };
    }

    showFilterEditor(column, value) {
        let me  = this,
            col = typeof column === 'string' ? me.grid.columns.getById(column) : column;

        if (col.filterable !== false && col.type === 'tree' && col.field === 'name') {
            let filterId       = 'resource-filter',
                filter         = me.grid.resourceStore.filters.get(filterId),
                nonSelectedIds = filter && filter.value || [];

            let resourceFilterStore = getResourceFilterStore(me.grid.resourceStore);

            let resourceFilterGrid = new TreeGrid({
                store           : resourceFilterStore,
                selectedRecords : resourceFilterStore.query(record => !nonSelectedIds.includes(record.id)),
                style           : 'background: #fff;', // move to CSS
                features        : {
                    cellEdit      : false,
                    columnPicker  : false,
                    columnReorder : false,
                    columnResize  : false,
                    contextMenu   : false,
                    group         : false,
                    sort          : false
                },
                selectionMode : {
                    'row'         : true,
                    'cell'        : false,
                    'multiSelect' : true,
                    'checkbox'    : true
                },
                columns : [{
                    type     : 'tree',
                    field    : 'name',
                    text     : '<i class="b-fw-icon b-icon-filter-equal"></i>Select resources...',
                    sortable : false,
                    flex     : 1
                }],
                listeners : {
                    selectionchange({ selection }) {
                        let selectedIds        = selection.map(record => record.id),
                            notSelectedRecords = resourceFilterStore.query(record => !selectedIds.includes(record.id)),
                            notSelectedIds     = notSelectedRecords.map(record => record.id);

                        me.grid.resourceStore.filter({
                            id       : filterId,
                            value    : notSelectedIds,
                            filterBy : record => !notSelectedIds.includes(record.id)
                        });
                    }
                }
            });

            me.filterEditorPopup = WidgetHelper.openPopup(col.element, {
                width  : '30em',
                height : '15em',
                cls    : 'b-filter-popup',
                items  : [resourceFilterGrid]
            });
        }
        else {
            return this._overridden.showFilterEditor.apply(this, arguments);
        }
    }
}

Override.apply(FilterOverride);

new Scheduler({
    appendTo   : 'container',
    minHeight  : '20em',
    eventColor : null,
    eventStyle : null,

    features : {
        filter       : true,
        timeRanges   : true,
        tree         : true,
        regionResize : true
    },

    rowHeight : 45,
    barMargin : 5,

    columns : [
        {
            type  : 'tree',
            text  : 'Name',
            width : 220,
            field : 'name'
        }, {
            type  : 'number',
            text  : 'Capacity',
            width : 90,
            field : 'capacity'
        }
    ],

    startDate  : new Date(2017, 11, 2, 8),
    //endDate   : new Date(2017, 11, 3),
    viewPreset : 'hourAndDay',

    crudManager : {
        autoLoad      : true,
        resourceStore : {
            modelClass : Gate
        },
        transport : {
            load : {
                url : 'data/data.json'
            }
        }
    },

    eventRenderer({eventRecord, resourceRecord, tplData}) {
        const {isLeaf} = resourceRecord;

        // Custom icon
        tplData.iconCls = 'b-fa b-fa-plane';

        // Add custom CSS classes to the template element data by setting property names
        tplData.cls.leaf = isLeaf;
        tplData.cls.group = !isLeaf;

        return isLeaf ? eventRecord.name : '\xa0';
    }

});

Also there was a similar question not so far ago about the filtering resources with empty lines. Here is the advice: viewtopic.php?f=44&t=10515#p57143
Please take a look, might be useful for you too. And please keep in mind that "store.filterBy()" will clear all the filters that has been set before. To be able to combine few filterBy functions please provide an id like "store.filter({id: 'test', filterBy: () => {...}})".

I hope this will help you to achieve your goals.

P.S. I've created a feature request to have it in our sources one day: https://app.assembla.com/spaces/bryntum/tickets/7622

Best wishes,
Pavel

Pavlo Miklashevych
Sr. Frontend Developer


Post by gitdev »

Thank you very much, I'll give it a try !

Post Reply