Our pure JavaScript Scheduler component


Post by eangelov »

Summary: The idea of the below example is to simulate a case where we render a SchedulerPro instance without data while the data is being pulled async. Once the data is being pulled it is loaded & another request that pulls updated data is being simulated. After the updated data is pulled we want to merge it with the existing data instead of replacing it that is where syncDataOnLoad comes into play. The problem is that after the loaded is updated visually the rendered events do not changes at the same time event tooltips show the updated values.

image_2021-04-02_153406.png
image_2021-04-02_153406.png (125.02 KiB) Viewed 987 times

If i call manually the method( in my case 'await updateData();' from the browser console ) that loads the updated data events are visually updated.

after_2nd_call_of_uploadData.png
after_2nd_call_of_uploadData.png (109.25 KiB) Viewed 987 times

If i remove 'repopulateOnDataset : false' i get 'Uncaught (in promise) Error: Already entered replica'

image_2021-04-02_154125.png
image_2021-04-02_154125.png (198.3 KiB) Viewed 987 times

Removing both 'syncDataOnLoad: true' & 'repopulateOnDataset : false' works as expected

Static example:

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="utf-8">

    <script type="text/javascript" src="./schedulerpro-4.0.8/build/schedulerpro.umd.js"></script>
    <link rel="stylesheet" href="./schedulerpro-4.0.8/build/schedulerpro.material.css" id="bryntum-theme" />
	<style>
		html, body {
			height: 100%;
			margin: 0;
			padding: 0;
		}
	</style>
</head>
<body id="rootContainer">
    <script type="text/javascript">
        const resources = [
                {
					"id":"userID1"
					,"name":"Test User"
				}
            ]
            , events = [
				{
					"id": "eventRecord1",
					"name": "Task #1",
					"startDate": new Date( 2021, 2, 3, 9, 0, 0 ),
					"endDate": new Date( 2021, 2, 5, 18, 0, 0 )
				}, {
					"id": "eventRecord2",
					"name": "Task #2",
					"startDate": new Date( 2021, 2, 3, 9, 0, 0 ),
					"endDate": new Date( 2021, 2, 5, 18, 0, 0 )
				}
			]
            , assignments = [
				{
					"id": 2,
					"eventId": "eventRecord1",
					"resourceId": "userID1",
					"event": "eventRecord1",
					"resource": "userID1"
				}, {
					"id": 3,
					"eventId": "eventRecord2",
					"resourceId": "userID1",
					"event": "eventRecord2",
					"resource": "userID1"
				}
			];

        const scheduler = new bryntum.schedulerpro.SchedulerPro({
            appendTo         : 'rootContainer'
			
			, project: {
				eventStore: new bryntum.schedulerpro.EventStore({
					data: []
					, syncDataOnLoad: true
				})
				, repopulateOnDataset : false
				
				, resourceStore: new bryntum.schedulerpro.ResourceStore({
					data: []
				})
				
				, assignmentStore: new bryntum.schedulerpro.AssignmentStore({
					data: []
				})
				
				, calendarManagerStore: new bryntum.schedulerpro.CalendarManagerStore({
					data: [{
						id: "workhours"
						, name: "Working hours"
						, unspecifiedTimeIsWorking: false
						, intervals: [
							{
								recurrentStartDate: "at 9:00"
								, recurrentEndDate: "at 13:00"
								, isWorking: true
							},{
								recurrentStartDate: "at 14:00"
								, recurrentEndDate: "at 18:00"
								, isWorking: true
							}
						]
					}]
				})
				, calendar: "workhours"
			}
            
			, columns : [
                { text: 'Employee', field: 'name' }
				, { text: 'Email', field: 'email' }
            ]
			
            , readOnly: true
			, features : {
				taskEdit : false
			}
			
			//set calendar columns visualisation 
			, viewPreset: {
				base: 'hourAndDay'
				, tickWidth: 30
				, displayDateFormat: 'll HH:mm'
				, headers: [
					{
						unit: 'day'
						, dateFormat: 'ddd DD/MM' //Mon 01/10
					},{
						unit: 'hour'
						, dateFormat: 'HH'
					}
				]
			}
        });

		function loadData(){
			console.log( "load data start" );
			return scheduler.resourceStore.loadDataAsync( resources )
				.then( () => {
					return scheduler.assignmentStore.loadDataAsync( assignments );
				})
				.then( () => {
					let loadDataPromise = scheduler.eventStore.loadDataAsync( events );
					
					loadDataPromise.then( _onEventStoreLoad );
					return loadDataPromise;
				});
		}
		
		const _onEventStoreLoad = function(){
			let eventStore = scheduler.eventStore
				, startDate = null
				, endDate = null;
				
			for( let index = 0, count = eventStore.getCount(); index < count; index++ ){
				let eventObj = eventStore.getAt( index );
				if( startDate === null || startDate > eventObj.startDate ){
					startDate = eventObj.startDate;
				}
				
				if( endDate === null || endDate < eventObj.endDate ){
					endDate = eventObj.endDate;
				}
			}

			if( startDate && endDate === null ){
				endDate = startDate;
			}
			else if( endDate && startDate === null ){
				startDate = endDate;
			}
			
			let schedulerStart = new Date( startDate );
			schedulerStart.setHours( schedulerStart.getHours() - 2 );
			
			let schedulerEnd = new Date( endDate );
			schedulerEnd.setHours( schedulerEnd.getHours() + 2 );
			
			if( scheduler.startDate > schedulerStart && scheduler.endDate < schedulerEnd ){
				scheduler.setTimeSpan( schedulerStart, schedulerEnd );
			}
			else if( scheduler.startDate > schedulerStart ){
				scheduler.setStartDate( schedulerStart, false );
			}
			else if( scheduler.endDate < schedulerEnd ){
				scheduler.setEndDate( schedulerEnd, false );
			}
		};
				
		//simulate a rquest that pulls data from an external source
		setTimeout( 
			() => {
				loadData()
					.then( () => {
						console.log( "load data end" );
						//simulate another rquest that pulls updated data
						setTimeout(
							async function(){
								console.log( "udpate data start" );
								await updateData();
								console.log( "udpate data end" );
							}
							, 3000
						)
					});
			}
			, 3000 
		);
		
		
		async function updateData(){
			return scheduler.resourceStore.loadDataAsync( resources ).then( () => {
				return scheduler.assignmentStore.loadDataAsync( assignments );
			}).then( () => {
				let loadDataPromise = scheduler.eventStore.loadDataAsync([
					{
						"id": "eventRecord1",
						"name": "Task #1",
						"startDate": new Date( 2021, 2, 3, 12, 0, 0 ),
						"endDate": new Date( 2021, 2, 5, 18, 0, 0 )
					}, {
						"id": "eventRecord2",
						"name": "Task #2",
						"startDate": new Date( 2021, 2, 3, 13, 0, 0 ),
						"endDate": new Date( 2021, 2, 5, 18, 0, 0 )
					}
				]);
				
				loadDataPromise.then( _onEventStoreLoad );
				return loadDataPromise;
			});
		}
    </script>
</body>
</html>

Post by pmiklashevich »

If i remove 'repopulateOnDataset : false' i get 'Uncaught (in promise) Error: Already entered replica'

Thanks for the report. It's a known issue. Ticket here: https://github.com/bryntum/support/issues/1913

Pavlo Miklashevych
Sr. Frontend Developer


Post by eangelov »

Hi,

I'm aware of the issue and that a suggested workaround for it is to set repopulateOnDataset to false. What i'm trying to explain is that when i use the suggested workaround no error is generated but the events visually are not updated as seen in the 1st screenshot from my original post. The start time of the tasks remains 9 when in reality it has changed to 12, this is visible in the tooltip of the selected event.


Post by arcady »

Thank you for the feedback. Here is a ticket for the issue: https://github.com/bryntum/support/issues/2606


Post by arcady »

BTW a note regarding the provided test case. Code like this can be rewritten more efficiently:

function loadData() {
    console.log('load data start');
    return scheduler.resourceStore.loadDataAsync(resources)
        .then(() => {
            return scheduler.assignmentStore.loadDataAsync(assignments);
        })
        .then(() => {
            const loadDataPromise = scheduler.eventStore.loadDataAsync(events);

            loadDataPromise.then(_onEventStoreLoad);
            return loadDataPromise;
        });
}

Thing is loadDataAsync triggers rescheduling by calling project commitAsync which is not really needed after each store loading. Since you know that other stores data is not loaded yet and rescheduling could be a quite expensive operation.

Instead it's better to load all data and trigger rescheduling only once. Something like this:

    // set stores data
    scheduler.resourceStore.data = resources;
    scheduler.assignmentStore.data = assignments;
    scheduler.eventStore.data = events;
    // trigger changes propagation
    const promise = scheduler.project.commitAsync();

Or for such purposes project model has https://www.bryntum.com/docs/gantt/#SchedulerPro/model/ProjectModel#function-loadInlineData method which loads data and triggers change propagation.


Post by arcady »

FYI the issue is resolved now. You can get/test the patched version of Scheduler Pro in tomorrow nightly build in the customerzone.


Post Reply