Our pure JavaScript Scheduler component


Post by jk@datapult.dk »

Howdy,

I love this product! Good job team Byntum on building it!

I have two partnered schedulers that does not render so nicely.

What I want

  1. Share one CrudManager between the two schedulers
  2. Scheduler 1 filters to some resources, while scheduler 2 filter to the rest
  3. On load scroll to a given day.
  4. Some custom sorting to take place for overlapping events in scheduler 1.

My question is: Which parts of the code below is causing the following issues? And how to rework the code to make it "smoother" and only require a single HTTP request?

Issues

  1. On the initial load, I see two identical GET calls are issued to the backend, most likely one from each scheduler so double up on load time
  2. It renders rather "ugly", see this video (I am just sharing part of the view due to privacy). See how it renders events, then disappears only to reload them again? https://www.dropbox.com/s/i7zutvwxv4xbjz5/Screen%20Recording%202022-11-23%20at%2007.15.09.mov?dl=0

What I have

Just sharing the key points here (I set a very basic, standard CrudManager inside useSchedulesConfig):

<script setup lang="ts">
import {onMounted, reactive, ref, Ref, UnwrapRef} from "vue";
import {BryntumScheduler} from "@bryntum/scheduler-vue-3";
import {DateHelper, Scheduler} from "@bryntum/scheduler";

const unassignedSchedulerRef: Ref<UnwrapRef<BryntumScheduler>> = ref(null);
const assignedSchedulerRef: Ref<UnwrapRef<BryntumScheduler>> = ref(null);

const sharedConfig = useSchedulesConfig()
const unassignedConfig = ref(reactive({
    ...sharedConfig,
    ref: "unassignedSchedulerRef",
    cls: "auto-height-unassigned-shifts",
    hideHeaders: false,
}))

const assignedConfig = reactive({
    ...sharedConfig,
    ref: "assignedSchedulerRef",
    hideHeaders: true,
    tbar: null,
})

const maxEndDate = ref(new Date())
const minStartDate = ref(new Date())
const firstDayOfSchedule = ref(new Date())

const onScrollTo = (obj) => {
    const unassignedScheduler = unassignedSchedulerRef.value.instance.value;
    unassignedScheduler.scrollToDate(obj.value, {block: 'start', animate: 500})
}

onMounted(() => {
    const assignedScheduler = assignedSchedulerRef.value.instance.value;
    const unassignedScheduler = unassignedSchedulerRef.value.instance.value;
    unassignedScheduler.addPartner(assignedSchedulerRef.value.instance.value)
    unassignedScheduler.eventStore.filter(event => event.resourceId === 0)
    unassignedScheduler.resourceStore.filter(resource => resource.id === 0)
    assignedScheduler.eventStore.filter(event => event.resourceId !== 0)
    assignedScheduler.resourceStore.filter(resource => resource.id !== 0)
    unassignedScheduler.widgetMap.scrollTo.on({change: onScrollTo, thisObj: assignedScheduler});

    assignedScheduler.crudManager.on({
        beforeLoad() {
        },
        load({source, requestType, response, responseOptions}) {
            firstDayOfSchedule.value = new Date(Math.min(...assignedScheduler.eventStore.allRecords.filter(e => e.historic === false && e.eventType === "shift").map(r => r.startDate)))
            maxEndDate.value = new Date(Math.max(...assignedScheduler.eventStore.allRecords.filter(e => e.eventType === "shift").map(r => r.endDate)));
            minStartDate.value = new Date(Math.min(...assignedScheduler.eventStore.allRecords.filter(e => e.eventType === "shift").map(r => r.startDate)));
            assignedScheduler.setTimeSpan(minStartDate.value, maxEndDate.value)
            assignedScheduler.scrollToDate(firstDayOfSchedule.value, {block: 'start', animate: 500})
            unassignedScheduler.eventStore.sort([{field: 'startDate', ascending: true,},])
            unassignedScheduler.overlappingEventSorter = function (a, b) {
                const startA = DateHelper.format(a.startDate, "YYYY-MM-DD")
                const startB = DateHelper.format(b.startDate, "YYYY-MM-DD")
                if (startA !== startB)
                    return 0;
                if (a.name.toLowerCase() == b.name.toLowerCase())
                    return 0;
                if (a.name.toLowerCase() < b.name.toLowerCase())
                    return -1;
                if (a.name.toLowerCase() > b.name.toLowerCase())
                    return 1;
            }
        },
    });
})
</script>
<template>
  <div></div>
  <div id="schedule-calendar">
    <bryntum-scheduler ref="unassignedSchedulerRef" v-bind="unassignedConfig" />
    <bryntum-splitter appendTo="schedule-calendar" />
    <bryntum-scheduler ref="assignedSchedulerRef" v-bind="assignedConfig" />
  </div>
