There is my config:
I'm trying to find the best way to play with the component.
LocaleManager.locale = 'FrFr';
const today = DateHelper.clearTime(new Date());
this._eventStore = new EventStore({
modelClass: TaskWorkEvent,
createUrl: 'php/create',
readUrl: 'php/read',
updateUrl: 'php/update',
deleteUrl: 'php/delete',
onException: async ({ source, exception, action, exceptionType, response, json } : any) => {
console.warn("Failed to save");
this._crudManager?.revertChanges();
this.scheduler?.project.stm.resetQueue();
//this.scheduler.eventStore.revertChanges()
},
onChange: () => {
this._eventStore?.commit();
}
});
this._resourceStore = new ResourceStore({
modelClass: TaskWork,
//onBeforeCommit: ({ source, changes }: any) => {
// console.log(source);
// console.log(changes);
// return false;
//},
createUrl: 'php/createResource',
readUrl: 'php/readResource',
updateUrl: 'php/updateResource',
deleteUrl: 'php/deleteResource',
autoCommit: true,
onCommit: () => {
console.info("Debug");
},
onChange: async ( event: any) => {
console.info("Tree change");
console.info(event);
//await this._resourceStore?.project.commitAsync();
// return false;
},
// Make it read only to prevent changes while committing
listeners: {
beforeCommit: () => {
this.scheduler?.readOnly = true;
},
commit: () => {
this.scheduler?.readOnly = false;
},
exception: (event : object) => {
//processException(event);
this.scheduler?.readOnly = false;
}
}
//onBeforeRequest: ({ source, changes }: any) => {
// console.log(source);
// console.log(changes);
// return false;
//}
});
this._crudManager = new CrudManager({
autoLoad: true,
eventStore: this._eventStore,
resourceStore: this._resourceStore ,
//autoSync: false,
loadUrl: 'templates/data.json',
//syncUrl: 'template/save',
validateResponse: true,
onBeforeSync: ({ source, pack }: any) => {
console.log(source);
console.log(pack);
console.log(source.id);
},
onRequestFail: ({ source, requestType, response }: any) => {
source.revertChanges();
if (this.scheduler) {
this.scheduler.project.stm.resetQueue();
// this.scheduler.eventStore.revertChanges()
// this.scheduler.eventStore.project.stm.resetQueue();
// console.log(this.scheduler.project.stm.position);
}
let serverMessage = response && response.message;
let exceptionText = `Action "${requestType}" failed. ${serverMessage ? ` Server response: ${serverMessage}` : ''}`;
BootstrapDangerToast.show(exceptionText);
console.error(exceptionText);
}
});
this.scheduler = new Scheduler({
appendTo: document.getElementById('scheduler') ?? document.body,
enableUndoRedoKeys: true,
startDate: new Date(2023, 4, 1),
endDate: new Date(2023, 7, 10),
viewPreset: 'weekAndMonth',
stepUnit: 'minute',
barMargin: 1,
rowHeight: 50,
height: '80vh',
flex: 4,
// This allows the timeline to be scrolled infinitely in time.
infiniteScroll: true,
multiEventSelect: true,
visibleDate: {
date: new Date(),
block: 'center',
animate: true
},
onAfterEventSave: () => {
console.info("After save");
},
tbar: {
items: {
scrollTo: {
label: 'Scroll to date',
inputWidth: '10em',
width: 'auto',
type: 'datefield',
value: today,
step: '1d',
listeners: {
change: ({ userAction, value }: any) => {
if (userAction) {
this.scheduler?.scrollToDate(DateHelper.set(value, 'hour', 12), { block: 'center', animate: 500 });
}
}
},
highlightExternalChange: false
},
viewPresetCombo: {
type: 'viewpresetcombo',
width: '7em'
},
checkBoxSnap: {
type: 'checkbox',
ref: 'snap',
label: 'Use snapping',
checked: true,
onChange: ({ checked }: any) => {
if (this.scheduler) {
this.scheduler.snap = checked;
}
}
},
sliderResolution: {
type: 'slider',
ref: 'resolution',
width: 130,
text: 'Time resolution',
showValue: true,
min: 5,
max: 60,
step: 5,
value: 30,
onChange: ({ value }: any) => {
if (this.scheduler) {
this.scheduler.timeResolution = value;
}
}
},
sliderZoom: {
type: 'slider',
ref: 'zoom',
width: 130,
text: 'Zoom',
showValue: true,
min: 0,
max: 25,
value: 15,
onInput: ({ value }: any) => {
if (this.scheduler) {
this.scheduler.zoomLevel = value;
}
}
},
undoRedo: {
type: 'undoredo',
items: {
transactionsCombo: {
width: 250,
displayValueRenderer(value: any, combo: any) {
const stmPos = combo.up('panel', true).project.stm.position || 0;
return stmPos + ' undo actions / ' + ((<any>this).store.count - stmPos) + ' redo actions';
}
}
}
},
rowHeight: {
type: 'slider',
ref: 'rowHeight',
text: 'Row height',
showValue: true,
min: 20,
onInput: ({ value }: any) => {
if (this.scheduler && resourceMargin && barMargin) {
this.scheduler.rowHeight = value;
// Limit margins to somewhat sane values
resourceMargin.maxHeight = barMargin.maxHeight = Math.max(0, (value - 10) / 2);
}
}
},
barMargin: {
type: 'slider',
ref: 'barMargin',
text: 'Bar margin',
showValue: true,
onInput: ({ value }: any) => {
if (this.scheduler) {
this.scheduler.barMargin = value;
}
}
},
resourceMargin: {
type: 'slider',
ref: 'resourceMargin',
text: 'Resource margin',
showValue: true,
max: 20,
onInput: ({ value }: any) => {
if (this.scheduler) {
this.scheduler.resourceMargin = value;
}
}
}
}
},
// Render some extra elements for the assignment equipment items
eventBodyTemplate: (data: any) => {
let v = data.myObject;
let logo = 'fa-helmet-safety';
if (!data.foreman) {
logo = '';
}
let html = `<div class="p-0"><i class="fa-regular ${logo} pe-2"></i><strong>${StringHelper.encodeHtml(data.name)}</strong></div>`;
if (this.scheduler && this.scheduler.rowHeight >= 30) {
html += `<div class="p-0">${StringHelper.encodeHtml(data.message)}</div>`;
}
return `
<div class="d-flex flex-column ">
${html}
</div>
`;
},
eventRenderer: ({ eventRecord }: any) => {
//console.log(eventRecord);
return {
name: eventRecord.name || '',
message: eventRecord.message,
foreman: eventRecord.data.foreman
// equipment: eventRecord.equipment.map((itemId) => this.equipmentStore.getById(itemId) || {})
};
},
// Use the CrudManager's stores.
//eventStore: crudManager.eventStore,
//resourceStore: crudManager.resourceStore,
crudManager: this._crudManager,
features: {
tree: true,
cellEdit: false,
filter: true,
rowReorder: true,
mergeCells: true,
eventMenu: {
items: {
// Add extra items shown for each event
move: {
text: 'Move event',
icon: 'b-fa b-fa-fw b-fa-arrows-alt-h',
cls: 'b-separator',
weight: 510,
// Add submenu
menu: {
// Submenu items
moveLeft: {
text: 'Move left',
icon: 'b-fa b-fa-fw b-fa-arrow-left',
cls: 'b-separator',
weight: 511,
onItem: ({ eventRecord }: any) => {
eventRecord.startDate = DateHelper.add(eventRecord.startDate, -1, 'hour');
}
},
moveRight: {
text: 'Move right',
icon: 'b-fa b-fa-fw b-fa-arrow-right',
weight: 512,
onItem: ({ eventRecord }: any) => {
eventRecord.startDate = DateHelper.add(eventRecord.startDate, 1, 'hour');
}
}
}
},
split: {
text: 'Split',
icon: 'b-fa b-fa-fw b-fa-cut',
weight: 520,
onItem: ({ eventRecord }: any) => {
eventRecord.split();
}
},
lock: {
text: 'Lock',
icon: 'b-fa b-fa-fw b-fa-lock',
cls: 'b-separator',
weight: 530,
onItem: ({ eventRecord }: any) => {
eventRecord.locked = true;
eventRecord.draggable = false;
eventRecord.resizable = false;
}
},
// Edit a built in item
editEvent: {
text: 'Update'
},
// Hide a built in item
deleteEvent: false
},
// Process items before context menu is shown, add or remove or prevent it
processItems: ({ eventRecord, items }: any) => {
if (eventRecord.eventType === 'meeting') {
// Add a custom item for meetings
items.cancel = {
text: 'Cancel',
icon: 'b-fa b-fa-fw b-fa-ban',
cls: 'b-separator',
weight: 540,
onItem: ({ eventRecord }: any) => {
eventRecord.canceled = true;
}
};
}
if (eventRecord.eventType === 'activity') {
// Remove "Edit" items for activities
items.editEvent = false;
// Add a "Done" item
items.done = {
text: 'Done',
icon: 'b-fa b-fa-fw b-fa-check',
cls: 'b-separator',
weight: 550,
onItem: ({ eventRecord }: any) => {
eventRecord.done = true;
}
};
}
// Not possible to lock canceled or completed events, disable the item
if (eventRecord.canceled || eventRecord.done) {
items.lock.disabled = true;
}
// Prevent menu for "locked" event
return !eventRecord.locked;
}
}
},
columns: [
{
type: 'tree',
field: 'name',
text: 'Name',
flex: 1
},
{
text: 'Available',
field: 'available',
mergeCells: true,
// Apply some CSS to hide the header text, looks ugly in the narrow header
cls: 'hide-header-text',
// Custom renderer for the merged cells, allows you to affected the generated merged cell (the contents are
// determined using the normal renderer/record value)
//renderer: ({ domConfig, value } : any) => {
// // Add a background based on team to the merged range
// Object.assign(domConfig.className, {
// 'bg-info': value === true,
// 'bg-primary': value === false,
// });
//}
},
{
text: 'Nbr tasks',
editor: false,
width: 100,
renderer: ({ record }: any) => record.events.length || '',
align: 'center',
sortable: (a: any, b: any) => a.events.length < b.events.length ? -1 : 1
},
{
text: 'Total Days',
editor: false,
width: 100,
renderer: ({ record }: any) =>
record.events.reduce((accumulator: number, object: any) => {
return accumulator + object.duration;
}, 0) || 0
,
align: 'center',
sortable: (a: any, b: any) => a.events.length < b.events.length ? -1 : 1
}
],
subGridConfigs: {
left: {
width: 350
},
},
// Scheduler always has a Project instance internally, we need to configure its internal StateTrackingManager
// so that the UndoRedo widget gets customized titles in its transaction dropdown.
project: {
stm: {
autoRecord: true,
getTransactionTitle(transaction: any) {
const lastAction = transaction.queue[transaction.queue.length - 1];
let { type, model } = lastAction;
if (lastAction.modelList && lastAction.modelList.length) {
model = lastAction.modelList[0];
}
let title = 'Transaction ' + (<any>this).position;
if (type === 'UpdateAction' && model instanceof EventModel) {
title = 'Edit flight ' + model.name;
}
else if (type === 'UpdateAction' && model instanceof ResourceModel) {
title = 'Edit gate ' + model.name;
}
else if (type === 'RemoveAction' && model instanceof EventModel) {
title = 'Remove flight ' + model.name;
}
else if (type === 'RemoveAction' && model instanceof ResourceModel) {
title = 'Remove gate ' + model.name;
}
else if (type === 'AddAction' && model instanceof EventModel) {
title = 'Add flight ' + model.name;
}
else if (type === 'AddAction' && model instanceof DependencyModel) {
title = StringHelper.xss`Link ${(<any>model.fromEvent).name} -> ${(<any>model.toEvent).name}`;
}
return title;
}
}
},
// Customize feature's keyMap in Scheduler's keyMap
//keyMap: {
// 'Ctrl+Shift+C': 'eventCopyPaste.copy',
// 'Ctrl+Shift+X': 'eventCopyPaste.cut',
// 'Ctrl+C': null,
// 'Ctrl+X': null
//},
listeners: {
// Listener called before the built in editor is shown
beforeEventEdit: ({ eventRecord, resourceRecord }: any) => {
this._editingEvent = { eventRecord: eventRecord, resourceRecord: resourceRecord };
const teams = eventRecord.resources;
// Show custom editor
//$('#customEditor').modal('show');
//// Fill its fields
//if (teams.length === 0) {
// // New match being created
// $('#home').val(resourceRecord.id);
//}
//else {
// $('#home').val(teams[0].id);
// $('#away').val(teams[1]?.id || '');
//}
//$('#startDate').val(DateHelper.format(eventRecord.startDate, 'YYYY-MM-DD'));
//$('#startTime').val(DateHelper.format(eventRecord.startDate, 'HH:mm'));
this.name(eventRecord.name);
this.startDate(eventRecord.startDate);
this.editor();
// Prevent built in editor
return false;
},
paint: ({ firstPaint }: any) => {
if (firstPaint) {
console.log('First paint');
}
},
beforeDestroy: () => {
alert("U are going away?");
console.log("Before destroy");
}
},
});
const { rowHeight, barMargin, resourceMargin } = this.scheduler.widgetMap;
And i'm using dataset from Tree example.
{
"success": true,
"resources": {
"rows": [
{
"id": 100,
"name": "Team Alpha",
"expanded": true,
"children": [
{
"id": 1,
"name": "Arcady"
},
{
"id": 2,
"name": "Dave",
"eventColor": "green",
"available": false
},
{
"id": 3,
"name": "Henrik",
"eventColor": "green",
"available": true
},
{
"id": 4,
"name": "Linda",
"eventColor": "red",
"available": false,
"statusMessage": "I'm ok!"
}
]
},
{
"id": 200,
"name": "Team Beta",
"expanded": true,
"children": [
{
"id": 5,
"name": "Maxim",
"eventColor": "red"
},
{
"id": 6,
"name": "Mike",
"eventColor": "red"
},
{
"id": 7,
"name": "Lee"
},
{
"id": 8,
"name": "Amit"
}
]
},
{
"id": 300,
"name": "Team Black Ops",
"expanded": true,
"children": [
{
"id": 9,
"name": "Kate",
"eventColor": "blue"
},
{
"id": 10,
"name": "Jong",
"eventColor": "blue"
},
{
"id": 11,
"name": "Lola"
},
{
"id": 12,
"name": "Lisa"
}
]
}
]
},
"events": {
"rows": [
{
"id": 1,
"name": "Hack server (fixed)",
"iconCls": "b-fa b-fa-server",
"startDate": "2023-05-08T00:00:00",
"duration": 5,
"durationUnit": "d",
"draggable": false,
"resizable": true,
"message": "Use flipper",
"foreman": {
"id": 100,
"login": "New one"
},
"test": "toto"
},
{
"id": 2,
"name": "Cyber attack",
"iconCls": "b-fa b-fa-laptop",
"startDate": "2023-05-09T00:00:00",
"duration": 12,
"durationUnit": "d",
"test": "tata",
"foreman": {
"id": 100,
"login": "New one"
}
},
{
"id": 3,
"name": "Port scan",
"iconCls": "b-fa b-fa-user",
"startDate": "2023-05-09T00:00:00",
"duration": 8,
"durationUnit": "d",
"test": "tata",
"foreman": {
"id": 100,
"login": "New one"
}
},
{
"id": 4,
"name": "Planning",
"iconCls": "b-fa b-fa-users",
"startDate": "2023-05-09T00:00:00",
"duration": 7,
"durationUnit": "d"
},
{
"id": 5,
"name": "Enforce firewall",
"startDate": "2023-05-09T00:00:00",
"iconCls": "b-fa b-fa-server",
"duration": 3,
"durationUnit": "d"
},
{
"id": 6,
"name": "Backup server",
"iconCls": "b-fa b-fa-server",
"startDate": "2023-05-09T00:00:00",
"cls": "Special",
"duration": 20,
"durationUnit": "d"
},
{
"id": 7,
"name": "Presentation",
"iconCls": "b-fa b-fa-video",
"startDate": "2023-05-11T00:00:00",
"cls": "Special",
"duration": 15,
"durationUnit": "d"
},
{
"id": 21,
"name": "Project Black",
"duration": 16,
"durationUnit": "d",
"iconCls": "b-fa b-fa-fw b-fa-cog"
},
{
"id": 22,
"name": "Project X",
"duration": 12,
"durationUnit": "d",
"iconCls": "b-fa b-fa-fw b-fa-cog"
},
{
"id": 23,
"name": "X-Mission",
"duration": 23,
"durationUnit": "d",
"iconCls": "b-fa b-fa-fw b-fa-user-secret"
},
{
"id": 24,
"name": "Installation",
"duration": 12,
"durationUnit": "d",
"iconCls": "b-fa b-fa-fw b-fa-circle-check"
},
{
"id": 25,
"name": "Meeting X",
"duration": 10,
"durationUnit": "d",
"iconCls": "b-fa b-fa-fw b-fa-question"
},
{
"id": 26,
"name": "Meeting",
"duration": 2,
"durationUnit": "d",
"iconCls": "b-fa b-fa-fw b-fa-user-check"
},
{
"id": 27,
"name": "Fly onsite",
"duration": 17,
"durationUnit": "d",
"iconCls": "b-fa b-fa-fw b-fa-plane"
},
{
"id": 29,
"name": "Book flight",
"duration": 3,
"durationUnit": "d",
"iconCls": "b-fa b-fa-fw b-fa-plane"
},
{
"id": 30,
"name": "Covert ops",
"duration": 2,
"durationUnit": "d",
"iconCls": "b-fa b-fa-fw b-fa-phone"
},
{
"id": 31,
"name": "Covert ops",
"duration": 1,
"durationUnit": "d",
"iconCls": "b-fa b-fa-fw b-fa-bug"
},
{
"id": 32,
"name": "Secret mission",
"duration": 1,
"durationUnit": "d",
"iconCls": "b-fa b-fa-fw b-fa-cog"
}
]
},
"assignments": {
"rows": [
{
"id": 1,
"resourceId": 1,
"eventId": 1
},
{
"id": 2,
"resourceId": 2,
"eventId": 1
},
{
"id": 3,
"resourceId": 3,
"eventId": 3
},
{
"id": 4,
"resourceId": 4,
"eventId": 4
},
{
"id": 5,
"resourceId": 5,
"eventId": 5
},
{
"id": 6,
"resourceId": 6,
"eventId": 6
},
{
"id": 7,
"resourceId": 7,
"eventId": 7
},
{
"id": 8,
"resourceId": 8,
"eventId": 8
}
]
}
}