Our powerful JS Calendar component


Post by bin_yu »

Hi All,

I want to add Actual Start, Hours, Progress, Message fields into editor, remove Calendar/AllDay/Repeat/Delete info.

Customized Event Editor.png
Customized Event Editor.png (396.14 KiB) Viewed 154 times

How should I do in the following config. In addition, if I want to save these values of new fields, where should I add saveHandler function?

CalendarConfig.ts

//import { Calendar } from '@bryntum/calendar';
import { type BryntumCalendarProps } from '@bryntum/calendar-react';
import AppEventModel from './AppEventModel';
//import './CalendarToolbar';

export function formatDateToDDMMYYYY(date: string | Date): string | undefined {
    if (!date) return undefined;
    const dateObj = new Date(date);
    const day = String(dateObj.getDate()).padStart(2, '0');
    const month = String(dateObj.getMonth() + 1).padStart(2, '0'); // month start from 0
    const year = dateObj.getFullYear();
    //const hour = String(dateObj.getHours()).padStart(2, '0');
    //const minute = String(dateObj.getMinutes()).padStart(2, '0');
    //const second = String(dateObj.getSeconds()).padStart(2, '0');
    //return `${year}-${month}-${day}T${hour}:${minute}:${second}`;
    return `${year}-${month}-${day}`;
}

const calendarProps: BryntumCalendarProps = {
    // Features named by the properties are included.

// An object is used to configure the feature.
timeRangesFeature: {
    // configure timeRanges feature...
    headerWidth: 42
},

// An object is used to configure the feature.
eventTooltipFeature: {
    align: 'l-r'
},

date: new Date(),//new Date(2020, 9, 11),
hideNonWorkingDays: false,
// // Modes are the views available in the Calendar.
// // An object is used to configure the view.
// modes : {
//     year : false
// }

// Show the event list
modes: {
    year: false,
    list: {
        title: 'Task List', // The title of the list button
        weight: 100,
        // If we use field names which the EventList creates for itself, our config
        // gets merged into the default, so we can affect the EventList's own columns.
        columns: [{
            type: 'column',
            field: 'name',
            flex: '0 0 12em',
            renderer({ record }) {
                const event = record as AppEventModel;
                return [{
                    tag: 'i',
                    className: 'b-icon b-icon-circle',
                    style: `color:${event.resource?.eventColor}`
                }, {
                    text: event.name
                }];
            }
        },
        {
            type: 'column',
            field: 'projectId',
            text: 'Project Id',
            flex: '0 0 8em',
            renderer({ record }) {
                const event = record as AppEventModel;
                return [{
                    tag: 'i',
                    //className: 'b-icon b-icon-circle',
                    style: `color:${event.resource?.eventColor}`
                }, {
                    text: (event as any).projectId
                }];
            }
        },
        {
            type: 'column',
            field: 'projectName',
            text: 'Project Name',
            flex: '0 0 12em',
            renderer({ record }) {
                const event = record as AppEventModel;
                return [{
                    tag: 'i',
                    //className: 'b-icon b-icon-circle',
                    style: `color:${event.resource?.eventColor}`
                }, {
                    text: (event as any).projectName
                }];
            }
        },
        {
            type: 'column',
            field: 'startDate',
            text: 'Plan Start',
            flex: '0 0 8em',
            renderer({ record }) {
                const event = record as AppEventModel;
                return [{
                    tag: 'i',
                    //className: 'b-icon b-icon-circle',
                    style: `color:${event.resource?.eventColor}`
                }, {
                    text: formatDateToDDMMYYYY(event.startDate)
                }];
            }
        },
        {
            type: 'column',
            field: 'endDate',
            text: 'Plan End',
            flex: '0 0 8em',
            renderer({ record }) {
                const event = record as AppEventModel;
                return [{
                    tag: 'i',
                    //className: 'b-icon b-icon-circle',
                    style: `color:${event.resource?.eventColor}`
                }, {
                    text: formatDateToDDMMYYYY(event.endDate)
                }];
            }
        },
        {
            type: 'column',
            field: 'actualStart',
            text: 'Actual Start',
            flex: '0 0 8em',
            renderer({ record }) {
                const event = record as AppEventModel;
                return [{
                    tag: 'i',
                    //className: 'b-icon b-icon-circle',
                    style: `color:${event.resource?.eventColor}`
                }, {
                    text: (event as any).actualStart
                }];
            }
        },
        {
            type: 'column',
            field: 'hour',
            text: 'Hours',
            flex: '0 0 12em',
            renderer({ record }) {
                const event = record as AppEventModel;
                return [{
                    tag: 'i',
                    //className: 'b-icon b-icon-circle',
                    style: `color:${event.resource?.eventColor}`
                }, {
                    text: (event as any).hour
                }];
            }
        },
        {
            type: 'column',
            field: 'progress',
            text: 'Progress',
            flex: '0 0 12em',
            renderer({ record }) {
                const event = record as AppEventModel;
                return [{
                    tag: 'i',
                    //className: 'b-icon b-icon-circle',
                    style: `color:${event.resource?.eventColor}`
                }, {
                    text: (event as any).progress
                }];
            }
        },
        {
            type: 'column',
            field: 'message',
            text: 'message',
            flex: '0 0 12em',
        },
        {
            type: 'column',
            field: 'resources',
            text: 'Assignee',
            flex: '0 0 12em',
            renderer({ record }) {
                const event = record as AppEventModel;
                return [{
                    tag: 'i',
                    //className: 'b-icon b-icon-circle',
                    style: `color:${event.resource?.eventColor}`
                }, {
                    text: event.resource !== undefined && event.resource !== null ? event.resource.name : '' //event.resourceId
                }];
            }
        },        
        ],

        features: {
            headerMenu: {
                // We can't group by other fields, so disable all grouping menu options
                items: {
                    groupAsc: false,
                    groupDesc: false,
                    groupRemove: false
                }
            },


        }
    },
    day: {
        weight: 200,
        dayStartTime: 8,
        dayEndTime: 22,
        hourHeight: 70
    },
    week: {
        weight: 201,
        dayStartTime: 8,
        dayEndTime: 22,
        hourHeight: 70
    },
    month:{
        weight: 202,
    },
    agenda: {
        weight: 203,
    },
    weekResources: {
        weight: 204,
        // Type has the final say over which view type is created
        type: 'resource',
        title: 'By Resources',
    }
},

mode: 'list',// list, week // Default display

cls: 'custom-styles',
tbar: {
    items: {

        addButton: {
            type: 'button',
            text: 'Non-Working Time',
            //weight : 100, // Add before the Today button
            //weight: 600, // Add before the Day view
            weight: 101, // Add before the Day view
            onClick({ source }) {

                calendar.project.timeRangeStore.add({
                    name: 'no-working',
                    startDate: new Date(),
                    endDate: new Date(Date.now() + 3600 * 1000 * 2),
                    color: 'red',
                    cls: 'hatch-small b-cal-timerange-stretch',
                    recurrenceRule: 'FREQ=DAILY;INTERVAL=1;COUNT=10'
                });
            }
        }
    }
},

};



