Our powerful JS Calendar component


Post by Vitaly Kroivets »

Hi, I hope you guys are doing well.

At last time, I have requested the multi-assign issue and I can see it's fixed in the v4.1.4.
But unfortunately, I faced another issue that is my event editor is broken.

I have added 3 custom fields information, quantity, hours in the event editor modal.
The custom field data is not saving to store after I click the save button.

For example, I have entered values information to first job, quantity to 20, hours: 8 and clicked the save button.
After that, I double clicked the same event to check if data is saved properly.
But they are still blank.....

Please help me this asap.
Also, I faced same issue on scheduler library after I upgraded to v4.1.4.

Looking forward to hearing from you soon.
Regards.

Vitaly.


Post by mats »

Can you please share your configuration and code?


Post by Vitaly Kroivets »

This is the code:


import React, {useEffect, useRef, useState} from "react";
import {BryntumCalendar} from "@bryntum/calendar-react";
// import useCalendarViewModel from "../ViewModels/Calendar/CalendarViewModel";
import "../Styles/Calendar.scss"
import CalendarHeader from "../_Components/CalendarHeader";
import VendorListManager from "../../Dashboard/Models/VendorListManager";
import {Observable} from "../../_CommonModels/ViewModelBase";
import {Toast} from "@bryntum/calendar/calendar.umd.js";
const date = new Date();
const startDate = new Date(date.getUTCFullYear(),date.getMonth(),date.getDate());
const Calendar = () => {
  const calendar = useRef(null);
  const [projectId, setProjectId] = useState(null);
  const getCrudManager = () => {
    return {
      transport: {
        load: {
          url: `${process.env.MIX_REACT_APP_API_URL}api/v1/calendars`,
          headers: {
            'Content-Type': 'application/json',
            Authorization: 'Bearer ' + Observable.getReduxValue('token')
          },
          credentials: 'include'
        },
        sync: {
          url: `${process.env.MIX_REACT_APP_API_URL}api/v1/calendars`,
          headers: {
            'Content-Type': 'application/json',
            Authorization: 'Bearer ' + Observable.getReduxValue('token')
          },
          credentials: 'include'
        },
      },
      listeners: {
        sync: {
          fn() {
            //@ts-ignore
            Toast.show('Successfully saved.');
            //@ts-ignore
            calendar.current.calendarInstance.crudManager.load()
          }
        }
      }
    };
  };
  //@ts-ignore
  const filterChangeHandler = ({value}) => {
    /** We filter using a RegExp, so quote significant characters**/
    value = value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
    /**
     * A filter with an id replaces any previous filter with that id.
     * Leave any other filters which may be in use in place.
     * **/
    // @ts-ignore
    calendar.current.calendarInstance.eventStore.filter({
      id: "eventNameFilter",
      filterBy: (event) => event.name.match(new RegExp(value, "i"))
    });
  };
  const highlightChangeHandler = ({value}) => {
    value = value.toLowerCase();
    // @ts-ignore
    const instance = calendar.current.calendarInstance;
    const eventStore = instance.eventStore;
    /** Calendars refresh on any data change so suspend that.
     * We will trigger the store's change event when we're done.**/
    eventStore.suspendEvents();
    /** Loop through all events in the store**/
    eventStore.forEach((task) => {
      /** The cls field is a DomClassList with add and remove methods**/
      if (value !== "" && task.name.toLowerCase().includes(value)) {
        task.cls.add("b-match");
      } else {
        task.cls.remove("b-match");
      }
    });
    eventStore.resumeEvents();
    /** Announce that data has changed which will refresh UIs.**/
    eventStore.trigger("change");
    instance.element.classList[value.length > 0 ? "add" : "remove"](
      "b-highlighting"
    );
  };
  const projectChangeHandler = (e) => {
    setProjectId(e.value)
  };
  useEffect(() => {
    if (projectId) {
      initCurrentStore(projectId);
      //@ts-ignore
      calendar.current.calendarInstance.crudManager.load().then(res => res);
      // calendar.current.calendarInstance.crudManager.load();
    }
  }, [projectId]);
  const sync = () => {
    //@ts-ignore
    calendar.current.calendarInstance.crudManager.sync();
  };
  const initCurrentStore = (projectId) => {
    //@ts-ignore
    calendar.current.calendarInstance.crudManager.on('beforeSend', function ({params, type}) {
      try {
        //@ts-ignore
        const {n_Equipment,n_Labor,n_Material,n_Quantity,n_Total,s_Craft,s_Status,s_Unit} = calendar.current.calendarInstance.eventEdit.values;
        params.n_Equipment = n_Equipment;
        params.n_Labor = n_Labor;
        params.n_Material = n_Material;
        params.n_Quantity = n_Quantity;
        params.n_Total = n_Total;
        params.s_Craft = s_Craft;
        params.s_Status = s_Status;
        params.s_Unit = s_Unit;
      } catch (e) {

  }
  params.personId = VendorListManager.getPersonIdByVendorId(Observable.getReduxValue('selected_vendor_id'));
  params.projectId = projectId;
});
  };

  return (
    <>
      <CalendarHeader
        onFilterChange={filterChangeHandler}
        onHighlightChange={(e) => {
          highlightChangeHandler(e)
        }}
        onProjectionChanged={(e) => {
          projectChangeHandler(e)
        }}
        onSync={() => sync()}
      />
      <BryntumCalendar
        ref={calendar}
        theme={'theme-triton'}
        crudManager={getCrudManager()}
        eventColor={null}
        date={startDate}
        eventEditFeature={{
          autoClose: false,
          items: {
            nameField: false,
            resourceField: {
              label: 'Member',
              // multiSelect:true,
            },
            locationField : {
              type    : 'text',
              name    : 'location',
              label   : 'Location',
              weight  : 120,
              // This field is only displayed for meetings
            },
            s_Info1: {
              type: 'text',
              label: 'Info',
              name: 's_Info1'
            },
            n_Quantity: {
              type: 'text',
              label: 'Quantity',
              name: 'n_Quantity'
            },
            s_Craft: {
              type: 'text',
              label: 'Craft',
              name: 's_Craft'
            },
            n_Hours: {
              type: 'text',
              label: 'Hours',
              name: 'n_Hours'
            },
            s_Unit: {
              type: 'text',
              label: 'Unit',
              name: 's_Unit'
            },
            n_Material: {
              type: 'text',
              label: 'Material',
              name: 'n_Material',
              width: 100
            },
            n_Labor: {
              type: 'text',
              label: 'Labor',
              name: 'n_Labor'
            },
            n_Equipment: {
              type: 'text',
              label: 'Equipment',
              name: 'n_Equipment'
            },
            n_Total: {
              type: 'text',
              label: 'Total',
              name: 'n_Total'
            },
          }
        }}

    columns={[
      {
        type: 'resourceInfo',
        text: 'Staff',
        width: 200
      }
    ]}
    eventStyle="border"
    listeners={{
      afterEventEditShow:(data)=>{
        let resourceId = null
        if(data.source.crudManager.resourceStore.data.length>1){
          resourceId = data.source.crudManager.resourceStore.data[1].id;
        }
        data.source.crudManager.assignmentStore.add({resourceId, eventId: data.eventRecord.data.id, event: data.eventRecord.data.id, resource: resourceId});
      },
      beforeEventSave: (data) => {
       	data.context.finalize(false)
      },
      sync: {
        fn() {
          Toast.show('Successfully saved.');
          this.refs.scheduler.schedulerInstance.crudManager.load()
        }
      }
    }}
    viewPreset={
      {
        base: 'hourAndDay',
        tickWidth: 10,
        columnLinesFor: 0,
        headers: [
          {
            unit: 'd',
            align: 'center',
            dateFormat: 'ddd DD MMM'
          },
          {
            unit: 'h',
            align: 'center',
            dateFormat: 'HH'
          }
        ]
      }
    }
  />
</>
  );
};