</template>

Post by alex.l »

Hi jk@datapult.dk,

For the scheduler with filtered data you need to use chained store and not apply filter to original (master) store.

    eventStore : firstScheduler.eventStore.chain(filterEventsFn),
    resourceStore : firstScheduler.resourceStore.chain(filterResourcesFn)

https://bryntum.com/products/scheduler/docs/api/Core/data/mixin/StoreChained#function-chain
https://bryntum.com/products/scheduler/docs/api/Core/data/mixin/StoreChained

All the best,
Alex


Post by jk@datapult.dk »

Thanks, Alex. This works just for the resources and not the events, so no the second scheduler has resources but no events. Here is the new code and please see git diff attached as well as outcome:

<script setup lang="ts">
import {onMounted, reactive, ref, Ref, UnwrapRef} from "vue";
import {BryntumScheduler} from "@bryntum/scheduler-vue-3";
import {DateHelper, Scheduler} from "@bryntum/scheduler";

const unassignedSchedulerRef: Ref<UnwrapRef<BryntumScheduler>> = ref(null);
const assignedSchedulerRef: Ref<UnwrapRef<BryntumScheduler>> = ref(null);

const sharedConfig = useSchedulesConfig()
const unassignedConfig = ref(reactive({
  ...sharedConfig,
  ref: "unassignedSchedulerRef",
  cls: "auto-height-unassigned-shifts",
  hideHeaders: false,
}))

const assignedConfig = reactive({
  ...sharedConfig,
  crudManager: null,
  ref: "assignedSchedulerRef",
  hideHeaders: true,
  tbar: null,
})

const maxEndDate = ref(new Date())
const minStartDate = ref(new Date())
const firstDayOfSchedule = ref(new Date())

const onScrollTo = (obj) => {
  const unassignedScheduler = unassignedSchedulerRef.value.instance.value;
  unassignedScheduler.scrollToDate(obj.value, {block: 'start', animate: 500})
}

onMounted(() => {
  const assignedScheduler = assignedSchedulerRef.value.instance.value;
  const unassignedScheduler = unassignedSchedulerRef.value.instance.value;
  assignedScheduler.addPartner(unassignedSchedulerRef.value.instance.value)
  assignedScheduler.eventStore = unassignedScheduler.eventStore.chain(event => event.resourceId !== 0)
  assignedScheduler.resourceStore = unassignedScheduler.resourceStore.chain(resource => resource.id !== 0)
  unassignedScheduler.eventStore.filter(event => event.resourceId === 0)
  unassignedScheduler.resourceStore.filter(resource => resource.id === 0)
  unassignedScheduler.widgetMap.scrollTo.on({change: onScrollTo, thisObj: assignedScheduler});

  assignedScheduler.crudManager.on({
    beforeLoad() {
    },
    load({source, requestType, response, responseOptions}) {
      firstDayOfSchedule.value = new Date(Math.min(...assignedScheduler.eventStore.allRecords.filter(e => e.historic === false && e.eventType === "shift").map(r => r.startDate)))
      maxEndDate.value = new Date(Math.max(...assignedScheduler.eventStore.allRecords.filter(e => e.eventType === "shift").map(r => r.endDate)));
      minStartDate.value = new Date(Math.min(...assignedScheduler.eventStore.allRecords.filter(e => e.eventType === "shift").map(r => r.startDate)));
      assignedScheduler.setTimeSpan(minStartDate.value, maxEndDate.value)
      assignedScheduler.scrollToDate(firstDayOfSchedule.value, {block: 'start', animate: 500})
      unassignedScheduler.eventStore.sort([{field: 'startDate', ascending: true,},])
      unassignedScheduler.overlappingEventSorter = function (a, b) {
        const startA = DateHelper.format(a.startDate, "YYYY-MM-DD")
        const startB = DateHelper.format(b.startDate, "YYYY-MM-DD")
        if (startA !== startB)
          return 0;
        if (a.name.toLowerCase() == b.name.toLowerCase())
          return 0;
        if (a.name.toLowerCase() < b.name.toLowerCase())
          return -1;
        if (a.name.toLowerCase() > b.name.toLowerCase())
          return 1;
      }
    },
  });
})
</script>
<template>
  <div></div>
  <div id="schedule-calendar">
    <bryntum-scheduler ref="unassignedSchedulerRef" v-bind="unassignedConfig" />
    <bryntum-splitter appendTo="schedule-calendar" />
    <bryntum-scheduler ref="assignedSchedulerRef" v-bind="assignedConfig" />
  </div>
