Jquery Datatables expand row and get detail via Ajax - datatables

Is it possible to get the detail for each row through Ajax?
I found a starting point here:
http://datatables.net/release-datatables/examples/api/row_details.html
but it doesn't use ajax.
I'm thinking about modifying fnFormatDetails() function and place the ajax call there.
But i'm looking for another better answer.
Thanks.

It's very simple. All you have to do is put your details in a separate field within the "data" array:
E.g. your JSON might look like as follows:
{
"draw": "${drawId}",
"recordsTotal": "${totalRecords}",
"recordsFiltered": "${filteredRecords}",
"data": [
{
"empName": "${employee.name}",
"empNumber": "${employee.number}",
"empEmail": "${employee.email}",
"extraDetails" : [
["${employee.salary}", "${employee.title}"]
]
}
]
}
Then in your javascript, you can simply access this extra details by using JavaScript arrays. E.g.
var row = employeeTable.row( tr );
var rowData = row.data();
alert(rowData.extraDetails[0][0]);
alert(rowData.extraDetails[0][1]);

You need not to go for ajax if you have the data in your row.
Try oTable.fnGetData(rowIndexor|trNode)

you can try this and it will work.
First: create your datatable.
var table = $('#myTable').DataTable( {
ajax: '/api/staff',
columns: [
{
className: 'details-control',
orderable: false,
data: null,
defaultContent: ''
},
{ data: "name" },
{ data: "position" },
{ data: "office" },
{ data: "salary" }
],
order: [[1, 'asc']] } );
Second: Event handlers
$('#myTable tbody').on('click', 'td.details-control', function () {
var tr = $(this).closest('tr');
var row = table.row( tr );
if ( row.child.isShown() ) {
row.child.hide();
tr.removeClass('shown');
}
else {
row.child( format(row.data()) ).show();
tr.addClass('shown');
} } );
Third: Ajax request and formatting the response
function format ( rowData ) {
var div = $('<div/>')
.addClass( 'loading' )
.text( 'Loading...' );
$.ajax( {
url: '/api/staff/details',
data: {
name: rowData.name
},
dataType: 'json',
success: function ( json ) {
div
.html( json.html )
.removeClass( 'loading' );
}
} );
return div; }
you can pass any row argument to format method.
Check This For More Details

Related

How to update data after ajax reload using initComplete callback for DataTables when using ajax source data?

I’m using a select object to trigger an ajax reload for a DataTable.
I need to add individual column searching with select inputs for a given column (not for every column) but the select is filled with the previous ajax response.
How can I update the data that the initCompleteFunction callback uses to fill the select input in the individual column searching?
// this is the select that triggers the ajax.reload
$('#proveedor').on('change', function () {
$datatable
.DataTable()
.ajax
.reload(initCompleteFunction, false);
});
// this is my initCompleteFunction callback
function initCompleteFunction(settings, json){
var api = new $.fn.dataTable.Api( settings );
api.columns().every( function () {
var column = this;
if ($(column.header()).hasClass('select')) {
var select = $('<select><option value="">' + $(column.header()).html() + '</option></select>')
.appendTo( $(column.footer()).empty() )
.on( 'change', function () {
var val = $.fn.dataTable.util.escapeRegex(
$(this).val()
);
column
.search( val ? '^'+val+'$' : '', true, false )
.draw();
return false;
} );
//this is the part that keeps previous data insted of the new one from the ajax reload
column.data().unique().sort().each( function ( d, j ) {
select.append( '<option value="'+d+'">'+d+'</option>' );
} );
}
});
}
// and this is how I’m setting the DataTable
var $datatable = $('#table_materiales');
$datatable
.on('xhr.dt', function ( e, settings, json, xhr ) {
initCompleteFunction(settings, json);
})
.DataTable({
"ajax": {
"url": "http://my_endpoint",
"dataSrc": "",
"type": "POST",
"data": {
id_proveedor: function () {
return $('#proveedor').val(); // to get the value in the provider’s filter (select)
}
}
},
"columns": [
{
data: 'row_num'
},{
className: "select",
data: 'material'
},
// here goes the rest of the column definitions
],
"paging": false,
'columnDefs': [
{
'targets': 0,
'checkboxes': {
'selectRow': true
}
}
],
'select': {
'style': 'multi'
},
'order': [
[3, 'asc']
],
"createdRow": function (row, data, dataIndex) {
$(row).attr('data-id-material', data.id_material);
$(row).attr('data-pedido_sugerido', data.pedido_sugerido);
$(row).attr('id', 'id_' + data.row_num);
if(data['status_de_tiempo']=='FUERA'){
$(row).addClass('redClass');
}
},
});
During research I found that the xhr.dt event is triggered before the ajax.reload() is completed so the data keeps outdated when the select for the individual column search is populated. See this reference
User grozni posted this on April, 2019:
I have used console logs and was able to confirm that the event fires before the XHR event concludes, and does not pull the latest JSON. I used XHR tracking where I could to get around it but it's still really inconvenient and complicating matters alot. I need to be able to do certain things after the data is loaded and drawn. Perhaps it's worthy of a bug report
I found this post (See here) where user conangithub needed to
count DataTables item after I reload DataTable successfully
User lovecoding-git suggested this approach:
table= $('#example').DataTable();
$('#example').on('draw.dt', function() {
console.log(table.ajax.json().recordsTotal);
});
So, for my own issue, instead of
.on('xhr.dt', function ( e, settings, json, xhr ) {
initCompleteFunction(settings, json);
})
I wrote
.on('draw.dt', function ( e, settings, json, xhr ) {
initCompleteFunction(settings, json);
})
Et voilà.
I got the needed solution.

Single dropdown filtering for Datatable

I've been struggling for a few days now, trying to get a single dropdown to filter my table. Upon selection of the eraId, the columns should be refreshed to only show the columns of the selected eraId.
This is how my tables looks like:
I've read a lot of examples on Datatables website or forums but I can't seem to find something working.
I have managed to create a dropdown menu containing the different EraIds as filter (I have simplified the example below with only 3 eraIds) but after selecting an entry in the dropdown, the table gets empty and the column list is not refreshed.
I think the problem is that I first retrieve the columns names, based on the eraId and then draw the table accordingly, displaying only the resources from the specific eraId. I have tried several things but did not manage.
Ideally I should callback getPlayerResourceTable with the selected eraId or update the column list with the resources on the selected eraId.
Javascript:
var columns = [];
function getPlayerResourceTable($selectedEraId) {
$.ajax({
type: "POST",
url: "./graphs.php",
data: { call_function : 'getResourceTableColumns', eraId: $selectedEraId},
success: function (data) {
data = JSON.parse(data);
columnNames = Object.keys(data.data);
for (var i in data.data) {
columns.push({data: data.data[i],
title: data.data[i]});
}
$('#playerResourceTable').DataTable( {
processing: true,
serverSide: false,
filter: true,
columns: columns,
ajax: {
url: './graphs.php',
type: 'POST',
data: { call_function: 'playerResourceTable', column_fields : data.data, eraId: $selectedEraId}
},
initComplete: function () {
this.api().columns( 0 ).every( function () {
var column = this;
var select = $('<select><option value=""></option></select>')
.appendTo( $("#playerResourceTablesWrapper .dataTables_filter"))
.on( 'change', function () {
var val = $.fn.dataTable.util.escapeRegex(
$(this).val()
);
column.search( this.value ).draw();
} );
select.append( '<option value="1">Era 1</option>' )
select.append( '<option value="2">Era 2</option>' )
select.append( '<option value="3">Era 3</option>' )
} );
}
});
}
});
}
$(document).ready(function() {
$selectedEraId = 1;
getPlayerResourceTable($selectedEraId);
} );
PHP:
getResourceTableColumns returns the column list with query similar to SELECT columnName FROM ages WHERE eraId = ?
playerResourceTable returns the resources for each column (type of resource) with query similar to SELECT ".$field_list." FROM user_resources
I also thought of removing WHERE eraId = ? in my MySQL query and filtering the columns on the client side but no luck either.
I eventually ended up separating both functions and destroying/re-creating the table when changing Era.
function getColumns($selectedEraId) {
var columns = [];
$.ajax({
type: "POST",
url: "./graphs.php",
data: { call_function : 'getResourceTableColumns', eraId: $selectedEraId},
success: function (data) {
data = JSON.parse(data);
for (var i in data.data) {
columns.push({data: data.data[i],
title: data.data[i]});
}
if ( $.fn.dataTable.isDataTable( '#playerResourceTable' ) ) { // If the table already exists, detroy it before creating it again
$('#playerResourceTable').DataTable().destroy();
}
getPlayerResourceTable($selectedEraId, columns); // Will recreate the table with the new columns
}
});
}
function getPlayerResourceTable($selectedEraId, columns_to_show) {
$city_id = 91;
$playerResourceTable = $('#playerResourceTable').DataTable( {
processing: true,
serverSide: false,
filter: true,
columns: columns_to_show,
ajax: {
url: './graphs.php',
type: 'POST',
data: { call_function: 'playerResourceTable', column_fields : columns_to_show, city_id : parseInt($city_id)}
},
initComplete: function () {
this.api().columns( 0 ).every( function () {
var column = this;
var select = $('<select id="selectEraId" ><option value=""></option></select>')
.appendTo( $("#playerResourceTablesWrapper .dataTables_filter"))
.on( 'change', function () {
var val = $.fn.dataTable.util.escapeRegex(
$(this).val()
);
column.search( this.value ).draw();
} );
dropdown_string = getEraDropdown($selectedEraId);
} );
$('#selectEraId').on('change', function() {
$selectedEraId = this.value;
columns_to_show = getColumns($selectedEraId);
});
}
});
}

How to change the columns collection set of a kendo TreeList dynamically?

Try to change the columns list dynamically via a query ...
When I construct the TreeList, I call for columns :
$("#treelist").kendoTreeList({
columns: AnalyseCenterSKUService.getKPIColumnList($scope)
If I return a simple array with the fields, it's working ..
If I call a $http.get (inside my getKPIColumnList(..) function) which add some columns to the existing array of columns, the TreeList is not constructed correctly.
Any suggestion will be really appreciated ! :)
EDIT 22-10-2019 09:00
Treelist init
$("#treelist").kendoTreeList({
columns: AnalyseCenterSKUService.getKPIColumnList($scope),
scrollable: true,
columnMenu : {
columns : true
},
height: "100%",
dataBound: function (e) {
ExpandAll();
},
dataSource: {
schema: {
model: {
id: "id",
parentId: "parentId",
fields: {
id: { type: "number" },
parentId: { type: "number", nullable: true },
fields: {
id: { type: "number" },
parentId: { type: "number", nullable: false }
}
}
}
},
transport: {
read: {
url: "/api/AnalyseCenter/GetWorkOrderTree/0",
dataType: "json"
}
}
}
The getKPIColumnList return an static array + some push with dynamic columns (from DB)
angular.module('AnalyseCenterDirectives')
.service ('AnalyseCenterSKUService', function ($http) {
var toReturn = [ {field: "Name", title: "Hiérachie SKU", width: "30%" }, ..., ..., .... ];
I try in this function to push DB result
return $http.get("/api/AnalyseCenter/GetWorkOrderHistorianAdditonalColumns?equipmentName=" + equipmentName)
.then(function (result) {
var data = result.data;
if (data && data !== 'undefined') {
var fromDB = data;
angular.forEach(fromDB, function (tag) {
var tagName = tag.replace(".","_");
toReturn.push({
field: tagName, title: tag, width: '10%',
attributes: { style: "text-align:right;"} })
})
The stored procedure GetWorkOrderHistorianAdditonalColumns returns a list of string (future column)
That is because ajax is async, that means your tree list is being initialized before the request finishes. A classic question for JavaScript newcomers. I suggest you take a while to read about ajax, like How does AJAX works for instance.
Back to your problem. You need to create your tree list inside the success callback(I can't give you a more complete solution since I don't know what you're doing inside your function or which framework you're using to open that ajax request) with the result data, which is probably your columns. Then it would work as if you're initializing it with arrays.

Column Visibility is not restored from a saved state via stateLoadCallback

I have added the Column Visibility button to choose to show or hide certain columns. I'm saving the state in a database, I call the stateSaveCallback function via a click on a button.
I cant find documentation about retrieving data this way, so I just link to the page and pass variables to get the data back from the database, and then load that using stateLoadCallback.
Now all this works fine, EXCEPT the column visibility is not restored. It is in the JSON data being returned though.
Here is my full code:
$(document).ready(function() {
$.extend( jQuery.fn.dataTableExt.oSort, {
"date-uk-pre": function (a){
return parseInt(moment(a, "DD/MM/YYYY").format("X"), 10);
},
"date-uk-asc": function (a, b) {
return a - b;
},
"date-uk-desc": function (a, b) {
return b - a;
}
});
var edit_date_col_num = $('th:contains("Edit Date")').index();
var entry_date_col_num = $('th:contains("Entry Date")').index();
var table = $('.mainTable').DataTable( {
pageLength: 50,
colReorder: true,
stateSave: true,
columnDefs: [
{ "type": "date-uk", targets: [ edit_date_col_num, entry_date_col_num ] }
],
dom: 'Blfrtip',
buttons: [
'copy', 'csv', 'excel', 'print',
{
extend: 'colvis',
collectionLayout: 'fixed four-column',
postfixButtons: [ 'colvisRestore' ]
}
],
<?php
$id = $this->input->get('id');
$action = $this->input->get('action');
if(isset($action) && $action == 'load' && isset($id) && $id != '') :
?>
"stateLoadCallback": function (settings) {
var o;
// Send an Ajax request to the server to get the data. Note that
// this is a synchronous request since the data is expected back from the
// function
$.ajax( {
"url": EE.BASE + "&C=addons_modules&M=show_module_cp&module=ion&method=state_save&action=load&id=<?php echo $id;?>",
"async": false,
"dataType": "json",
"success": function (response) {
response = JSON.parse(response);
o = response;
}
});
return o;
},
<?php
endif;
?>
initComplete: function (settings) {
this.api().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>' )
} );
} );
// Need to re-apply the selection to the select dropdowns
var cols = settings.aoPreSearchCols;
for (var i = 0; i < cols.length; i++)
{
var value = cols[i].sSearch;
if (value.length > 0)
{
value = value.replace("^", "").replace("$","");
console.log(value);
$("tfoot select").eq(i).val(value);
}
}
},
} );
// Save a datatables state by clicking the save button
$( ".save_state" ).click(function(e) {
e.preventDefault();
table.destroy();
$('.mainTable').DataTable( {
colReorder: true,
stateSave: true,
"stateSaveCallback": function (settings, data) {
var save_name = $('.save_name').val();
// Send an Ajax request to the server with the state object
$.ajax( {
"url": EE.BASE + "&C=addons_modules&M=show_module_cp&module=ion&method=state_save&action=save&save_name="+save_name,
"data": data,
"dataType": "json",
"type": "POST",
"success": function (response)
{
//console.log(response);
}
} );
},
});
//table.state.save();
window.location.replace(EE.BASE + "&C=addons_modules&M=show_module_cp&module=ion&method=applications");
});
$( ".clear_state" ).click(function(e) {
e.preventDefault();
table.state.clear();
window.location.replace(EE.BASE + "&C=addons_modules&M=show_module_cp&module=ion&method=applications");
});
} );
Here is the saved JSON with several visible false in the beginning (which are visible once loaded):
{"time":"1449338856556","start":"0","length":"50","order":[["0","asc"]],"search":{"search":"","smart":"true","regex":"false","caseInsensitive":"true"},"columns":[{"visible":"false","search":{"search":"","smart":"false","regex":"true","caseInsensitive":"true"}},{"visible":"false","search":{"search":"","smart":"true","regex":"false","caseInsensitive":"true"}},{"visible":"false","search":{"search":"","smart":"true","regex":"false","caseInsensitive":"true"}},{"visible":"false","search":{"search":"","smart":"true","regex":"false","caseInsensitive":"true"}},{"visible":"false","search":{"search":"","smart":"true","regex":"false","caseInsensitive":"true"}},{"visible":"true","search":{"search":"","smart":"true","regex":"false","caseInsensitive":"true"}},{"visible":"true","search":{"search":"","smart":"true","regex":"false","caseInsensitive":"true"}},{"visible":"true","search":{"search":"","smart":"true","regex":"false","caseInsensitive":"true"}},{"visible":"true","search":{"search":"","smart":"true","regex":"false","caseInsensitive":"true"}},{"visible":"true","search":{"search":"","smart":"true","regex":"false","caseInsensitive":"true"}},{"visible":"true","search":{"search":"","smart":"true","regex":"false","caseInsensitive":"true"}},{"visible":"true","search":{"search":"","smart":"true","regex":"false","caseInsensitive":"true"}},{"visible":"true","search":{"search":"","smart":"true","regex":"false","caseInsensitive":"true"}},{"visible":"true","search":{"search":"","smart":"true","regex":"false","caseInsensitive":"true"}},{"visible":"true","search":{"search":"","smart":"true","regex":"false","caseInsensitive":"true"}},{"visible":"true","search":{"search":"","smart":"true","regex":"false","caseInsensitive":"true"}},{"visible":"true","search":{"search":"","smart":"true","regex":"false","caseInsensitive":"true"}},{"visible":"true","search":{"search":"","smart":"true","regex":"false","caseInsensitive":"true"}},{"visible":"true","search":{"search":"","smart":"true","regex":"false","caseInsensitive":"true"}},{"visible":"true","search":{"search":"","smart":"true","regex":"false","caseInsensitive":"true"}},{"visible":"true","search":{"search":"","smart":"true","regex":"false","caseInsensitive":"true"}},{"visible":"true","search":{"search":"","smart":"true","regex":"false","caseInsensitive":"true"}},{"visible":"true","search":{"search":"","smart":"true","regex":"false","caseInsensitive":"true"}},{"visible":"true","search":{"search":"","smart":"true","regex":"false","caseInsensitive":"true"}},{"visible":"true","search":{"search":"","smart":"true","regex":"false","caseInsensitive":"true"}},{"visible":"true","search":{"search":"","smart":"true","regex":"false","caseInsensitive":"true"}},{"visible":"true","search":{"search":"","smart":"true","regex":"false","caseInsensitive":"true"}},{"visible":"true","search":{"search":"","smart":"true","regex":"false","caseInsensitive":"true"}},{"visible":"true","search":{"search":"","smart":"true","regex":"false","caseInsensitive":"true"}},{"visible":"true","search":{"search":"","smart":"true","regex":"false","caseInsensitive":"true"}},{"visible":"true","search":{"search":"","smart":"true","regex":"false","caseInsensitive":"true"}},{"visible":"true","search":{"search":"","smart":"true","regex":"false","caseInsensitive":"true"}},{"visible":"true","search":{"search":"","smart":"true","regex":"false","caseInsensitive":"true"}},{"visible":"true","search":{"search":"","smart":"true","regex":"false","caseInsensitive":"true"}},{"visible":"true","search":{"search":"","smart":"true","regex":"false","caseInsensitive":"true"}},{"visible":"true","search":{"search":"","smart":"true","regex":"false","caseInsensitive":"true"}},{"visible":"true","search":{"search":"","smart":"true","regex":"false","caseInsensitive":"true"}},{"visible":"true","search":{"search":"","smart":"true","regex":"false","caseInsensitive":"true"}},{"visible":"true","search":{"search":"","smart":"true","regex":"false","caseInsensitive":"true"}},{"visible":"true","search":{"search":"","smart":"true","regex":"false","caseInsensitive":"true"}},{"visible":"true","search":{"search":"","smart":"true","regex":"false","caseInsensitive":"true"}},{"visible":"true","search":{"search":"","smart":"true","regex":"false","caseInsensitive":"true"}},{"visible":"true","search":{"search":"","smart":"true","regex":"false","caseInsensitive":"true"}},{"visible":"true","search":{"search":"","smart":"true","regex":"false","caseInsensitive":"true"}},{"visible":"true","search":{"search":"","smart":"true","regex":"false","caseInsensitive":"true"}},{"visible":"true","search":{"search":"","smart":"true","regex":"false","caseInsensitive":"true"}},{"visible":"true","search":{"search":"","smart":"true","regex":"false","caseInsensitive":"true"}},{"visible":"true","search":{"search":"","smart":"true","regex":"false","caseInsensitive":"true"}},{"visible":"true","search":{"search":"","smart":"true","regex":"false","caseInsensitive":"true"}},{"visible":"true","search":{"search":"","smart":"true","regex":"false","caseInsensitive":"true"}},{"visible":"true","search":{"search":"","smart":"true","regex":"false","caseInsensitive":"true"}},{"visible":"true","search":{"search":"","smart":"true","regex":"false","caseInsensitive":"true"}},{"visible":"true","search":{"search":"","smart":"true","regex":"false","caseInsensitive":"true"}},{"visible":"true","search":{"search":"","smart":"true","regex":"false","caseInsensitive":"true"}},{"visible":"true","search":{"search":"","smart":"true","regex":"false","caseInsensitive":"true"}},{"visible":"true","search":{"search":"","smart":"true","regex":"false","caseInsensitive":"true"}},{"visible":"true","search":{"search":"","smart":"true","regex":"false","caseInsensitive":"true"}},{"visible":"true","search":{"search":"","smart":"true","regex":"false","caseInsensitive":"true"}},{"visible":"true","search":{"search":"","smart":"true","regex":"false","caseInsensitive":"true"}},{"visible":"true","search":{"search":"","smart":"true","regex":"false","caseInsensitive":"true"}},{"visible":"true","search":{"search":"","smart":"true","regex":"false","caseInsensitive":"true"}},{"visible":"true","search":{"search":"","smart":"true","regex":"false","caseInsensitive":"true"}},{"visible":"true","search":{"search":"","smart":"true","regex":"false","caseInsensitive":"true"}},{"visible":"true","search":{"search":"","smart":"true","regex":"false","caseInsensitive":"true"}},{"visible":"true","search":{"search":"","smart":"true","regex":"false","caseInsensitive":"true"}},{"visible":"true","search":{"search":"","smart":"true","regex":"false","caseInsensitive":"true"}},{"visible":"true","search":{"search":"","smart":"true","regex":"false","caseInsensitive":"true"}},{"visible":"true","search":{"search":"","smart":"true","regex":"false","caseInsensitive":"true"}},{"visible":"true","search":{"search":"","smart":"true","regex":"false","caseInsensitive":"true"}},{"visible":"true","search":{"search":"","smart":"true","regex":"false","caseInsensitive":"true"}},{"visible":"true","search":{"search":"","smart":"true","regex":"false","caseInsensitive":"true"}}],"ColReorder":["0","1","2","3","4","5","6","7","8","9","10","11","12","13","14","15","16","17","18","19","20","21","22","23","24","25","26","27","28","29","30","31","32","33","34","35","36","37","38","39","40","41","42","43","44","45","46","47","48","49","50","51","52","53","54","55","56","57","58","59","60","61","62","63","64","65","66","67","68","69","70"]}
Thanks
In my case datatables rejects old data according to "stateDuration" and "time" properties..
Solution: ignore state duration
"stateSave": true,
"stateDuration": -1,
Above case:
"visible":"false" may should be "visible":false
After a while of debugging this myself here's what worked for me..
This issue is that all the values in your JSON are strings and they need to be of correct datatypes for the datatables plugin.
Within the "stateSaveCallback" ajax request to save your state I did the following to the json string and then it saved all the values properly which then loaded the state as it should.
"stateSaveCallback": function (settings, data) {
var save_name = $('.save_name').val();
// Send an Ajax request to the server with the state object
$.ajax( {
"url": EE.BASE + "&C=addons_modules&M=show_module_cp&module=ion&method=state_save&action=save&save_name="+save_name,
//"data": data,
"data": JSON.stringify(data), // change to this..
"dataType": "json",
"type": "POST",
"success": function (response)
{
//console.log(response);
}
} );
},

Using Rally Cardboard UI to Display Predecessor/Successor Hierarchy

I'm currently trying to write a Rally app that will display the Predecessor/Successor hierarchy for a selected User Story. To illustrate, the user will select a User Story from a Chooser UI element. Then, a three-column Cardboard UI element will be generated--the leftmost column will contain all of the selected User Story's Predecessors (in card form), the middle column will contain the selected User Story's card, and the rightmost column will contain all of the selected User Story's Successors (in card form). From there, the Predecessor and Successor cards can be removed (denoting that they won't be Predecessors or Successors for the selected User Story) and new Predecessor/Successor cards can be added (denoting that they will become new Predecessors/Successors for the selected User Story).
However, my issue is this: the Cardboard UI was designed to display sets of different values for one particular attribute, but "Predecessor" and "Successor" don't fall into this category. Is there a possible way for me to display a User Story and then get its Predecessors and Successors and populate the rest of the board? I realize that it will take a substantial amount of modifications to the original board.
I've found a way to hack it so that you can do the display. Not sure if it's worth it or not and what you want to do for adding/removing, but this might help set you on the right path.
In summary, the problem is that the cardboard/column classes are designed to work with single value fields and each column that's created does an individual query to Rally based on the column configuration. You'll need to override the rallycardboard and the rallycolumn. I'll give the full html that you can paste in below, but let's hit this one piece at a time.
If nothing else, this might be a good example of how to take the source code of rally classes and make something to override them.
Data:
The existing cardboard is given a record type and field and runs off to create a column for each valid value for the field. It gets this information by querying Rally for the stories and for the attribute definitions. But we want to use our data in a slightly different way, so we'll have to make a different data store and feed it in. So we want to use a wsapidatastore to go ahead and get the record we asked for (in this example, I have a story called US37 that has predecessors and successors). In a way, this is like doing a cross-tab in Excel. Instead of having one record (37) that's related to others, we want to make a data set of all the stories and define their relationship in a new field, which I called "_column." Like this:
Ext.create('Rally.data.WsapiDataStore', {
model: "hierarchicalrequirement",
autoLoad: true,
fetch: ['Name','Predecessors','Successors','FormattedID','ObjectID','_ref'],
filters: [ {
property: 'FormattedID', operator: 'contains', value: '37'
} ] /* get the record US37 */,
listeners: {
load: function(store,data,success) {
if ( data.length === 1 ) {
var base_story = data[0].data;
var modified_records = [];
base_story._column = "base";
modified_records.push( base_story );
Ext.Array.each( base_story.Predecessors, function( story ) {
story._column = "predecessor";
modified_records.push( story );
} );
Ext.Array.each( base_story.Successors, function(story) {
story._column = "successor";
modified_records.push( story );
} );
We push the data into an array of objects with each having a new field to define which column it should go in. But this isn't quite enough, because we need to put the data into a store. The store needs a model -- and we have to define the fields in a way that the cardrenders know how to access the data. There doesn't seem to be an easy way to just add a field definition to an existing rally model, so this should do it (the rally model has unique field information and a method called getField(), so we have to add that:
Ext.define('CardModel', {
extend: 'Ext.data.Model',
fields: [
{ name: '_ref', type: 'string' },
{ name: 'ObjectID', type: 'number'},
{ name: 'Name', type: 'string', attributeDefinition: { AttributeType: 'STRING'} },
{ name: 'FormattedID', type: 'string'},
{ name: '_column', type: 'string' },
{ name: 'ScheduleState', type: 'string' } ] ,
getField: function(name) {
if ( this.data[name] ) {
var return_field = null;
Ext.Array.each( this.store.model.getFields(), function(field) {
if ( field.name === name ) {
return_field = field;
}
} );
return return_field;
} else {
return null;
}
}
});
var cardStore = Ext.create('Ext.data.Store',{
model: 'CardModel',
data: modified_records
});
Now, we'll create a cardboard, but instead of the rally cardboard, we'll make one from a class we're going to define below ('DependencyCardboard'). In addition, we'll pass along a new definition for the columns that we'll also define below ('dependencycolumn').
var cardboard = Ext.create('DependencyCardboard', {
attribute: '_column',
store: cardStore, /* special to our new cardboard type */
height: 500,
columns: [{
xtype: 'dependencycolumn',
displayValue: 'predecessor',
value: 'predecessor',
store: cardStore
},
{
xtype: 'dependencycolumn',
displayValue: 'base',
value: 'base',
store: cardStore
},
{
xtype: 'dependencycolumn',
displayValue: 'successor',
value: 'successor',
store: cardStore
}]
});
Cardboard:
Mostly, the existing Rally cardboard can handle our needs because all the querying is done down in the columns themselves. But we still have to override it because there is one function that is causing us problems: _retrieveModels. This function normally takes the record type(s) (e.g., User Story) and based on that creates a data model that is based on the Rally definition. However, we're not using the UserStory records directly; we've had to define our own model so we could add the "_columns" field. So, we make a new definition (which we use in the create statement above for "DependencyCardboard").
(Remember, we can see the source code for all of the Rally objects in the API just by clicking on the title, so we can compare the below method to the one in the base class.)
We can keep all of the stuff that the Rally cardboard does and only override the one method by doing this:
Ext.define( 'DependencyCardboard', {
extend: 'Rally.ui.cardboard.CardBoard',
alias: 'widget.dependencycardboard',
constructor: function(config) {
this.mergeConfig(config);
this.callParent([this.config]);
},
initComponent: function() {
this.callParent(arguments);
},
_retrieveModels: function(success) {
if ( this.store ) {
this.models = [ this.store.getProxy().getModel() ];
success.apply( this, arguments );
}
}
});
Column:
Each column normally goes off to Rally and says "give me all of the stories that have a field equal to the column name". But we're passing in the store to the cardboard, so we need to override _queryForData. In addition, there is something going on about defining the column height when we do this (I don't know why!) so I had to add a little catch in the getColumnHeightFromCards() method.
_queryForData: function() {
var allRecords = [];
var records = this.store.queryBy( function( record ) {
if ( record.data._column === this.getValue() ) { allRecords.push( record ); }
}, this);
this.createAndAddCards( allRecords );
},
getColumnHeightFromCards: function() {
var contentMinHeight = 500,
bottomPadding = 30,
cards = this.query(this.cardConfig.xtype),
height = bottomPadding;
for(var i = 0, l = cards.length; i < l; ++i) {
if ( cards[i].el ) {
height += cards[i].getHeight();
} else {
height += 100;
}
}
height = Math.max(height, contentMinHeight);
height += this.down('#columnHeader').getHeight();
return height;
}
Finish
So, if you add all those pieces together, you get one long html file that we can push into a panel (and that you can keep working on to figure out how to override dragging results and to add your chooser panel for the first item. (And you can make better abstracted into its own class)).
Full thing:
<!DOCTYPE html>
<html>
<head>
<title>cardboard</title>
<script type="text/javascript" src="/apps/2.0p3/sdk.js"></script>
<script type="text/javascript">
Rally.onReady(function() {
/*global console, Ext */
Ext.define( 'DependencyColumn', {
extend: 'Rally.ui.cardboard.Column',
alias: 'widget.dependencycolumn',
constructor: function(config) {
this.mergeConfig(config);
this.callParent([this.config]);
},
initComponent: function() {
this.callParent(arguments);
},
_queryForData: function() {
var allRecords = [];
var records = this.store.queryBy( function( record ) {
if ( record.data._column === this.getValue() ) { allRecords.push( record ); }
}, this);
this.createAndAddCards( allRecords );
},
getColumnHeightFromCards: function() {
var contentMinHeight = 500,
bottomPadding = 30,
cards = this.query(this.cardConfig.xtype),
height = bottomPadding;
for(var i = 0, l = cards.length; i < l; ++i) {
if ( cards[i].el ) {
height += cards[i].getHeight();
} else {
height += 100;
}
}
height = Math.max(height, contentMinHeight);
height += this.down('#columnHeader').getHeight();
return height;
}
});
/*global console, Ext */
Ext.define( 'DependencyCardboard', {
extend: 'Rally.ui.cardboard.CardBoard',
alias: 'widget.dependencycardboard',
constructor: function(config) {
this.mergeConfig(config);
this.callParent([this.config]);
},
initComponent: function() {
this.callParent(arguments);
},
_retrieveModels: function(success) {
if ( this.store ) {
this.models = [ this.store.getProxy().getModel() ];
success.apply( this, arguments );
}
}
});
/*global console, Ext */
Ext.define('CustomApp', {
extend: 'Rally.app.App',
componentCls: 'app',
items: [ { xtype: 'container', itemId: 'outer_box' }],
launch: function() {
Ext.create('Rally.data.WsapiDataStore', {
model: "hierarchicalrequirement",
autoLoad: true,
fetch: ['Name','Predecessors','Successors','FormattedID','ObjectID','_ref'],
filters: [ {
property: 'FormattedID', operator: 'contains', value: '37'
} ],
listeners: {
load: function(store,data,success) {
if ( data.length === 1 ) {
var base_story = data[0].data;
var modified_records = [];
base_story._column = "base";
modified_records.push( base_story );
Ext.Array.each( base_story.Predecessors, function( story ) {
story._column = "predecessor";
modified_records.push( story );
} );
Ext.Array.each( base_story.Successors, function(story) {
story._column = "successor";
modified_records.push( story );
} );
Ext.define('CardModel', {
extend: 'Ext.data.Model',
fields: [
{ name: '_ref', type: 'string' },
{ name: 'ObjectID', type: 'number'},
{ name: 'Name', type: 'string', attributeDefinition: { AttributeType: 'STRING'} },
{ name: 'FormattedID', type: 'string'},
{ name: '_column', type: 'string' },
{ name: 'ScheduleState', type: 'string' } ] ,
getField: function(name) {
if ( this.data[name] ) {
var return_field = null;
Ext.Array.each( this.store.model.getFields(), function(field) {
if ( field.name === name ) {
return_field = field;
}
} );
return return_field;
} else {
return null;
}
}
});
var cardStore = Ext.create('Ext.data.Store',{
model: 'CardModel',
data: modified_records
});
var cardboard = Ext.create('DependencyCardboard', {
attribute: '_column',
store: cardStore,
height: 500,
columns: [{
xtype: 'dependencycolumn',
displayValue: 'predecessor',
value: 'predecessor',
store: cardStore
},
{
xtype: 'dependencycolumn',
displayValue: 'base',
value: 'base',
store: cardStore
},
{
xtype: 'dependencycolumn',
displayValue: 'successor',
value: 'successor',
store: cardStore
}]
});
this.down('#outer_box').add( cardboard );
}
},
scope: this
}
});
}
});
Rally.launchApp('CustomApp', {
name: 'cardboard'
});
});
</script>
<style type="text/css">
.app {
/* Add app styles here */
}
</style>
</head>
<body></body>
</html>