DayBack lets you set up multiple calendars from different data sources. When creating a new event in DayBack, a few different rules govern to which calendar the event will belong. First, you can specify a single default calendar for new events in DayBack’s settings: this can be for the whole group or an individual user. You can also select the target calendar for new events in DayBack’s standard popover. After that, DayBack will suggest a calendar by looping through the active calendars in the sidebar from top to bottom.
How can we give the users a quicker method of selecting the calendar for new events?
Custom Action wizard KC Embrey came up with a great solution to this problem by adding a contextual menu to the New Event plus button. Now users can set the target calendar without worrying about the default configuration in settings or the order of calendars in the sidebar. Instead, users can right-click on the plus button and set the target calendar. Additionally, with a little custom CSS, the plus button will take on the color of the selected calendar, making it easy to keep track of which calendar they’re working with.
Update: July 30, 2021
KC has extended this action to work in FileMaker calendars. Just copy the code below into your instance of DayBack for FileMaker 19 or higher.
You could use a “selector” like this for choosing other things besides the target calendar. This would be a great interface for selecting event templates, for example.
Creating the Custom Action
There is no Event action attached to the plus button in DayBack and no existing contextual menu, so how can we modify its behavior? Several of DayBack’s Custom App Actions run when the calendar is first loaded. Since Custom App Actions have access to the same scope as the Custom Event and Button actions, a function can be defined as part of this routine and triggered by the plus button using standard JavaScript to define an oncontextmenu event.
Note: FileMaker overrides the right-click context menu, so in FileMaker, we’ve added the ability to use a key combo to bring up the calendar selector menu. You can define the modifier and key for this combo on lines 33 and 36 of this code.
In this case, the Custom App Action will be an After Calendar Rendered action so we can be sure all the relevant DOM elements are present. This action will also define the function for handling which source to reference when a new event is created. Under the hood, the function will duplicate the default event and apply the calendar chosen in the selector and destroy the original event. Here’s the sample code for the After Calendar Rendered action.
//New Event Selector After Calendar Rendered v1.10 //Purpose: //Adds a button for selecting the source on which new events will be created //Depends on the accompanying On Event Create action and CSS //Action Type: After Calendar Rendered //Prevent Default Action: No //More info on On Calendars Fetched actions and objects here: //https://docs.dayback.com/article/140-custom-app-actions // Declare globals var options = {}; var inputs = {}; try { //----------- Configuration ------------------- // Options specified for this action // Seconds to wait to allow this action to run before reporting an error (set to 0 to deactivate) options.runTimeout = 8; // Array of account emails for whom this action will run. Leave blank to allow the action to run for everyone. // Example: ['person@domain.com', 'someone@domain.com'] options.restrictedToAccounts = []; //DayBack for FileMaker doesn't support right-click in web viewers //Specify here which modifier key you'd like to use along with left click to bring up the menu in DayBack for FileMaker //Options are shiftKey, ctrlKey, altKey, metaKey inputs.modifierKey = 'altKey'; //key (use lower case) inputs.key = 'n'; //----------- End Configuration ------------------- } catch(error) { reportError(error); } //----------- You shouldn't need to edit below this line ------------------- // Action code goes inside this function function run() { var newEventSource; var schedules = seedcodeCalendar.get('schedules'); var isFileMaker = utilities.getDBKPlatform() === 'dbkfmjs'; function menuFunction(e) { //This was a right-button click and target is the add event button if ((isFileMaker && e[inputs.modifierKey] && e.key.toLowerCase() === inputs.key) || schedules && schedules.length > 0 && e.target.classList.contains('add-event-button') && ("which" in e && e.which == 3 || "button" in e && e.button == 2) ) { showSourceSelector(e, isFileMaker); // cancel default menu return false; } else { return true; } } if (isFileMaker) { //add key listener to window removeEventListener('keydown', menuFunction); addEventListener('keydown', menuFunction); } else { oncontextmenu = menuFunction; } function showSourceSelector(e, keyCombo) { var template = '<ul class="newEventSourceSelector">'; for (var index = 0; index < schedules.length; index++) { if (!schedules[index].readOnly && schedules[index].editable) { template += '<li class="newEventSelectorListItem' + (newEventSource === schedules[index].id ? ' selected' : '') + '" id="eventSelector_' + schedules[index].id + '" role="presentation" ng-click="popover.config.selectNewEventSource(\'' + schedules[index].id + '\', \'' + schedules[index].backgroundColor + '\');"><div style="width:7%;height:14px;margin-bottom:5px;margin-left:6px;border-radius:4px;border:1px gray solid;display: inline-block;background-color:' + schedules[index].backgroundColor + '"></div><a role="menuitem" class="newEventSourceSelectorItem">' + schedules[index].name + '</a></li>'; } } template += '</ul>'; utilities.popover( { id: 'newEventSourceSelector', container: $('body'), type: 'popover', target: keyCombo ? $('.add-event-button') : $(e.target), class: 'popover-menu newEventSourceSelectorPopover', show: true, destroy: true, onShow: '', onShown: '', onHide: '', onHidden: '', width: 300, direction: 'left', // anchorTop: true, useTargetOffset: true, selectNewEventSource: selectNewEventSourceAction, }, template); } function selectNewEventSourceAction(scheduleId, scheduleColor) { var newEventSelectorDiv = document.querySelector('.newEventSourceSelectorPopover'); var selectorListItems = document.querySelectorAll('.newEventSelectorListItem'); var addEventButton = document.querySelector('.add-event-button'); for (var i = 0; i < selectorListItems.length; i++) { selectorListItems[i].classList.remove('selected'); } newEventSource = scheduleId; addEventButton.style.transition = 'all 0.5s'; addEventButton.style.backgroundColor = scheduleColor; document.getElementById('eventSelector_' + scheduleId).classList.add('selected'); newEventSelectorDiv.style.transition = 'all 0.5s'; newEventSelectorDiv.style.opacity = 0; setTimeout( function() { $rootScope.$broadcast('closePopovers'); addEventButton.style.transition = 'none'; }, 500); } seedcodeCalendar.init('checkForNewEventSource'); seedcodeCalendar.init('checkForNewEventSource', function(event, action) { var newSchedule; if (newEventSource) { for (var index = 0; index < schedules.length; index++) { if (schedules[index].id === newEventSource) { newSchedule = schedules[index]; } } if (newSchedule && newSchedule != event.schedule) { cancelCallback(); duplicateEvent(event, newSchedule); return false; } else { return true; } } else { return true; } }); function duplicateEvent(event, schedule) { var eventResult = {}; var fieldMap = schedule.fieldMap; for (var property in fieldMap) { //recordID is included as we use that for sources like fielamker server that have a seperate non user facing id // Don't include recurringEventID as we don't want to make duplicated events repeating if (event[property] && property !== 'eventID' && property !== 'recordID' && property !== 'recurringEventID' && property !== 'timeStart' && property !== 'timeEnd') { eventResult[property] = event[property]; } } //Make sure we set the appropriate event source (scheduleID) eventResult.eventSource = schedule.source; if (eventResult.allDay && eventResult.end) { eventResult.end.subtract(1, 'day'); } eventResult.schedule = schedule; eventResult.eventStatus = { isClone: false, showPopover: true, firstOpen: true, wasModified: false, }; eventResult.newSource = { editable: true, id: eventResult.schedule.id, name: eventResult.schedule.name, }; //Run event actions if any exist var actionCallbacks = { confirm: function(action) { action.preventAction = true; dbk.addEvent(eventResult); }, cancel: function(action) { //Some functionality can go here that represents a cancel callback } }; var actionResult = callEventActions('eventCreate', eventResult, true, actionCallbacks); if (!actionResult) { return; } dbk.addEvent(eventResult); function callEventActions(type, editEvent, preventSave, actionCallbacks, callback, revertObject, changesObject, isUndo) { var event = editEvent.event ? editEvent.event : editEvent; var eventActions = getValidActions(editEvent.schedule.eventActions, event); var preventDefaultAction; var preventDefaultActionOverride; var preventActions; var validActions = {}; //Get an event action if any exist if (eventActions) { for (var property in eventActions) { if (eventActions[property].type === type && eventActions[property].url) { validActions[property] = eventActions[property]; if (validActions[property].preventAction) { preventActions = true; } } } for (var property in validActions) { if (preventActions || eventActions[property].preventAction) { //We need to reset this after we have looped through all valid actions as we are calling the second action if more than one exists eventActions[property].preventAction = null; } else { eventActions[property].callbacks = {}; for (var actionCallback in actionCallbacks) { var callbackName = actionCallback; eventActions[property].callbacks[actionCallback] = createActionCallback(actionCallback, actionCallbacks, eventActions[property]); } var actionResult = eventAction(editEvent, eventActions[property], callback, preventSave, revertObject, changesObject, null, null, isUndo); if (actionResult === true) { //If the result of a event action function is true we override prevent default preventDefaultActionOverride = true; } if (eventActions[property].preventDefault && !preventDefaultActionOverride) { preventDefaultAction = true; } } } if (preventDefaultAction && !preventDefaultActionOverride) { return false; } } return true; function getValidActions(actions, event) { var config = seedcodeCalendar.get('config'); var result = {}; if (!actions) { return; } for (var property in actions) { if (!actions[property].eventType) { continue; } else if (config.isShare && actions[property].eventType.shared) { result[actions[property].id] = actions[property]; } else if (!config.isShare && !isEventEditable(event) && actions[property].eventType.readonly) { result[actions[property].id] = actions[property]; } else if (isEventEditable(event) && actions[property].eventType.editable) { result[actions[property].id] = actions[property]; } } return Object.keys(result).length ? result : null; function isEventEditable(event) { return config.readOnly || (event.schedule && !event.schedule.editable) ? false : true; } } function createActionCallback(callbackName, actionCallbacks, action) { return function() { actionCallbacks[callbackName](action); }; } //Run event action. These are passed in from our datasource (Filemaker) and run scripts there function eventAction(editEvent, action, callback, preventSave, revertObject, changesObject, targetElement, jsEvent, isUndo) { if (!action || !action.url) { return; } var url = action.url; var newWindow = action.newWindow; var config = seedcodeCalendar.get('config'); var event = editEvent.event ? editEvent.event : editEvent; //regex for finding all open functions (we want to replace them if this is going to be in a callback) var openPatternMatch = /open\((.*?)\);?/g; var matches; var match; var result; var modifiedEvent; var functionDetected = isFunction(url); var options = {}; options.isUndo = !!isUndo; //Test if our event has been modified and needs to be saved. Returns the editEvent object modifiedEvent = !editEvent.event || preventSave ? false : eventChanged(editEvent, editEvent.event); if (!functionDetected) { //We didn't detect any function calls so treat as url if (newWindow) { url = "open('" + url + "','_blank');"; } else { url = "location.href = '" + url + "';"; } } //function for opening windows in event actions function newEventActionWindow(openUrl, target, features, replace) { window.open(openUrl, target, features, replace); } //If the event needs to be saved if (modifiedEvent) { if (url.indexOf('open(') !== -1) { //Get an array of open function matches matches = url.match(openPatternMatch); for (var i = 0; i < matches.length; i++) { //Get an array of the match and the function argument match = openPatternMatch.exec(url); //Only replace if this is not a function of another object if (url.substring(match.index - 1, match.index) !== '.') { url = url.replace(match[0], 'newEventActionWindow(' + match[1] + ');'); } } } options.showConfirmation = saveRequiresConfirmation(editEvent.event); options.isCustomAction = true; //Normalize end date in edit object for all day events options.endShifted = false; //If our event has been modified we need to save it getEventChanges( editEvent.event, editEvent, null, eventModifiedCallback, options ); } else { //If the event doesn't need to be saved first runAction(event); } //Run our routine for after checking if an event was saved or not (should be callback for updateEvent) function eventModifiedCallback(event) { //We make sure and update our popover data because saving an event (like we most likely did) adds a day to all day events. updateEditEvent(event || editEvent.event, editEvent); runAction(event); } function isFunction(string) { return (url.indexOf('(') !== -1 && url.indexOf(')') !== -1) || url.indexOf(';') !== -1; } function getValidShareDataMap(type, source, event) { var dataMap = source[type] ? source[type] : null; var sourceScheduleID = utilities.stringToID(event.shareScheduleID); var scheduleDataMap; if (source[sourceScheduleID]) { scheduleDataMap = source[sourceScheduleID][type]; if (!dataMap && scheduleDataMap) { dataMap = {}; } for (var property in scheduleDataMap) { dataMap[property] = scheduleDataMap[property]; } } return dataMap; } //Execute function runAction(event) { var config = seedcodeCalendar.get('config'); //If we don't have an event don't execute action. This would happen if there was an error saving the event for example. if (!event) { return; } var shareSources = seedcodeCalendar.get('shareSources'); var fieldMap; var customFields; if (config.isShare) { for (var property in shareSources) { if ((event.shareSourceID === shareSources[property].id && !shareSources[property].localParent) || (event.shareScheduleID === shareSources[property].id) ) { fieldMap = getValidShareDataMap('fieldMap', shareSources[property], event); customFields = getValidShareDataMap('customFields', shareSources[property], event); for (var field in customFields) { fieldMap[field] = customFields[field].field; } } } } else { fieldMap = event.schedule.fieldMap; } var eventProperty; //Assign helper functions //this is the dbk object in the custom action scope //helper functions are now defined at the top of the calendarInit.init. var helpers = seedcodeCalendar.get('actionHelpers'); helpers.tooltip = function(content, options) { //Options is an object containing additional properties if (!options) { options = {}; } var toolTipElement = options.targetElement ? options.targetElement : targetElement; var placement = options.placement ? options.placement : 'auto'; var container = options.container ? options.container : 'body'; var delay = options.delay >= 0 ? options.delay : 350; var hideOnClick = options.hideOnClick === false ? false : true; // Don't bother initiating if on mobile and hide on click as mobile will hide before it's shown if (environment.isMobileDevice && hideOnClick) { return; } utilities.tooltip(toolTipElement, content, options.className, placement, container, null, delay, hideOnClick); }; //Loop through our field map and replace tokens in the url with event data for (var property in fieldMap) { if (property === 'end' && event.allDay) { eventProperty = event[property].clone().subtract(1, 'days').toISOString(); } else { eventProperty = moment.isMoment(event[property]) ? event[property].clone().toISOString() : String(event[property]); } //Escape double, single quotes and returns eventProperty = eventProperty ? eventProperty.replace(/'/g, "\\'").replace(/"/g, '\\"').replace(/\n/g, '\\n') : eventProperty; url = url.split('[[' + fieldMap[property] + ']]').join(eventProperty); } //Assign the executed result ot our result var so the parent function can return it result = executeFunction(url, helpers, targetElement, jsEvent); //Our function run environment, creates an isolated environment that should minimize harm function executeFunction(func, dbk, targetElement, jsEvent) { //Sanitize window and global scope var window = undefined; //Assign a window var to prevent access to the window object. This is faster than trying to sanitize the whole window object. //Assign local vars that match global vars we want to block access to var angular = undefined; var firebase = undefined; var crypt = undefined; //extract showMessage from utilities before removing var calendarIO = undefined; //Execute function try { var executed = eval(func); } catch(e) { if (e instanceof SyntaxError) { dbk.showMessage('Syntax Error: ' + e.message, 0, 8000, 'error'); } else { dbk.showMessage('Error: ' + e.message, 0, 8000, 'error'); } } return executed; } return result; } return result; } } } } //----------- Run function wrapper and helpers - do not edit unless you know what these do ------------------- // Variables used for helper functions below var timeout; // Execute the run function as defined above try { if (!options.restrictedToAccounts || !options.restrictedToAccounts.length || (options.restrictedToAccounts && options.restrictedToAccounts.indexOf(inputs.account) > -1) ) { if (action.preventDefault && options.runTimeout) { timeoutCheck(); } run(); } else if (action.preventDefault) { confirmCallback(); } } catch(error) { reportError(error); } // Run confirm callback when preventDefault is true. Used for async actions function confirmCallback() { cancelTimeoutCheck(); if (action.callbacks.confirm) { action.callbacks.confirm(); } } // Run cancel callback when preventDefault is true. Used for async actions function cancelCallback() { cancelTimeoutCheck(); if (action.callbacks.cancel) { action.callbacks.cancel(); } } // Check if the action has run within the specified time limit when preventDefault is enabled function timeoutCheck() { timeout = setTimeout(function() { var error = { name: 'Timeout', message: 'The action was unable to execute within the alloted time and has been stopped' }; reportError(error, true); }, (options && options.runTimeout ? options.runTimeout * 1000 : 0) ); } function cancelTimeoutCheck() { if (timeout) { clearTimeout(timeout); } } // Function to report any errors that occur when running this action // Follows standard javascript error reporter format of an object with name and message properties function reportError(error) { var errorTitle = 'Error Running Custom Action'; var errorMessage = '<p>There was a problem running the action "<span style="white-space: nowrap">' + action.name + '</span>"</p><p>Error: ' + error.message + '.</p><p>This may result in unexpected behavior of the calendar.</p>' if (action.preventDefault && timeout) { confirmCallback(); } else { cancelCallback(); } setTimeout(function() { utilities.showModal(errorTitle, errorMessage, null, null, 'OK', null, null, null, true, null, true); }, 1000); }
And here’s the example CSS you’ll add to DayBack’s custom CSS section so the button will take on the active calendar’s appropriate color.
/*Event Source Selector*/ .newEventSourceSelector .selected { background-color: gray; } .newEventSourceSelector .selected a{ color: white; } .newEventSourceSelector a { display: inline-block; clear: both; font-weight: normal; line-height: 1.42857143; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; padding: 3px 18px; cursor: pointer; display: inline-block; padding-left: 5%; text-decoration: none; } .newEventSourceSelector ul { margin: 0; list-style-type: none; } .newEventSourceSelector li { cursor: pointer; } .newEventSourceSelector li:hover:not(.selected) { color: #262626; text-decoration: none; background-color: #f5f5f5; } .newEventSourceSelector { padding: 0; overflow: hidden; clear: both; margin-top: 5px; margin-bottom: 0; }
Hijacking the OnClick Action
Now that the selector and the function to switch sources have been defined, we also need to add On Create actions to all our sources to use the new custom function, so the event shows up in the new source. This action needs to be set to prevent the default behavior of the triggering event so the user doesn’t end up with two events. This action is pretty simple and references the function defined in the After Render action. Since this action prevents the default behavior, branching has been added based on the function result, so additional behavior, e.g., additional on-click actions, can also be executed at the proper time in the flow.
//New Event Selector On Event Create v1.10 //Purpose: //This creates an event on the manually selected source if there is one //Depends on the accompanying After Calendar Rendered app action and CSS //Action Type: On Event Create //Prevent Default Action: Yes //More info on custom actions here: //https://docs.dayback.com/article/20-event-actions // Declare globals var options = {}; var inputs = {}; try { //----------- Configuration ------------------- // Options specified for this action // Seconds to wait to allow this action to run before reporting an error (set to 0 to deactivate) options.runTimeout = 8; // Array of account emails for whom this action will run. Leave blank to allow the action to run for everyone. // Example: ['person@domain.com', 'someone@domain.com'] options.restrictedToAccounts = []; // The custom function registered in the accompanying After Calendar Rendered action inputs.checkForNewEventSource = seedcodeCalendar.get('checkForNewEventSource'); //----------- End Configuration ------------------- } catch(error) { reportError(error); } //----------- You shouldn't need to edit below this line ------------------- // Action code goes inside this function function run() { function onCreate() { //Perform all other create actions here confirmCallback(); } if (inputs.checkForNewEventSource) { if (inputs.checkForNewEventSource(event, action) ) { onCreate(); } else { cancelCallback(); } } else { onCreate(); } } //----------- Run function wrapper and helpers - do not edit unless you know what these do ------------------- // Variables used for helper functions below var timeout; // Execute the run function as defined above try { if (!options.restrictedToAccounts || !options.restrictedToAccounts.length || (options.restrictedToAccounts && options.restrictedToAccounts.indexOf(inputs.account) > -1) ) { if (action.preventDefault && options.runTimeout) { timeoutCheck(); } run(); } else if (action.preventDefault) { confirmCallback(); } } catch(error) { reportError(error); } // Run confirm callback when preventDefault is true. Used for async actions function confirmCallback() { cancelTimeoutCheck(); if (action.callbacks.confirm) { action.callbacks.confirm(); } } // Run cancel callback when preventDefault is true. Used for async actions function cancelCallback() { cancelTimeoutCheck(); if (action.callbacks.cancel) { action.callbacks.cancel(); } } // Check if the action has run within the specified time limit when preventDefault is enabled function timeoutCheck() { timeout = setTimeout(function() { var error = { name: 'Timeout', message: 'The action was unable to execute within the alloted time and has been stopped' }; reportError(error, true); }, (options && options.runTimeout ? options.runTimeout * 1000 : 0) ); } function cancelTimeoutCheck() { if (timeout) { clearTimeout(timeout); } } // Function to report any errors that occur when running this action // Follows standard javascript error reporter format of an object with name and message properties function reportError(error) { var errorTitle = 'Error Running Custom Action'; var errorMessage = '<p>There was a problem running the action "<span style="white-space: nowrap">' + action.name + '</span>"</p><p>Error: ' + error.message + '.</p><p>This may result in unexpected behavior of the calendar.</p>' if (action.preventDefault && timeout) { confirmCallback(); } else { cancelCallback(); } setTimeout(function() { utilities.showModal(errorTitle, errorMessage, null, null, 'OK', null, null, null, true, null, true); }, 1000); }
Additional OnCreate Behavior – Using Your Own Modal Window
In Salesforce, a common modification people add to their DayBack is the ability to use Salesforce’s standard modal windows instead of the DayBack popover, as demonstrated here.
The ability to choose the target calendar becomes especially useful in this scenario since the Salesforce modal window will not allow users to choose the target calendar like the DayBack popover does. Since both actions prevent the default On Create behavior, you’ll want to incorporate all the logic into a single action and use the branch provided in the above On Create action to execute the code that brings up the Salesforce modal. Here’s an example of this where the code to bring up the modal window is contained within the branch provided by the action. As with most of our blog posts, this example uses the standard Salesforce Event object, so make the necessary modifications if you’re using a different object.
//New Event Selector On Event Create - Salesforce Modal v1.10 //Purpose: //This creates an event on the manually selected source if there is one //Depends on the accompanying After Calendar Rendered app action and CSS //Action Type: On Event Create //Prevent Default Action: Yes //More info on custom actions here: //https://docs.dayback.com/article/20-event-actions // Declare globals var options = {}; var inputs = {}; try { //----------- Configuration ------------------- // Options specified for this action // Seconds to wait to allow this action to run before reporting an error (set to 0 to deactivate) options.runTimeout = 8; // Array of account emails for whom this action will run. Leave blank to allow the action to run for everyone. // Example: ['person@domain.com', 'someone@domain.com'] options.restrictedToAccounts = []; // The custom function registered in the accompanying After Calendar Rendered action inputs.checkForNewEventSource = seedcodeCalendar.get('checkForNewEventSource'); //----------- End Configuration ------------------- } catch(error) { reportError(error); } //----------- You shouldn't need to edit below this line ------------------- // Action code goes inside this function function run() { function onCreate() { //Perform all other create actions here createInModal(); } if (inputs.checkForNewEventSource) { if (inputs.checkForNewEventSource(event, action) ) { onCreate(); } else { cancelCallback(); } } else { onCreate(); } function createInModal() { //create a new record and bring up its modal box //create POST request from values captured from our click. //Edit properties here to match your target object fields //IsAllDayEvent, StartDateTime, OwnerId, and DurationInMinutes are for the standard Event object, replace them accordingly var request = {}; request.IsAllDayEvent = event.allDay; request.StartDateTime = event.allDay ? event.start.format('YYYY-MM-DD') : event.start.format() ; if (!event.allDay) { request.DurationInMinutes = 60; } //specify resource. this example is using Assigned To as the resource, so a SOQL query //is required to get the id for the actual resourceId and then the request will be transformed. //Depending on your object and resource the SOQL may not be required. if (event.resource[0] && event.resource[0]!=='none') { request.OwnerId = event.resource[0]; } //resource object info var resourceObject = 'User'; var resourceNameField = 'Name'; //--------------You shouldn't need to edit below this line------------------ //scope variables var id; var modStamp; var i=0; var killRecursion = false; //retrieve our canvas client object for authentication using fbk object var client = fbk.client(); //retrieve our canvas context object for REST links/urls using fbk object var context = fbk.context(); if (request.OwnerId && request.OwnerId!=='none') { //attempting to edit the owner. Need to SOQL for the id based on the name. getResourceId(request.OwnerId); } else { //resource not specified, go right to creation if (request.OwnerId) { delete request.OwnerId; } createEvent(); } function getResourceId(name) { //retrieve the query URL from context var url = context.links.queryUrl; //encode name for passing as URI component name = encodeURIComponent(name); //create SOQL statement var soql = 'SELECT+ID+FROM+' + resourceObject + '+WHERE+' + resourceNameField + '=\'' + name + '\''; //final URL for GET var finalUrl = url + '?q=' + soql; //additional settings for our GET var settings = {}; settings.client = client; settings.contentType = 'application/json'; settings.success = createEvent; settings.method = 'GET'; //call to Salesforce Sfdc.canvas.client.ajax ( finalUrl , settings ); } function createEvent(data) { if (data) { if (data.status===200 && data.payload.records && data.payload.records[0]) { request.OwnerId = data.payload.records[0].Id; } else { utilities.showModal( 'Error creating event', data.payload.errors[0].message, 'ok' ); return; } } //additional settings for our POST var settings = {}; settings.client = client; settings.contentType = 'application/json'; settings.success = eventCallback; settings.method = 'POST'; settings.data = JSON.stringify(request); var url = context.links.sobjectUrl + event.schedule.objectName; //call to Salesforce Sfdc.canvas.client.ajax ( url , settings ); } function eventCallback(data) { if (data.status===201) { //POST successful, retrieve the id of our new Event id = data.payload.id.substring(0,15); //publish canvas event to visualforce page to open modal for this event. Sfdc.canvas.client.publish( client , {name : 'dbk.editRecord', payload : {recordId : id} }); //begin polling our event object for changes ping(); //add a mouseover event listener to the document so we can quickly detect if we've left our modal //this is how we detect the user has returned to DayBack after a cancel //add this listener on a timeout, so it's not accidentally triggered before the modal opens setTimeout ( function() {document.addEventListener('mouseover', modalCancelled);},2000); } else { //POST Unsuccessful, throw an error utilities.showModal('Error creating event in Salesforce', data.payload.errors[0].message, 'ok'); } } function modalCancelled() { //this listener should only fire once, so remove it now document.removeEventListener('mouseover', modalCancelled); //set this variable on a delay so we don't outrun our ping function setTimeout(function() {killRecursion = true;},1100); } function ping() { //Query this event's mod timestamp to see if the event has been aupdated/saved in SF. //These are universal fields and should work on all sf objects var select = 'SELECT+Id,LastModifiedDate+FROM+' + event.schedule.objectName + '+WHERE+Id=\'' + id + '\''; //GET url to poll our record var url = context.links.queryUrl + '?q=' + select; //settings for our canvas GET call to salesforce var settings = {}; settings.client = client; settings.contentType = 'application/json'; settings.success = pingCallback; //call to Salesforce Sfdc.canvas.client.ajax ( url , settings ); function pingCallback(data) { if (data.status===200) { //success if (!modStamp) { //first run, get the current last modified modStamp = data.payload.records[0].LastModifiedDate; } else if (modStamp!==data.payload.records[0].LastModifiedDate) { //event has been modified or our dom has been moused over. //refresh calendar, slight timeout for modal to close var element = $('.calendar'); setTimeout(function() {element.fullCalendar('refetchEvents');},400); return; } else if (killRecursion || i>200) { //our dom has been moused over, so they've cancelled //also a timeout (5 minutes?), kill recursion //delete this event deleteRecord(id); return; } i++; //call recursively, with slight delay setTimeout(ping,1000); } else { //throw salesforce rest error alert ( data.payload[0].errorCode ); } } } function deleteRecord(id) { //settings for our delete var settings = {}; settings.client = client; settings.contentType = 'application/json'; settings.success = deleteCallback; settings.method = 'DELETE'; var url = context.links.sobjectUrl + event.schedule.objectName + '/' + id + '/'; //call to Salesforce Sfdc.canvas.client.ajax ( url , settings ); function deleteCallback(data) { if (data.status!==204) { console.log(data); } } } } } //----------- Run function wrapper and helpers - do not edit unless you know what these do ------------------- // Variables used for helper functions below var timeout; // Execute the run function as defined above try { if (!options.restrictedToAccounts || !options.restrictedToAccounts.length || (options.restrictedToAccounts && options.restrictedToAccounts.indexOf(inputs.account) > -1) ) { if (action.preventDefault && options.runTimeout) { timeoutCheck(); } run(); } else if (action.preventDefault) { confirmCallback(); } } catch(error) { reportError(error); } // Run confirm callback when preventDefault is true. Used for async actions function confirmCallback() { cancelTimeoutCheck(); if (action.callbacks.confirm) { action.callbacks.confirm(); } } // Run cancel callback when preventDefault is true. Used for async actions function cancelCallback() { cancelTimeoutCheck(); if (action.callbacks.cancel) { action.callbacks.cancel(); } } // Check if the action has run within the specified time limit when preventDefault is enabled function timeoutCheck() { timeout = setTimeout(function() { var error = { name: 'Timeout', message: 'The action was unable to execute within the alloted time and has been stopped' }; reportError(error, true); }, (options && options.runTimeout ? options.runTimeout * 1000 : 0) ); } function cancelTimeoutCheck() { if (timeout) { clearTimeout(timeout); } } // Function to report any errors that occur when running this action // Follows standard javascript error reporter format of an object with name and message properties function reportError(error) { var errorTitle = 'Error Running Custom Action'; var errorMessage = '<p>There was a problem running the action "<span style="white-space: nowrap">' + action.name + '</span>"</p><p>Error: ' + error.message + '.</p><p>This may result in unexpected behavior of the calendar.</p>' if (action.preventDefault && timeout) { confirmCallback(); } else { cancelCallback(); } setTimeout(function() { utilities.showModal(errorTitle, errorMessage, null, null, 'OK', null, null, null, true, null, true); }, 1000); }
Conclusions
Although DayBack provides several stock actions where behavior can be overridden and customized, developers aren’t limited to just these actions. If additional custom behavior is required, custom actions can be used to create and modify DOM elements to define all sorts of additional behaviors.
Leave a Reply