export { calendarProps };

CustomCalendar.tsx

import { createElement, Fragment, ReactElement, useRef, useState, useMemo } from 'react';
import {
    BryntumCalendarProjectModel, BryntumCalendar,

} from '@bryntum/calendar-react';
import {
    calendarProps,
} from './components/CalendarConfig';
import { CustomCalendarContainerProps } from "../typings/CustomCalendarProps";
import { projectData } from './components/AppData';
import { ObjectItem, ValueStatus } from "mendix";
import './ui/CustomCalendar.scss';
import {
    ResourceModel,
    TimeRangeModelConfig 
} from '@bryntum/calendar';

const mapMxID = (mxID: string | Big | undefined): string | undefined =>
    mxID ? (typeof mxID === "string" ? mxID : mxID.toString()) : undefined;

export interface ITask {
    obj: ObjectItem;
    id: string;
    projectId: string;
    projectName: string;
    progress: number | undefined,
    hour: number | undefined,
    message: string,
    actualStart: Date | undefined
    name: string | undefined;
    startDate: Date | undefined;
    endDate: Date | undefined;
    resourceId: string,
    eventColor: string
}

export interface IResource {
    obj: ObjectItem;
    id: string;
    resourceName: string | undefined;
    resourceRole: string | undefined;
}

function formatDateToDDMMYYYY(date: string | Date | undefined): string | undefined {
    if (!date || date === undefined) return undefined;
    const dateObj = new Date(date);
    const day = String(dateObj.getDate()).padStart(2, '0');
    const month = String(dateObj.getMonth() + 1).padStart(2, '0'); // 月份从0开始
    const year = dateObj.getFullYear();

return `${year}-${month}-${day}`;
}

export function convertTask2Event(oldTasks: ITask[]): any[] {
    let eventArray: any[] = [];
    for (const oldTask of oldTasks) {
        console.info("convertTask2Event-", formatDateToDDMMYYYY(oldTask.startDate ?? new Date()));

    const eventItem = {
        id: oldTask.id,
        name: oldTask.name,
        projectId: oldTask.projectId,
        projectName: oldTask.projectName,
        progress: oldTask.progress,
        hour: oldTask.hour,
        message: oldTask.message,
        
        actualStart: oldTask.id === undefined ? undefined : formatDateToDDMMYYYY(oldTask.actualStart ?? undefined),
        startDate: oldTask.id === undefined ? undefined : formatDateToDDMMYYYY(oldTask.startDate ?? ''),
        endDate: oldTask.id === undefined ? undefined : formatDateToDDMMYYYY(oldTask.endDate ?? ''),
        resourceId: oldTask.resourceId,
        eventColor: oldTask.eventColor
    };
    eventArray.push(eventItem);
}

return eventArray;
}

export function convertResourceList(resources: IResource[]) {
    let newResources: ResourceModel[] = [];
    for (const resource of resources) {
        if (resource.resourceName !== undefined && resource.resourceRole !== undefined) {
            const newResource = new ResourceModel({
                id: resource.id,
                name: resource.resourceName,
                //taskRole: resource.resourceRole,
                //events: [],
                //allowOverlap: false,
                //barMargin: 0
            });
            newResources.push(newResource);
        }
        //console.info("convertResourceList", newResources);
    }
    return newResources;

}

export function CustomCalendar(props: CustomCalendarContainerProps): ReactElement {
    const calendarRef = useRef<BryntumCalendar>(null);
    const project = useRef<BryntumCalendarProjectModel>(null);

const [events] = useState(projectData.events);
//const [resources] = useState(projectData.resources);
const [timeRanges] = useState<TimeRangeModelConfig>(projectData.timeRanges.rows as unknown as TimeRangeModelConfig);
//const [teamUserSet, setTeamUserSet] = useState<IResource[]>([]);
console.info(props.sampleText);

const resourceList = useMemo(() => {
    if (props.resourceList.status === ValueStatus.Available && props.resourceList.items) {
        const iResourceList: IResource[] = [];
        for (const objItem of props.resourceList.items) {
            const newId = mapMxID(props.resourceId.get(objItem).value);
            if (newId) {
                const newResource: IResource = {
                    obj: objItem,
                    //id: newId,
                    //id: props.resourceName.get(objItem).value || '1',
                    id: props.resourceId.get(objItem).value?.toString() ?? '1',
                    resourceName: props.resourceName.get(objItem).value,
                    resourceRole: props.resourceRole.get(objItem).value
                };
                iResourceList.push(newResource);
            }
        }

        //setTeamUserSet(iResourceList);
        return convertResourceList(iResourceList);
    }
}, [props.resourceList,
props.resourceName,
props.resourceRole,
props.resourceId
]);

const events2 = useMemo(() => {
    if (props.objectList.status === ValueStatus.Available && props.objectList.items) {
        const flatTaskList: ITask[] = [];
        // Iterator through the Mendix data and make a flat array of tasks (all children attributes are empty)
        for (const objItem of props.objectList.items) {
            const newId = mapMxID(props.id.get(objItem).value); // Example of how to parse the data from Mendix

            // only create a task if it's ID is valid
            if (newId) {
                const newTask: ITask = {
                    obj: objItem,
                    id: newId,
                    projectId: props.projectId.get(objItem).value ?? 'NULL',
                    projectName: props.projectName.get(objItem).value ?? 'NULL',
                    progress: props.progress.get(objItem).value?.toNumber() ?? undefined,
                    hour: props.hour.get(objItem).value?.toNumber() ?? undefined,
                    message: props.message.get(objItem).value ?? '',
                    actualStart: props.actualStart.get(objItem).value,
                    startDate: props.planStartDate.get(objItem).value,
                    endDate: props.planEndDate.get(objItem).value,
                    name: props.taskName.get(objItem).value ?? 'NULL',
                    //resourceId: props.taskOwner.get(objItem).value || '1',//'bryntum',
                    resourceId: ( props.projectId.get(objItem).value ?? 'NULL') + '-' + props.taskOwner.get(objItem).value || '1',//'bryntum',
                    eventColor: 'green'
                };
                flatTaskList.push(newTask);
            }
        }

        console.info(`useMemo processed flatTaskList count=${flatTaskList.length} `, flatTaskList);
        if (flatTaskList) {
            return convertTask2Event(flatTaskList);
        }
        else {
            console.info("useMemo no task found with parentId undefined");
            return undefined;
        }

    }
}, [props.objectList,
props.id,
props.taskName,
props.planStartDate,
props.planEndDate]);
let bryntumEvents = events2 != undefined ? events2 : [];
console.info("bryntumEvents", bryntumEvents);
console.info("events", events);
let resourcesCollection = resourceList != undefined ? resourceList : [];


return (
    <div id="container">
        <Fragment>

            <BryntumCalendarProjectModel
                ref={project}
                //events={events}
                events={bryntumEvents}
                //resources={resources}
                resources={resourcesCollection}
                timeRanges={Array.isArray(timeRanges) ? timeRanges : undefined}
            />
            <BryntumCalendar
                ref={calendarRef}
                project={project}
                {...calendarProps}
            />
        </Fragment>
    </div>
);
}

Post by ghulam.ghous »

Hi,

To customize the event editor in Bryntum Calendar, you can modify the eventEdit feature configuration. Here's how you can add new fields and remove existing ones:

  1. Add New Fields: You can add new fields like Actual Start, Hours, Progress, and Message by specifying them in the items config of the eventEdit feature.

  2. Remove Existing Fields: To remove fields like Calendar, AllDay, Repeat, and Delete, set their ref to null in the items config.

  3. Save Handler: Once configured, these values will be automatically set on the modal once you hit Save. But you can manipulate this process by using the beforeEventSave event to capture and process the data before it is saved.

Here's an example of how you can configure this:

const calendarProps: BryntumCalendarProps = {
    // Other configurations...

    eventEditFeature: {
            items: {
                // Add new fields
                actualStartField: {
                    type: 'datefield',
                    name: 'actualStart',
                    label: 'Actual Start'
                },
                hoursField: {
                    type: 'numberfield',
                    name: 'hours',
                    label: 'Hours'
                },
                progressField: {
                    type: 'numberfield',
                    name: 'progress',
                    label: 'Progress'
                },
                messageField: {
                    type: 'textfield',
                    name: 'message',
                    label: 'Message'
                },
                // Remove existing fields
                calendarField: null,
                allDayField: null,
                recurrenceCombo: null,
                deleteButton: null
            }
    },
};

export { calendarProps };

Make sure to replace the field names and types with the appropriate ones that match your data model.

Please also checkout the demo here: https://bryntum.com/products/calendar/examples/eventedit/
and the detailed guide for the customization: https://bryntum.com/products/calendar/docs/guide/Calendar/customization/eventedit


Post by bin_yu »

Hi @ghulam.ghous, I update config by your codes, and it works partially. The delete button/ALL Day checkbox have not disappeared

it-works-partially.png
it-works-partially.png (195.57 KiB) Viewed 145 times

one more style need your help, how to adjust the width of the actual start input
see the following config please.

eventEditFeature: {
        items: {
            // Add new fields
            actualStartField: {
                type: 'datefield',
                name: 'actualStart',
                label: 'Actual Start',
                width: '18em', // it doesn't work.
            },
            hoursField: {
                type: 'numberfield',
                name: 'hours',
                label: 'Hours'
            },
            progressField: {
                type: 'numberfield',
                name: 'progress',
                label: 'Progress'
            },
            messageField: {
                type: 'textfield',
                name: 'message',
                label: 'Message'
            },
            // Remove existing fields
            //calendarField: null,
            allDayField: null,
            recurrenceCombo: null,
            // Don't render the resource (calendar) selection input.
            // We're only ever using the "bryntum" calendar.
            resourceField : null,
            deleteButton: null
        }
    },

Post by ghulam.ghous »

Sorry it is not allDayField rather it is allDay so:

eventEditFeature : {
            items : {
                allDay : null
            },
            editorConfig : {
                bbar : {
                    items : {
                        deleteButton : null
                    }
                }
            }
        }

What about width? How much you wanna apply it? You will have to override it using the css. Can you please give it a go and if it does not help, I'll prepare a example for you.


Post by bin_yu »

Hi @ghulam.ghous, ALL Day and deleteButton work, thanks for your help.

Regarding to editable fields width of editor, I want to all input box align, but when field name is Actual Start(see the following screenshot), its width is a bit small.

width1.png
width1.png (94.84 KiB) Viewed 130 times

when I replaced Actual Start as Actual, its width is suitable(see the following screenshot). if field name is Acutal Start, how to adjust style and let all fileds' input box align.

width2.png
width2.png (87.53 KiB) Viewed 130 times

Post by ghulam.ghous »

Hey you can control that by defining a width for the label. For example you can use this css:

		.b-eventeditor.b-panel .b-eventeditor-content.b-eventeditor-content .b-field > label {
			flex: 0 0 10em;
		}

Please inspect the DOM and change the css according to your needs.


Post by bin_yu »

Thank you, ghulam.ghous. Everything worked. Thanks for your help.


Post by Animal »

Your code can be more efficient. There is a built in date formatting function.

Instead of

formatDateToDDMMYYYY(event.startDate)

You should use

DateHelper.format(event.startDate, 'YYYY-MM-DD')

https://bryntum.com/products/calendar/docs/api/Core/helper/DateHelper#function-format-static


Post by bin_yu »

Thanks @Animal, I have updated my codes by the way you provided. it works well.


Post Reply