Permalink
Browse files

simplify further by utilizing Javascript Remoting in this widget

  • Loading branch information...
jefftrull committed Dec 21, 2011
1 parent 7773378 commit 10f790ae6eec89dd8cc11036eedac52ede09beef
Showing with 155 additions and 186 deletions.
  1. +94 −78 classes/HierarchyController.cls
  2. +61 −108 components/Hierarchy_Editor.component
@@ -1,5 +1,5 @@
/*
-Copyright 2011 Jeff Trull <jetrull@sbcglobal.net>
+Copyright 2011 Jeff Trull <jetrull@sbcpublic.net>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -14,83 +14,99 @@ Copyright 2011 Jeff Trull <jetrull@sbcglobal.net>
limitations under the License.
*/
public with sharing class HierarchyController {
- // Controller for displaying and modifying record hierarchy of any object using ExtJS TreePanel view
- // by Jeff Trull <jetrull@sbcglobal.net> 2010-12-03
+ // Controller for displaying and modifying record hierarchy of any object using ExtJS TreePanel view
+ // by Jeff Trull <jetrull@sbcglobal.net> 2010-12-03
- // functionality for populating tree nodes
- public String fetchParentId {get; set;} // String here because it may be the literal "root"
- public String fetchObject {get; set;}
- public class FetchData {
- public ID id {get; set;}
- public String name {get; set;}
- public Boolean hasChildren {get; set;}
- }
- public List<FetchData> fetchResults {get; set;}
- public void findChildObjects() {
- // because we have to determine if each child object has children, we must do two separate queries:
- // one to get the children of the supplied object ID, the other to count the children of the children
- if (fetchParentId == 'root') {
- fetchParentId = ''; // workaround because an Ext TreeNode cannot have an empty ID
- }
- // dynamic SOQL so we can change object name
- String querystr = 'select Id, Name from ' + fetchObject + ' where ParentId=\'' + fetchParentId + '\'';
- Map<ID, String> Id2Name = new Map<ID, String>(); // record data as we get it back
- String fetchedIds = '';
- for (sObject qobj : Database.query(querystr)) {
- Id2Name.put(qobj.id, (String)qobj.get('Name'));
- String qid = '\'' + qobj.id + '\'';
- if (fetchedIds == '') {
- fetchedIds = qid;
- } else {
- fetchedIds += (',' + qid);
+ // functionality for populating tree nodes
+ // Corresponds directly to values expected by ExtJS NodeInterface class
+ public class FetchData {
+ public ID id {get; set;}
+ public String text {get; set;}
+ public Boolean loaded {get; set;}
+ public Boolean expandable {get; set;}
+ public Boolean leaf {get; set;}
+ }
+ public class Request {
+ public String node {get; set;}
+ public String sobjname {get; set;}
+ }
+ @RemoteAction
+ public static List<FetchData> findChildObjects(Request req) {
+ String fetchObject = req.sobjname;
+ String fetchParentId = req.node;
+ // because we have to determine if each child object has children, we must do two separate queries:
+ // one to get the children of the supplied object ID, the other to count the children of the children
+ if (fetchParentId == 'root') {
+ fetchParentId = ''; // workaround because an Ext TreeNode cannot have an empty ID
+ }
+ // dynamic SOQL so we can change object name
+ String querystr = 'select Id, Name from ' + fetchObject + ' where ParentId=\'' + fetchParentId + '\'';
+ Map<ID, String> Id2Name = new Map<ID, String>(); // record data as we get it back
+ String fetchedIds = '';
+ for (sObject qobj : Database.query(querystr)) {
+ Id2Name.put(qobj.id, (String)qobj.get('Name'));
+ String qid = '\'' + qobj.id + '\'';
+ if (fetchedIds == '') {
+ fetchedIds = qid;
+ } else {
+ fetchedIds += (',' + qid);
+ }
+ }
+ // Do a SOQL query to find which of the records returned in step 1 have children
+ // and which are lvl5 (and thus mandatory leaf)
+ Set<ID> hasChildRecords = new Set<ID>();
+ Set<ID> isLvl5 = new Set<ID>();
+ if (fetchedIds != '') {
+ // look for records whose "parentId" field matches one of those ids
+ querystr = 'SELECT ParentId FROM ' + fetchObject + ' WHERE ParentId IN (' + fetchedIds + ') GROUP BY ParentId';
+ for (AggregateResult pc : Database.query(querystr)) {
+ // AggregateResult values are returned as Objects, requiring casting
+ hasChildRecords.add((ID)pc.get('ParentId'));
+ }
+ querystr = 'SELECT Id, Parent.Parent.Parent.Parent.Id FROM ' + fetchObject +
+ ' WHERE Id IN (' + fetchedIds + ') AND Parent.Parent.Parent.Parent.Id != null';
+ for (SObject obj : Database.query(querystr)) {
+ isLvl5.add((ID)obj.get('Id'));
}
- }
- // Do a SOQL query to find which of the records returned in step 1 have children
- Set<ID> hasChildRecords = new Set<ID>();
- if (fetchedIds != '') {
- // that's just the records whose "parentId" field matches one of those ids
- querystr = 'select ParentId from ' + fetchObject + ' where ParentId in (' + fetchedIds + ') group by ParentId';
- for (AggregateResult pc : Database.query(querystr)) {
- // AggregateResult values are returned as Objects, requiring casting
- System.debug('adding parentid ' + pc.get('ParentId'));
- hasChildRecords.add((ID)pc.get('ParentId'));
- }
- }
- // iterate over original set of campaigns, checking each for children and producing result list
- fetchResults = new List<FetchData>();
- for (ID childid : Id2Name.keySet()) {
- FetchData fd = new FetchData();
- fd.id = childid; fd.name = Id2Name.get(childid);
- if (hasChildRecords.contains(childid)) {
- fd.hasChildren = true;
- } else {
- fd.hasChildren = false;
- }
- fetchResults.add(fd);
- }
- }
- // Drop functionality
- public String parentIdToSet {get; set;}
- public ID childIdToSet {get; set;}
- public Boolean idSetSuccess {get; set;}
- public void setParent() {
- // get record corresponding to ChildId, set its parent, and update
- String querystr = 'select Id, ParentId from ' + fetchObject + ' where Id=\'' + childIdToSet + '\'';
- List<SObject> results = Database.query(querystr);
- idSetSuccess = false;
- if (parentIdToSet == '') {
- parentIdToSet = null;
- }
- if (results.size() == 1) {
- results[0].put('ParentId', parentIdToSet);
- try {
- update results[0];
- } catch (Exception e) {
- System.debug('setParent got exception');
- return;
- }
- System.debug('setParent returning true');
- idSetSuccess = true;
- }
- }
+ }
+
+ // iterate over original set of campaigns, checking each for children and producing result list
+ List<FetchData> fetchResults = new List<FetchData>();
+ for (ID childid : Id2Name.keySet()) {
+ FetchData fd = new FetchData();
+ fd.id = childid; fd.text = Id2Name.get(childid);
+ if (hasChildRecords.contains(childid)) {
+ fd.expandable = true;
+ fd.loaded = false;
+ } else {
+ fd.expandable = false;
+ fd.loaded = true;
+ }
+ fd.leaf = isLvl5.contains(childid);
+ fetchResults.add(fd);
+ }
+ return fetchResults;
+ }
+ // Drop functionality
+ @RemoteAction
+ public static Boolean setParent(String fetchObject, String parentIdToSet, ID childIdToSet) {
+ // get record corresponding to ChildId, set its parent, and update
+ String querystr = 'SELECT Id, ParentId FROM ' + fetchObject + ' WHERE Id=\'' + childIdToSet + '\'';
+ List<SObject> results = Database.query(querystr);
+ if (results.size() == 1) {
+ if (parentIdToSet == '')
+ parentIdToSet = null;
+ results[0].put('ParentId', parentIdToSet);
+ try {
+ update results[0];
+ } catch (Exception e) {
+ System.debug('setParent got exception');
+ return false;
+ }
+ System.debug('setParent returning true');
+ return true;
+ } else {
+ return false;
+ }
+ }
}
@@ -14,144 +14,97 @@ Copyright 2011 Jeff Trull <jetrull@sbcglobal.net>
limitations under the License.
-->
<apex:component controller="HierarchyController" allowDML="true">
- <!-- a VF component for modifying the hierarchy of an SObject (Campaign, Account, etc.) using an ExtJS TreePanel -->
- <!-- Jeff Trull jetrull@sbcglobal.net 2010-12-01 -->
- <apex:attribute name="object" type="Object" description="sObject to edit hierarchy on" default="Campaign"/>
- <apex:attribute name="fn" type="String" description="name of a Javascript function to call with ID once a Campaign is selected." required="false" default=""/>
+ <!-- a VF component for modifying the hierarchy of an SObject (Campaign, Account, etc.) using an ExtJS TreePanel -->
+ <!-- Jeff Trull jetrull@sbcglobal.net 2010-12-01 -->
+ <apex:attribute name="object" type="Object" description="sObject to edit hierarchy on" default="Campaign"/>
+ <apex:attribute name="fn" type="String" description="name of a Javascript function to call with ID once a Campaign is selected." required="false" default=""/>
- <!-- load ExtJS -->
+ <!-- load ExtJS -->
<apex:stylesheet value="{!$Resource.ExtJS4}/ext-4.0.7-gpl/resources/css/ext-all.css" />
<apex:includeScript value="{!$Resource.ExtJS4}/ext-4.0.7-gpl/ext-all-debug.js"/>
<script type="text/javascript">
Ext.BLANK_IMAGE_URL="{!$Resource.ExtJS4}/ext-4.0.7-gpl/resources/themes/images/default/tree/s.gif"
</script>
- <!-- Create a JS function that calls the child object finding Apex method, -->
- <!-- then loads the results when the AJAX request returns -->
- <apex:actionFunction name="fetchObjects" action="{!findChildObjects}" rerender="unrollpanel"
- oncomplete="fetchCallback.call();">
- <apex:param name="sobjname" assignTo="{!fetchObject}" value=""/>
- <apex:param name="parentid" assignTo="{!fetchParentId}" value=""/>
- </apex:actionFunction>
-
- <!-- Define function returning child data. Function definition changes after each AJAX response -->
- <!-- to contain the result of our child record search -->
- <apex:outputPanel id="unrollpanel">
- <!-- if not in an outputPanel, the rerender fails! -->
- <!-- A function that just takes the results from a single tree query and returns it -->
- <!-- regenerated after every tree node expansion and then called from the actionFunction's oncomplete -->
- <script type="text/javascript">
- function nodeResults(forceLeaf) {
- var loadData = new Array();
- // unroll child nodes list with apex:repeat
- // node properties: if level 5, a leaf (cannot have children)
- // if no children, show as folder, but without "expand" button
- // if has children, folder with expand button
- <apex:repeat value="{!fetchResults}" var="obj">
- loadData.push({id: "{!obj.id}",
- text: "{!obj.name}",
- // in Ext 3.4.0, "loaded and expanded" gave you a leaf icon if there were no children
- // not so in 4.0, but at least we can unshow expansion arrows
- loaded: {!!obj.hasChildren},
- expandable: {!obj.hasChildren},
- leaf: forceLeaf
- });
- </apex:repeat>
- return loadData;
- }
- </script>
- </apex:outputPanel>
-
- <!-- Create a JS function to call the Apex method that reassigns a record parent -->
- <apex:actionFunction name="updateParent" action="{!setParent}" rerender="setstatuspanel">
- <apex:param name="parent" assignTo="{!parentIdToSet}" value=""/>
- <apex:param name="child" assignTo="{!childIdToSet}" value=""/>
- </apex:actionFunction>
-
- <!-- a function regenerated by the parent ID set result after server completes -->
- <apex:outputPanel id="setstatuspanel">
- <script type="text/javascript">
- function getSetStatus() { return {!idSetSuccess}; }
- </script>
- </apex:outputPanel>
+ <!-- Create a JS function to call the Apex method that reassigns a record parent -->
+ <apex:actionFunction name="updateParent" action="HierarchyController." rerender="setstatuspanel">
+ <apex:param name="parent" assignTo="{!parentIdToSet}" value=""/>
+ <apex:param name="child" assignTo="{!childIdToSet}" value=""/>
+ </apex:actionFunction>
+
<script type="text/javascript">
Ext.onReady(function() {
- // BUG WORKAROUND
- // DirectProxy gets perfectly fine formatted data from api calls, then discards it
- // This may simply be a difference between ExtJS 3 (used by Remoting) and 4...
- Ext.data.proxy.Direct.prototype.createRequestCallback =
- function(request, operation, callback, scope){
- var me = this;
- return function(data, event){
- // supply "data" (properly processed data), not "event", as fourth arg
- me.processResponse(event.status, operation, request,
- {data: data}, callback, scope);
- };
- };
-
- var store = Ext.create('Ext.data.TreeStore', {
+ // BUG WORKAROUND
+ // DirectProxy gets perfectly fine formatted data from api calls, then discards it
+ // This may simply be a difference between ExtJS 3 (used by Remoting) and 4...
+ Ext.data.proxy.Direct.prototype.createRequestCallback =
+ function(request, operation, callback, scope){
+ var me = this;
+ return function(data, event){
+ // supply "data" (properly processed data), not "event", as fourth arg
+ me.processResponse(event.status, operation, request,
+ {data: data}, callback, scope);
+ };
+ };
+
+ var store = Ext.create('Ext.data.TreeStore', {
root: {
text: 'All {!object}s',
expandable: true,
allowDrag: false,
id: 'root' // because you cannot use a blank id (Ext would create one)
},
- proxy: {
- type: 'direct',
- directFn: Ext.apply(function(nodeobj, callback) {
- // determine if we are expanding a level 4 record (current limit is 5 -> leaf)
- var lvl4 = (store.getNodeById(nodeobj.node).getDepth() >= 4);
- // establish callback for when AJAX request completes
- fetchCallback = Ext.bind(function () {
- callback.apply(window, [nodeResults(lvl4), {status: true}]);
- }, this);
- // call actionFunction created for loading
- fetchObjects('{!object}', nodeobj.node);
- }, { directCfg: {method: {len: 1}}}) // fake out Direct by supplying this property
+ proxy: {
+ type: 'direct',
+ directFn: HierarchyController.findChildObjects,
+ extraParams: {sobjname: '{!object}'}
}
});
-
+
- var tree = Ext.create('Ext.tree.Panel', {
+ var tree = Ext.create('Ext.tree.Panel', {
renderTo: '{!$Component.myTree}',
height: 460,
useArrows: true,
autoScroll: true,
animate: true,
containerScroll: true,
border: false,
- viewConfig: {
- plugins: {
- ptype: 'treeviewdragdrop'
- }
- },
- store: store,
+ viewConfig: {
+ plugins: {
+ ptype: 'treeviewdragdrop'
+ }
+ },
+ store: store,
listeners: {beforeitemmove: function(node, oldParent, newParent) {
- if (oldParent.getId() == newParent.getId()) {
- // just a change to ordering; no need for server request
- return true;
- }
- var parent = newParent.getId();
- if (parent == 'root') {
- parent = ''; // translate back into Apex world
- }
- // this is, unfortunately, a synchronous approach
- updateParent(parent, node.getId());
- // if level has changed to/from L5, adjust "leaf" property
- if ((oldParent.getDepth() == 4) || (newParent.getDepth() == 4)) {
- node.set('leaf', (newParent.getDepth() == 4));
- }
- return true;
- },
+ if (oldParent.getId() == newParent.getId()) {
+ // just a change to ordering; no need for server request
+ return true;
+ }
+ var parent = newParent.getId();
+ if (parent == 'root') {
+ parent = ''; // translate back into Apex world
+ }
+ HierarchyController.setParent('{!object}', parent, node.getId(),
+ function(response, event) {
+ if (event.status && response) {
+ // if level has changed to/from L5, adjust "leaf" property
+ if ((oldParent.getDepth() == 4) || (newParent.getDepth() == 4)) {
+ node.set('leaf', (newParent.getDepth() == 4));
+ }
+ }
+ });
+ return true;
+ },
beforeitemdblclick: function(view, record) {
- if (('{!fn}' != '') && (record.getId() != 'root')) {
- {!fn}(record.getId());
- }
- }}
+ if (('{!fn}' != '') && (record.getId() != 'root')) {
+ {!fn}(record.getId());
+ }
+ }}
});
-
+
tree.getRootNode().expand(); // trigger load of top level objects
});
</script>

0 comments on commit 10f790a

Please sign in to comment.