Our pure JavaScript Scheduler component


Post by c3ui@c3.ai »

Use Case
I am using BryntumSchedulerPro in a React component to display a Gantt chart. The component is initialized with data (events, resources, dependencies, etc.), and these data sets can be loaded at different times. This part works fine.

However, I also need the component to support updates programatically. Such as:

  1. Adding or removing events.
  2. Filtering or editing events or resources.
  3. Expanding or collapsing resources programmatically (e.g., via external buttons).
  4. To handle updates, I am using store.applyChangeset to calculate and apply the necessary changes to the stores. While this works for some cases, it does not work consistently for others.

Problem Description

  1. Hiding/Removing Events: When I remove an event (e.g., filtering it out) and later make it visible again, the event is correctly added back to the eventStore (verified via store.records), but it does not appear in the UI.
  2. UI Not Updating: Even after calling project.acceptChanges() and project.commitAsync(), the UI does not reflect the changes.
  3. Inconsistent Behavior: Removing events works fine (the UI updates correctly), but adding them back does not.
import React, { useRef, useEffect } from 'react';
import { BryntumSchedulerPro, BryntumSchedulerProProps, BryntumSchedulerProProjectModel } from '@bryntum/scheduler-react';
import {
  PresetManager,
  ViewPreset,
  ProjectModel,
  CalendarManagerStore,
  ResourceTimeRangeStore,
  DependencyStore,
  AssignmentStore,
  EventStore,
  ResourceStore,
  TimeRangeStore,
} from '@bryntum/schedulerpro';

// Function to calculate differences between two objects
const getDifferences = (obj1, obj2) => {
  const diff = {};
  for (const key in obj1) {
    if (obj1[key] !== obj2[key] && obj2[key] != null) {
      diff[key] = obj2[key];
    }
  }
  return diff;
};

// Reusable function to apply changes to a store
const applyStoreChanges = (store, newData, project) => {
  const currentData = store.records?.map((record) => record.data) ?? [];

  // Find records to add
  const recordsToAdd = newData.filter((newRecord) => {
    return !currentData.some((record) => record.id === newRecord.id);
  });

  // Find records to update
  const recordsToUpdate = newData
    .filter((newRecord) => {
      const existingRecord = currentData.find((record) => record.id === newRecord.id);
      return existingRecord && JSON.stringify(existingRecord) !== JSON.stringify(newRecord);
    })
    .map((newRecord) => {
      const existingRecord = currentData.find((record) => record.id === newRecord.id);
      return { ...getDifferences(existingRecord, newRecord), id: newRecord.id };
    });

  // Find records to remove
  const recordsToRemove = currentData.filter(
    (record) => !newData.some((newRecord) => newRecord.id === record.id)
  );
  project.acceptChanges()
  // Apply changes to the store
  store.applyChangeset({
    added: recordsToAdd,
    updated: recordsToUpdate,
    removed: recordsToRemove,
  });

  project.commitAsync();
};


