Rally Tag Cloud - javascript

I found some code on GitHub (https://github.com/RallyCommunity/TagCloud) for displaying a TagCloud in Rally. Conceptually it looks great but I cannot seem to get it to work and wondered if there were any Rally JavaScript experts out there who could take a quick look.
I modified it slightly as the URL for the Analytics was incorrect (according to the docs) and the API's were hard coded to depreciated versions so I updated those.
I'm not a JavaScript expert. When I run this it doesn't find any Tags against stories.
When I run this in Chrome debugging mode I can take the URL and execute it in my browser but it also does not come back with any Tags. It comes back with a full result response from Analytics, but has no tags and I know there are tags against stories in the selected project.
Any ideas from anyone?
<!DOCTYPE html>
<html>
<head>
<title>TagCloud</title>
<script type="text/javascript" src="/apps/2.0p/sdk.js"></script>
<script type="text/javascript">
Rally.onReady(function() {
Ext.define('CustomApp', {
extend: 'Rally.app.App',
componentCls: 'app',
layout: 'border',
items: [
{
title: 'Tag Cloud',
xtype: 'panel',
itemId: 'cloud',
region: 'west',
width: '30%',
collapsible: true,
bodyStyle: 'padding:15px',
listeners: { 'afterRender': function(el) { setTimeout(function() { el.setLoading(); }, 500);}} // needs a little delay
},
{
title: '<<< Select a tag from the Tag Cloud',
xtype: 'panel',
itemId: 'grid',
region: 'center',
width: '70%',
collapsible: false
}
],
tagMap : [],
maxFont : 24, // largest desired font size
minFont : 8, // and smallest
_renderTag : function renderHandler(tagLabel) {
tagLabel.getEl().on('click',this._tagSelected, this);
},
// does the actual building of the cloud from 'tagMap'
_buildCloud: function(app, response) {
//title: '<<< Select a tag from the Tag Cloud - Building',
var i, tag;
for (i=0;i<response.Results.length;i++) {
tag = response.Results[i];
if(typeof app.tagMap[tag.ObjectID] !== "undefined") {
app.tagMap[tag.ObjectID].Name = tag._refObjectName;
}
}
if(response.StartIndex+response.PageSize < response.TotalResultCount) {
app._queryForTagNames(response.StartIndex+response.PageSize, app, app._buildCloud);
} else {
if(app.tagMap.length === 0) {
tag = new Ext.form.Label({
id: 'tagNone',
text: ' No tagged Stories found '
});
app.down('#cloud').add(tag);
} else {
var minFrequency = Number.MAX_VALUE;
var maxFrequency = Number.MIN_VALUE;
var tuples = [];
for (var x in app.tagMap) {
if (app.tagMap.hasOwnProperty(x)) {
tuples.push([x, app.tagMap[x]]);
if(app.tagMap[x].count > maxFrequency) {
maxFrequency = app.tagMap[x].count;
}
if(app.tagMap[x].count < minFrequency) {
minFrequency = app.tagMap[x].count;
}
}
}
tuples.sort(function(a,b) { a = a[1]; b = b[1]; return a.Name > b.Name ? 1 : a.Name < b.Name ? -1 : 0 ;});
for (i = 0; i < tuples.length; i++) {
var ftsize = ((tuples[i][1].count-minFrequency)*(app.maxFont-app.minFont) / (maxFrequency-minFrequency)) + app.minFont;
tag = new Ext.form.Label({
id: 'tag'+tuples[i][0],
text: ' ' + tuples[i][1].Name + ' ',
overCls: 'link',
style:"font-size: "+ftsize+"pt;",
listeners: { scope: app, render: app._renderTag }
});
app.down('#cloud').add(tag);
}
}
app.getComponent('cloud').setLoading(false);
}
},
// collects the _queryForTags responses and calls _queryForTagNames when it has them all
_buildTagMap: function(app, response) {
for (var i=0;i<response.Results.length;i++) {
var ent = response.Results[i];
for (var j=0; j < ent.Tags.length; j++) {
var tag = ent.Tags[j];
var mapent = app.tagMap[tag];
if(typeof mapent === "undefined") {
mapent = { count: 1 };
} else {
mapent.count++;
}
app.tagMap[tag] = mapent;
}
}
if(response.StartIndex+response.PageSize < response.TotalResultCount) {
app._queryForTags(response.StartIndex+response.PageSize, app, app._buildTagMap);
} else {
app._queryForTagNames(0, app, app._buildCloud);
}
},
// get a list of the tags from the Lookback API, iterating if necessary (see _buildTagMap)
_queryForTags: function(start, app, callback) {
var params = {
find: "{'Tags':{'$exists':true}, '__At':'current', '_Type':'HierarchicalRequirement', '_ProjectHierarchy':"+ this.getContext().getProject().ObjectID +" }",
fields: "['Tags']",
pagesize: 20000,
start: start
};
Ext.Ajax.request({
url: 'https://rally1.rallydev.com/analytics/v2.0/service/rally/workspace/'+ this.context.getWorkspace().ObjectID + '/artifact/snapshot/query.js',
method: 'GET',
params: params,
withCredentials: true,
success: function(response){
var text = response.responseText;
var json = Ext.JSON.decode(text);
callback(app, json);
}
});
},
// once all the tags have been collected, get a list of the tag names from the WSAPI, iterating if necessary (see _buildCloud)
_queryForTagNames: function(start, app, callback) {
Ext.Ajax.request({
url: 'https://rally1.rallydev.com/slm/webservice/1.41/tag.js',
method: 'GET',
params: { fetch: "ObjectID", pagesize: 200, "start": start},
withCredentials: true,
success: function(response){
callback(app, Ext.JSON.decode(response.responseText).QueryResult);
}
});
},
_queryForStories: function(tagOid) {
Ext.create('Rally.data.WsapiDataStore', {
model: 'UserStory',
autoLoad: true,
fetch: ['Rank', 'FormattedID', 'Name', 'ScheduleState'],
filters: [{
property:'Tags',
operator: '=',
value: "tag/" + tagOid
}],
sorters: [{
property: 'Rank',
direction: 'ASC'
}],
listeners: {
load: this._onDataLoaded,
scope: this
}
});
},
_tagSelected: function(app, elem) {
this.getComponent('grid').setLoading();
this._queryForStories(elem.id.substring(3)); // cheesy, id is "tag"+tagOid, we need the oid
this.tagName = elem.innerText;
},
_onDataLoaded: function(store, data) {
var records = [], rankIndex = 1;
Ext.Array.each(data, function(record) {
records.push({
Ranking: rankIndex,
FormattedID: record.get('FormattedID'),
Name: record.get('Name'),
State: record.get('ScheduleState')
});
rankIndex++;
});
var customStore = Ext.create('Rally.data.custom.Store', {
data: records,
pageSize: 25
});
if(!this.grid) {
this.grid = this.down('#grid').add({
xtype: 'rallygrid',
store: customStore,
columnCfgs: [
{ text: 'Ranking', dataIndex: 'Ranking' },
{ text: 'ID', dataIndex: 'FormattedID' },
{ text: 'Name', dataIndex: 'Name', flex: 1 },
{ text: 'State', dataIndex: 'State' }
]
});
} else {
this.grid.reconfigure(customStore);
}
this.getComponent('grid').setTitle('Stories tagged: ' + this.tagName);
this.getComponent('grid').setLoading(false);
},
launch: function() {
this._queryForTags(0, this, this._buildTagMap);
}
});
Rally.launchApp('CustomApp', {
name: 'TagCloud'
});
});
</script>
<style type="text/css">
.app {
}
.link {
color: #066792;
cursor: pointer;
} </style>
</head>
<body></body>
</html>

The code was out-of-date with respect to the most-recent LBAPI changes - specifically the use of _Type vs _TypeHierarchy and of course, the url, as you'd already discovered. Please pick up the changes and give it a whirl.

Related

jquery datatable - applying value for select.className is not working as expected

I am trying to change the background color of selected row(s) in jquery datatable using my own css class but, the tick mark in the checkbox is not appearing.
If I remove className: 'selected-row' from the below code, then everything works normal but, without the color I want.
Fiddler: https://jsfiddle.net/8f63kmeo/12/
HTML:
<table id="CustomFilterOnTop" class="table table-bordered table-condensed" width="100%"></table>
JS
var Report4Component = (function () {
function Report4Component() {
//contorls
this.customFilterOnTopControl = "CustomFilterOnTop"; //table id
//data table object
this.customFilterOnTopGrid = null;
//variables
this.result = null;
}
Report4Component.prototype.ShowGrid = function () {
var instance = this;
//create the datatable object
instance.customFilterOnTopGrid = $('#' + instance.customFilterOnTopControl).DataTable({
columns: [
{ title: "<input name='SelectOrDeselect' value='1' id='ChkBoxSelectAllOrDeselect' type='checkbox'/>" },
{ data: "Description", title: "Desc" },
{ data: "Status", title: "Status" },
{ data: "Count", title: "Count" }
],
"paging": true,
scrollCollapse: true,
"scrollX": true,
scrollY: "300px",
deferRender: true,
scroller: true,
dom: '<"top"Bf<"clear">>rt <"bottom"<"Notes">i<"clear">>',
buttons: [
{
text: 'Load All',
action: function (e, dt, node, config) {
instance.ShowData(10000);
}
}
],
columnDefs: [{
orderable: false,
className: 'select-checkbox text-center',
targets: 0,
render: function (data, type, row) {
return '';
}
}],
select: {
style: 'multi',
selector: 'td:first-child',
className: 'selected-row'
}
});
};
Report4Component.prototype.ShowData = function (limit) {
if (limit === void 0) { limit = 100; }
var instance = this;
instance.customFilterOnTopGrid.clear(); //latest api function
instance.result = instance.GetData(limit);
instance.customFilterOnTopGrid.rows.add(instance.result.RecordList);
instance.customFilterOnTopGrid.draw();
};
Report4Component.prototype.GetData = function (limit) {
//structure of the response from controller method
var resultObj = {};
resultObj.Total = 0;
resultObj.RecordList = [];
for (var i = 1; i <= limit; i++) {
resultObj.Total += i;
var record = {};
record.Description = "This is a test description of record " + i;
record.Status = ["A", "B", "C", "D"][Math.floor(Math.random() * 4)] + 'name text ' + i;
record.Count = i;
resultObj.RecordList.push(record);
}
return resultObj;
};
return Report4Component;
}());
$(function () {
var report4Component = new Report4Component();
report4Component.ShowGrid();
report4Component.ShowData();
});
function StopPropagation(evt) {
if (evt.stopPropagation !== undefined) {
evt.stopPropagation();
}
else {
evt.cancelBubble = true;
}
}
Issue:
Any suggestion / help will be greatly appreciated.
It's the class selected that sets the checkbox tick etc. and by using a different class the selected class is no longer added.
You can just add both those classes instead, and it should work
select: {
style: 'multi',
selector: 'td:first-child',
className: 'selected-row selected'
}
FIDDLE

Not storing cell value when other grid row is selected in extjs

I'm having two grid in application & I'm using rowSelectionModel. Now after user write anything in editable cell & then (w/o clicking anywhere) clicks on main grid row; rowselect function is getting called.
Please tell me way to retain the data in cell ???
/*********main tree *************/
var gridtree = new Ext.ux.maximgb.tg.EditorGridPanel({
id: 'mainTree',
store: storeCMP,
autoScroll: true,
layout: 'fit',
cm: colModeltree,
title: 'Text Execution Form',
master_column_id: 'NAME',
width: screen.width - 120,
height: 170,
columnWidth: 0.1,
sm: new Ext.grid.RowSelectionModel({
singleSelect: true,
autoExpandColumn: 'NAME',
listeners: {
rowselect: function(sm, row, rec) {
renderFloatingArray();
var sgrid = Ext.getCmp("mainTree");
if (rec.data.level === 0) {
titlegrid = rec.data.NAME;
Ext.getCmp("questionGrid").hide();
Ext.getCmp("sampleGrid").hide();
} else if (rec.data.level === 1) {
titlegrid = rec.data.NAME;
Ext.getCmp('sampleGrid').setTitle('Samples for ' + titlegrid + ' ');
if (F.TRO_GRC_OBJECT_TYPE.read(1) === "") {
filterdataQs(gridStoreQs, rec.data.TST_TEST_EXECUTION_ID, rec.data.TESTING_ID);
if (Ext.getCmp('questionGrid').getStore().data.items.length === 0) {
Ext.getCmp("questionGrid").hide();
} else {
Ext.getCmp('questionGrid').setTitle(' Questions for ' + titlegrid + ' ');
Ext.getCmp("questionGrid").show();
}
Ext.getCmp('sampleGrid').setTitle('Samples for ' + titlegrid + ' ');
Ext.getCmp("sampleGrid").show();
filterdataQs(gridStoreQs, rec.data.TST_TEST_EXECUTION_ID, rec.data.TESTING_ID);
filterdatasample(sampleGridstore, rec.data.TST_TEST_EXECUTION_ID, rec.data.TESTING_ID);
} else {
Ext.getCmp("questionGrid").hide();
Ext.getCmp("sampleGrid").hide();
}
} else if (rec.data.level == 2) {
Ext.getCmp("questionGrid").show();
Ext.getCmp("sampleGrid").show();
filterdataQs(gridStoreQs, rec.data.TST_TEST_EXECUTION_ID, rec.data.TRID_OBJID_ORG_ID);
filterdatasample(sampleGridstore, rec.data.TST_TEST_EXECUTION_ID, rec.data.TRID_OBJID_ORG_ID);
}}}}),
listeners: {
viewready: function(g) {
if (F.TRO_GRC_OBJECT_TYPE.read(1) === "") {
g.getSelectionModel().selectRow(1);
} else {
g.getSelectionModel().selectRow(2);
}
},
'rowcontextmenu': gridContextMenu
},
viewConfig: {
enableRowBody: true
},
plugins: [{
ptype: 'editable-grid'
}]
});
question - grid
var grid2 = new Ext.grid.EditorGridPanel({
id: 'questionGrid',
store: gridStoreQs,
hidden: false,
layout: 'fit',
autoScroll: true,
sm: new Ext.grid.RowSelectionModel({
singleSelect: true,
listeners: {
rowselect: function(sm, row, rec) {
currentgrid = 'questionGrid';}}}),
width: screen.width - 120,
autoHeight: true,
columnLines: true,
viewConfig: {
},
title: 'Question Grid',
cm: colModelQs,
enableKeyEvents: true,
plugins: [{ ptype: 'editable-grid' }],
onEditComplete: function(ed, value, startValue) {
if (ed.field.getXType() == "numberfield" || ed.field.getXType() == "textfield") {
this.editing = false;
this.activeEditor = null;
ed.un("specialkey", this.selModel.onEditorKey, this.selModel);
var r = ed.record;
var field = this.colModel.getDataIndex(ed.col);
var e = {
grid: this,
record: r,
field: field,
originalValue: startValue,
value: value,
row: ed.row,
column: ed.col,
cancel: false
};
if (this.fireEvent("validateedit", e) !== false && !e.cancel) {
r.set(field, value);
delete e.cancel;
this.fireEvent("afteredit", e);
}
this.view.focusCell(ed.row, ed.col);
}
},
listeners: {
'afteredit': afterTreeGridCellEdit
}
});
plugin used : editable-grid
The value is saved into the underlying record on cell editor blur event. Only if the entered value is invalid it is reverted back. So check if you are entering a valid value and if it does not help prepare a showcase on https://fiddle.sencha.com

ExtJS TaskRunner

I'm still in the learning process with EXTJS and any help would be much appreciated.
The goal for me is to load data into a grid where one cell needs to have the value incremented every minute. From my research using the TaskRunner function is the way to go but can't figure out where to put it and how to use it. My assumption is that it needs to go into the model or the controller but I'm not sure. In my simple project I'm using the MVC architecture.
Currently my gird works as expected. It reads in a file does a date conversion that produces a minute value. It's that minute value that I need to increment.
The code below is a sample TaskRunner snippit that I'm working with, right or wrong I don't know yet.
var runner = new Ext.util.TaskRunner();
var task = runner.newTask({
run: store.each(function (item)
{
var incReq = item.get('request') + 1;
item.set('request', incReq);
}),
interval: 60000 // 1-minute interval
});
task.start();
Model:
Ext.define("myApp.model.ActionItem", {
extend : "Ext.data.Model",
fields : [
{
name: 'pri',
type: 'int'
},
{
name: 'request',
type: 'int',
defaultValue: 0,
convert : function(v, model) {
return Math.round((new Date() - new Date(v)) / 60000);
}
}
]
});
Controller:
Ext.define("myApp.controller.HomeController", {
extend: "Ext.app.Controller",
id: "HomeController",
refs: [
{ ref: "ActionItemsGrid", selector: "[xtype=actionitemgrid]" },
{ ref: "DetailsPanel", selector: "[xtype=actionitemdetails]" }
],
pri: "",
models: ["ActionItem"],
stores: ["myActionStore"],
views:
[
"home.ActionItemDetailsPanel",
"home.ActionItemGrid",
"home.HomeScreen"
],
init: function () {
this.control({
"#homescreen": {
beforerender: this.loadActionItems
},
"ActionItemsGrid": {
itemclick: this.displayDetails
}
});
},
displayDetails: function (model, record) {
this.getDetailsPanel().loadRecord(record);
},
loadActionItems: function () {
var store = Ext.getStore("myActionStore");
store.load();
this.getActionItemsGrid().reconfigure(store);
}
});
View:
Ext.define("myApp.view.home.ActionItemGrid", {
extend: "Ext.grid.Panel",
xtype: "actionitemgrid",
resizable: true,
multiSelect: false,
enableDragDrop : false,
store: null,
columns:
[
{ header: "Pri", dataIndex: "pri", sortable: false, autoSizeColumn: true},
{ header: "Req", dataIndex: "request", sortable: false, autoSizeColumn: true}
],
viewConfig:
{
listeners: {
refresh: function(dataview) {
Ext.each(dataview.panel.columns, function(column) {
if (column.autoSizeColumn === true)
column.autoSize();
})
}
}
}
});
Sample JSON File:
{
"actionitems" : [
{
"pri": 1,
"request": "2014-12-30T03:52:48.516Z"
},
{
"pri": 2,
"request": "2014-12-29T05:02:48.516Z"
}
]
}
You have error in code which creates task - you should provide function to run configuration property, not result of invocation. Try this:
var runner = new Ext.util.TaskRunner();
var task = runner.newTask({
run: function() {
store.each(function (item)
{
var incReq = item.get('request') + 1;
item.set('request', incReq);
})
},
interval: 60000 // 1-minute interval
});
task.start();
You can put that code in loadActionItems method.

User stories with dependecies Based on iteration

I am new to Rally as well as javascript.I have taken the script from this site and i wanted to include the Iteration name along with the records.But it gives me object Object message when i executing the HTML Page.I need to solve this issue.Looking for assistance and thanks in advance.
-Mani
description here][1]
<!DOCTYPE html>
<html>
*emphasized text*<head>
<title>UserStoryWithPredecessors</title>
<script type="text/javascript" src="https://rally1.rallydev.com/apps/2.0p5/sdk.js"></script>
<script type="text/javascript">
Rally.onReady(function() {
Ext.define('CustomApp', {
extend: 'Rally.app.App',
componentCls: 'app',
launch: function() {
Ext.create('Rally.data.WsapiDataStore', {
model: 'UserStory',
fetch:
['FormattedID','Name','Iteration','Predecessors','FormattedID','CreationDate'],
autoLoad: true,
listeners: {
load: this._onDataLoaded,
///query: q,
scope: this
}
});
},
_onDataLoaded: function(store, data) {
var records = [];
Ext.Array.each(data, function(record) {
//Perform custom actions with the data here
//Calculations, etc.
var myPredecessors = record.get('Predecessors');
var predecessorData = "";
var predecessorCreationDate = "";
// Loop through and process Predecessor data
for (var i=0; i<myPredecessors.length; i++) {
thisPredecessor = myPredecessors[i];
thisPredecessorFormattedID = thisPredecessor["FormattedID"];
thisPredecessorName = thisPredecessor["Name"];
// Re-format Date/time string
thisPredecessorCreationDateString = thisPredecessor["CreationDate"];
thisPredecessorCreationDate = new
Date(thisPredecessorCreationDateString);
thisYear = thisPredecessorCreationDate.getFullYear();
thisMonth = thisPredecessorCreationDate.getMonth();
thisDay = thisPredecessorCreationDate.getDate();
thisPredecessorFormattedDate = thisMonth + "/" + thisDay + "/" +
thisYear;
// Post-pend updated data to value for array
predecessorData += thisPredecessorFormattedID + ": " +
thisPredecessorName + "<br>"
predecessorCreationDate += thisPredecessorFormattedDate + "<br>";
}
records.push({
FormattedID: record.get('FormattedID'),
Name: record.get('Name'),
Iteration: String(record.get('Iteration')),
Predecessors: predecessorData,
PredecessorCreationDate: predecessorCreationDate,
});
});
this.add({
xtype: 'rallygrid',
store: Ext.create('Rally.data.custom.Store', {
data: records,
pageSize: 20
}),
columnCfgs: [
{
text: 'FormattedID', dataIndex: 'FormattedID', width: '60px'
},
{
text: 'Name', dataIndex: 'Name', width: '400px'
},
{
text: 'Iteration', dataIndex: 'Iteration', width: '500px'
},
{
text: 'Predecessors', dataIndex: 'Predecessors', width: '200px'
},
{
text: 'Predecessor Creation Date(s)', dataIndex:
'PredecessorCreationDate', width: '200px'
}
]
});
}
});
Rally.launchApp('CustomApp', {
name: 'UserStoryWithPredecessors'
});
});
</script>
<style type="text/css">
.app {
/* Add app styles here */
}
</style>
</head>
<body></body>
</html>
Iteration attribute on a user story object is a reference to the actual Iteration object in the WS API. You need to traverse to name using a syntax like this:
Iteration: (record.get('Iteration') && record.get('Iteration')._refObjectName) || 'None'
You want to check that Iteration is not null before you output record.get('Iteration')._refObjectName)

