support icon indicating copy to clipboard operation
support copied to clipboard

[REACT] Gantt component crash when use dispatcher to bind `viewPreset`

Open chuckn0rris opened this issue 8 months ago • 1 comments

Forum post

Hello Bryntum Team,

Please see a demo application attached. The Bryntum component crashes on view preset changes when there is a partner BryntumGantt component. Steps to reproduce:

  1. Open the app project and run "npm install" command"
  2. Run "npm start" command to lauch the application
  3. Click on the following buttons: "Show Partner Gantt" -> "Month" -> "Year" and see that app crashes after it.

This is a critical issue for us. Need help ASAP.

Thank you.

BryntumTestApp (1).zip

chuckn0rris avatar Apr 30 '25 06:04 chuckn0rris

Perhaps it would be worth to try to reproduce it in vanilla by repeatedly assigning viewPreset to the Gantt instance. I doubt it's a bug in a wrapper.

jsakalos avatar Apr 30 '25 07:04 jsakalos

I have re-investigated the showcase and the final conclusion is that the problem stems from partnering the gantts with one of the not-existent. The offending code from the original showcase:

            {state.bottomGanttVisible && (
                <div>
                    <BryntumGantt
                        tasks={state.tasks}
                        columns={state.columns}
                        viewPreset={state.viewPreset}
                        partner={gantt.current?.instance}
                        {...ganttConfig}
                    />
                </div>
            )}

where bottomGanttVisible is initially false. This code does not create gantt if condition is not met. The correct solution is to start with the invisible but existing gantt:

            {state.bottomGanttVisible && (
                <div>
                    <BryntumGantt
                        tasks={state.tasks}
                        columns={state.columns}
                        viewPreset={state.viewPreset}
                        partner={gantt.current?.instance}
                        {...ganttConfig}
                    />
                </div>
            )}

The complete code is in the attached zip. It is upgraded from react scripts to Vite. Closing the ticket.

basic.zip

jsakalos avatar May 07 '25 11:05 jsakalos

@jsakalos This is not the reason of this issue. In your version of the code the horizontal scroll bars synchronization doesn't work (the partnership functionality). Once you add this functionality via calling "addPartner" function, you will see the same error.

Andemki avatar May 07 '25 12:05 Andemki

Yes, unfortunately there's still a bug. We will try to reproduce it without React and subsequently fix.

jsakalos avatar May 07 '25 13:05 jsakalos

I have tried to reproduce the problem in an vanilla application w/o any framework and I was successful. I have used multiple-gantt-charts demo and replaced the content of app.js with:

import '../_shared/shared.js'; // not required, our example styling etc.
import Gantt from '../../lib/Gantt/view/Gantt.js';
import '../../lib/Scheduler/column/DurationColumn.js';
import '../../lib/Gantt/column/StartDateColumn.js';
import '../../lib/Gantt/column/EndDateColumn.js';
import '../../lib/Gantt/feature/Labels.js';
import Splitter from '../../lib/Core/widget/Splitter.js';
import ViewPreset from '../../lib/Scheduler/preset/ViewPreset.js';

const columns = [
    { type : 'name', width : 250, field : 'name' },
    { type : 'startdate', field : 'startDate' },
    { type : 'enddate', field : 'endDate' },
    { type : 'duration' }
];

const tasks = [
    {
        id                : 1,
        name              : 'Portfolio A',
        startDate         : '2025-01-01',
        endDate           : '2025-12-31',
        manuallyScheduled : true,
        children          : [
            {
                id                : 11,
                name              : 'Program A',
                startDate         : '2025-01-01',
                endDate           : '2025-08-01',
                manuallyScheduled : true,
                children          : [
                    {
                        id                : 111,
                        name              : 'Project A',
                        startDate         : '2025-01-01',
                        endDate           : '2025-05-01',
                        manuallyScheduled : true
                    },
                    {
                        id                : 112,
                        name              : 'Project B',
                        startDate         : '2025-05-01',
                        endDate           : '2025-08-01',
                        manuallyScheduled : true
                    }
                ]
            },
            {
                id                : 12,
                name              : 'Program B',
                startDate         : '2025-04-01',
                endDate           : '2025-12-31',
                manuallyScheduled : true,
                children          : [
                    {
                        id                : 121,
                        name              : 'Project A',
                        startDate         : '2025-04-01',
                        endDate           : '2025-10-01',
                        manuallyScheduled : true
                    },
                    {
                        id                : 122,
                        name              : 'Project B',
                        startDate         : '2025-10-01',
                        endDate           : '2025-12-31',
                        manuallyScheduled : true
                    }
                ]
            }
        ]
    },
    {
        id                : 2,
        name              : 'Portfolio B',
        startDate         : '2025-01-01',
        endDate           : '2026-12-31',
        manuallyScheduled : true,
        children          : [
            {
                id                : 21,
                name              : 'Program A',
                startDate         : '2025-01-01',
                endDate           : '2026-03-01',
                manuallyScheduled : true,
                children          : [
                    {
                        id                : 211,
                        name              : 'Project A',
                        startDate         : '2025-01-01',
                        endDate           : '2025-08-01',
                        manuallyScheduled : true
                    },
                    {
                        id                : 212,
                        name              : 'Project B',
                        startDate         : '2025-08-01',
                        endDate           : '2026-03-01',
                        manuallyScheduled : true
                    }
                ]
            },
            {
                id                : 22,
                name              : 'Program B',
                startDate         : '2026-03-01',
                endDate           : '2026-12-31',
                manuallyScheduled : true,
                children          : [
                    {
                        id                : 221,
                        name              : 'Project A',
                        startDate         : '2026-03-01',
                        endDate           : '2026-11-01',
                        manuallyScheduled : true
                    },
                    {
                        id                : 222,
                        name              : 'Project B',
                        startDate         : '2026-11-01',
                        endDate           : '2026-12-31',
                        manuallyScheduled : true
                    }
                ]
            }
        ]
    }
];

const topGantt = new Gantt({
    appendTo   : 'container',
    columns,
    tasks,
    viewPreset : 'manyYears',
    tbar       : [
        {
            type    : 'slidetoggle',
            checked : true,
            text    : 'Enable row dragging',
            onChange({ value }) {
                topGantt.features.rowReorder.disabled = bottomGantt.features.rowReorder.disabled = !value;
            }
        },
        {
            type    : 'slidetoggle',
            checked : true,
            text    : 'Sync scrolling',
            cls     : 'scrollToggle',
            style   : {
                'margin-inline-start' : '1em'
            },
            onChange({ value }) {
                bottomGantt.hideHeaders = value;
                if (value) {
                    topGantt.addPartner(bottomGantt);
                }
                else {
                    topGantt.removePartner(bottomGantt);
                }
            }
        },
        {
            type     : 'button',
            ref      : 'zoomInButton',
            cls      : 'b-transparent',
            icon     : 'b-icon-search-plus',
            tooltip  : 'Zoom in',
            onAction : () => topGantt.zoomIn()
        },
        {
            type     : 'button',
            ref      : 'zoomOutButton',
            cls      : 'b-transparent',
            icon     : 'b-icon-search-minus',
            tooltip  : 'Zoom out',
            onAction : () => topGantt.zoomOut()
        },
        '->',
        {
            type     : 'button',
            text     : 'Set year view',
            onAction : () => {
                topGantt.viewPreset = bottomGantt.viewPreset = 'manyYears';
                bottomGantt.viewPreset = bottomGantt.viewPreset = 'manyYears';
            }
        },
        {
            type     : 'button',
            text     : 'Set month view',
            onAction : () => {
                topGantt.viewPreset = bottomGantt.viewPreset = 'monthAndYear';
                bottomGantt.viewPreset = bottomGantt.viewPreset = 'monthAndYear';
            }
        },
        {
            type     : 'button',
            text     : 'Set visibility',
            onAction : () => {
                bottomGantt.hidden = !bottomGantt.hidden;
            }
        }
    ]
});

const bottomGantt = new Gantt({
    appendTo   : 'container',
    columns,
    tasks,
    viewPreset : 'manyYears',
    hidden     : true,
    partner    : topGantt
});

topGantt.expandAll();

Then click in this order: Set visibility, Set month view, Set year view. The crash will result:

Image

jsakalos avatar May 12 '25 10:05 jsakalos

Being that this will not reproduce on vanilla classes, I suspect the wrapper can be improved.

If multiple properties are being updated, these should be gathered into one config object and applied using instance.setConfig(object)

In that way, the ordering of ingestion of configs will be correct. Some configs depend upon the value of others. This is handled using setConfig

this method:

function applyPropValue(configOrInstance: any, prop: string, value: any, isConfig = true): void {
    // Assigning React wrapper component instance
    if (value?.current?.instance) {
        value = value.current.instance;
    }

    if (prop === 'features' && typeof value === 'object') {
        Object.keys(value).forEach(key =>
            applyPropValue(configOrInstance, `${key}Feature`, value[key], isConfig)
        );
    }
    else if (prop === 'config' && typeof value === 'object') {
        Object.keys(value).forEach(key =>
            applyPropValue(configOrInstance, key, value[key], isConfig)
        );
    }
    else if (prop === 'columns' && !isConfig) {
        configOrInstance.columns = value;
    }
    else if (prop.endsWith('Feature')) {
        const
            { features } = configOrInstance,
            featureName  = prop.replace('Feature', '');
        if (isConfig) {
            features[featureName] = value;
        }
        else {
            const feature = features[featureName];
            if (feature) {
                feature.setConfig(value);
            }
        }
    }
    else {
        configOrInstance[prop] = value;
    }
}

Any forEach loop there should build a config object, and apply the changes in bulk using setConfig

ExtAnimal avatar May 27 '25 07:05 ExtAnimal

Can we experiment to see how the following works?

function applyPropValue(configOrInstance: any, prop: string, value: any, isConfig = true): void {
    // Assigning React wrapper component instance
    if (value?.current?.instance) {
        value = value.current.instance;
    }

    if (prop === 'features' && typeof value === 'object') {
        const config = {};
        Object.keys(value).forEach(key =>
            applyPropValue(config, `${key}Feature`, value[key], isConfig)
        );
        configOrInstance.setConfig(config); // Apply all props <-----------------------
    }
    else if (prop === 'config' && typeof value === 'object') {
        const config = {};
        Object.keys(value).forEach(key =>
            applyPropValue(config, key, value[key], isConfig)
        );
        configOrInstance.setConfig(config); // Apply all props <-----------------------
    }
    else if (prop === 'columns' && !isConfig) {
        configOrInstance.columns = value;
    }
    else if (prop.endsWith('Feature')) {
        const
            { features } = configOrInstance,
            featureName  = prop.replace('Feature', '');
        if (isConfig) {
            features[featureName] = value;
        }
        else {
            const feature = features[featureName];
            if (feature) {
                feature.setConfig(value);
            }
        }
    }
    else {
        configOrInstance[prop] = value;
    }
}

ExtAnimal avatar May 27 '25 07:05 ExtAnimal