</template>

SD1: https://www.dropbox.com/s/20uxt64zz45uyhq/Screenshot%202022-11-23%20at%2016.50.27.png?dl=0
SD2: https://www.dropbox.com/s/0lp5cs22lb5klcs/Screenshot%202022-11-23%20at%2016.53.00.png?dl=0


Post by alex.l »

Any errors in console? Any data in chained eventStore exists after filtering?
Could you please attach a runnable test case here? We need more context for the answer.

All the best,
Alex


Post by jk@datapult.dk »

Sure. Here you go. Thank you!

bryntum.zip
Functioning demo based on your NPM setup from examples/frameworks/vue-3/javascript/inline-data
(769.21 KiB) Downloaded 33 times

Post by jk@datapult.dk »

alex.l wrote: Wed Nov 23, 2022 4:25 pm

Hi jk@datapult.dk,

For the scheduler with filtered data you need to use chained store and not apply filter to original (master) store.

    eventStore : firstScheduler.eventStore.chain(filterEventsFn),
    resourceStore : firstScheduler.resourceStore.chain(filterResourcesFn)

https://bryntum.com/products/scheduler/docs/api/Core/data/mixin/StoreChained#function-chain
https://bryntum.com/products/scheduler/docs/api/Core/data/mixin/StoreChained

After more testing, I can say that the suggested solution is not right.

Steps to reproduce here:

  1. Copy paste this snippet scheduler2.resourceStore = scheduler1.resourceStore.chain(resource => resource.id === 1) into https://bryntum.com/products/scheduler/examples/partners/
  2. Notice that this filters the resources in BOTH partnered schedulers.

See this video: https://www.dropbox.com/s/dkefb9d5zoe87zy/Screen%20Recording%202022-11-25%20at%2007.41.21.mov?dl=0

Last edited by jk@datapult.dk on Fri Nov 25, 2022 8:43 am, edited 1 time in total.

Post by alex.l »

I can't run your example because of errors:

1 ERROR in child compilations (Use 'stats.children: true' resp. '--stats-children' for more details)
webpack compiled with 4 errors

After more testing, I can say that the suggested solution is not right.

As I mentioned above:

not apply filter to original (master) store.

But you did scheduler1.resourceStore.filter(resource => resource.id === 1), so you filtered master store that used in both instances. The result - data is filtered in both instances.

So if you need different filtered data for both schedulers, you need to use chained stores in both schedulers and one original crudManager instance.

All the best,
Alex


Post by jk@datapult.dk »

Thanks. I'll pose my question differently.

  1. Copy the code below into https://bryntum.com/products/scheduler/examples/partners/
  2. Why are there no events shown in either of the partnered schedulers?
import {Splitter, Scheduler, CrudManager, EventStore, ResourceStore} from '../../build/scheduler.module.js?463519';
import shared from '../_shared/shared.module.js?463519';

//region Data

const
    resources = [
        {id: 1, name: 'Arcady', role: 'Core developer', eventColor: 'purple'},
        {id: 2, name: 'Dave', role: 'Tech Sales', eventColor: 'indigo'},
        {id: 3, name: 'Henrik', role: 'Sales', eventColor: 'blue'},
        {id: 4, name: 'Linda', role: 'Core developer', eventColor: 'cyan'},
        {id: 5, name: 'Maxim', role: 'Developer & UX', eventColor: 'green'},
        {id: 6, name: 'Mike', role: 'CEO', eventColor: 'lime'},
        {id: 7, name: 'Lee', role: 'CTO', eventColor: 'orange'}
    ],
    events = [
        {
            id: 1,
            resourceId: 1,
            name: 'First Task',
            startDate: new Date(2018, 0, 1, 10),
            endDate: new Date(2018, 0, 1, 12)
        },
        {
            id: 2,
            resourceId: 2,
            name: 'Second Task',
            startDate: new Date(2018, 0, 1, 12),
            endDate: new Date(2018, 0, 1, 13)
        },
        {
            id: 3,
            resourceId: 3,
            name: 'Third Task',
            startDate: new Date(2018, 0, 1, 14),
            endDate: new Date(2018, 0, 1, 16)
        },
        {
            id: 4,
            resourceId: 4,
            name: 'Fourth Task',
            startDate: new Date(2018, 0, 1, 8),
            endDate: new Date(2018, 0, 1, 11)
        },
        {
            id: 5,
            resourceId: 5,
            name: 'Fifth Task',
            startDate: new Date(2018, 0, 1, 15),
            endDate: new Date(2018, 0, 1, 17)
        },
        {
            id: 6,
            resourceId: 6,
            name: 'Sixth Task',
            startDate: new Date(2018, 0, 1, 16),
            endDate: new Date(2018, 0, 1, 18)
        }
    ];

//endregion

    const eventStore = new EventStore({data: events})
    const resourceStore = new ResourceStore({data: resources})


const scheduler1 = new Scheduler({
    ref: 'top-scheduler',
    appendTo: 'container',
    flex: '1 1 50%',
    resourceImagePath: '../_shared/images/users/',

    features: {
        stripe: true,
        sort: 'name'
    },

    columns: [
        {
            type: 'resourceInfo',
            text: 'Staff',
            width: '10em'
        }
    ],

    resourceStore: resourceStore.chain(r => r.id === 1),
    eventStore: eventStore.chain(),

    startDate: new Date(2018, 0, 1, 6),
    endDate: new Date(2018, 0, 1, 20),
    viewPreset: 'minuteAndHour'
});

new Splitter({
    appendTo: 'container'
});

const scheduler2 = new Scheduler({
    ref: 'bottom-scheduler',
    cls: 'bottom-scheduler',
    appendTo: 'container',
    flex: '1 1 50%',
    partner: scheduler1,
    hideHeaders: true,
    resourceImagePath: '../_shared/images/users/',
    features: {
        stripe: true,
        sort: 'name'
    },

    columns: [
        {
            type: 'resourceInfo',
            text: 'Staff',
            width: '10em'
        }
    ],

    resourceStore: resourceStore.chain(r => r.id !== 1),
    eventStore: eventStore.chain(),
});

Post by alex.l »

I found that a bug with chained eventStore is still opened https://github.com/bryntum/support/issues/3995
I guess that's why it doesn't work this way.
I am afraid, we have to wait for that fix to use solution described here. For now, please try to use separate stores, that won't be possible to share one crudManager with 2 schedulers and have different filters applied without using chained stores.
That ticket has 5.2.x milestone, so it should be fixed in one of nearest releases.

All the best,
Alex


Post by jk@datapult.dk »

alex.l wrote: Fri Nov 25, 2022 1:28 pm

I found that a bug with chained eventStore is still opened https://github.com/bryntum/support/issues/3995
I guess that's why it doesn't work this way.
I am afraid, we have to wait for that fix to use solution described here. For now, please try to use separate stores, that won't be possible to share one crudManager with 2 schedulers and have different filters applied without using chained stores.
That ticket has 5.2.x milestone, so it should be fixed in one of nearest releases.

Thanks. Could you suggest a solution?


Post Reply