export default Calendar;



Post by saki »

Here are my findings:

  1. It is not necessary to listen to beforeSend event and add values manually to the request, however, we must let event store to know that we have them; see below.

  2. We can configure event store with a custom model class (with the additional fields), for example:

    class MyModel extends EventModel {
        static $name = 'MyModel'
    
        static get fields() {
            return [{
                name : 'myField',
                type : 'string'
            }];
        }
    }
    
        crudManager : {
            eventStore : {
                modelClass : MyModel
            },
            // etc.
        }
    features : {
        eventEdit : {
            items : {
                myField : {
                    name  : 'myField',
                    type  : 'text',
                    label : 'My Field'
                }
            }
        },
        // other features...
    }

    Please mind that the field name in class override myField matches name in the eventEdit configuration.

  3. Now, you can test the above setup by creating a simple success.json file:

    {
        "success" : true
    }

    and configure crudManager:

    crudManager : {
        autoSync  : true,
        transport : {
            load : {
                url : 'data/data.json'
            },
            sync : {
                url : 'data/success.json'
            }
        },
        // etc

    Now, when you change the text in myField in the editor and click the Save button, the change is committed to the event record and automatically send to the server:

    Screen Shot 2021-06-11 at 09.50.46.png
    Screen Shot 2021-06-11 at 09.50.46.png (429.81 KiB) Viewed 1334 times

Consult please the documentation for more details:


Post by Vitaly Kroivets »

Hi, I can get custom field is working but in this case, multiple assignment is not working.
Can you please provide me the example that integrate the custom field and multi-assignment functions in react?
It says that duplicate event

Thanks.

This is calendar config code:

/**
 * Application configuration
 */
import {Observable} from "../../_CommonModels/ViewModelBase";
import {Toast} from "@bryntum/calendar/calendar.umd";
import Task from "../../Scheduler/lib/Task";

const date = new Date();
const startDate = new Date(date.getUTCFullYear(),date.getMonth(),date.getDate());
export default function getCalendarConfig(calendarRef){
  return {
    calendarConfig:{
      crudManager: {
        transport: {
          load: {
            url: `${process.env.MIX_REACT_APP_API_URL}api/v1/calendars`,
            headers: {
              'Content-Type': 'application/json',
              Authorization: 'Bearer ' + Observable.getReduxValue('token')
            },
            credentials: 'include'
          },
          sync: {
            url: `${process.env.MIX_REACT_APP_API_URL}api/v1/calendars`,
            headers: {
              'Content-Type': 'application/json',
              Authorization: 'Bearer ' + Observable.getReduxValue('token')
            },
            credentials: 'include'
          },
        },
        listeners: {
          sync: {
            fn() {
              //@ts-ignore
              Toast.show('Successfully saved.');
              //@ts-ignore
              calendarRef.current.calendarInstance.crudManager.load()
            }
          }
        },
        eventStore:{
          modelClass:Task
        }
      },
      eventColor: null,
      theme:'theme-triton',
      date:startDate,
      columns:[
        {
          type: 'resourceInfo',
          text: 'Staff',
          width: 200
        }
      ],
      eventStyle:'border',
      listeners:{
        beforeEventSave:({resourceRecord})=>{
        },
        sync: {
          fn() {
            Toast.show('Successfully saved.');
            calendarRef.current.calendarInstance.crudManager.load()
          }
        }
      },
      eventEditFeature: {
        autoClose: false,
        items: {
          nameField: false,
          resourceField: {
            label: 'Member',
            multiSelect:true,
          },
          s_Info1: {
            type: 'text',
            label: 'Info',
            name: 's_Info1'
          },
          n_Quantity: {
            type: 'text',
            label: 'Quantity',
            name: 'n_Quantity'
          },
          s_Craft: {
            type: 'text',
            label: 'Craft',
            name: 's_Craft'
          },
          n_Hours: {
            type: 'text',
            label: 'Hours',
            name: 'n_Hours'
          },
          s_Unit: {
            type: 'text',
            label: 'Unit',
            name: 's_Unit'
          },
          n_Material: {
            type: 'text',
            label: 'Material',
            name: 'n_Material',
            width: 100
          },
          n_Labor: {
            type: 'text',
            label: 'Labor',
            name: 'n_Labor'
          },
          n_Equipment: {
            type: 'text',
            label: 'Equipment',
            name: 'n_Equipment'
          },
          n_Total: {
            type: 'text',
            label: 'Total',
            name: 'n_Total'
          },
        }
      },
      viewPreset:{
        base: 'hourAndDay',
        tickWidth: 10,
        columnLinesFor: 0,
        headers: [
          {
            unit: 'd',
            align: 'center',
            dateFormat: 'ddd DD MMM'
          },
          {
            unit: 'h',
            align: 'center',
            dateFormat: 'HH'
          }
        ]
      },
      // Modes are the views available in the Calendar.
      // An object is used to configure the view.
      modes: {
        year: false
      }
    }
  }
}

This is task model:

/**
 * Custom Task model
 *
 * Taken from the vanilla dragfromgrid example
 */
// we import scheduler.umd for IE11 compatibility only. If you don't use IE import:
// import { EventModel } from '@bryntum/scheduler';
import {EventModel} from '@bryntum/scheduler/scheduler.umd';

export default class Task extends EventModel {
  static get fields() {
    return [
      {name: 's_Info1', type: 'string'},
      {name: 'n_Quantity', type: 'string'},
      {name: 's_Craft', type: 'string'},
      {name: 'n_Hours', type: 'string'},
      {name: 's_Unit', type: 'string'},
      {name: 'n_Material', type: 'string'},
      {name: 'n_Labor', type: 'string'},
      {name: 'n_Equipment', type: 'string'},
      {name: 'n_Total', type: 'string'},
    ];
  }

  static get defaults() {
    return {
      // in this demo, default duration for tasks will be hours (instead of days)
      durationUnit: 'h'
    };
  }
}
Attachments
error.png
error.png (145.35 KiB) Viewed 1309 times

Post by saki »

Would you please wrap it in a showcase so that we can run, investigate and debug?


Post by Vitaly Kroivets »

It's too difficult to wrap up the project because it's very complex.
Can you please just provide me simple (Custom Field + Multi-assignment) sample that are using bryntum calendar 4.1.5?
Thanks.


Post by Vitaly Kroivets »

Also, i would be very appreciated if you make the (Custom Field + Multi-assignment) sample for bryntum scheduler 4.1.5?
I had already done in version 4.0.8 and upgraded.
After I upgraded the version, everything is broken.
Maybe there are too many differences between v4.0.8 and v4.1.5 (Calendar and Scheduler)


Post by saki »

I've put together a simple React demo based on https://bryntum.com/examples/calendar/multiassign/. It is enough to drop it somewhere under your document root and then run npm i && npm start and it should work.

Attachments
multi-assign.zip
(9.75 KiB) Downloaded 82 times

Post Reply