const GanttChart: React.FunctionComponent<Props> = (props) => {
  const schedulerProRef = useRef<BryntumSchedulerPro>(null);
  const projectRef      = useRef<BryntumSchedulerProProjectModel>({resources: [], events: [], assignments: [], dependencies: [], calendars: [], resourceTimeRanges: []});
  const getSchedulerConfig = (): BryntumSchedulerProProps => {
    const { xAxis, yAxis, bryntumConfig, eventStyle, eventLayout, timelineZoomable } = props;
    return {
      autoHeight: !bryntumConfig?.height,
      ...
      columns: [
        {
          type: 'tree',
          field: 'name',
          text: yAxis?.label,
          showImage: false,
          width: 200,
        },
      ],
    };
  };

  const stringCalendar = JSON.stringify(props.calendars);
  const stringEvents = JSON.stringify(props.events);
  const stringResources = JSON.stringify(props.resources);
  const stringResourceTimeRanges = JSON.stringify(props.resourceTimeRanges);
  const stringAssignments = JSON.stringify(props.assignments);
  const stringDependencies = JSON.stringify(props.dependencies);
  const stringTimeRanges = JSON.stringify(props.timeRanges);
   
useEffect(() => { if (projectRef.current) { applyStoreChanges(projectRef.current.instance.assignmentStore, props.assignments || [], projectRef.current.instance); } }, [stringAssignments]); useEffect(() => { if (projectRef.current) { applyStoreChanges(projectRef.current.instance.dependencyStore, props.dependencies || [], projectRef.current.instance); } }, [stringDependencies]); useEffect(() => { if (projectRef.current) { applyStoreChanges(projectRef.current.instance.timeRangeStore, props.timeRanges || [], projectRef.current.instance); } }, [stringTimeRanges]); useEffect(() => { if (projectRef.current) { applyStoreChanges(projectRef.current.instance.calendarManagerStore, props.calendars || [], projectRef.current.instance); } }, [stringCalendar]); useEffect(() => { if (projectRef.current) { applyStoreChanges(projectRef.current.instance.resourceTimeRangeStore, props.resourceTimeRanges || [], projectRef.current.instance); } }, [stringResourceTimeRanges]); useEffect(() => { if (projectRef.current) { applyStoreChanges(projectRef.current.instance.resourceStore, props.resources || [], projectRef.current.instance); } }, [stringResources]); useEffect(() => { if (projectRef.current) { applyStoreChanges(projectRef.current.instance.eventStore, props.events || [], projectRef.current.instance); } }, [stringEvents]); const schedulerConfig = getSchedulerConfig(); React.useEffect(() => { if (schedulerProRef.current) { const start = new Date(props.xAxis?.startTime); const end = new Date(props.xAxis?.endTime); if (!isNaN(start?.getTime()) && !isNaN(end?.getTime())) { schedulerProRef.current.instance.setTimeSpan(start, end); } } }, [props.xAxis?.startTime, props.xAxis?.endTime]); return ( <div className={`c3-gantt-chart bryntum-theme-${bryntumTheme}`}> {props.header} <div className="c3-scheduler"> <BryntumSchedulerProProjectModel ref={projectRef} /> <BryntumSchedulerPro {...schedulerConfig} ref={schedulerProRef} project={projectRef} // more props for events callbacks /> </div> {props.footer} </div> ); }; export default GanttChart;

Bryntum SchedulerPro Version: 5.6.8

Attachments
Screen Recording 2025-05-08 at 5.14.16 p.m..mp4
(1.93 MiB) Downloaded 8 times

Post by c3ui@c3.ai »

It seems that the assigments was broken/unnasigned internally after I removed a resource or event then when I added the event back it was not displayed on any resource because I need to trigger the assigments relations internally.
I updated the code this way, it seems more like a workaround but I just provoked a fake change in assigmentsStore so it can create the relations internally.

useEffect(() => {
    if (projectRef.current) {
      projectRef.current.resources = JSON.parse(stringResources ?? '[]');
      projectRef.current.assignments = [];
      projectRef.current.assignments = JSON.parse(stringAssignments ?? '[]');
      projectRef.current.commitAsync();
    }
  }, [stringResources, stringAssignments]);

  useEffect(() => {
    if (projectRef.current) {
      projectRef.current.events = JSON.parse(stringEvents ?? '[]');
      projectRef.current.assignments = [];
      projectRef.current.assignments = JSON.parse(stringAssignments ?? '[]');
      projectRef.current.commitAsync();
    }
  }, [stringEvents, stringAssignments]);

Post by johan.isaksson »

Hi,

Thanks for reporting. Yes that is the default behavior when removing resources and events. I have not found a way of configuring the behavior in Scheduler Pro, but have asked the scheduling engine team to chime in on this

Best regards,
Johan Isaksson

Post by johan.isaksson »

According to them the behavior is not configurable, so unfortunately seems you have to keep working around it for the time being. Please let us know if that is too inconvenient or you get issues with your approach

Best regards,
Johan Isaksson

Post by c3ui@c3.ai »

Hi, thanks for your quick response.
I understand there is no direct solution for this. Now I am facing different issue due events update. If the project has a timezone specified, then the dates of the updated events does not pass through the process where the dates are converted to the project timeZone. Do you have a way to make it work?
On the other hand, it seems that I have to do many manual things to make the component work as expected after I update the data. Should I use a different method to update the data?


Post by ghulam.ghous »

Is there a way, you can provide us some steps to reproduce this issue? Are you setting string dates?


Post by c3ui@c3.ai »

Yeah, I am setting the dates in string every time I want to update the events data in the project.
Here I am updating the data setting the start date using date string. In this case the startdate is equals to the existing event which already is at 8pm.

Screenshot 2025-05-12 at 10.42.21 a.m..png
Screenshot 2025-05-12 at 10.42.21 a.m..png (685.09 KiB) Viewed 417 times

After I set events data I noticed that the event is using local time '-06:00' instead of the project timezone 'US/Eastern'.

Screenshot 2025-05-12 at 10.47.16 a.m..png
Screenshot 2025-05-12 at 10.47.16 a.m..png (675.14 KiB) Viewed 417 times

The events dates with project timeZone are only correct the first time I set the events data

Screenshot 2025-05-12 at 10.50.16 a.m..png
Screenshot 2025-05-12 at 10.50.16 a.m..png (693.92 KiB) Viewed 417 times

Post by joakim.l »

When trying to reproduce in our online examples, I think I get get something similiar when syncDataOnLoad is true on the EventStore (which it is by default when using a framework wrapper). Try configuring that to false and see if it helps.

Regards
Joakim


Post by c3ui@c3.ai »

Hi, I changed my code to only refresh project data when events, assignments, resources and dependencies are updated, otherwise re-create the project object. This seems to be working fine, it uses the project timezone for the updated events and the relations between resources and events are not broken.
BTW I did not know where to set syncDataOnLoad prop.

...
const stringCalendar = JSON.stringify(props.calendars);
  const stringEvents = JSON.stringify(props.events);
  const stringResources = JSON.stringify(props.resources);
  const stringResourceTimeRanges = JSON.stringify(props.resourceTimeRanges);
  const stringAssignments = JSON.stringify(props.assignments);
  const stringDependencies = JSON.stringify(props.dependencies);
  const stringTimeRanges = JSON.stringify(props.timeRanges);
  const stringDataRef = React.useRef(null);
  stringDataRef.current = {
    stringResources,
    stringResourceTimeRanges,
    stringAssignments,
    stringDependencies,
    stringTimeRanges,
    stringEvents,
    stringCalendar,
  };

  const project = React.useMemo(() => {
    return new ProjectModel({
      calendar: props.calendarId,
      calendarManagerStore: new CalendarManagerStore({
        data: stringCalendar ? JSON.parse(stringCalendar) : [],
      }),
      eventStore: new EventStore({
        data: stringDataRef.current?.stringEvents ? JSON.parse(stringDataRef.current.stringEvents).map(x => ({...x, resourceId: undefined, _resourceId: x.resourceId })) : [],
        tree: true,
        transformFlatData: true,
      }),
      // useRef to avoid re-rendering. We are not triggering any re-rendering/project recreation when the resources are updated.
      // reources updates are handled in a separate useEffect without recreating the project :)
      resourceStore: new ResourceStore({
        data: stringDataRef.current?.stringResources ? JSON.parse(stringDataRef.current.stringResources) : [],
        autoTree: true,
      }),
      resourceTimeRangeStore: new ResourceTimeRangeStore({
        data: stringResourceTimeRanges ? JSON.parse(stringResourceTimeRanges) : [],
        autoTree: true,
      }),
      assignmentStore: new AssignmentStore({
        data: stringDataRef.current?.stringAssignments ? JSON.parse(stringDataRef.current.stringAssignments) : [],
        autoTree: true,
      }),
      dependencyStore: new DependencyStore({
        data: stringDataRef.current?.stringDependencies ? JSON.parse(stringDataRef.current.stringDependencies) : [],
      }),
      timeRangeStore: new TimeRangeStore({
        data: stringTimeRanges ? JSON.parse(stringTimeRanges) : [],
      }),
      timeZone: props.timeZone,
      data: [],
    });
  }, [
    stringResourceTimeRanges,
    stringCalendar,
    stringTimeRanges,
    props.timeZone,
    props.calendarId,
  ]);

  const schedulerConfig = getSchedulerConfig();

  const ref = React.useRef<BryntumSchedulerPro>(null);
  React.useEffect(() => {
    if (ref.current) {
      const start = new Date(props.xAxis?.startTime);
      const end = new Date(props.xAxis?.endTime);
      if (!isNaN(start?.getTime()) && !isNaN(end?.getTime())) {
        ref.current.instance.setTimeSpan(start, end);
      }
    }
  }, [props.xAxis?.startTime, props.xAxis?.endTime]);

  // Update the project resources and assignments when the props change
  // Updating project resources helps to avoid re-rendering the project when the resources are updated
  // We need to include the assigments update here as well, because the relation between resources and events needs to be refreshed.
  React.useEffect(() => {
    if (ref.current) {
      ref.current.instance.project.resources = JSON.parse(stringResources ?? '[]');
      ref.current.instance.project.assignments = JSON.parse(stringAssignments ?? '[]');
      ref.current.instance.project.commitAsync()
    }
  }, [stringResources, stringAssignments]);

  React.useEffect(() => {
    if (ref.current) {
      ref.current.instance.project.events = JSON.parse(stringEvents ?? '[]').map(x => ({...x, resourceId: undefined, _resourceId: x.resourceId }));
      ref.current.instance.project.assignments = JSON.parse(stringAssignments ?? '[]');
      ref.current.instance.project.dependencies = JSON.parse(stringDependencies ?? '[]');
      ref.current.instance.project.commitAsync()
    }
  }, [stringEvents, stringAssignments, stringDependencies]);
  
...

Post by ghulam.ghous »

BTW I did not know where to set syncDataOnLoad prop.

You need to set it on the individual stores like eventStore, resourceStore etc. See this example:

eventStore: {
      syncDataOnLoad : true
}

Post Reply