Hello I have tried to implement inline grid editing in my datatable .It's working.... But I need to implement the inline editing in other tables .... So I would like to make a global function for this .....
But it shows some error .... This is my code ....
var slug='sales'
window.drawDataTable = function(){
"columnDefs": [
{
targets: [10,11,27,28],
render:function(data){
return moment(data).format('LLLL');
},
},
{'targets': '_all',
'createdCell': function (td, cellData, rowData, row, col) {
$(td).attr('data-pk', rowData['id']);
const key = Object.keys(rowData)[Object.values(rowData).indexOf(cellData)];
$(td).attr('data-name', key );
}},
{ targets: [0,1], className: ""},
//{targets:'_all',className:"querytruncate"},
{
"targets": [29,30],
"orderable": false
}
],
"fnDrawCallback":function(){
$('#data_table_list td').editable({
params: function(params) {
var pk = $(this).data('pk');
var name = $(this).data('name');
var data = {};
data['field'] = name;
data['value'] = params.value;
data['id'] = pk;
data['slug']=slug;
return data;
},
url: "{% url 'request_access' %}",
success : function(data) {
if (data.status == true) {
toastr.success(data.msg);
} else {
toastr.error(data.msg);
}
},
error: function () {
toastr.error('Something went wrong');
}
});
},
}
So I try to implement like this.... in my core.js
function inline(slug){
$('#data_table_list td').editable({
params: function(params) {
var pk = $(this).data('pk');
var name = $(this).data('name');
var data = {};
data['field'] = name;
data['value'] = params.value;
data['id'] = pk;
data['slug']=slug;
return data;
},
url: "{% url 'request_access' %}",
success : function(data) {
if (data.status == true) {
toastr.success(data.msg);
} else {
toastr.error(data.msg);
}
},
error: function () {
toastr.error('Something went wrong');
}
});
}
and in my html
"columnDefs": [
{
targets: [10,11,27,28],
render:function(data){
return moment(data).format('LLLL');
},
},
{'targets': '_all',
'createdCell': function (td, cellData, rowData, row, col) {
$(td).attr('data-pk', rowData['id']);
const key = Object.keys(rowData)[Object.values(rowData).indexOf(cellData)];
$(td).attr('data-name', key );
}},
{ targets: [0,1], className: ""},
//{targets:'_all',className:"querytruncate"},
{
"targets": [29,30],
"orderable": false
}
],
"fnDrawCallback": inline(slug),
Related
I am having some trouble integrating some custom functionalities with Datatable and Datatable Editor at the same time.
Without server-side processing my current functionalities works perfectly but I want to be able to implement server-side filtering with Editor.
With server-side filtering these work:
1.Pagination
2.Global Search
3.Sorting
4.Row reorder
5.Dynamic Page length
With server-side filtering these don't work:
1.Custom column input filtering
2.Custom footer select filtering
My serverside script for Datatable and Editor:
Editor::inst($db, 'article_categories')
->fields(
Field::inst('article_categories.id')->validator('Validate::numeric'),
Field::inst('article_categories.name')->validator('Validate::notEmpty'),
Field::inst('article_categories.description'),
Field::inst('article_categories.rowOrder')->validator('Validate::numeric')
)
->on('preCreate', function ($editor, $values) {
if (!$values['article_categories']['rowOrder']) {
$next = $editor->db()->sql('select IFNULL(MAX(rowOrder)+1, 1) as next FROM article_categories')->fetch();
$editor->field('article_categories.rowOrder')->setValue($next['next']);
} else {
$editor->db()
->query('update', 'article_categories')
->set('rowOrder', 'rowOrder+1', false)
->where('rowOrder', $values['article_categories']['rowOrder'], '>=')
->exec();
}
})
->on('preRemove', function ($editor, $id, $values) {
$order = $editor->db()
->select('article_categories', 'rowOrder', array('id' => $id))
->fetch();
$editor->db()
->query('update', 'article_categories')
->set('rowOrder', 'rowOrder-1', false)
->where('rowOrder', $order['rowOrder'], '>')
->exec();
})
->process($request->all())
->json();
My client-side script:
Default config:
jQuery(function () {
$.extend(true, $.fn.dataTable.defaults, {
serverSide: true,
fixedHeader: true,
searchDelay: 800,
paging: true,
processing: true,
pageLength: 10,
info: true,
dom: "Blfrtip",
select: true,
responsive: true,
lengthMenu: [
[10, 25, 50, -1],
[10, 25, 50, "All"],
],
});
});
Datatable and Editor setup:
var editor;
jQuery(function () {
//Editor
editor = new $.fn.dataTable.Editor({
table: "#article-category",
ajax: {
url: "article-categories",
type: "POST",
},
fields: [
{
label: "Order:",
name: "article_categories.rowOrder",
type: "hidden",
fieldInfo:
"This field can only be edited via click and drag row reordering.",
},
{
label: "FAQ Category Name:",
name: "article_categories.name",
},
{
label: "Description (optional):",
name: "article_categories.description",
type: "textarea",
},
],
});
//Datatable
var table = $("#article-category").DataTable({
ajax: {
url: "article-categories",
type: "POST",
},
rowReorder: {
dataSrc: "article_categories.rowOrder",
editor: editor,
},
buttons: cms.editorFormButtons(editor),
initComplete: function () {
cms.headerInputFilter("article-category", this.api(), [1, 2]);
cms.footerSelectFilter(this.api(), [1, 2]);
},
columns: [
{
data: "article_categories.rowOrder",
name: "article_categories.rowOrder",
className: "reorder no-inline",
},
{
data: "article_categories.name",
name: "article_categories.name",
},
{
data: "article_categories.description",
name: "article_categories.description",
},
],
});
//Inline Editor
cms.inlineEdit("article-category", editor);
editor
.on("postCreate postRemove", function () {
table.ajax.reload(null, false);
})
.on("initCreate", function () {
editor.field("article_categories.rowOrder").enable();
})
.on("initEdit", function () {
editor.field("article_categories.rowOrder").disable();
});
});
Custom functions:
let footerSelectFilter = function (table, columns) {
if (typeof columns != "undefined" && typeof table != "undefined") {
table.columns(columns).every(function () {
var column = this;
var select = $('<select><option value=""></option></select>')
.appendTo($(column.footer()).empty())
.on("change", function () {
var val = $.fn.dataTable.util.escapeRegex($(this).val());
column
.search(val ? "^" + val + "$" : "", true, false)
.draw();
});
column
.data()
.unique()
.sort()
.each(function (d, j) {
select.append(
'<option value="' + d + '">' + d + "</option>"
);
});
});
}
};
let headerInputFilter = function (target, table, searchableColumns) {
if (
typeof searchableColumns != "undefined" &&
typeof target != "undefined" &&
typeof table != "undefined"
) {
$("#" + target + " thead tr")
.clone(true)
.addClass("filters")
.appendTo("#" + target + " thead");
var i = 0;
var api = table;
api.columns()
.eq(0)
.each(function (colIdx) {
if (
searchableColumns.includes(
$(api.column(colIdx).header()).index()
)
) {
var cell = $(".filters th").eq(
$(api.column(colIdx).header()).index()
);
var title = $(cell).text();
$(cell).html(
'<input style="width:100% !important" type="text" placeholder="' +
title +
'" />'
);
$(
"input",
$(".filters th").eq(
$(api.column(colIdx).header()).index()
)
)
.off("keyup change")
.on("keyup change", function (e) {
e.stopPropagation();
$(this).attr("title", $(this).val());
var regexr = "({search})";
var cursorPosition = this.selectionStart;
api.column(colIdx)
.search(
this.value != ""
? regexr.replace(
"{search}",
"(((" + this.value + ")))"
)
: "",
this.value != "",
this.value == ""
)
.draw();
$(this)
.focus()[0]
.setSelectionRange(
cursorPosition,
cursorPosition
);
});
} else {
return true;
}
});
}
};
let editorFormButtons = function (editor) {
if (typeof editor != "undefined") {
return [
{
extend: "create",
editor: editor,
formButtons: [
{
label: "Save",
fn: function () {
var that = this;
this.submit(function () {
that.close();
});
},
},
],
},
{
extend: "edit",
text: "Edit",
editor: editor,
formButtons: [
{
label: "Save & close",
fn: function () {
var that = this;
this.submit(function () {
that.close();
});
},
},
{
label: "Update",
fn: function () {
this.submit(function () {
editor.edit(
table.rows({ selected: true }).indexes()
);
});
},
},
],
},
{
extend: "remove",
editor: editor,
formButtons: [
{
label: "Delete",
fn: function () {
var that = this;
this.submit(function () {
that.close();
});
},
},
],
},
];
}
};
let inlineEdit = function (target, editor) {
if (typeof target != "undefined" && typeof editor != "undefined") {
$("#" + target + "").on(
"click",
"tbody td:not(.no-inline)",
function (e) {
if (
$(this).hasClass("editor-edit") ||
$(this).hasClass("control") ||
$(this).hasClass("select-checkbox") ||
$(this).hasClass("dataTables_empty")
) {
return;
}
editor.inline(this, {
submit: "allIfChanged",
});
}
);
}
};
module.exports = {
headerInputFilter,
footerSelectFilter,
editorFormButtons,
inlineEdit,
};
When I set server-side to false the custom functionalities I need works perfectly but I need these with server-side processing as it will significantly improve overall performance . I would appreciate any help and suggestion.
Regards,
Shovon Choudhury
The error message is as below:
Requested unknown parameter 'emPONumber' for row 0, column 0
But emPONumber is in the JSON, why datatables still prompt this error?
<script type="text/javascript">
$(document).ready(function () {
var tableId = 'tablePurchaseOrders';
var table = $('#' + tableId).DataTable({
"processing": true,
"serverSide": true,
"ajax": {
url: 'http://localhost/ControlTower2WebAPI/api/PurchaseOrder/GetAllUploadedPurchaseOrders',
type: 'GET',
data: function (data) {
//debugger;
var model = {
draw: data.draw,
start: data.start,
length: data.length,
columns: data.columns,
search: data.search,
order: data.order
};
return model;
},
failure: function (result) {
debugger;
alert("Error occurred while trying to get data from server: " + result.sEcho);
},
error: function (XMLHttpRequest, textStatus, errorThrown) {
debugger;
alert("Error occurred while trying to get data from server!");
},
dataSrc: function (json) {
var _data = JSON.stringify(json.Data);
debugger;
return _data;
}
}
,
"columns": [
{ "data": "emPONumber", title: "emPONumber" },
{ "data": "ASMPONumber", title: "ASMPONumber" },
{ "data": "material", title: "material" }
]
});
});
</script>
json in dataSrc: function (json):
{"ContentEncoding":null,"ContentType":null,"Data":{"draw":1,"recordsTotal":1,"recordsFiltered":1,"data":[{"emPONumber":"EM1234567","ASMPONumber":"A741000013","material":"26-00010","quantity":5,"UOM":"EA","asmPOQuantity":5,"createBy":"m","ASMPOYear":2018,"ASMPOMonth":6,"ASMPOVendor":"10008"}]},"JsonRequestBehavior":1,"MaxJsonLength":null,"RecursionLimit":null}
json (_data) return by ajax in dataSrc:
{
"draw":1,
"recordsTotal":1,
"recordsFiltered":1,
"data":
[{
"emPONumber":"EM1234567",
"ASMPONumber":"A741000013",
"material":"26-00010",
"quantity":5,
"UOM":"EA",
"asmPOQuantity":5,
"createBy":"m",
"ASMPOYear":2018,
"ASMPOMonth":6,
"ASMPOVendor":"10008"
}]
}
Try
dataSrc: function (json) {
for(key in json.Data){ json[key] = json.Data[key]; }
delete json['Data'];
return json.data;
}
Here I have two JS files in which one is main JS file for parent page which contains Datatable which gets populated through ajax call. And other one is specifically for update functionality in which function is defined to update required values into database. That update is done in dialog box. So after successful update sweet alert comes which shows product is successfully updated. So what I want is updated values should be immediately reflected in Datatable of Parent Page. Currently it is showing old value only untill I refresh my page manually. Below are both JS files. And I am new in Javascript and Jquery. So Please help
var oTable;
var url_data_source;
(function(window, undefined) {
window.lastQuery = null;
function initDataSource(after_id) {
url_data_source = '/event/productlocation/ajax?action=data_source';
oTable = $('#datatable').dataTable({
"sAjaxSource": url_data_source,
"bProcessing": true,
"bServerSide": true,
"bFilter": false,
"bRetrieve": true,
"bScrollCollapse": true,
"iCookieDuration": 60*60*24,
"sCookiePrefix": "tkpd_datatable_",
"sPaginationType": "simple",
"scrollX": true,
"aaSorting": [ [0,'desc'] ],
"iDisplayLength": 20,
"aLengthMenu": [[20, 40, 60, 80, 100], [20, 40, 60, 80, 100]],
"oLanguage": {
"sInfoFiltered": "",
"sProcessing": " "
},
"aoColumns": [
{ "sName": "id", "mDataProp": "id"},
{ "sName": "location_id", "mDataProp": "location_id"},
{ "sName": "product_id", "mDataProp": "product_id"},
{ "sName": "status_str", "mDataProp": "status_str"},
{ "sName": "actions", "mDataProp": "actions"},
],
"fnDrawCallback":function(){
},
"initComplete": function(settings, json) {
$("#datatable_paginate").on("click", "#datatable_next", function(){
var after_id = ""
url_data_source = '/event/productlocation/ajax?action=data_source';
after_id_url = $("#after_id_url").val();
after_id = after_id_url.split("after_id=")
url_data_source += "&after_id="+after_id[1];
redraw();
});
},
"fnServerData": function ( sSource, aoData, fnCallback ) {
aoData.push( {"name": "email", "value": $('#search_email').val()} );
if (after_id != "") {
aoData.push( {"name": "after_id", "value": after_id} );
}
if ($('#search_id_product').val()) {
aoData.push( {"name": "product_id", "value": $('#search_id_product').val()} );
}
$.ajax({
"dataType": 'json',
"type": "GET",
"url": url_data_source,
"data": aoData,
"success": function(result) {
var message_error = result.message_error
if (message_error) {
$('#datatable_processing').css("visibility","hidden");
swal({ title: message_error, type: "error"});
} else if (result.status == "OK") {
result.data = formatingDatatable(result.data)
fnCallback(result)
lastQuery = aoData;
} else {
$('#datatable_processing').css("visibility","hidden");
swal({ title: "Tidak ada hasil", type: "info"});
}
},
error: function(XMLHttpRequest, textStatus, errorThrown) {
swal({ title: manager.getAjaxError(XMLHttpRequest), type: "error"});
$('#datatable_processing').css("visibility","hidden");
$('.load__overlay').hide();
},
beforeSend: function() {
$('#datatable_processing').css("visibility","visible");
$('.load__overlay').show();
},
complete: function() {
$('#datatable_processing').css("visibility","hidden");
$('.load__overlay').hide();
}
});
}
});
}
function redraw() {
$("#datatable").dataTable().fnClearTable();
}
function formatingDatatable(events) {
var result = [];
var uri_next;
$.each(events.location_products, function(i, productlocation) {
result.push({
id: productlocation.id,
location_id: productlocation.location_id,
product_id: productlocation.product_id,
status_str: productlocation.status_str,
actions:'<button class="btn btn-primary waves-effect waves-light m-b-5 btn-sm m-r-5 btn-sm detail-productlocation" data-target="#modal-event-productlocation-detail" data-toggle="modal" data-id="' + productlocation.id + '" product-id="' + productlocation.product_id + '" location-id="' + productlocation.location_id + '" status-str="' + productlocation.status_str + '"><i class="glyphicon glyphicon-list"></i> Details</button>'
});
})
$("#after_id_url").val(events.page.uri_next)
uri_next = events.page.uri_next
result.after_id_url = uri_next
return result
}
function initFilter() {
$("#filter-form").submit(function() {
url_data_source = '/event/productlocation/ajax?action=data_source';
if(oTable == null){
initDataSource("");
} else {
oTable.fnDraw();
}
return false;
});
}
$('#datatable_paginate').on('click', '#datatable_next', function() {
initDataSource("");
})
$('#datatable').on('click', '.detail-productlocation', function() {
$('.load__overlay').show();
$("#modal-event-productlocation-detail").remove();
$(this).removeAttr("data-target", "#modal-event-productlocation-detail");
$(this).removeAttr("data-toggle", "modal");
//var id = $(this).attr("data-id");
var dt = {
"id": $(this).attr("data-id"),
"product_id": $(this).attr("product-id"),
"location_id": $(this).attr("location-id"),
"status": $(this).attr("status-str"),
}
$.ajax({
"url": '/event/productlocation/ajax?action=show_dialog_details',
"type": 'GET',
"data": dt,
success: function (result) {
$('.load__overlay').hide();
$("body").append(result);
$(this).attr("data-target", "#modal-event-productlocation-detail");
$(this).attr("data-toggle", "modal");
$('#modal-event-productlocation-detail').modal('show');
},
error: function(XMLHttpRequest, textStatus, errorThrown) {
$('.load__overlay').hide();
swal({ title: manager.getAjaxError(XMLHttpRequest), type: "error", });
},
});
});
$(document).ready(function() {
//initDate();
initFilter();
//initCategoryList();
var after_id = "";
initDataSource(after_id);
});
})(window);
event-productlocation-update.js
function sendProductLocationData() {
var jsonData = {};
var formFields = $(":input");
jQuery.each(formFields, function(i, field) { //Accessing all the element of form and get Key and Value
var key = field.name; //Keyname of each Field
var elementid = field.id; //Id of each Field
elementid = "#" + elementid;
var value = $(elementid).val();
jsonData[key] = parseInt(value);
if (key == "status") {
if ($("#event_checkbox_status").is(':checked') == true) {
jsonData.status = 1;
} else {
jsonData.status = 0;
}
}
});
productlocationdetail = JSON.stringify(jsonData); //Creating json Object
$.ajax({
url: '/update/product/productlocation',
type: 'post',
dataType: 'json',
contentType: 'application/json',
data: productlocationdetail,
"success": function(result) {
var message_error = result.message_error
if (message_error) {
$('#modal-event-productlocation-detail').css("visibility", "hidden")
swal({
title: message_error,
type: "error"
});
$('.modal-backdrop').remove();
} else {
$('#modal-event-productlocation-detail').css("visibility", "hidden")
swal({
title: "Product Location Updated Successfully",
type: "info"
});
$('.modal-backdrop').remove();
}
},
error: function(XMLHttpRequest, textStatus, errorThrown) {
swal({
title: manager.getAjaxError(XMLHttpRequest),
type: "error"
});
$('#modal-event-productlocation-detail').css("visibility", "hidden")
$('.modal-backdrop').remove();
$('.load__overlay').hide();
},
beforeSend: function() {
$('#modal-event-productlocation-detail').css("visibility", "visible")
$('.modal-backdrop').remove();
},
complete: function() {
swal({
title: "Product Location Updated Successfully",
type: "info"
});
$('#modal-event-productlocation-detail').css("visibility", "hidden")
$('.modal-backdrop').remove();
$('.load__overlay').hide();
}
});
}
There are a few way you could handle this.
One way you could handle this is to store the item/s in a variable in a jSON array.
When get confirmation back from the server that that the the update was successful you could then create function to update the dataTable with with this function.
function updateDataTable(dataTableObj,jsonArray)
/*
Clears and populates the dataTable with JSON array
*/
dataTableObj.clear();
jsonArray.forEach(function(row){
dataTableObj.row.add(row);
});
dataTableObj.draw();
}
The other way you could update the table is to create a route on your server that sends the jsonArray but this requires an additional request to the server.
Note: If you use the Ajax call be sure to invoke the function inside the the ajax callback function
DataTables alerts me if there is an empty cell in my table. Here is the warning:
Requested unknown parameter 'Description' for row 1.
My code is here:
var columns = [
{
mDataProp: 'Description',
sTitle: 'Description'
},
//
// other columns
//
];
var dt = $('#myDataTable').dataTable({
sAjaxSource: '/JobScheduler/GetJobs',
bServerSide: true,
fnServerData: function (sSource, aoData, fnCallback) {
$(aoData).each(function(i, o){
var params = o.name.split('_');
if (params[0] == 'mDataProp')
{
var value = $.grep(aoData, function(e){ return e.name == "sSearch_" + params[1]; })[0].value;
if (value != "")
{
aoData.push({ name: o.value, value: value });
}
}
});
$.getJSON(sSource, aoData, function (data) {
if (isSuccess(data)) {
fnCallback(data.message);
} else {
showMessage(data);
}
});
},
bProcessing: true,
sDom: 'T<"new">Rrlptip',
fnRowCallback: function (nRow, aData) {
nRow.setAttribute('id', aData['JobId']);
},
aoColumns: columns
})
What should I do to prevent this warning? Thanks in advance.
Check your columns and add a defaultContent option for the one(s) causing you issues.
Something like this:
...
{
mDataProp: 'YourData',
sTitle: 'YourDescription',
sDefaultContent: ''
},
...
I m using jquery datatables in many pages and its working good in every page and in a single page its not working properly, i mean when i use tab to go through datatable, it works fine for the first time and when i try to do the same for second time, if i try to focus on any thing in few seconds in data table, the focus disappears and the foucs starts from the starting of the page.
Here goes the code
function tableCall(){
var clientId = $('#clientId').val();
var versionCount = $('#versionCount').val();
var fromDate = $('#fromDate').val();
var toDate = $('#toDate').val();
$.ajax({
url: auditTrail.props.reporttableURL,
type: "POST",
dataType: "json",
data: {
clientId:clientId,
versionCount:versionCount,
fromDate:fromDate,
toDate:toDate
},
success: function (data) {
searchJSON = data.data;
var len=searchJSON.length;
if (len > 0){
$('.no-data, #warning-sign').hide();
createTable();
}else{
$('.no-data').hide();
$('#warning-sign').show();
}
}
});
}
function createTable() {
wwhs.setADAAttrDynamic($('#auditTable'));
if ($.fn.DataTable.isDataTable('#auditTable')) {
// table.destroy();
$("#auditTable tbody").empty();
}
table = $('#auditTable').dataTable({
"searching":false,
"destroy":true,
"autoWidth": false,
"ordering": true,
"destroy":true,
"stateSave": true,
"drawCallback": attachEvents,
"stateLoadParams": function (settings, data) {
return false;
},
data: searchJSON,
columns: [{
data: "reportName"
}, {
data: "reportStatus"
}, {
data: "timeStamp"
}, {
data: "requestedBy"
}],
"columnDefs": [{
"render": function (data, type, row) {
if(row.reportStatus.toUpperCase() == 'PROCESSED')
return '<a class="blue-text" " data-name="'+ row.reportName +'">' + row.reportName + '</a>';
else
return row.reportName;
},
"width": "50%",
"targets": 0
}, {
"width": "15%",
"targets": 1
}, {
"width": "20%",
"targets": 2
}, {
"width": "15%",
"targets": 3
}]
});
}
I think the problem is because the datatable instance is not destroyed.
Since datatables saves all the nodes into an object and renders it when ever it wants, emptying the tbody doesnt do anything. it just removes the elements in the page which can be re-drawn/re-rendered by datatable from its stored object.
You can check if the object is already initialized and the destroy it before re-initializing.
if(typeof table != "undefined")
table.destroy();
So the final code will look something like
function tableCall(){
var clientId = $('#clientId').val();
var versionCount = $('#versionCount').val();
var fromDate = $('#fromDate').val();
var toDate = $('#toDate').val();
$.ajax({
url: auditTrail.props.reporttableURL,
type: "POST",
dataType: "json",
data: {
clientId:clientId,
versionCount:versionCount,
fromDate:fromDate,
toDate:toDate
},
success: function (data) {
searchJSON = data.data;
var len=searchJSON.length;
if (len > 0){
$('.no-data, #warning-sign').hide();
createTable();
} else {
$('.no-data').hide();
$('#warning-sign').show();
}
}
});
}
function createTable() {
wwhs.setADAAttrDynamic($('#auditTable'));
if ($.fn.DataTable.isDataTable('#auditTable')) {
if(typeof table != "undefined")
table.destroy();
}
table = $('#auditTable').dataTable({
"searching":false,
"destroy":true,
"autoWidth": false,
"ordering": true,
"destroy":true,
"stateSave": true,
"drawCallback": attachEvents,
"stateLoadParams": function (settings, data) {
return false;
},
data: searchJSON,
columns: [{
data: "reportName"
}, {
data: "reportStatus"
}, {
data: "timeStamp"
}, {
data: "requestedBy"
}],
"columnDefs": [{
"render": function (data, type, row) {
if(row.reportStatus.toUpperCase() == 'PROCESSED')
return '<a class="blue-text" " data-name="'+ row.reportName +'">' + row.reportName + '</a>';
else
return row.reportName;
},
"width": "50%",
"targets": 0
}, {
"width": "15%",
"targets": 1
}, {
"width": "20%",
"targets": 2
}, {
"width": "15%",
"targets": 3
}]
});
}