ExtJS - Complex form serialization

I have a complex form which I can't use form serialization technique. There are many fields as well as dynamic grid ( the grid dynamically generating upon user choosing some criteria ) inside the form.
What I want to do, collect user inputs/selections + adding selected records that available in the grid then finally making a JSON array with those datas to be able to post server side.
My guess, I can use getCmp function of the ExtJS to take whole datas but as you might guess this way little bit hard to maintain. Also, I have no idea to get grid data with this way!
PS : Code is little bit long so that I cropped some parts.
MODEL AND STORE DEFINITIONS
Ext.Loader.setConfig({enabled: true});
Ext.Loader.setPath('Ext.ux', '<?php echo js_url(); ?>resources/ux');
Ext.require([
'Ext.grid.*',
'Ext.data.*',
'Ext.form.*',
'Ext.state.*',
'Ext.util.*',
'Ext.layout.container.Column',
'Ext.selection.CheckboxModel',
'Ext.ux.RowExpander',
'Ext.ux.statusbar.StatusBar'
]);
var navigate = function (panel, direction) {
var layout = panel.getLayout();
layout[direction]();
Ext.getCmp('move-prev').setDisabled(!layout.getPrev());
Ext.getCmp('move-next').setDisabled(!layout.getNext());
};
// Article Model
Ext.define('Article', {
extend: 'Ext.data.Model',
fields: [
{name: 'ARTICLE_ID', type: 'int'},
{name: 'DESCRIPTION', type: 'string'}
]
});
// Article Json Store
var articles = new Ext.data.JsonStore({
model: 'Article',
proxy: {
type: 'ajax',
url: '<?php echo base_url() ?>create/get_articles',
reader: {
type: 'json',
root: 'artList',
idProperty: 'ARTICLE_ID'
}
}
});
winArticle = new Ext.Window({
width: 600,
modal: true,
title: 'Artikel Seçimi',
closeAction: 'hide',
bodyPadding: 10,
items: new Ext.Panel({
items: [
{
xtype: 'fieldset',
title: 'Artikel Sorgulama',
defaultType: 'textfield',
layout: 'anchor',
defaults: {
anchor: '100%'
},
height: '76px',
items: [
{
xtype: 'fieldcontainer',
layout: 'hbox',
defaultType: 'textfield',
items: [
{
xtype: 'combobox',
id: 'articleNo',
inputWidth: 320,
fieldLabel: 'ARTİKEL NO',
fieldStyle: 'height: 26px',
margin: '10 15 0 0',
triggerAction: 'query',
pageSize: true
},
{
xtype: 'button',
text: 'SORGULA',
width: 100,
scale: 'medium',
margin: '8 0 0 0'
}
]
}
]
},
{
xtype: 'fieldset',
title: 'Artikel Bilgileri',
height: '140px',
layout: 'fit',
items: [
{
xtype: 'fieldcontainer',
layout: 'hbox',
defaultType: 'textfield',
fieldDefaults: {
labelAlign: 'top'
},
items: [
{
fieldLabel: 'ARTİKEL TANIMI',
name: 'artDesc',
flex: 3,
margins: '0 5 0 0'
},
{
fieldLabel: 'PAKET İÇERİĞİ',
name: 'artgebi',
flex: 1
}
]
},
{
xtype: 'fieldcontainer',
layout: 'hbox',
defaultType: 'textfield',
id: 'artContainer1',
fieldDefaults: {
labelAlign: 'top'
},
items: [
{
fieldLabel: 'SUBSYS',
name: 'artSubsys',
flex: 1,
margins: '0 5 0 0'
},
{
fieldLabel: 'VARIANT',
name: 'artVariant',
flex: 1,
margins: '0 5 0 0'
},
{
fieldLabel: 'VARIANT TANIMI',
name: 'artVariantDesc',
flex: 2
}
]
}
]
},
{
xtype: 'fieldset',
title: 'Aksiyon Seviyeleri',
id: 'article-fieldset',
items: [
{
xtype: 'button',
id: 'btnArticleLevelAdd',
text: 'LEVEL EKLE',
scale: 'medium',
width: 100,
style: 'float: right',
margin: '0 7 0 0',
handler: function() {
var getLevels = function() {
var count = winArticle.down('fieldset[id=article-fieldset]').items.items.length;
return count;
}
var count = getLevels();
if (count === 3) {
Ext.getCmp('btnArticleLevelAdd').disable();
}
var container = 'artContainer' + count;
//console.log(container);
Ext.getCmp('article-fieldset').add([
{
xtype: 'fieldcontainer',
layout: 'hbox',
id: 'artContainer' + count,
defaultType: 'textfield',
fieldDefaults: {
labelAlign: 'top'
},
items: [
{
name: 'artLevel' + count,
allowBlank: false,
inputWidth: 216,
fieldStyle: 'text-align: right; font-size: 13pt; background-color: #EAFFCC;',
margins: '0 5 5 0'
},
{
name: 'artValue' + count,
allowBlank: false,
inputWidth: 216,
fieldStyle: 'text-align: right; font-size: 13pt; background-color: #EAFFCC;',
margins: '0 5 0 0'
},
{
xtype: 'button',
text: 'SİL',
width: 40,
cls: 'btn-article-remove',
handler: function() {
if(count <= 3) {
Ext.getCmp('btnArticleLevelAdd').enable();
} else {
Ext.getCmp('btnArticleLevelAdd').disable();
}
winArticle.down('fieldset[id=article-fieldset]').remove(container);
}
}
]
}
]);
}
},
{
xtype: 'fieldcontainer',
layout: 'hbox',
id: 'article-level-container',
defaultType: 'textfield',
fieldDefaults: {
labelAlign: 'top'
},
items: [
{
fieldLabel: 'LEVEL',
name: 'artLevel',
inputWidth: 216,
margins: '0 5 5 0',
allowBlank: false,
fieldStyle: 'text-align: right; font-size: 13pt; background-color: #EAFFCC;'
},
{
fieldLabel: 'VALUE',
name: 'artValue',
inputWidth: 216,
allowBlank: false,
blankText: 'zorunlu alan, boş bırakılamaz',
fieldStyle: 'text-align: right; font-size: 13pt; background-color: #EAFFCC;',
listeners: {
change: function(textfield, newValue, oldValue) {
if(oldValue == 'undefined' || newValue == '') {
Ext.getCmp('btnArticleSave').disable();
} else {
Ext.getCmp('btnArticleSave').enable();
}
}
}
}
]
}
]
}
]
}),
buttons: [
{
text: 'KAPAT',
scale: 'medium',
width: 100,
cls: 'btn-article-close',
listeners: {
click: function() {
winArticle.close();
}
}
},
'->',
{
text: 'EKLE',
scale: 'medium',
disabled: true,
width: 100,
margin: '0 9 0 0',
cls: 'btn-article-save',
id: 'btnArticleSave'
}
]
});
EXT.ONREADY FUNCTION
Ext.onReady(function () {
Ext.QuickTips.init();
Ext.state.Manager.setProvider(new Ext.state.CookieProvider({
expires: new Date(new Date().getTime() + (1000 * 60 * 60 * 24 * 7))
}));
var Discounts = Ext.create('Ext.form.Panel', {
id: 'discount-types',
bodyPadding: 10,
width: 760,
height: 600,
title: 'DNR TANIMLAMA / SCREEN 0',
layout: 'card',
bodyStyle: 'padding:20px',
defaults: {
border: false,
anchor: '100%'
},
style: {
'box-shadow': '0 2px 5px rgba(0, 0, 0, 0.6)',
'-webkit-box-shadow': '0 0 8px rgba(0, 0, 0, 0.5)'
},
frame: true,
buttons: [
{
text: 'ÖNCEKİ ADIM',
id: 'move-prev',
cls: 'np-button',
scale: 'medium',
iconCls: 'dnr-prev-icon',
iconAlign: 'left',
handler: function (btn) {
navigate(btn.up('panel'), 'prev');
var itemd = Discounts.getLayout().getActiveItem();
Discounts.setTitle('DNR TANIMLAMA ' + ' / ' + itemd.cardTitle);
Ext.getCmp('dnr-submit').disable();
Ext.getCmp('dnr-submit').setVisible(false);
},
disabled: true
},
{
text: 'SONRAKİ ADIM',
id: 'move-next',
scale: 'medium',
cls: 'np-button',
iconCls: 'dnr-next-icon',
iconAlign: 'right',
handler: function (btn) {
navigate(btn.up('panel'), 'next');
var itemd = Discounts.getLayout().getActiveItem();
Discounts.setTitle('DNR TANIMLAMA ' + ' / ' + itemd.cardTitle);
var cardNum = Discounts.items.indexOf(itemd);
if (cardNum == 3) {
Ext.getCmp('dnr-submit').enable();
Ext.getCmp('dnr-submit').setVisible(true);
}
},
disabled: true
},
'->',
{
text: '&nbsp KAYDET ',
id: 'dnr-submit',
scale: 'medium',
iconCls: 'dnr-submit-icon',
iconAlign: 'right',
cls: 'dnr-submit',
disabled: true,
hidden: true,
handler: function (btn) {
}
}
],
items: [
{
id: 'screen-0',
cardTitle: 'SCREEN 0',
layout: 'form',
items: [
{
layout: {
type: 'vbox',
align: 'center'
},
margin: '60 0 0 0',
items: [
{
xtype: 'combobox',
inputWidth: 295,
fieldLabel: 'DNR TİPİ',
fieldStyle: 'height: 26px',
id: 'discount-type',
store: discounts,
valueField: 'DNR_TYPE_ID',
displayField: 'DNR_TYPE_DESC',
queryMode: 'remote',
forceSelection: true,
stateful: true,
stateId: 'cmb_disc_type',
allowBlank: false,
emptyText: 'DNR tipini seçiniz...',
triggerAction: 'all',
listeners: {
select: function (e) {
var discType = Ext.getCmp('discount-type').getValue();
var discDetail = Ext.getCmp('discount-detail');
discdetails.removeAll();
if (discType != 0) {
discDetail.setDisabled(false);
discdetails.proxy.extraParams = { 'dnrtype': discType };
discdetails.load();
}
}
}
},
{
xtype: 'combobox',
inputWidth: 400,
fieldStyle: 'height: 26px',
id: 'discount-detail',
valueField: 'ID',
displayField: 'DNR_TITLE',
store: discdetails,
forceSelection: true,
stateful: true,
stateId: 'cmb_disc_detail',
margin: '25 0 0 0',
disabled: true,
allowBlank: false,
msgTarget: 'side',
emptyText: 'İNDİRİM TİPİNİ SEÇİNİZ...',
blankText: 'İndirim tipi boş olamaz!',
triggerAction: 'all',
listeners: {
select: function (e) {
var discDetail = Ext.getCmp('discount-detail').getValue();
if (discDetail != 'null') {
var value = discdetails.getAt(discdetails.find('ID', discDetail)).get('DNR_DESCRIPTION');
Ext.getCmp('dnr-type-desc-panel').setVisible(true);
Ext.getCmp('dnr-type-desc-panel').update(value);
}
}
}
},
{
xtype: 'textarea',
grow: false,
name: 'invoiceText',
fieldLabel: 'FATURA METNİ',
id: 'invoice-text',
blankText: 'Fatura metni boş olamaz!',
width: 400,
height: 60,
margin: '30 0 0 0',
allowBlank: false,
msgTarget: 'side',
listeners: {
change: function (e) {
if (Ext.getCmp('invoice-text').getValue().length === 0) {
Ext.getCmp('move-next').disable();
} else {
Ext.getCmp('move-next').enable();
}
}
}
},
{
xtype: 'panel',
id: 'dnr-type-desc-panel',
layout: {type: 'hbox', align: 'stretch'},
height: 145,
width: 400,
cls: 'dnr-desc-panel',
margin: '60 0 0 0',
html: '&nbsp',
hidden: true
}
]
}
]
},
{
id: 'screen-1',
cardTitle: 'SCREEN 1',
layout: 'form',
items: [
{
layout: 'column',
width: 730,
height: 90,
items: [
{
xtype: 'fieldset',
title: 'ARTİKEL / HEDEF GRUP / MAL GRUBU SEÇİMİ',
cls: 'dnr-fieldset',
width: 730,
height: 80,
margin: '0',
items: [
{
xtype: 'buttongroup',
columns: 5,
columnWidth: 140,
frame: false,
margin: '5 0 0 18',
items: [
{
text: 'ARTİKEL',
scale: 'medium',
margin: '0 18px 0 0',
width: 120,
height: 36,
id: 'btn-article',
cls: 'btn-grp-choose btn-grp-article',
listeners: {
click: function () {
winArticle.center();
winArticle.show();
}
}
},
{
text: 'PUAR',
scale: 'medium',
margin: '0 18px 0 0',
width: 120,
height: 36,
cls: 'btn-grp-choose btn-grp-puar',
listeners: {
click: function() {
winPuar.show();
}
}
},
{
text: 'MAL GRUBU',
scale: 'medium',
margin: '0 18px 0 0',
width: 120,
height: 36,
cls: 'btn-grp-choose btn-grp-choose',
listeners: {
click: function() {
winArticleGroup.show();
}
}
},
{
text: 'HEDEF GRUP',
scale: 'medium',
margin: '0 18px 0 0',
width: 120,
height: 36,
cls: 'btn-grp-choose btn-grp-target',
listeners: {
click: function() {
winTargetGroup.show();
}
}
},
{
text: 'SUPPLIER',
scale: 'medium',
width: 120,
height: 36,
cls: 'btn-grp-choose btn-grp-supplier',
listeners: {
click: function() {
winSupplier.show();
}
}
}
]
}
]
}
]
},
{
xtype: 'gridpanel',
id: 'article-grid',
selType: 'rowmodel',
elStatus: true,
plugins: [
{ ptype: 'cellediting', clicksToEdit: 1},
{ ptype: 'datadrop'}
],
/* ***************************************************************
* here is the tricky part! when user change any fields above
* this grid will dynamically generate upon user request. So that
* we arent sure which columns will be available.
* ***************************************************************/
columns: [
{
text: 'COLUMN A',
dataIndex: ''
}
]
}
]
},
renderTo: 'content'
})
});
Updated answer
After some clarification I think the answer should be quite easy (at least I think so) For the following answer I assume that you are inside the form at the time you want to fetch the form & grid data and that there is only one Ext.form.Panel:
// Navigate up to the form:
var form = this.up('form'),
// get the form values
data = form.getValues(),
// get the selected record from the grid
gridRecords = form.down('grid').getSelectionModel().getSelected(),
// some helper variables
len = gridRecords.length,
recordData = [];
// normalize the model data by copying just the data objects into the array
for(i=0;i<len;i++) {
recordData .push(gridRecords[i].data);
}
// apply the selected grid records to the formdata. For that you will need a property name, I will use just 'gridRecords' but you may change it
data.gridRecords = recordData;
// send all back via a ajax request
Ext.Ajax.request({
url: 'demo/sample',
success: function(response, opts) {
// your handler
},
failure: function(response, opts) {
// your handler
},
jsonData: data
});
That should it be
To provide some more options of data that can be fetched from/by the grid
// get all data that is currently in the store
form.down('grid').getStore().data.items
// get all new and updated records
form.down('grid').getStore().getModifiedRecords()
// get all new records
form.down('grid').getStore().getNewRecords()
// get all updated records
form.down('grid').getStore().getUpdatedRecords()
Old answer (for more complex scenarios) below
What you told:
You have a grid with forms and maybe grids. Where you need to also
readout the grids when fetching the data from the form.
In the answer below I will just cover getValues, binding/unbinding events to each grid and not
form load/submit
record load/update
setting values
My recommendation is to make your form much more intelligent so that it is capable of handling this.
What do I mean by?
A default form cares about all fields that are inserted anywhere in it's body. In 99,9% this is perfectly fine but not for all. Your form also need to take care about grids that get inserted.
How it can be done
First thing is that when you make your Grids parts of a form I recommend to give them a name property. Second is that you need to know how a form gather and utilizes the fields so that you be able to copy that for grids. For that you need to take a look at the Ext.form.Basic class constructor where the important part is this
// We use the monitor here as opposed to event bubbling. The problem with bubbling is it doesn't
// let us react to items being added/remove at different places in the hierarchy which may have an
// impact on the dirty/valid state.
me.monitor = new Ext.container.Monitor({
selector: '[isFormField]',
scope: me,
addHandler: me.onFieldAdd,
removeHandler: me.onFieldRemove
});
me.monitor.bind(owner);
What happens here is that a monitor get initialized that from this on will look for any field that get inserted into the bound componenent where the monitor will call the appropriate handler. Currently the monitor is looking for fields but you will need one that is looking for grids. Such a monitor would look like:
me.gridMonitor = new Ext.container.Monitor({
selector: 'grid',
scope: me,
addHandler: me.onGridAdd,
removeHandler: me.onGridRemove
});
me.gridMonitor.bind(owner);
Because I don't know much about your datastructure I cannot tell you which gridevents you might need but you should register/unregister them in the addHandler/removeHandler like
onGridAdd: function(grid) {
var me = this;
me.mon(grid,'select',me.yourHandler,me);
},
onGridRemove: function(grid) {
var me = this;
me.mun(grid,'select',me.yourHandler,me);
}
In addition you will need the following helper methods
/**
* Return all the {#link Ext.grid.Panel} components in the owner container.
* #return {Ext.util.MixedCollection} Collection of the Grid objects
*/
getGrids: function() {
return this.gridMonitor.getItems();
},
/**
* Find a specific {#link Ext.grid.Panel} in this form by id or name.
* #param {String} id The value to search for (specify either a {#link Ext.Component#id id} or
* {#link Ext.grid.Panel name }).
* #return {Ext.grid.Panel} The first matching grid, or `null` if none was found.
*/
findGrid: function(id) {
return this.getGrids().findBy(function(f) {
return f.id === id || f.name === id;
});
},
And most important the method that get's the data from the grids. Here we need to override
getValues: function(asString, dirtyOnly, includeEmptyText, useDataValues) {
var values = {},
fields = this.getFields().items,
grids = this.getGrids().items, // the grids found by the monitor
f,
fLen = fields.length,
gLen = grids.length, // gridcount
isArray = Ext.isArray,
grid, gridData, gridStore, // some vars used while reading the grid content
field, data, val, bucket, name;
for (f = 0; f < fLen; f++) {
field = fields[f];
if (!dirtyOnly || field.isDirty()) {
data = field[useDataValues ? 'getModelData' : 'getSubmitData'](includeEmptyText);
if (Ext.isObject(data)) {
for (name in data) {
if (data.hasOwnProperty(name)) {
val = data[name];
if (includeEmptyText && val === '') {
val = field.emptyText || '';
}
if (values.hasOwnProperty(name)) {
bucket = values[name];
if (!isArray(bucket)) {
bucket = values[name] = [bucket];
}
if (isArray(val)) {
values[name] = bucket.concat(val);
} else {
bucket.push(val);
}
} else {
values[name] = val;
}
}
}
}
}
}
// begin new part
for (g = 0; g < gLen; g++) {
grid = grids[f];
gridStore = grid.getStore();
gridData = [];
// You will need a identification variable to determine which data should be taken from the grid. Currently this demo implement three options
// 0 only selected
// 1 complete data within the store
// 2 only modified records (this can be splitted to new and updated)
var ditems = grid.submitData === 0 ? grid.getSelectionModel().getSelection() :
grid.submitData === 1 ? gridStore.getStore().data.items : gridStore.getStore().getModifiedRecords(),
dlen = ditems.length;
for(d = 0; d < dLen; d++) {
// push the model data to the current data list. It doesn't matter of which type the models (records) are, this will simply read the whole known data. Alternatively you may access the rawdata property if the reader does not know all fields.
gridData.push(ditems[d].data);
}
// assign the array of record data to the grid-name property
data[grid.name] = gridData;
}
// end new part
if (asString) {
values = Ext.Object.toQueryString(values);
}
return values;
}
Clued together should it look something like
Ext.define('Ext.ux.form.Basic', {
extend: 'Ext.form.Basic',
/**
* Creates new form.
* #param {Ext.container.Container} owner The component that is the container for the form, usually a {#link Ext.form.Panel}
* #param {Object} config Configuration options. These are normally specified in the config to the
* {#link Ext.form.Panel} constructor, which passes them along to the BasicForm automatically.
*/
constructor: function(owner, config) {
var me = this;
me.callParent(arguments);
// We use the monitor here as opposed to event bubbling. The problem with bubbling is it doesn't
// let us react to items being added/remove at different places in the hierarchy which may have an
// impact on the dirty/valid state.
me.gridMonitor = new Ext.container.Monitor({
selector: 'grid',
scope: me,
addHandler: me.onGridAdd,
removeHandler: me.onGridRemove
});
me.gridMonitor.bind(owner);
},
onGridAdd: function(grid) {
var me = this;
me.mon(grid,'select',me.yourHandler,me);
},
onGridRemove: function(grid) {
var me = this;
me.mun(grid,'select',me.yourHandler,me);
},
/**
* Return all the {#link Ext.grid.Panel} components in the owner container.
* #return {Ext.util.MixedCollection} Collection of the Grid objects
*/
getGrids: function() {
return this.gridMonitor.getItems();
},
/**
* Find a specific {#link Ext.grid.Panel} in this form by id or name.
* #param {String} id The value to search for (specify either a {#link Ext.Component#id id} or
* {#link Ext.grid.Panel name }).
* #return {Ext.grid.Panel} The first matching grid, or `null` if none was found.
*/
findGrid: function(id) {
return this.getGrids().findBy(function(f) {
return f.id === id || f.name === id;
});
},
getValues: function(asString, dirtyOnly, includeEmptyText, useDataValues) {
var values = {},
fields = this.getFields().items,
grids = this.getGrids().items, // the grids found by the monitor
f,
fLen = fields.length,
gLen = grids.length, // gridcount
isArray = Ext.isArray,
grid, gridData, gridStore, // some vars used while reading the grid content
field, data, val, bucket, name;
for (f = 0; f < fLen; f++) {
field = fields[f];
if (!dirtyOnly || field.isDirty()) {
data = field[useDataValues ? 'getModelData' : 'getSubmitData'](includeEmptyText);
if (Ext.isObject(data)) {
for (name in data) {
if (data.hasOwnProperty(name)) {
val = data[name];
if (includeEmptyText && val === '') {
val = field.emptyText || '';
}
if (values.hasOwnProperty(name)) {
bucket = values[name];
if (!isArray(bucket)) {
bucket = values[name] = [bucket];
}
if (isArray(val)) {
values[name] = bucket.concat(val);
} else {
bucket.push(val);
}
} else {
values[name] = val;
}
}
}
}
}
}
// begin new part
for (g = 0; g < gLen; g++) {
grid = grids[f];
gridStore = grid.getStore();
gridData = [];
// You will need a identification variable to determine which data should be taken from the grid. Currently this demo implement three options
// 0 only selected
// 1 complete data within the store
// 2 only modified records (this can be splitted to new and updated)
var ditems = grid.submitData === 0 ? grid.getSelectionModel().getSelection() :
grid.submitData === 1 ? gridStore.getStore().data.items : gridStore.getStore().getModifiedRecords(),
dlen = ditems.length;
for(d = 0; d < dLen; d++) {
// push the model data to the current data list. It doesn't matter of which type the models (records) are, this will simply read the whole known data. Alternatively you may access the rawdata property if the reader does not know all fields.
gridData.push(ditems[d].data);
}
// add the store data as array to the grid-name property
data[grid.name] = gridData;
}
// end new part
if (asString) {
values = Ext.Object.toQueryString(values);
}
return values;
}
});
Next is to modify the form to use this basic form type
Ext.define('Ext.ux.form.Panel', {
extend:'Ext.form.Panel',
requires: ['Ext.ux.form.Basic'],
/**
* #private
*/
createForm: function() {
var cfg = {},
props = this.basicFormConfigs,
len = props.length,
i = 0,
prop;
for (; i < len; ++i) {
prop = props[i];
cfg[prop] = this[prop];
}
return new Ext.ux.form.Basic(this, cfg);
}
});
Note:
This is all untested! I've done something similar for various
customers to extend the capabilities of forms and I can tell tell that
this way will work very well and fast. At least it should show how it can be done and it can easily be tweaked to also set forms and/or load/update records.
I have not used this myself, but there is a thread that tries to deal with associated models via hasMany relationship. The problem is that everyone has slightly different expectations of what should happen during write operation of records. Serever side ORMs deal with this issue in somewhat difficult to understand manner and is often a sore spot for new developers.
Here is the forum thread that details a custom JSON writer to persist a parent record with it's children records.
Here is the code that seems to work at least for some folks:
Ext.data.writer.Json.override({
/*
* This function overrides the default implementation of json writer. Any hasMany relationships will be submitted
* as nested objects. When preparing the data, only children which have been newly created, modified or marked for
* deletion will be added. To do this, a depth first bottom -> up recursive technique was used.
*/
getRecordData: function(record) {
//Setup variables
var me = this, i, association, childStore, data = record.data;
//Iterate over all the hasMany associations
for (i = 0; i < record.associations.length; i++) {
association = record.associations.get(i);
data[association.name] = null;
childStore = record[association.storeName];
//Iterate over all the children in the current association
childStore.each(function(childRecord) {
if (!data[association.name]){
data[association.name] = [];
}
//Recursively get the record data for children (depth first)
var childData = this.getRecordData.call(this, childRecord);
/*
* If the child was marked dirty or phantom it must be added. If there was data returned that was neither
* dirty or phantom, this means that the depth first recursion has detected that it has a child which is
* either dirty or phantom. For this child to be put into the prepared data, it's parents must be in place whether
* they were modified or not.
*/
if (childRecord.dirty | childRecord.phantom | (childData != null)){
data[association.name].push(childData);
record.setDirty();
}
}, me);
/*
* Iterate over all the removed records and add them to the preparedData. Set a flag on them to show that
* they are to be deleted
*/
Ext.each(childStore.removed, function(removedChildRecord) {
//Set a flag here to identify removed records
removedChildRecord.set('forDeletion', true);
var removedChildData = this.getRecordData.call(this, removedChildRecord);
data[association.name].push(removedChildData);
record.setDirty();
}, me);
}
//Only return data if it was dirty, new or marked for deletion.
if (record.dirty | record.phantom | record.get('forDeletion')){
return data;
}
}
});
The full thread is located here:
http://www.sencha.com/forum/showthread.php?141957-Saving-objects-that-are-linked-hasMany-relation-with-a-single-Store/page5

Categories

Resources