Skip to content

Instantly share code, notes, and snippets.

@adambankin
Created December 16, 2016 22:17
Show Gist options
  • Save adambankin/7a8e10e09f3e527c7d91233f5d9b1b33 to your computer and use it in GitHub Desktop.
Save adambankin/7a8e10e09f3e527c7d91233f5d9b1b33 to your computer and use it in GitHub Desktop.
src/js/modules/strategy/views/item.js
/*global define*/
define([
'jQuery',
'Underscore',
'T1',
'T1View',
'T1Model',
'T1Layout',
'text!modules/strategy/templates/item.html',
'text!modules/strategy/templates/spendMode.html',
'text!modules/strategy/templates/impressionMode.html',
'text!modules/strategy/templates/performanceMode.html',
'T1Form',
'T1Permissions',
'models/sessionUser',
'T1Menu',
'utils/CampaignStrategyUtils',
'T1Tooltip',
'T1Accordion',
'T1DatePicker'
], function ($, _, T1, T1View, T1Model, T1Layout, template, spendMode, impressionMode, performanceMode, T1Form,
T1Permissions, User, T1Menu, CampaignStrategyUtils) {
'use strict';
// TODO: When new freq cap goes global, remove line below
var useNewFreqCap = T1Permissions.check('feature', 'use_new_freq_cap');
var toInitialCaps = T1.Utils.toInitialCaps;
var frequencyIntervalText = {
'hour': 'Hour',
'day': 'Day',
'week': '7 Days',
'month': '30 Days'
};
var frequencyIntervalDisplayText = {
'hour': '/h',
'day': '/d',
'week': '/w',
'month': '/m'
};
var frequencyNoLimitText = 'No Cap';
var impressionNoLimitText = 'No Cap';
var dropdownList = {
'goal_type': {
options: [
{ value: 'spend', text: 'SPEND' },
{ value: 'reach', text: 'REACH' },
{ value: 'cpc', text: 'CPC' },
{ value: 'cpe', text: 'CPE' },
{ value: 'cpa', text: 'CPA' },
{ value: 'roi', text: 'ROI' },
{ value: 'vcr', text: 'VCR' },
{ value: 'ctr', text: 'CTR' }
],
bodyClassName: 'goal-type-dropdown-body',
headClassName: 'goal-type-dropdown-head'
},
'type': {
options: [
{ value: 'REM', text: 'REM' },
{ value: 'GBO', text: 'GBO' },
{ value: 'AUD', text: 'AUD' }
],
bodyClassName: 'small-body',
headClassName: 'small-head'
},
'pacing_type': {
options: [
{ value: 'even', text: 'Even' },
{ value: 'asap', text: 'ASAP' }
]
},
'pacing_amount': {
isCurrency: true
},
'pacing_interval': {
options: [
{ value: 'hour', text: 'hour' },
{ value: 'day', text: 'day' }
]
},
'impression_pacing_type': function () {
var options = [
{ value: 'even', text: 'Even' },
{ value: 'asap', text: 'ASAP' },
{ value: 'no-limit', text: impressionNoLimitText }
];
return {
options: options
};
},
'impression_pacing_amount': {
isCurrency: false
},
'impression_pacing_interval': {
options: [
{ value: 'hour', text: 'hour' },
{ value: 'day', text: 'day' }
]
},
'frequency_type': function (model) {
var goalType = model && model.get('goal_type');
// TODO When new freq cap goes global, remove refs to useNewFreqCap from function
var options = [
{ value: 'even', text: 'Even' },
{ value: 'asap', text: 'ASAP' },
{ value: 'no-limit', text: frequencyNoLimitText }
];
if (useNewFreqCap && goalType !== 'reach' && goalType !== 'spend') {
options.unshift({ value: 'standard', text: 'T1 Optimized' });
}
return {
options: options
};
},
'frequency_interval': {
options: [
{ value: 'hour', text: frequencyIntervalText.hour },
{ value: 'day', text: frequencyIntervalText.day },
{ value: 'week', text: frequencyIntervalText.week },
{ value: 'month', text: frequencyIntervalText.month }
]
}
};
var pacingASAPCaution = '<p>ASAP pacing is designed to bolster spend for strategies targeting small audiences. ' +
'When targeting larger audiences, ASAP pacing may result in spending more than your pacing amount.</p>' +
'<p class="confirm-message">By clicking the ' + "'Confirm'" +
' button, you are acknowledging the possibility of overspend.</p>';
var impPacingASAPCaution = '<p>ASAP pacing is designed to bolster delivery for strategies targeting small ' +
'audiences. When targeting larger audiences, ASAP pacing may result in delivering more than your pacing amount.' +
'</p><p class="confirm-message">By clicking the ' + "'Confirm'" + ' button, you are acknowledging the ' +
'possibility of over delivery.</p>';
var frequencyCap = function ($groupEditEl, frequencyType) {
var $frequencyEls;
var isNoCap = frequencyType === 'no-limit' || frequencyType === 'standard';
// Have to create a stub model here as it's what T1.Form.InlineEdit expects for a function that returns a DDL config
var freqType = dropdownList.frequency_type(new T1Model());
var freqTypeIndex = freqType.options.findIndex(function (freqTypeItem) {
return freqTypeItem.value === frequencyType;
});
$groupEditEl.toggleClass('no-cap', isNoCap);
$frequencyEls = $('[data-bind]', $groupEditEl).not('[data-bind="frequency_type"]');
if (isNoCap) {
$('[data-bind="frequency_type"]', $groupEditEl)
.val(freqType.options[freqTypeIndex].value)
.trigger('liszt:updated');
}
$.each($frequencyEls, function (key, el) {
$(el).closest('dd').toggle(!isNoCap);
});
$('.info', $groupEditEl).toggle(!isNoCap);
};
var impressionCap = function ($groupEditEl, impressionType, notRMX) {
var $impressionEls;
var isNoImpCap = impressionType === 'no-limit';
// Have to create a stub model here as it's what T1.Form.InlineEdit expects for a function that returns a DDL config
var impType = dropdownList.impression_pacing_type(new T1Model());
var impTypeIndex = impType.options.findIndex(function (impTypeItem) {
return impTypeItem.value === impressionType;
});
$groupEditEl.toggleClass('no-cap', isNoImpCap);
if (notRMX === true) {
$impressionEls = $('[data-bind]', $groupEditEl).not('[data-bind="impression_pacing_type"]');
}
if (isNoImpCap) {
$('[data-bind="impression_pacing_type"]', $groupEditEl)
.val(impType.options[impTypeIndex].value)
.trigger('liszt:updated');
}
if ($impressionEls){
$.each($impressionEls, function (key, el) {
$(el).closest('dd').toggle(!isNoImpCap);
});
}
$('.info', $groupEditEl).toggle(!isNoImpCap);
};
var useCampFreqCap = CampaignStrategyUtils.useCampaignFrequencyCap;
var ItemView, totalSpendEcpa, totalSpendEcpc, totalSpendEcpe, totalSpendEcpm, totalSpendRoi,
totalSpend, goalMonitoringReport;
var statusActiveTitle = 'Active';
var statusInactiveTitle = 'Inactive';
var readOnlyTitle = "You don't have permission to edit this strategy";
var addToChartTitle = 'Add to Chart';
var Date = T1.Date;
var formatsRolledUpNumbersNoAbbreviation = CampaignStrategyUtils.formatsRolledUpNumbersNoAbbreviation;
var divideByZero = CampaignStrategyUtils.divideByZero;
var getDatePickerLowerBoundForStrategy = CampaignStrategyUtils.getDatePickerLowerBoundForStrategy;
var populatePerformanceAlert = CampaignStrategyUtils.populatePerformanceAlert;
var enableTrackingSupplyTypeFeatureFlag = T1Permissions.check('feature', 'supply_type_tracking');
ItemView = T1View.extend({
template: template,
partials: {
spend1: spendMode,
performance1: performanceMode,
impression1: impressionMode,
spend7: spendMode,
performance7: performanceMode,
impression7: impressionMode,
spend14: spendMode,
performance14: performanceMode,
impression14: impressionMode,
spend30: spendMode,
performance30: performanceMode,
impression30: impressionMode,
spendFTD: spendMode,
performanceFTD: performanceMode,
impressionFTD: impressionMode,
spendCTD: spendMode,
performanceCTD: performanceMode,
impressionCTD: impressionMode
},
menuConfig: {
menuTriggerEleSelector: '.strategy-settings',
menuColumns: [
{
'columnTitle': null,
'hideIcons': true,
'menuItems': [
{
'label': 'Edit',
'visible': false
},
{
'label': 'View Summary',
'visible': false
},
{
'label': 'Duplicate',
'handler': 'duplicateStrategy',
'visible': false
},
{
'label': 'Change log',
'handler': 'viewChangeLog',
'visible': false
},
{
'label': 'View Summary',
'visible': false
},
{
'label': 'CheckList',
'handler': 'showHealth',
'visible': true
},
{
'label': 'Deactivate',
'handler': 'deactivateHandler',
'visible': false
},
{
'label': 'Activate',
'handler': 'activateHandler',
'visible': false
}
]
}
]
},
dataEvents: {
'model': {
'reset': 'reload',
'change:status': 'updateView',
'remove': 'unload',
'change:performanceDataArrived': 'performanceDataCompletedHandler'
},
'campaign': {
'reset': 'reload',
'change:status': 'updateView',
'change:start_date': 'updateView',
'change:end_date': 'updateView'
}
},
loaderOpts: {
contentClass: 'inline-edit-loader',
spinnerConfig: {
lines: 10,
length: 4,
radius: 3,
left: 15
}
},
eventhubEvents: {
'chart.add': function (data) {
var el = $('.chart-icon', this.el);
if (data.model) {
this.chartable = this.model.id === data.model.id;
if (this.chartable) {
el.removeClass('disabled');
} else {
el.addClass('disabled');
}
}
},
'onCloseInlineEdit': function (event) {
var self = this;
if (event && event.view && event.view.cid === this.cid) {
self.updateView();
}
},
'change:reportInterval': 'changeReportInterval',
'onConfigClick': 'applyGearHover',
'onConfigDismiss': 'removeGearHover'
},
events: {
'accordionaction .accordion': 'onAccordionAction',
'click .section .chart-icon': 'toggleForChart',
'mouseenter .spendMode .date-range .section-c': 'datesMouseEnterHandler',
'mouseleave .spendMode .date-range .section-c': 'datesMouseLeaveHandler',
'mouseenter .impressionMode .date-range .section-c': 'datesMouseEnterHandler',
'mouseleave .impressionMode .date-range .section-c': 'datesMouseLeaveHandler',
'summaryButtonClick': 'strategyButtonClickHandler',
'summaryPanelClose': 'summaryPanelCloseHandler',
'click .strategy-edit': 'editStrategy',
'contextmenu .strategy-edit-link': 'applyEditHover',
'mousemove .strategy': 'removeEditHover',
'contextmenu .strategy-actions-holder': 'stopEvent'
},
initialize: function (args) {
var self = this;
var model = this.model;
var campaign = model.get('campaign');
var campaignCollection = campaign.collection;
var isVideoStrategy = model.get('media_type') === 'VIDEO';
var lazyLoadBadges;
//Allow inline-editing
model.allowInlineValidation = true;
self.asDecimalCurrency = T1.Utils.getDecimalCurrencyTemplateFunc(model.get('currency_code'));
self.isVideoStrategy = isVideoStrategy;
self.hasVideoPermission = T1Permissions.check('feature', 'video', model);
self.canEditStrategy = (T1Permissions.check('strategy.edit', model));
self.canViewStrategy = (T1Permissions.check('strategy.readonly', model));
self.canViewChangeLog = (T1Permissions.check('changeLog.view'));
self.strategyEditable = ((self.canEditStrategy === true && isVideoStrategy === false) ||
(self.canEditStrategy === true && self.hasVideoPermission === true));
self.viewModeFields = args.viewModeFields;
self.viewMode = args.viewMode;
goalMonitoringReport = 'rpt_goal_monitoring';
totalSpend = 'total_spend';
totalSpendEcpa = 'total_spend_ecpa';
totalSpendEcpc = 'total_spend_ecpc';
totalSpendEcpe = 'total_spend_ecpe';
totalSpendEcpm = 'total_spend_ecpm';
totalSpendRoi = 'total_spend_roi';
self.layout = new T1Layout({
el: function () {
return self.el;
},
selectorPath: '.strategy-drawer',
layout: {}
});
//initialize summary badges layout early so view would available
self.layout.append('.summary-badges', {
'module': 'strategy',
'viewType': 'summaryInfo',
options: {
model: self.model
}
});
self.canEdit = T1Permissions.check('strategy.edit', model) && (isVideoStrategy ?
T1Permissions.check('feature', 'video') : true);
self.campaignCollection = campaignCollection;
self.campaign = campaign;
self.reportInterval = args.reportInterval;
self.currentGoalMode = args.currentGoalMode;
self.defaultDrawerExpandedState = args.defaultDrawerExpandedState;
lazyLoadBadges = this.lazyLoadBadges();
T1.EventHub.subscribe('action:drawer:load-' + self.model.id, lazyLoadBadges);
},
lazyLoadBadges: function () {
var self = this;
return _.debounce(function () {
var expandAction = self.isDrawerExpanded();
if (!self.isBadgesDataLoaded || !expandAction) {
self.isBadgesDataLoaded = false;
self.fetchStrategyBadgesData();
} else {
self.loadStrtegyBadges();
}
}, 100);
},
updateView: function () {
var $el = $(this.el);
var serialize = this.serialize();
var renderQueue = ['1', '7', '14', '30', 'FTD', 'CTD'];
var i, addNoCapClass, impNoCapClass;
var freqCapValGroupNode = $el.find('.frequencyCapValGroup');
var impCapValGroupNode = $el.find('.impCapValGroup');
this.renderViewModes(renderQueue);
this.renderPartial('.item.accordion .name .name-part');
for (i = 0; i < renderQueue.length; i++) {
this.attachDatePicker(renderQueue[i]);
}
this.updateDataBind({
name: serialize.name,
type: serialize.type,
status_class: serialize.status_class,
statusTitle: serialize.statusTitle,
frequency_type: serialize.frequency_optimization ? 'T1 Optimized' : serialize.frequency_type,
frequency_amount: serialize.frequency_optimization ? '' : serialize.frequency_amount,
frequency_interval: serialize.frequency_optimization ? '' : serialize.frequency_interval,
frequency_optimization: serialize.frequency_optimization ? '1' : '0',
pacing_type: serialize.pacing_type,
pacing_amount: serialize.pacing_amount,
pacing_interval: serialize.pacing_interval,
impression_pacing_type: serialize.impression_pacing_type,
impression_pacing_amount:
$.trim(formatsRolledUpNumbersNoAbbreviation(serialize.impression_pacing_amount.replace(/,/g, ''))),
impression_pacing_interval: serialize.impression_pacing_interval,
roi_target: CampaignStrategyUtils.parseNumberToWhole(serialize.roi_target) + ':1'
}, this.el);
if (useNewFreqCap) {
// TODO: When new freq cap goes global, remove above test and below else block.
this.bindFrequencyCap();
} else {
this.activateFrequencyCap();
}
this.bindImpressionCap();
this.prepareDataPoints();
this.delegateEvents();
this.prepareT1Menu(); //this is needed because the status change
addNoCapClass = serialize.frequency_type === frequencyNoLimitText;
freqCapValGroupNode.toggleClass('frequency-no-cap', addNoCapClass);
impNoCapClass = serialize.impression_pacing_type === impressionNoLimitText;
impCapValGroupNode.toggleClass('impression-no-cap', impNoCapClass);
this.updateDrawerExpander();
},
stopEvent: function (e) {
e.preventDefault();
e.stopPropagation();
},
changeReportInterval: function (data) {
this.reportInterval = data.reportInterval;
},
attachDatePicker: function (reportInterval) {
var self = this;
var currentInterval;
var model = this.model;
var campaign = model.get('campaign');
var startDate = model.get('start_date');
var hasStarted = CampaignStrategyUtils.hasEntityStarted(model);
var hasCampaignStarted = CampaignStrategyUtils.hasEntityStarted(campaign);
var modes = ['.spend', '.impression'];
_.each(modes, function(view) {
currentInterval = $('.views ' + view + reportInterval, self.el);
//Date picker only gets open in spend and impression mode.
//if strategy has use_campaign_start === "1" then we check if campaign has started becouse
//strategy date might be in the past
hasStarted = model.get('use_campaign_start') !== '0' ? hasCampaignStarted : hasStarted;
if (self.canEdit) {
T1.DoubleDatePicker({
topOffset: 80,
leftOffset: 20,
chooseTime: true,
chooseTimezone: true,
zoneName: function () {
return campaign.get('zone_name');
},
timezoneEditable: false,
startTimeEditable: function () {
return !hasStarted;
},
lowerBounds: function () {
return getDatePickerLowerBoundForStrategy(hasStarted, hasCampaignStarted, Date.parse(startDate),
model.get('use_campaign_start') !== '0' ?
Date.parse(campaign.get('initial_start_date')) : Date.parse(campaign.get('start_date')));
},
startDisabled: function () {
return hasStarted;
},
defaultStartDate: function () {
return campaign.get('start_date');
},
defaultEndDate: function () {
return campaign.get('end_date');
},
defaultStartMessage: 'use campaign date',
useDefaultStart: parseInt(model.get('use_campaign_start'), 10),
useDefaultEnd: parseInt(model.get('use_campaign_end'), 10),
defaultEndMessage: 'use campaign date',
openPickerSelector: '.date-range .data',
getStartDate: function () {
return model.get('start_date');
},
getEndDate: function () {
return model.get('end_date');
},
onSave: function (args) {
var saveValues = args.data.saveValues;
var saveStartDate = campaign.get('start_date');
var saveEndDate = campaign.get('end_date');
var zoneName = campaign.get('zone_name');
var loader = self.loader();
var saveObj = {
use_campaign_start: saveValues.start.useDefault ? 1 : 0,
use_campaign_end: saveValues.end.useDefault ? 1 : 0
};
if (!saveObj.use_campaign_start) {
saveStartDate = saveValues.start.date;
}
if (!saveObj.use_campaign_end) {
saveEndDate = saveValues.end.date;
}
saveObj.start_date = saveObj.use_campaign_start === 0 ? saveStartDate + ' ' + zoneName : '';
saveObj.end_date = saveObj.use_campaign_end === 0 ? saveEndDate + ' ' + zoneName : '';
loader.start();
self.model.save(saveObj, {
success: function () {
loader.stop();
args.success();
self.updateView.call(self);
},
conflict: function () {
loader.stop();
self.updateView.call(self);
},
statusInvalid: function (data) {
loader.stop();
args.error(data[0]);
}
});
return true;
}
},
currentInterval);
}
currentInterval.find('[data-datepicker]').trigger('mousedown.attachpicker');
});
},
viewStrategySummary: function (e) {
T1.Location.set('#strategy/summary/' + this.model.id);
this.preventDefault(e);
},
datesMouseEnterHandler: function (event) {
this.datesMouseEventHandler(event, true);
},
datesMouseLeaveHandler: function (event) {
this.datesMouseEventHandler(event, false);
},
datesMouseEventHandler: function (event, isMouseEnter) {
var $el = this.el;
var $currentTarget = $(event.currentTarget);
var $elDates = $el.find('.date-range .dates');
var $elRemaining = $el.find('.days-remaining');
var $targetToShow = isMouseEnter === true ? $elDates : $elRemaining;
var $targetToHide = isMouseEnter === true ? $elRemaining : $elDates;
var onHoverClass = this.canEdit === true ? 'onhover' : 'onhover-noedit';
if (isMouseEnter === true) {
$currentTarget.addClass(onHoverClass);
} else {
$currentTarget.removeClass(onHoverClass);
}
$targetToShow.show();
$targetToHide.hide();
},
onAccordionAction: function () {
//$(this.el).find(".accordion").toggleClass("selected");
this.updateDrawerExpander();
T1.EventHub.publish('action:drawerExpandContract', {});
},
updateDrawerExpander: function () {
var $accordion = $(this.el).find('.accordion');
var needToAddExpendedClass, drawerView, viewList;
var drawerLayout = this.layout && this.layout.layout ? this.layout.layout : null;
needToAddExpendedClass = this.isDrawerExpanded();
$accordion.find('.drawer-expander').toggleClass('expanded', needToAddExpendedClass);
//we need to close the in-line editor on the drawer view obj associated with this view - only
//if the drawer is being closed down.
if (needToAddExpendedClass !== true) {
viewList = drawerLayout ? drawerLayout['.strategy-drawer'] : null;
drawerView = viewList && $.isArray(viewList) && viewList.length > 0 ? viewList[0].view : null;
if (drawerView && drawerView.closeInlineEditor) {
drawerView.closeInlineEditor(true);
}
}
},
fetchStrategyBadgesData: function () {
var self = this;
var view = self.layout.layout['.summary-badges'][0].view;
if (view) {
view.fetchData(true).then(function () {
self.loadStrtegyBadges();
self.isBadgesDataLoaded();
});
}
},
loadStrtegyBadges: function () {
this.layout.loadView('.summary-badges');
},
strategyButtonClickHandler: function () {
this.el.find('.cancel-action').click();
this.$strategyList.addClass('strategy-badge-panel-open');
this.$strategyListItem.addClass('strategy-badge-panel-open');
},
summaryPanelCloseHandler: function () {
this.$strategyList.removeClass('strategy-badge-panel-open');
this.$strategyListItem.removeClass('strategy-badge-panel-open');
},
applyEditHover: function () {
this.el.find('.strategy-edit-holder').addClass('hover');
},
removeEditHover: function () {
this.el.find('.strategy-edit-holder').removeClass('hover');
},
applyGearHover: function (args) {
if (args && args.view && args.view.cid === this.cid) {
this.el.find('.strategy-actions-holder').addClass('hover');
this.el.find('.strategy-edit-holder').addClass('suppress-hover');
}
},
removeGearHover: function () {
this.el.find('.strategy-actions-holder').removeClass('hover');
this.el.find('.strategy-edit-holder').removeClass('suppress-hover');
},
expandCloseDrawer: function (toExpand) {
var self = this;
var $drawer = $(self.el).find('.accordion').parent().find('.drawer');
var isExpanded = self.isDrawerExpanded();
if ($drawer.length > 0 && toExpand !== isExpanded) {
//we ONLY take action if the current state is not what is desired
$drawer[(toExpand === true) ? 'show' : 'hide']();
self.updateDrawerExpander();
}
},
isDrawerExpanded: function () {
var $drawer = $(this.el).find('.accordion').parent().find('.drawer');
return $drawer.length > 0 && $drawer.css('display') !== 'none';
},
toggleForChart: function (event) {
var model = this.model;
this.stopEvent(event);
this.chartable = true;
T1.EventHub.publish('chart.add', { model: model });
return false;
},
prepareDataPoints: function () {
var $el = $(this.el);
T1.Tooltip($el.find('.pointer'), {
offset: '4'
});
T1.Tooltip($el.find('.label'));
},
load: function () {
var self = this;
var $el = this.el;
var $freqAmt, serialize;
self.render().then(function () {
self.prepareT1Menu();
self.attachDatePicker(self.reportInterval);
self.$strategyList = $el.closest('.w-StrategyList');
self.$strategyListItem = $el.find('.w-StrategyListItem');
setTimeout(function () {
self.performanceDataCompletedHandler();
}, 0);
serialize = self.serialize();
$freqAmt = $el.find('[data-bind=frequency_amount]');
if (!serialize.frequency_amount || serialize.frequency_amount === '0') {
$freqAmt.data('current-value', '1');
} else {
$freqAmt.data('current-value', serialize.frequency_amount + '');
}
});
self.layout.append('.strategy-drawer', {
'module': 'strategy',
'viewType': 'drawer',
options: {
model: self.model,
defaultDrawerExpandedState: self.defaultDrawerExpandedState
}
}).then(function () {
setTimeout(function () {
self.updateDrawerExpander();
}, 200);
});
},
unload: function () {
this.$strategyList = null;
this.$strategyListItem = null;
T1.EventHub.unsubscribe('action:drawer:load-' + this.model.id);
},
performanceDataCompletedHandler: function () {
var self = this;
var i;
var renderQueue = ['1', '7', '14', '30', 'FTD', 'CTD'];
//we only check the ready switch before rendering for rpt-perf-data users.
//will remove this outer 'if' condition once this feature is no longer hidden
//behind the currency feature flag. the reason is, the call can come from data
//notification and render method - we check to ensure this runs only once.
if (self.model.get('perfDataReady') !== true) {
return;
} else {
self.model.set({ 'perfDataReady': false });
}
renderQueue = _.without(renderQueue, self.reportInterval);
self.delayedRender(renderQueue);
if (useNewFreqCap) {
// TODO: When new freq cap goes global, remove above test and below else block.
self.bindFrequencyCap();
} else {
self.activateFrequencyCap();
}
self.bindImpressionCap();
self.activateFrequencyCap();
for (i = 0; i < renderQueue.length; i++) {
self.attachDatePicker(renderQueue[i]);
}
T1.EventHub.publish('strategies:itemLoaded');
},
prepareT1Menu: function () {
var self = this;
var model = self.model;
var isVideoStrategy = self.isVideoStrategy;
var hasVideoPermission = self.hasVideoPermission;
var canEditStrategy = self.canEditStrategy;
var canViewStrategy = self.canViewStrategy;
var canViewChangeLog = self.canViewChangeLog;
var menuConfig = $.extend(true, {}, self.menuConfig);
var menuItems = menuConfig.menuColumns[0].menuItems;
var menuIndexRef = {
'Edit': 0,
'ViewSummary': 1,
'Duplicate': 2,
'ChangeLog': 3,
'ViewReadOnly': 4,
'CheckList': 5,
'Deactivate': 6,
'Activate': 7
};
self.el.find(menuConfig.menuTriggerEleSelector).off();
if (canViewChangeLog === true) {
menuItems[menuIndexRef.ChangeLog].visible = true;
}
if ((canEditStrategy === true && isVideoStrategy === false) ||
(canEditStrategy === true && hasVideoPermission === true)) {
menuItems[menuIndexRef.Edit].visible = true;
menuItems[menuIndexRef.Edit].url = '#strategy/edit/' + model.id;
menuItems[menuIndexRef.ViewSummary].visible = true;
menuItems[menuIndexRef.ViewSummary].url = '#strategy/summary/' + model.id;
menuItems[menuIndexRef.Duplicate].visible = true;
//we need to dynamically set the deactivate/activate menu command according to the current model status
//we only do so if campaign level is active
if (self.campaign && self.campaign.get('status') === '1') {
if (model.get('status') === '0') {
menuItems[menuIndexRef.Activate].visible = true;
} else {
menuItems[menuIndexRef.Deactivate].visible = true;
}
}
} else if (canViewStrategy === true || (isVideoStrategy === true)) {
menuItems[menuIndexRef.ViewReadOnly].visible = true;
menuItems[menuIndexRef.ViewReadOnly].url = '#strategy/readonly/' + model.id;
}
T1Menu(menuConfig, self);
},
editStrategy: function (event) {
var id = this.model.id;
this.stopEvent(event);
T1.Location.set('strategy/edit/' + id + '/details');
},
// TODO: When new freq cap goes global, remove this method
activateFrequencyCap: function () {
T1.Tooltip(this.el, {
getExternalTip: true,
tooltipEle: '.frequency-cap-icon',
className: 'w-StrategyFrequencyCap'
});
this.bindFrequencyCap();
},
renderViewModes: function (queue) {
var self = this;
var el = self.el;
var selector, dataset, i, j;
var selectors = queue;
var modes = ['.spend', '.impression', '.performance'];
for (i = 0; i < modes.length; i++) {
for (j = 0; j < selectors.length; j++) {
dataset = this.serialize(selectors[j]);
selector = modes[i] + selectors[j];
self.renderPartialTemplate({
templateTarget: selector,
targetEl: el.find(selector),
data: dataset,
context: el,
template: template,
partials: self.partials,
skipDelegateEvents: true
});
}
}
},
getTimeStamp: function (dateObj) {
var dt = dateObj || new Date();
return dt.getHours() + ':' + dt.getMinutes() + ':' + dt.getSeconds() + ':' + dt.getMilliseconds();
},
bindFrequencyCap: function () {
var $el = $(this.el);
var $frequencyEl = $('.budget', this.el);
var model = this.model;
$('.editable-content', $frequencyEl).on('click', function () {
function onFrequencyCapEdit() {
var $groupEditEl = $('.w-GroupInlineEditForm', $frequencyEl);
var freqType = (model.get('frequency_optimization') === '1') ? 'standard' : model.get('frequency_type');
frequencyCap($groupEditEl, freqType);
$el.unbind('inlineFormEdit.open', onFrequencyCapEdit);
}
$el.bind('inlineFormEdit.open', onFrequencyCapEdit);
});
},
bindImpressionCap: function () {
var self = this;
var $el = $(this.el);
var modeInterval = '.impression' + this.reportInterval;
var $impressionEl = $el.find('.views').find(modeInterval).find('.remaining');
var model = this.model;
$('.editable-content', $impressionEl).on('click', function () {
function onImpressionCapEdit() {
var $groupEditEl = $('.w-GroupInlineEditForm', $impressionEl);
var impType = model.get('impression_pacing_type');
var notRMX = (self.model.get('supply_type') !== 'RMX_API');
impressionCap($groupEditEl, impType, notRMX);
$el.unbind('inlineFormEdit.open', onImpressionCapEdit);
}
$el.bind('inlineFormEdit.open', onImpressionCapEdit);
});
},
editElementBindings: {
'pacing_type': {
'type': 'change',
'action': function (e) {
var target = $(e.currentTarget);
if (target.val() === 'asap') {
$('<div>' + pacingASAPCaution + '</div>').dialog({
'autoOpen': true,
'modal': true,
'title': '<b></b>ASAP pacing confirmation',
'dialogClass': 'w-PacingASAPAlert',
'buttons': [
{
'text': 'Confirm',
'class': 'confirm',
'click': function () {
$(this).dialog('close');
}
},
{
'text': 'Cancel',
'class': 'cancel',
'click': function () {
target.val('even').trigger('liszt:updated');
$(this).dialog('close');
}
}
],
'close': function () {
$(this).remove();
}
});
}
}
},
'frequency_type': {
'type': 'change',
'action': function (e) {
var target = $(e.currentTarget);
var $groupEditEl = target.closest('.w-GroupInlineEditForm');
frequencyCap($groupEditEl, target.val());
}
},
'impression_pacing_type': {
'type': 'change',
'action': function (e) {
var target = $(e.currentTarget);
var $groupEditEl = target.closest('.w-GroupInlineEditForm');
impressionCap($groupEditEl, target.val());
if (target.val() === 'asap') {
$('<div>' + impPacingASAPCaution + '</div>').dialog({
'autoOpen': true,
'modal': true,
'title': '<b></b>ASAP pacing confirmation',
'dialogClass': 'w-PacingASAPAlert',
'buttons': [
{
'text': 'Confirm',
'class': 'confirm',
'click': function () {
$(this).dialog('close');
}
},
{
'text': 'Cancel',
'class': 'cancel',
'click': function () {
target.val('even').trigger('liszt:updated');
$(this).dialog('close');
}
}
],
'close': function () {
$(this).remove();
}
});
}
}
}
},
delayedRender: function (list) {
var self = this;
self.renderViewModes(list);
self.prepareDataPoints.call(self);
self.delegateEvents();
},
deactivateHandler: function (e) {
this.preventDefault(e);
this.activationHandler(false);
},
activateHandler: function (e) {
this.preventDefault(e);
this.activationHandler(true);
},
activationHandler: function (toActivate) {
var strategy = this.model;
var loader = this.loader();
var status = (toActivate === true) ? '1' : '0';
function stopLoader() {
loader.stop();
}
loader.start();
strategy.save({ 'status': status }, {
success: stopLoader,
conflict: stopLoader
});
},
showHealth: function (event) {
var strategy = this.model;
//close settings menu
$(document).trigger('click');
this.stopEvent(event);
this.layout.append('.healthDialogue', {
'module': 'strategy',
'viewType': 'health',
'options': {
model: strategy
}
});
},
duplicateStrategy: function (e) {
var strategy = this.model;
//close settings menu
$(document).trigger('click');
this.stopEvent(e);
this.layout.append('.duplicationDialog', {
'module': 'strategy',
'viewType': 'duplicateStrategy',
'options': {
model: strategy
}
});
},
viewChangeLog: function (e) {
var strategy = this.model;
//close settings menu
$(document).trigger('click');
this.stopEvent(e);
this.layout.append('.changeDialog', {
'module': 'changelog',
'viewType': 'dialog',
'options': {
model: strategy,
entityType: 'strategy'
}
});
},
serializeAdditionalInformation: function (interval) {
var self = this;
var model = self.model;
var campaign = self.campaign;
var info = model.get(goalMonitoringReport);
var dataset = {};
var start, end, loadDate, pacingRatio, start_date, end_date, budgetInformation, reports,
currentReportInterval, ctdReport, fieldNameForCampaignGoalPerformace, campaignPerformanceAlert,
strategyPerformanceAlert;
var startOffsetX = 5;
var outPacingRange = 10;
var coloredBarWidth = 95;
var effectivePerformanceField = {
'cpa': totalSpendEcpa,
'cpc': totalSpendEcpc,
'cpe': totalSpendEcpe,
'roi': totalSpendRoi
};
var showImpCounts = true;
reports = {};
$.each(info.toJSON(), function (index, report) {
reports[report.mm_interval] = report;
});
/*
* Calculation for the dates & duration start
*/
start_date = (model.get('use_campaign_start') !== '0') ?
campaign.get('initial_start_date') : model.get('start_date');
end_date = (model.get('use_campaign_end') !== '0') ? campaign.get('end_date') : model.get('end_date');
start = Date.parse(start_date);
end = Date.parse(end_date);
loadDate = reports.CTD && reports.CTD.load_date ? Date.parse(reports.CTD.load_date) : new Date();
function getTipDate(dateObj) {
return Date.CultureInfo.abbreviatedMonthNames[dateObj.getMonth()] + ' ' + dateObj.getDate();
}
if (T1.compareDates(start, end) !== 1) {
if (T1.compareDates(start, loadDate) === 1) {
dataset.tip_date = getTipDate(start);
pacingRatio = 0;
showImpCounts = false;
} else if (T1.compareDates(loadDate, end) === 1) {
pacingRatio = 100;
showImpCounts = false;
} else {
dataset.tip_date = getTipDate(loadDate);
pacingRatio = divideByZero(100 * (loadDate.getTime() - start.getTime()) / (end.getTime() - start.getTime()));
showImpCounts = true;
}
}
dataset.imp_count_ydy = showImpCounts && reports['1'].imp_count ?
$.trim(formatsRolledUpNumbersNoAbbreviation(reports['1'].imp_count)) : '--';
end_date = (model.get('use_campaign_end') !== '0') ? campaign.get('end_date') : model.get('end_date');
dataset.days_remaining = Math.ceil((end.valueOf() - T1.Date.parse('today')) / 86400000);
dataset.days_remaining = dataset.days_remaining > 0 ? dataset.days_remaining : 0;
/*
* Calculation for the dates & duration end
*/
dataset.durationRatio = Math.round(startOffsetX + (coloredBarWidth - startOffsetX) * (pacingRatio / 100));
dataset.displayPointer = pacingRatio === 100 || !dataset.durationRatio ? 'none' : 'block';
//return the dataset if reporting data unavailable
if (!info.length) {
//default coloredBarWidth is set to 0
dataset.coloredBarWidth = 0;
return dataset;
}
currentReportInterval = reports[interval] || {};
function getCurrentReportValue(fieldName) {
return currentReportInterval[fieldName] || '';
}
function getCurrentReportFormattedValueNoAbbreviation(fieldName) {
return currentReportInterval[fieldName] ?
$.trim(formatsRolledUpNumbersNoAbbreviation(currentReportInterval[fieldName])) : '';
}
ctdReport = reports.CTD || {};
dataset.daily_spend_to_pace = ctdReport.daily_spend_to_pace || '--';
dataset.mm_goal_performance = getCurrentReportValue('mm_goal_performance');
//performance view mode
dataset.daily_spend_to_pace = ctdReport.daily_spend_to_pace || '--';
dataset.total_conversion_count = getCurrentReportFormattedValueNoAbbreviation('total_conversion_count');
dataset.clicks_count = getCurrentReportFormattedValueNoAbbreviation('clicks_count');
dataset.imp_count = getCurrentReportFormattedValueNoAbbreviation('imp_count');
dataset.totalSpendEcpa = getCurrentReportValue(totalSpendEcpa);
dataset.totalSpendEcpc = getCurrentReportValue(totalSpendEcpc);
dataset.totalSpendEcpm = getCurrentReportValue(totalSpendEcpm);
dataset.totalSpendRoi = getCurrentReportValue(totalSpendRoi);
dataset.mm_ctr = currentReportInterval.mm_ctr ? currentReportInterval.mm_ctr + '%' : '--';
dataset.mm_ctc = currentReportInterval.mm_ctc ? currentReportInterval.mm_ctc + '%' : '--';
/**
* Calculations for the budget start
* */
if (typeof model.getBudgetInformation === 'function') {
dataset.budgetInformation = budgetInformation = model.getBudgetInformation();
} else {
dataset.budgetInformation = budgetInformation = model.get('campaign').getBudgetInformation();
}
dataset.spent = budgetInformation.budget_spent || '';
dataset.budgetMeterLabel = budgetInformation.budget_remaining;
dataset.total_budget = budgetInformation.total_budget;
/**
* Calculations for the budget end
* */
dataset.campaignEffectiveGoalValue = campaign.get('goal_value');
dataset.strategyEffectiveGoalValue = model.get('goal_value');
dataset.strategyEffectiveGoalValueRoi = model.get('roi_target');
fieldNameForCampaignGoalPerformace = effectivePerformanceField[campaign.get('goal_type')];
//As per Jason in ticket CS-212- In strategies we are using
//CTD ONLY to show info in Pacing Bar and Actual Performance
dataset.campaignEffectiveGoalPerformance = getCurrentReportValue(fieldNameForCampaignGoalPerformace ||
totalSpendEcpm);
dataset.strategyEffectiveGoalPerformance = dataset.mm_goal_performance;
if (model.get('goal_type') === 'roi') {
campaignPerformanceAlert = parseFloat(dataset.campaignEffectiveGoalPerformance) <
(parseFloat(dataset.campaignEffectiveGoalValue) / 1.25);
strategyPerformanceAlert = parseFloat(dataset.strategyEffectiveGoalPerformance) <
(parseFloat(dataset.strategyEffectiveGoalValueRoi) / 1.25);
} else {
campaignPerformanceAlert = parseFloat(dataset.campaignEffectiveGoalPerformance) >
(parseFloat(dataset.campaignEffectiveGoalValue) * 1.25);
strategyPerformanceAlert = parseFloat(dataset.strategyEffectiveGoalPerformance) >
(parseFloat(dataset.strategyEffectiveGoalValue) * 1.25);
}
dataset.campaignPerformanceAlert = campaignPerformanceAlert ? 'alert-item' : '';
dataset.strategyPerformanceAlert = strategyPerformanceAlert ? 'alert-item' : '';
dataset.totalSpend = getCurrentReportValue(totalSpend);
dataset.totalSpend_YDY = reports['1'] ? reports['1'][totalSpend] : '';
//dependent on partial calculation
dataset.spend_remaining = budgetInformation.total_budget - dataset.spent;
dataset.budgetMeterLabel = dataset.spend_remaining;
//done on UI since we need blank out put when there is no data
dataset.budgetMeterLabel = T1.Utils.formatCurrency(dataset.budgetMeterLabel.toString(), '',
self.getCurrencyCode());
dataset.daily_spend_to_pace = divideByZero(dataset.spend_remaining / dataset.days_remaining, true);
dataset = populatePerformanceAlert(dataset);
dataset.coloredBarWidth = budgetInformation.percnt_budget_spent ?
startOffsetX + Math.round(budgetInformation.percnt_budget_spent * ((coloredBarWidth - startOffsetX) / 100)) : 0;
dataset.coloredBarWidth = dataset.coloredBarWidth > coloredBarWidth ? coloredBarWidth : dataset.coloredBarWidth;
dataset.coloredBarClass = Math.abs(budgetInformation.percnt_budget_spent - pacingRatio) >= outPacingRange ?
'out-pacing' : 'pacing';
return dataset;
},
serializeFrequencyCapInfo: function (dataset) {
var self = this;
var model = self.model;
var campaignDataset = model.get('campaign').toJSON();
var advertiserDataset = campaignDataset.advertiser;
var useT1Optimized = model.get('frequency_optimization');
var frequencyType = model.get('frequency_type');
var frequencyInterval = model.get('frequency_interval');
var goalType = model.get('goal_type');
var isRMXAPI = model.get('supply_type') === 'RMX_API';
var isNoLimit = (frequencyType === 'no-limit' || useT1Optimized !== '0');
var campaignCap = useCampFreqCap(campaignDataset);
var strategyCap = useT1Optimized === '0';
var impType = model.get('impression_pacing_type');
var isNoImpLimit = impType === 'no-limit';
dataset.frequency_interval = (isRMXAPI && enableTrackingSupplyTypeFeatureFlag) ?
'' : frequencyIntervalDisplayText[frequencyInterval];
dataset.isBidPrice = (goalType === 'spend');
dataset.isNoLimit = isNoLimit;
//fill in tool tip data
dataset.frequency_cap_frequency_header = 'Campaign Cap: ';
dataset.frequency_cap_frequency_type = campaignDataset.frequency_type;
dataset.frequency_cap_frequency_amount = campaignDataset.frequency_amount;
dataset.frequency_cap_frequency_interval = campaignDataset.frequency_interval;
dataset.campaignCap = campaignCap;
if (campaignDataset.frequency_optimization === '1') {
dataset.upstream_cap_text = dataset.frequency_cap_frequency_header + 'T1 Optimized';
} else if (dataset.frequency_cap_frequency_type !== 'no-limit') {
dataset.upstream_cap_text = dataset.frequency_cap_frequency_header +
((dataset.frequency_cap_frequency_type.toLowerCase() === 'asap') ?
dataset.frequency_cap_frequency_type.toUpperCase() : toInitialCaps(dataset.frequency_cap_frequency_type)) +
' ' + dataset.frequency_cap_frequency_amount + ' per ' +
toInitialCaps(dataset.frequency_cap_frequency_interval);
}
// TODO when new freq cap goes global, remove useNewFreqCap from test below
dataset.upstream_cap = campaignCap || (useNewFreqCap && campaignDataset.frequency_optimization === '1');
if (advertiserDataset && advertiserDataset.frequency_type !== 'no-limit') {
dataset.upstream_cap_text = (dataset.upstream_cap_text) ? dataset.upstream_cap_text + '<br />' : '';
dataset.upstream_cap_text += 'Advertiser Cap: ' + ((advertiserDataset.frequency_type.toLowerCase() === 'asap') ?
advertiserDataset.frequency_type.toUpperCase() : toInitialCaps(advertiserDataset.frequency_type)) + ' ' +
advertiserDataset.frequency_amount + ' per ' + toInitialCaps(advertiserDataset.frequency_interval);
dataset.upstream_cap = true;
}
if (!dataset.upstream_cap_text) {
dataset.upstream_cap = false;
}
if (isNoLimit || (!strategyCap && campaignCap)) {
dataset.frequency_type = frequencyNoLimitText;
dataset.frequencyNoCapClass = 'frequency-no-cap';
}
if (isNoImpLimit) {
dataset.impression_pacing_type = self.model.get('supply_type') === 'RMX_API' ? '--' : impressionNoLimitText;
dataset.impNoCapClass = 'impression-no-cap';
}
if (!campaignCap) {
//not using the campaign cap so hide the icon
dataset.freq_cap_state = 'frequency-cap-icon-hide';
}
dataset.frequency_optimization = useT1Optimized === '1';
},
serialize: function (interval) {
var self = this;
var model = this.model;
var dateFormat = 'MM/dd/yy';
var campaign = this.campaign;
var dataset = model.toJSON();
var filler = '--';
var start_date, end_date, spaces;
var campaign_status = parseInt(campaign.get('status'), 10);
var status = parseInt(dataset.status, 10);
var isActive = (campaign_status && status) ? true : false;
var statusTitle = (isActive) ? statusActiveTitle : statusInactiveTitle;
var reportInterval = interval || self.reportInterval;
var isCustom = this.chartable;
var isBatch = (model.get('supply_type') === 'BATCH');
start_date = (dataset.use_campaign_start !== '0') ? campaign.get('initial_start_date') : dataset.start_date;
end_date = (dataset.use_campaign_end !== '0') ? campaign.get('end_date') : dataset.end_date;
dataset.startDateAsString = T1.Date.parse(start_date) ?
T1.Date.parse(start_date).toString(dateFormat) :
filler;
dataset.endDateAsString = T1.Date.parse(end_date) ?
T1.Date.parse(end_date).toString(dateFormat) :
filler;
dataset.readOnlyTitle = (self.canEdit) ? '' : readOnlyTitle;
dataset.currentStatusTitle = statusTitle;
dataset.status_class = function (target) {
var inactiveClass = 'inactive';
var result = (!isActive) ? inactiveClass : '';
if (target) {
$(target).toggleClass(inactiveClass, !isActive);
}
return result;
};
dataset.statusTitle = function (target) {
$(target).attr('title', statusTitle);
return statusTitle;
};
dataset.isActive = isActive;
dataset.typeInfo = (self.currentGoalMode === 'campaignGoal') ? campaign.get('goal_type') : dataset.goal_type;
dataset.strategyTypeInfo = dataset.goal_type === 'roi' ? 'ROI' : dataset.goal_type;
dataset.campaignTypeInfo = campaign.get('goal_type') === 'roi' ? 'ROI' : campaign.get('goal_type');
dataset.chart_class = (isCustom) ? '' : 'disabled';
dataset.chartActionTitle = addToChartTitle;
dataset = $.extend(dataset, self.serializeAdditionalInformation(reportInterval));
dataset.hasFrequencyCap = !isBatch;
this.serializeFrequencyCapInfo(dataset);
dataset.hasBatchPacing = !isBatch;
dataset.defaultText = function () {
return function (text) {
return (text === '') ? '&nbsp;--' : text;
};
};
dataset.pacing_interval_value = dataset.pacing_interval;
dataset.pacing_interval = frequencyIntervalDisplayText[dataset.pacing_interval] || '';
dataset.impression_pacing_interval = frequencyIntervalDisplayText[dataset.impression_pacing_interval] || '';
dataset.impression_pacing_amount = $.trim(formatsRolledUpNumbersNoAbbreviation(dataset.impression_pacing_amount));
dataset.hasMaxBid = dataset.use_optimization === '1';
dataset.isStrategyROI = dataset.goal_type === 'roi';
dataset.roi_target = CampaignStrategyUtils.parseNumberToWhole(dataset.roi_target) + ':1';
// cantEditRoi it temp var to not allow user edit roi_target and should be removed
//when the feature flag becomes absolite
dataset.asDecimalCurrency = this.asDecimalCurrency;
if (dataset.strategyTypeInfo === 'ROI' && dataset.strategyEffectiveGoalPerformance) {
dataset.isTypeROI = true;
}
dataset.showStrategyEdit = this.strategyEditable;
// TODO: When new freq cap goes global, remove next line
dataset.useNewFreqCap = useNewFreqCap;
if (dataset.frequency_optimization && model.get('supply_type') !== 'RMX_API') {
dataset.frequency_type = 'T1 Optimized';
dataset.frequency_amount = '';
dataset.frequency_interval = '';
}
if (enableTrackingSupplyTypeFeatureFlag) {
dataset.enableTrackingSupplyTypeFeatureFlag = model.get('supply_type') === 'RMX_API';
if (dataset.enableTrackingSupplyTypeFeatureFlag) {
dataset.isStrategyROI = false;
dataset.goal_type = 'tracker';
dataset.goal_value = '';
dataset.strategyEffectiveGoalPerformance = filler;
dataset.frequency_type = filler;
dataset.pacing_amount = filler;
dataset.frequency_amount = '';
}
}
if (dataset.goal_type === 'vcr' || dataset.goal_type === 'ctr') {
dataset.goal_type_is_percentage = true;
if (dataset.goal_value.length > 2) {
dataset.goal_value = dataset.goal_value.substring(0, dataset.goal_value.length - 2);
}
if (dataset.goal_type === 'ctr' && this.campaign.get('goal_type') === 'ctr') {
dataset.strategyEffectiveGoalPerformance = this.campaign.get('goal_value');
if (dataset.strategyEffectiveGoalPerformance.length > 2) {
dataset.strategyEffectiveGoalPerformance = dataset.strategyEffectiveGoalPerformance.substring(
0, dataset.strategyEffectiveGoalPerformance.length - 2);
}
dataset.effective_goal_value_is_percentage = true;
} else {
dataset.effective_goal_value_is_percentage = false;
}
} else {
dataset.goal_type_is_percentage = false;
}
// CAW-2091 - We now allow long names to wrap, and by default we prefer to break on spaces (of course).
// However, some clients use long names with no spaces (typically underscores instead) so for them we
// need to allow word breaks.
// CAW-2506 - Modified wrapping logic to only normal-wrap if there are 3 or more spaces... Otherwise break-all
spaces = (dataset.name.match(/\s/g) || []).length;
dataset.wordBreakName = !(spaces > 2);
dataset.useNewFreqCapAndEnableTrackingSupplyTypeFeatureFlag = dataset.useNewFreqCap ||
dataset.enableTrackingSupplyTypeFeatureFlag;
return dataset;
}
});
return T1Form.InlineEdit(ItemView, dropdownList);
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment