Use normal grid in site editor instead of dojox grid
diff --git a/bundles/org.eclipse.orion.client.core/web/orion/widgets/SiteEditor.js b/bundles/org.eclipse.orion.client.core/web/orion/widgets/SiteEditor.js
index 73b57f0..aff2ac4 100644
--- a/bundles/org.eclipse.orion.client.core/web/orion/widgets/SiteEditor.js
+++ b/bundles/org.eclipse.orion.client.core/web/orion/widgets/SiteEditor.js
@@ -7,397 +7,18 @@
  * 
  * Contributors: IBM Corporation - initial API and implementation
  ******************************************************************************/
-/*global dojo dijit dojox eclipse*/
+/*global define */
 /*jslint browser:true */
 
-define(['dojo', 'dijit', 'dojox', 'orion/util', 'orion/siteUtils','orion/commands',
-		'dijit/form/DropDownButton', 'dijit/form/ComboBox', 'dijit/form/Form', 'dijit/form/Select', 'dijit/form/Textarea', 'dijit/form/TextBox', 
-		'dijit/form/ValidationTextBox', 'dijit/Menu', 'dijit/layout/ContentPane', 'dijit/Tooltip', 'dijit/_Templated', 'dojo/data/ItemFileWriteStore', 
-		'dojo/DeferredList', 'dojox/grid/DataGrid', 'dojox/grid/cells', 'text!orion/widgets/templates/SiteEditor.html'], 
-        function(dojo, dijit, dojox, mUtil, mSiteUtils, mCommands) {
+define(['dojo', 'dijit', 'dojox', 'orion/util', 'orion/siteUtils', 'orion/commands', 'siteMappingsTable',
+		'dojo/DeferredList', 'dijit/layout/ContentPane', 'dijit/Tooltip', 'dijit/_Templated', 'dijit/form/Form',
+		'dijit/form/TextBox', 'dijit/form/ValidationTextBox',
+		'text!orion/widgets/templates/SiteEditor.html'],
+		function(dojo, dijit, dojox, mUtil, mSiteUtils, mCommands, mSiteMappingsTable) {
 
 /**
- * Visualizes the Mappings array of a SiteConfiguration as a data grid.
- */
-dojo.declare("orion.widgets.MappingsGrid", [dojox.grid.DataGrid], /** @lends orion.widgets.MappingsGrid */ {
-	
-	/**
-	 * @type {Array} The model object being edited by this grid.
-	 */
-	_mappings: null,
-	
-	_workspaceToChildren: null,
-	
-	constructor: function() {
-		this.inherited(arguments);
-	},
-	
-	setServices: function(commandService, fileClient) {
-		this._commandService = commandService;
-		this._fileClient = fileClient;
-		
-		// Register commands used for editing mappings
-		var deleteMappingCommand = new mCommands.Command({
-			name: "Delete",
-			image: "/images/delete.gif",
-			id: "eclipse.site.mappings.remove",
-			visibleWhen: function(item) {
-				// Only show on a Mappings object
-				return item.Source && item.Target;
-			},
-			callback: function(item) {
-				// "this" is {orion.widgets.MappingsGrid}
-				this._hideTooltip();
-				this.store.deleteItem(item);
-				this.store.save();
-				this.render();
-			}});
-		this._commandService.addCommand(deleteMappingCommand , "object");
-		this._commandService.registerCommandContribution("eclipse.site.mappings.remove", 0);
-		
-		var moveUpCommand = new mCommands.Command({
-			name: "Move Up",
-			image: "/images/move_up.gif",
-			id: "eclipse.site.mappings.moveUp",
-			visibleWhen: dojo.hitch(this, function(item) {
-				return item.Source && item.Target;
-			}),
-			callback: function(itemToMove) {
-				this._hideTooltip();
-				
-				var index = this.getItemIndex(itemToMove);
-				if (index === 0) { return; }
-				
-				// There must be a better way than this
-				var newOrder = this._deleteAll();
-				// swap index-1 with index
-				newOrder.splice(index-1, 2, newOrder[index], newOrder[index-1]);
-				for (var i=0; i < newOrder.length; i++) {
-					this.store.newItem(newOrder[i]);
-				}
-				
-				this.store.save();
-				this.render();
-			}});
-		this._commandService.addCommand(moveUpCommand, "object");
-		this._commandService.registerCommandContribution("eclipse.site.mappings.moveUp", 1);
-		
-		var moveDownCommand = new mCommands.Command({
-			name: "Move Down",
-			image: "/images/move_down.gif",
-			id: "eclipse.site.mappings.moveDown",
-			visibleWhen: dojo.hitch(this, function(item) {
-				return item.Source && item.Target;
-			}),
-			callback: function(itemToMove) {
-				this._hideTooltip();
-				
-				var index = this.getItemIndex(itemToMove);
-				if (index === this.get("rowCount")-1) { return; }
-				
-				var newOrder = this._deleteAll();
-				// swap index+1 with index
-				newOrder.splice(index, 2, newOrder[index+1], newOrder[index]);
-				for (var i=0; i < newOrder.length; i++) {
-					this.store.newItem(newOrder[i]);
-				}
-
-				this.store.save();
-				this.render();
-			}});
-		this._commandService.addCommand(moveDownCommand, "object");
-		this._commandService.registerCommandContribution("eclipse.site.mappings.moveDown", 2);
-	},
-	
-	/** @returns {Mappings[]} An array representing what used to be in the store. */
-	_deleteAll: function() {
-		var result = [],
-		    store = this.store,
-		    item;
-		while (this.get("rowCount") > 0) {
-			item = this.getItem(0);
-			result.push(this._createMappingObject(store.getValue(item, "Source"), store.getValue(item, "Target")));
-			store.deleteItem(item);
-		}
-		return result;
-	},
-	
-	/**
-	 * @param {Array} mappings The Mappings field of a site configuration
-	 */
-	setMappings: function(mappings) {
-		// Hang onto mappings object; will be mutated as user makes changes to grid store
-		this._mappings = mappings;
-		
-		// Wait until workspace children are loaded; we need them for making friendlyPaths field
-		dojo.when(this._editor._workspaceToChildren, dojo.hitch(this, 
-			function(map) {
-				this._workspaceToChildren = map;
-				this.setStore(this._createGridStore(mappings));
-			}));
-	},
-	
-	// Sets reference to the outer SiteEditor
-	setEditor: function(editor) {
-		this._editor = editor;
-	},
-	
-	/**
-	 * @param {Array} mappings
-	 * @returns {dojo.data.ItemFileWriteStore} A store which will power the grid.
-	 */
-	_createGridStore: function(mappings) {
-		var store = new dojo.data.ItemFileWriteStore({
-			data: {
-				items: dojo.map(mappings, dojo.hitch(this, function(mapping) {
-					return this._createMappingObject(mapping.Source, mapping.Target);
-				}))
-			}
-		});
-		dojo.connect(store, "setValue", store, function() {
-			// Save store whenever user edits an attribute
-			this.save();
-		});
-		store._saveEverything = dojo.hitch(this, function(saveCompleteCallback, saveFailedCallback, updatedContentString) {
-			// Save: push latest data from store back into the _mappings model object
-			var content = dojo.fromJson(updatedContentString);
-			while (this._mappings.length > 0) { this._mappings.pop(); }
-			dojo.forEach(content.items, dojo.hitch(this, function(item) {
-				var mapping = {};
-				mapping.Source = item.Source;
-				mapping.Target = item.Target;
-				this._mappings.push(mapping);
-			}));
-//			console.debug("Set Mappings to " + dojo.toJson(this._mappings));
-			saveCompleteCallback();
-		});
-		return store;
-	},
-	
-	_createMappingObject: function(source, target) {
-		return {Source: source, Target: target, _friendlyPath: this._toFriendlyPath(target)};
-	},
-	
-	_getRowNodeForItem: function(/** dojo.data.Item */ item) {
-		var rowIdx = this.getItemIndex(item);
-		return this.getRowNode(rowIdx);
-	},
-	
-	_hideTooltip: function() {
-		dijit.hideTooltip(dijit._masterTT && dijit._masterTT.aroundNode);
-	},
-	
-	_addMapping: function(source, target) {
-		source = typeof(source) === "string" ? source : "/mountPoint";
-		target = typeof(target) === "string" ? target : "/";
-		var store = this.get("store");
-		var newItem = this.store.newItem(this._createMappingObject(source, target));
-		var that = this;
-		store.save({
-			onComplete: function() {
-				// This is probably not the best way to wait until the table has updated
-				setTimeout(function() {
-					var rowNode = that._getRowNodeForItem(newItem);
-					if (rowNode) {
-						var cols = dojo.query("td", rowNode);
-						var mountAtCol = cols[that.MOUNT_AT_COL];
-						var thePath = that._toReadablePath(rowNode);
-						var message = "Click to set the remote path where <b>" + thePath + "</b> will be accessible.";
-						dijit.showTooltip(message, mountAtCol, "above");
-						dojo.connect("onclick", dojo.hitch(dijit._masterTT, dijit._masterTT.hide, dijit._masterTT.aroundNode));
-					}
-				}, 1);
-			}});
-	},
-	
-	_toReadablePath: function(/**DomNode*/ rowNode) {
-		var cols = dojo.query("td", rowNode);
-		return mUtil.getText(cols[this.PATH_COL]);
-	},
-	
-	postCreate: function() {
-		this.inherited(arguments);
-		// These constants must be kept up to date with structure
-		this.PATH_COL = 1;
-		this.MOUNT_AT_COL = 2;
-		var structure = [
-				{field: "Target", name: " ", editable: false,
-						width: "32px",
-						cellClasses: "isValidCell",
-						formatter: dojo.hitch(this, this._isValidFormatter)},
-				{field: "_friendlyPath", name: "Path", editable: true, commitOnBlur: true,
-						cellClasses: "pathCell",
-						width: "auto",
-						formatter: dojo.hitch(this, this._friendlyPathFormatter)},
-				{field: "Source", name: "Mount at", editable: true, commitOnBlur: true,
-						width: "30%",
-						cellClasses: "editablePathCell"},
-				{field: "_item", name: " ", editable: false, 
-						cellClasses: "actionCell",
-						width: "80px",
-						formatter: dojo.hitch(this, this._actionColumnFormatter)}
-			];
-		this.set("structure", structure);
-		
-		// Workaround for commitOnBlur not working right see dojox/grid/cells/_base.js
-		dojo.connect(this, "onStartEdit", this, function(inCell, inRowIndex) {
-			var grid = this;
-			var handle = dojo.connect(inCell, "registerOnBlur", inCell, function(inNode, inRowIndex) {
-				var handle2 = dojo.connect(inNode, "onblur", function(e) {
-					setTimeout(dojo.hitch(inCell, "_onEditBlur", inRowIndex), 250);
-					dojo.disconnect(handle2);
-					dojo.disconnect(handle);
-				});
-				var handle3 = dojo.connect(inNode, "onblur", inCell, function(inNode) {
-					// Propagate change to the _friendlyPath column into the Target column
-					if (inCell.index === grid.PATH_COL) {
-						var item = grid.getItem(inRowIndex);
-						grid._propagate(item);
-					}
-					dojo.disconnect(handle3);
-				});
-			});
-		});
-		dojo.connect(this, "onStartEdit", this, this._hideTooltip);
-		dojo.connect(this, "onResizeColumn", this, this._hideTooltip);
-		dojo.connect(this, "onStyleRow", this, this._renderCommands);
-	},
-	
-	_isWorkspacePath: function(/**String*/ path) {
-		return new RegExp("^/").test(path);
-	},
-	
-	/**
-	 * Formats the "is valid?" column. If the Target looks like a workspace path, we try a GET on it
-	 * to see if it exists.
-	 * @returns {dojo.Deferred}
-	 */
-	_isValidFormatter: function(/**String*/ target, i, inCell) {
-		if (inCell.lastTarget && inCell.lastTarget === target) {
-			// Re-use value from last time
-			return inCell.lastResult;
-		}
-		inCell.lastTarget = target;
-		
-		var result;
-		var href;
-		if (this._isWorkspacePath(target)) {
-			var location = mSiteUtils.makeFullFilePath(target);
-			var deferred = new dojo.Deferred();
-			href = mUtil.safeText(location);
-			// TODO: should use fileClient here, but we don't want its retrying or error dialog
-			//this._fileClient.fetchChildren(location)
-			dojo.xhrGet({
-				url: location,
-				headers: {
-					"Orion-Version": "1"
-				},
-				handleAs: "json"
-			}).then(
-				function(children) {
-					deferred.callback('<a href="' + href + '" target="_new"><img src="/images/folder.gif" title="Workspace folder ' + href + '"/></a>');
-				}, function(error) {
-					deferred.callback('<a href="' + href + '" target="_new"><img src="/images/error.gif" title="Workspace folder  not found: ' + href + '"/></a>');
-				});
-			result = deferred;
-		} else {
-			href = mUtil.safeText(target);
-			result = '<a href="' + href + '" target="_blank"><img src="/images/link.gif" title="External link to ' + href + '"/></a>';
-		}
-		inCell.lastResult = result;
-		return result;
-	},
-	
-	// Propagate value of _friendlyPath field to the Target field
-	_propagate: function(item) {
-		var friendlyPath = this.store.getValue(item, "_friendlyPath");
-		var newTarget;
-		var found = false;
-		if (this._isWorkspacePath(friendlyPath)) {
-			this._everyTopLevelFolder(dojo.hitch(this, 
-				function(child, childLoc, childName) {
-					if (this._pathMatch(friendlyPath, childName)) {
-						found = true;
-						// Change friendly path back to internal format
-						var rest = friendlyPath.substring(childName.length);
-						newTarget = childLoc + rest;
-						return false;
-					}
-				}));
-		}
-		if (!found) {
-			newTarget = friendlyPath;
-		}
-		this.store.setValue(item, "Target", newTarget);
-	},
-	
-	_toFriendlyPath: function(target) {
-		var result;
-		var found;
-		this._everyTopLevelFolder(dojo.hitch(this, 
-			function(child, childLoc, childName) {
-				if (this._pathMatch(target, childLoc)) {
-					found = true;
-					var newFriendlyPath = childName + target.substring(childLoc.length);
-					//console.debug("Resolving to " + newFriendlyPath);
-					result = newFriendlyPath;
-					return false;
-				}
-			}));
-		
-		if (!found) {
-			result = target;
-		}
-		return result;
-	},
-	
-	_everyTopLevelFolder: function(callback) {
-		var workspaceId = this._editor.getSiteConfiguration().Workspace;
-		var children = this._workspaceToChildren[workspaceId];
-		for (var i=0; i < children.length; i++) {
-			var child = children[i];
-			var childLoc = mSiteUtils.makeRelativeFilePath(child.Location);
-			var childName = "/" + child.Name;
-			if (callback(child, childLoc, childName) === false) {
-				break;
-			}
-		}
-	},
-	
-	/**
-	 * Returns the _friendlyPath field value. This is a virtual field -- kept in the store for display but not
-	 * saved to SiteConfig model. _friendlyPath's value is the same as Target except when Target is a workspace
-	 * path: in this case _friendlyPath refers to the project by its user-visible Name rather than the cryptic 
-	 * project UUID. Changes made by user to _friendlyPath are pushed into the Target field, which is persisted.
-	 * 
-	 * @returns {String | dojo.Deferred}
-	 */
-	_friendlyPathFormatter: function(/**String*/ friendlyPath, /*Number*/ rowIndex, /**Object*/ inCell) {
-		return mUtil.safeText(friendlyPath);
-	},
-	
-	/** @returns true if b is a sub-path of a */
-	_pathMatch: function(a, b) {
-		return a === b || a.indexOf(b + "/") === 0;
-	},
-	
-	_actionColumnFormatter: function(item) {
-		// Empty cell; command service will render in here later
-		return " ";
-	},
-	
-	// TODO: this is called often. Try to find event that fires only on row added/removed
-	_renderCommands: function(rowInfo) {
-		var item = this.getItem(rowInfo.index);
-		var actionCell = dojo.query("td.actionCell", rowInfo.node)[0];
-		if (actionCell && dojo.query("a", actionCell).length === 0) {
-			this._commandService.renderCommands(actionCell, "object", item, this, "image", "actionCellCommand");
-		}
-	}
-});
-
-/**
- * Editor for an individual SiteConfiguration model object.
+ * @name orion.widgets.SiteEditor
+ * @class Editor for an individual site configuration.
  * @param {Object} options Options bag for creating the widget.
  * @param {eclipse.FileClient} options.fileClient
  * @param {eclipse.SiteService} options.siteService
@@ -405,18 +26,17 @@
  * @param {String} [options.location] Optional URL of a site configuration to load in editor
  * upon creation.
  */
-dojo.declare("orion.widgets.SiteEditor", [dijit.layout.ContentPane/*dijit._Widget*/, dijit._Templated], {
+dojo.declare("orion.widgets.SiteEditor", [dijit.layout.ContentPane, dijit._Templated], {
 	widgetsInTemplate: true,
 	templateString: dojo.cache('orion', 'widgets/templates/SiteEditor.html'),
 	
 	/** dojo.Deferred */
 	_workspaces: null,
 	
-	/**
-	 * dojo.Deferred
-	 * Resolves with an Object that maps {String} workspaceId to {Array} children
-	 */
-	_workspaceToChildren: null,
+	/** dojo.Deferred */
+	_projects: null,
+	
+	_fetched: false,
 	
 	/** SiteConfiguration */
 	_siteConfiguration: null,
@@ -424,27 +44,29 @@
 	/** Array */
 	_modelListeners: null,
 	
+	/** MappingsTable */
+	mappings: null,
+	
+	_isDirty: false,
+	
 	constructor: function() {
 		this.inherited(arguments);
 		this.options = arguments[0] || {};
-		if (!this.options.fileClient) { throw new Error("options.fileClient is required"); }
-		if (!this.options.siteService) { throw new Error("options.siteService is required"); }
-		if (!this.options.commandService) { throw new Error("options.commandService is required"); }
-		if (!this.options.statusService) { throw new Error("options.statusService is required"); }
+		this.checkOptions(this.options, ["serviceRegistry", "fileClient", "siteService", "commandService", "statusService"]);
+
 		this._fileClient = this.options.fileClient;
 		this._siteService = this.options.siteService;
 		this._commandService = this.options.commandService;
 		this._statusService = this.options.statusService;
-		this._commandsContainer = this.options.commandsContainer;
 		
-		// Start loading workspaces right away
-		this._workspaces = new dojo.Deferred();
-		this._workspaceToChildren = new dojo.Deferred();
-		this._loadWorkspaces();
+		this._commandsContainer = this.options.commandsContainer;
 		
 		if (this.options.location) {
 			this.load(this.options.location);
 		}
+		
+		this._workspaces = this._fileClient.loadWorkspaces();
+		this._projects = new dojo.Deferred();
 	},
 	
 	postMixInProperties: function() {
@@ -452,7 +74,6 @@
 		this.siteConfigNameLabelText = "Name:";
 		this.mappingsLabelText = "Mappings:";
 		this.hostHintLabelText = "Hostname hint:";
-		this.addMappingButtonText = "Add&#8230;";
 		this.hostingStatusLabelText = "Status:";
 	},
 	
@@ -471,23 +92,18 @@
 			return focused || hostish.test(this.hostHint.get("value"));
 		}));
 		
-		// Mappings grid
-		this.mappings.setServices(this._commandService, this._fileClient);
-		this.mappings.setEditor(this);
-		
-		// dijit.form.Form doesn't work in dojoAttachPoint for some reason
 		dijit.byId("siteForm").onSubmit = dojo.hitch(this, this.onSubmit);
 		
-		dojo.when(this._workspaceToChildren, dojo.hitch(this, function(workspaceToChildrenMap) {
+		dojo.when(this._projects, dojo.hitch(this, function(projects) {
 			// Register command used for adding mapping
 			var addMappingCommand = new mCommands.Command({
-				name: "Add&#8230;",
+				name: "Add",
 				image: "/images/add.gif",
 				id: "eclipse.site.mappings.add",
 				visibleWhen: function(item) {
 					return true;
 				},
-				choiceCallback: dojo.hitch(this, this._makeAddMenuChoices, workspaceToChildrenMap)});
+				choiceCallback: dojo.hitch(this, this._makeAddMenuChoices, projects)});
 			this._commandService.addCommand(addMappingCommand, "dom");
 			var toolbarId = this.addMappingToolbar.id;
 			this._commandService.registerCommandContribution("eclipse.site.mappings.add", 1, toolbarId);
@@ -507,135 +123,57 @@
 		this._commandService.registerCommandContribution("eclipse.site.save", 0);
 	},
 	
+	checkOptions: function(options, names) {
+		for (var i=0; i < names.length; i++) {
+			if (typeof options[names[i]] === "undefined") {
+				throw new Error("options." + names[i] + " is required");
+			}
+		}
+	},
+	
 	/**
-	 * this._workspaceToChildren must be resolved before this is called, and site must be loaded.
-	 * @param workspaceToChildrenMap Maps {String} workspaceId to {Array} children
-	 * @param items {Array|Object}
-	 * @param userData
+	 * this._projects must be resolved before this is called
+	 * @param {Array} Projects in workspace
+	 * @param {Array|Object} items
+	 * @param {Object} userData
 	 * @returns {Array}
 	 */
-	_makeAddMenuChoices: function(workspaceToChildrenMap, items, userData) {
+	_makeAddMenuChoices: function(projects, items, userData) {
 		items = dojo.isArray(items) ? items[0] : items;
 		var workspaceId = this.getSiteConfiguration().Workspace;
-		var projects = workspaceToChildrenMap[workspaceId];
+		projects = projects.sort(function(projectA, projectB) {
+				return projectA.Name.localeCompare(projectB.Name);
+			});
 		var self = this;
 		
 		/**
 		 * @this An object from the choices array with shape {name:String, path:String, callback:Function}
-		 * @param item
+		 * @param {Object} item
 		 */
 		var editor = this;
 		var callback = function(item, event) {
 			if (event.shiftKey) {
 				// special feature for setting up self-hosting
-				var mappings = [
-				    {
-				      "Source": "/",
-				      "Target": this.path + "/bundles/org.eclipse.orion.client.core/web"
-				    },
-				    {
-				      "Source": "/",
-				      "Target": this.path + "/bundles/org.eclipse.orion.client.editor/web"
-				    },
-				    {
-				      "Source": "/file",
-				      "Target": "http://localhost:8080/file"
-				    },
-				    {
-				      "Source": "/prefs",
-				      "Target": "http://localhost:8080/prefs"
-				    },
-				    {
-				      "Source": "/workspace",
-				      "Target": "http://localhost:8080/workspace"
-				    },
-				    {
-				      "Source": "/org.dojotoolkit",
-				      "Target": "http://localhost:8080/org.dojotoolkit"
-				    },
-				    {
-				      "Source": "/users",
-				      "Target": "http://localhost:8080/users"
-				    },
-				    {
-				      "Source": "/auth2",
-				      "Target": "http://localhost:8080/auth2"
-				    },
-				    {
-				      "Source": "/login",
-				      "Target": "http://localhost:8080/login"
-				    },
-				    {
-				      "Source": "/loginstatic",
-				      "Target": "http://localhost:8080/loginstatic"
-				    },
-				    {
-				      "Source": "/site",
-				      "Target": "http://localhost:8080/site"
-				    },
-				    {
-				      "Source": "/",
-				      "Target": this.path + "/bundles/org.eclipse.orion.client.git/web"
-				    },
-				    {
-				      "Source": "/gitapi",
-				      "Target": "http://localhost:8080/gitapi"
-				    },
-				    {
-				      "Source": "/",
-				      "Target": this.path + "/bundles/org.eclipse.orion.client.users.ui/web"
-				    },
-				    {
-				      "Source": "/xfer",
-				      "Target": "http://localhost:8080/xfer"
-				    },
-				    {
-				      "Source": "/filesearch",
-				      "Target": "http://localhost:8080/filesearch"
-				    },
-				    {
-				      "Source": "/index.jsp",
-				      "Target": "http://localhost:8080/index.jsp"
-				    },
-				    {
-				      "Source": "/plugins/git",
-				      "Target": "http://localhost:8080/plugins/git"
-				    },
-				    {
-				      "Source": "/plugins/user",
-				      "Target": "http://localhost:8080/plugins/user"
-				    },
-				    {
-				      "Source": "/logout",
-				      "Target": "http://localhost:8080/logout"
-				    },
-				    {
-				      "Source": "/mixloginstatic",
-				      "Target": "http://localhost:8080/mixloginstatic"
-				    },
-				    {
-				      "Source": "/openids",
-				      "Target": "http://localhost:8080/openids"
-				    }
-				];
-				for (var i = 0; i<mappings.length; i++) {
-					editor.mappings._addMapping(mappings[i].Source, mappings[i].Target);
+				var mappings = editor.getSelfHostingMappings(this.path);
+				for (var i = 0; i < mappings.length; i++) {
+					editor.mappings.addMapping(mappings[i].Source, mappings[i].Target);
 				}
 				self.onSubmit();
 			} else {
-				editor.mappings._addMapping("/mountPoint", this.path);
+				editor.mappings.addMapping(null, this.path, this.name);
 			}
 		};
 //		var addOther = function() {
-//			editor.mappings._addMapping("/mountPoint", "/FolderId/somepath");
+//			editor.mappings.addMapping("/mountPoint", "/FolderId/somepath");
 //		};
 		var addUrl = function() {
-			editor.mappings._addMapping("/mountPoint", "http://someurl");
+			editor.mappings.addMapping("/web/somePath", "http://some-website.com");
 		};
 		
 		var choices = dojo.map(projects, function(project) {
 				return {
-					name: "Folder: /" + project.Name,
+					name: "/" + project.Name,
+					image: "/images/folder.gif",
 					path: mSiteUtils.makeRelativeFilePath(project.Location),
 					callback: callback
 				};
@@ -644,19 +182,89 @@
 		if (projects.length > 0) {
 			choices.push({}); // Separator
 		}
-		choices.push({name: "URL", callback: addUrl});
-//		choices.push({name: "Other", callback: addOther});
+		choices.push({
+			name: "Choose folder&#8230;",
+			image: "/images/folder.gif",
+			callback: function() { alert("Directory chooser dialog appears here."); }});
+		choices.push({name: "URL", image: "/images/link.gif", callback: addUrl});
 		return choices;
 	},
 	
-	startup: function() {
-		this.inherited(arguments);
+	getSelfHostingMappings: function(clientRepoPath) {
+		return [
+		    { "Source": "/",
+		      "Target": clientRepoPath + "/bundles/org.eclipse.orion.client.core/web"
+		    },
+		    { "Source": "/",
+		      "Target": clientRepoPath + "/bundles/org.eclipse.orion.client.editor/web"
+		    },
+		    { "Source": "/file",
+		      "Target": "http://localhost:8080/file"
+		    },
+		    { "Source": "/prefs",
+		      "Target": "http://localhost:8080/prefs"
+		    },
+		    { "Source": "/workspace",
+		      "Target": "http://localhost:8080/workspace"
+		    },
+		    { "Source": "/org.dojotoolkit",
+		      "Target": "http://localhost:8080/org.dojotoolkit"
+		    },
+		    { "Source": "/users",
+		      "Target": "http://localhost:8080/users"
+		    },
+		    { "Source": "/auth2",
+		      "Target": "http://localhost:8080/auth2"
+		    },
+		    { "Source": "/login",
+		      "Target": "http://localhost:8080/login"
+		    },
+		    { "Source": "/loginstatic",
+		      "Target": "http://localhost:8080/loginstatic"
+		    },
+		    { "Source": "/site",
+		      "Target": "http://localhost:8080/site"
+		    },
+		    { "Source": "/",
+		      "Target": clientRepoPath + "/bundles/org.eclipse.orion.client.git/web"
+		    },
+		    { "Source": "/gitapi",
+		      "Target": "http://localhost:8080/gitapi"
+		    },
+		    { "Source": "/",
+		      "Target": clientRepoPath + "/bundles/org.eclipse.orion.client.users.ui/web"
+		    },
+		    { "Source": "/xfer",
+		      "Target": "http://localhost:8080/xfer"
+		    },
+		    { "Source": "/filesearch",
+		      "Target": "http://localhost:8080/filesearch"
+		    },
+		    { "Source": "/index.jsp",
+		      "Target": "http://localhost:8080/index.jsp"
+		    },
+		    { "Source": "/plugins/git",
+		      "Target": "http://localhost:8080/plugins/git"
+		    },
+		    { "Source": "/plugins/user",
+		      "Target": "http://localhost:8080/plugins/user"
+		    },
+		    { "Source": "/logout",
+		      "Target": "http://localhost:8080/logout"
+		    },
+		    { "Source": "/mixloginstatic",
+		      "Target": "http://localhost:8080/mixloginstatic"
+		    },
+		    { "Source": "/openids",
+		      "Target": "http://localhost:8080/openids"
+		    }
+		];
 	},
 	
 	/**
 	 * Loads site configuration from a URL into the editor.
 	 * @param {String} location URL of the site configuration to load.
-	 * @return {dojo.Deferred} A deferred, resolved when the editor has loaded & refreshed itself.
+	 * @returns {dojo.Deferred} A deferred, resolved when the editor has loaded & refreshed itself.
 	 */
 	load: function(location) {
 		var deferred = new dojo.Deferred();
@@ -672,19 +280,60 @@
 		return deferred;
 	},
 	
+	/**
+	 * Fetches top-level children of the siteConfiguration's workspace
+	 * @returns {dojo.Deferred}
+	 */
+	_fetchProjects: function(siteConfiguration) {
+		if (!this._fetched) {
+			dojo.when(this._workspaces, dojo.hitch(this, function(workspaces) {
+				var workspace;
+				for (var i=0; i < workspaces.length; i++) {
+					if (workspaces[i].Id === siteConfiguration.Workspace) {
+						workspace = workspaces[i];
+						break;
+					}
+				}
+				if (workspace) {
+					this._fileClient.fetchChildren(workspace.Location).then(
+						dojo.hitch(this, function(projects) {
+							this._fetched = true;
+							this._projects.callback(projects);
+						}),
+						dojo.hitch(this, this._onError));
+				}
+			}));
+		}
+	},
+
 	_setSiteConfiguration: function(siteConfiguration) {
 		this._detachListeners(this._siteConfiguration);
 		this._siteConfiguration = siteConfiguration;
+		this._fetchProjects(siteConfiguration);
 		this._refreshFields();
-		this._attachListeners(this._siteConfiguration);
+		setTimeout(dojo.hitch(this, function() {
+			this._attachListeners(this._siteConfiguration);
+		}), 0);
+	},
+	
+	setDirty: function(value) {
+		this._isDirty = value;
+		if (!value) {
+			this.mappings.setDirty(false);
+		}
+	},
+	
+	isDirty: function() {
+		return this._isDirty || this.mappings.isDirty();
 	},
 	
 	_refreshFields: function() {
 		this.name.set("value", this._siteConfiguration.Name);
 		this.hostHint.set("value", this._siteConfiguration.HostHint);
-		this.mappings.setMappings(this._siteConfiguration.Mappings);
-		this.mappings.startup();
 		
+		this.mappings = new mSiteMappingsTable.MappingsTable(this.serviceRegistry, null, this.mappingsPlaceholder.id, this._siteConfiguration, this._projects);
+		this.mappings.startup();
+
 		var hostStatus = this._siteConfiguration.HostingStatus;
 		var status;
 		if (hostStatus && hostStatus.Status === "started") {
@@ -712,17 +361,22 @@
 		var editor = this;
 		function bindText(widget, modelField) {
 			// Bind widget's onChange to site[modelField]
-			var handle = dojo.connect(widget, "onChange", null, function(/**Event*/ e) {
+			var handle = dojo.connect(widget, "onChange", editor, function(/**Event*/ e) {
 				var value = widget.get("value");
 				site[modelField] = value;
-				
-//				console.debug("Change " + modelField + " to " + value);
+				this.setDirty(true);
 			});
 			editor._modelListeners.push(handle);
 		}
 		
 		bindText(this.name, "Name");
 		bindText(this.hostHint, "HostHint");
+		
+		this._modelListeners.push(dojo.connect(this.mappings, "setDirty", this, function(value) {
+				if (value) {
+					this.setDirty(true);
+				}
+			}));
 	},
 	
 	_detachListeners: function() {
@@ -731,54 +385,10 @@
 				dojo.disconnect(this._modelListeners[i]);
 			}
 		}
-		// Detach grid
 	},
 	
 	/**
-	 * Starts loading workspaces and resolves this._workspaces when they're ready.
-	 */
-	_loadWorkspaces: function() {
-		var editor = this;
-		editor._fileClient.loadWorkspaces().then(function(workspaces) {
-				editor._workspaces.callback(workspaces);
-				editor._loadWorkspaceChildren(workspaces);
-			},
-			function(error) {
-				editor._workspaces.errback(error);
-			});
-	},
-	
-	/**
-	 * Loads each workspace's children and resolves this._workspaceToChildren with the completed
-	 * Id-to-Children map.
-	 * @param workspaces
-	 */
-	_loadWorkspaceChildren: function(workspaces) {
-		var editor = this;
-		
-		// Promise for each workspace's children
-		var promises = dojo.map(workspaces, function(workspace) {
-			return editor._fileClient.fetchChildren(workspace.Location).then(function(children) {
-				return {id: workspace.Id, children: children};
-			});
-		});
-		
-		var deferredList = new dojo.DeferredList(promises);
-		deferredList.then(function(/*Array*/ results) {
-				var map = {};
-				dojo.forEach(results, function(/*Array*/ result) {
-					// result[0] is the workspace number, result[1] is the data
-					var data = result[1];
-					map[data.id] = data.children;
-				});
-				
-				// Finally, we've built the map
-				editor._workspaceToChildren.callback(map);
-			});
-	},
-	
-	/**
-	 * @return {SiteConfiguration} The site configuration that is being edited.
+	 * @returns {SiteConfiguration} The site configuration that is being edited.
 	 */
 	getSiteConfiguration: function() {
 		return this._siteConfiguration;
@@ -787,7 +397,7 @@
 	/**
 	 * Callback when 'save' is clicked.
 	 * @Override
-	 * @return True to allow save to proceed, false to prevent it.
+	 * @returns True to allow save to proceed, false to prevent it.
 	 */
 	onSubmit: function(/** Event */ e) {
 		var form = dijit.byId("siteForm");
@@ -800,6 +410,7 @@
 			var deferred = this._siteService.updateSiteConfiguration(siteConfig.Location, siteConfig).then(
 					function(updatedSiteConfig) {
 						editor._setSiteConfiguration(updatedSiteConfig);
+						editor.setDirty(false);
 						return { Result: "Saved \"" + updatedSiteConfig.Name + "\"." };
 					});
 			this._busyWhile(deferred, "Saving...");
@@ -825,7 +436,6 @@
 	},
 	
 	_onError: function(deferred) {
-		this.saveButton.set("disabled", false);
 		this._statusService.setErrorMessage(deferred);
 		this.onError(deferred);
 	},
diff --git a/bundles/org.eclipse.orion.client.core/web/orion/widgets/templates/SiteEditor.html b/bundles/org.eclipse.orion.client.core/web/orion/widgets/templates/SiteEditor.html
index 455997d..f0c4227 100644
--- a/bundles/org.eclipse.orion.client.core/web/orion/widgets/templates/SiteEditor.html
+++ b/bundles/org.eclipse.orion.client.core/web/orion/widgets/templates/SiteEditor.html
@@ -1,39 +1,45 @@
 <div>
 	<style type="text/css">
+		.siteEditorTable td, .siteEditorTable th {
+			padding: 10px 10px 5px 10px;
+		}
+		/* Style leftmost column of siteEditorTable */
+		.siteEditorTable>tbody>tr>td:first-child {
+			vertical-align: top;
+			min-width: 10em;
+			/*background-color: #fafafa;*/
+		}
+		.siteEditorTable>tbody>tr>td {
+			/*border-bottom: 1px solid #ddd;*/
+			padding-bottom: 20px;
+			vertical-align: middle;
+		}
+		.navColumn {
+			font-weight: bold;
+		}
+		.isValidColumn {
+			min-width: 32px;
+			width: 40px;
+		}
 		.isValidCell {
 			text-align: center;
+			vertical-align: middle;
 		}
-		.editablePathCell {
+		.pathInput, .serverPathInput {
+			min-width: 20em;
 		}
-		.actionCell {
-			height: 22px !important;
-			padding: 1px 1px 4px 1px;
-		}
-		.actionCell .commandLink {
-			margin: 0;
-		}
-		.dojoxGridRow .workspacePathCell, .dojoxGridRowOdd .workspacePathCell {
-			background-color: #eee;
-		}
-		td.dojoxGridCellFocus, th.dojoxGridCellFocus {
-			/* Remove annoying dashed border, but keep the width to avoid jiggling */
-			border: 1px solid transparent !important;
-		}
-		.dojoxGridRowOver .dojoxGridCell, .dojoxGridRowEditing .dojoxGridCell {
-			background-color: inherit !important;
-		}
-		.mappingsGridTable {
-			font-size: 9pt !important;
+		.addMappingToolbar {
+			padding-top: 15px;
 		}
 	</style>
 	<div dojoAttachPoint="containerNode">
 		<div dojoType="dijit.form.Form" id="siteForm">
-			<table>
+			<table class="siteEditorTable">
 				<tr>
-					<td style="padding: 5px; text-align: left;">
+					<td>
 						<label dojoAttachPoint="siteConfigNameLabel" for="${id}_name">${siteConfigNameLabelText}</label>
 					</td>
-					<td style="padding: 10px 5px 10px 5px; text-align: left;">
+					<td>
 						<input dojoType="dijit.form.ValidationTextBox" dojoAttachPoint="name" id="${id}_name"
 							required="true" value=""
 							style="display: inline-block; width: 15em;">
@@ -41,43 +47,38 @@
 				</tr>
 				
 				<tr>
-					<td style="padding: 5px; vertical-align: top">
-						<label dojoAttachPoint="mappingsLabel" for="${id}_mappings">${mappingsLabelText}</label>
+					<td>
+						<label dojoAttachPoint="hostHintLabel" for="${id}_hostHint">${hostHintLabelText}</label>
 					</td>
-					<td style="padding: 10px 25px 10px 5px; text-align: left; width:100%;">
-						<div dojoType="orion.widgets.MappingsGrid" dojoAttachPoint="mappings"
-								class="mappingsGridTable"
-								autoHeight="true" singleClickEdit="true" selectionMode="none"
-								selectable="true">
-						</div>
-						<div style="padding: 5px 0 0 5px;">
-							<div dojoAttachPoint="addMappingToolbar" id="${id}_addMappingToolbar"></div>
-						</div>
-					</td>
-				</td>
-				
-				<tr>
-					<td style="padding: 5px;">
-						<label dojoAttachPoint="hostHintLabel" for="${id}_hostHint" style="text-align: left;">${hostHintLabelText}</label>
-					</td>
-					<td style="padding: 10px 5px 10px 5px; text-align: left;">
+					<td>
 						<input dojoType="dijit.form.ValidationTextBox" dojoAttachPoint="hostHint" id="${id}_hostHint"
 							required="false" value=""
-							style="display: inline-block; width: 12em;">
+							style="display: inline-block; width: 12em;"><br />
+						<em>Optional; used to determine the URL where a started site can be accessed.</em>
 					</td>
 				</tr>
 				
 				<tr>
-					<td style="padding: 5px;">
-						<label dojoAttachPoint="hostingStatusLabel" style="text-align: left;">${hostingStatusLabelText}</label>
+					<td>
+						<label dojoAttachPoint="hostingStatusLabel">${hostingStatusLabelText}</label>
 					</td>
-					<td style="padding: 10px 5px 10px 5px; text-align: left;">
-						<p dojoAttachPoint="hostingStatus"></p>
-						<p dojoAttachPoint="siteStartedWarning" style="display: none;">
-						(Changes you make here won't affect the running copy.)
-						</p>
+					<td>
+						<span dojoAttachPoint="hostingStatus"></span>
+						<span dojoAttachPoint="siteStartedWarning" style="display: none;"><br />
+						<em>Changes you make here won't affect the running site.</em>
+						</span>
 					</td>
 				</tr>
+
+				<tr>
+					<td>
+						<label dojoAttachPoint="mappingsLabel" for="${id}_mappings">${mappingsLabelText}</label>
+					</td>
+					<td style="padding: 10px 25px 10px 5px; width:100%;">
+						<div dojoAttachPoint="mappingsPlaceholder" class="mappingsGridTable" id="${id}_mappingsPlaceholder"></div>
+						<div dojoAttachPoint="addMappingToolbar" id="${id}_addMappingToolbar" class="addMappingToolbar"></div>
+					</td>
+				</td>
 				
 			</table>
 		</div>
diff --git a/bundles/org.eclipse.orion.client.core/web/sites/site.js b/bundles/org.eclipse.orion.client.core/web/sites/site.js
index 8ea0d57..8292c15 100644
--- a/bundles/org.eclipse.orion.client.core/web/sites/site.js
+++ b/bundles/org.eclipse.orion.client.core/web/sites/site.js
@@ -8,8 +8,8 @@
  * Contributors:
  *     IBM Corporation - initial API and implementation
  *******************************************************************************/
-/*global eclipse dojo dijit widgets window*/
-/*jslint devel:true browser:true*/
+/*global define dojo dijit orion window*/
+/*jslint browser:true*/
 
 /*
  * Glue code for site.html
@@ -47,7 +47,7 @@
 			if (editor && site) {
 				var location = dojo.byId("location");
 				dojo.place(document.createTextNode(site.Name), location, "only");
-				window.document.title = site.Name + " - Edit Site";
+				document.title = site.Name + (editor.isDirty() ? "* " : "") + " - Edit Site";
 			}
 		};
 		
@@ -68,6 +68,7 @@
 		var widget;
 		(function() {
 			widget = new orion.widgets.SiteEditor({
+				serviceRegistry: serviceRegistry,
 				fileClient: fileClient,
 				siteService: siteService,
 				commandService: commandService,
@@ -78,10 +79,17 @@
 			widget.startup();
 			
 			dojo.connect(widget, "onSuccess", updateTitle);
+			dojo.connect(widget, "setDirty", updateTitle);
 			
 			onHashChange();
 		}());
 		
+		window.onbeforeunload = function() {
+			if (widget.isDirty()) {
+				return "There are unsaved changes.";
+			}
+		};
+		
 		// Hook up commands stuff
 		var refresher = dojo.hitch(widget, widget._setSiteConfiguration);
 		var errorHandler = statusService;
diff --git a/bundles/org.eclipse.orion.client.core/web/sites/siteMappingsTable.js b/bundles/org.eclipse.orion.client.core/web/sites/siteMappingsTable.js
new file mode 100644
index 0000000..fd232b8
--- /dev/null
+++ b/bundles/org.eclipse.orion.client.core/web/sites/siteMappingsTable.js
@@ -0,0 +1,340 @@
+/*******************************************************************************
+ * Copyright (c) 2010, 2011 IBM Corporation and others.
+ * All rights reserved. This program and the accompanying materials are made 
+ * available under the terms of the Eclipse Public License v1.0 
+ * (http://www.eclipse.org/legal/epl-v10.html), and the Eclipse Distribution 
+ * License v1.0 (http://www.eclipse.org/org/documents/edl-v10.html). 
+ * 
+ * Contributors: IBM Corporation - initial API and implementation
+ ******************************************************************************/
+/*global define */
+/*jslint browser:true regexp:true */
+
+define(['dojo', 'dijit', 'dojox', 'orion/util', 'orion/siteUtils', 'orion/commands', 'orion/explorer'],
+		function(dojo, dijit, dojox, mUtil, mSiteUtils, mCommands, mExplorer) {
+
+var mSiteMappingsTable = {};
+
+function mixin(target, source) {
+	for (var p in source) {
+		if (source.hasOwnProperty(p)) { target[p] = source[p]; }
+	}
+}
+
+function isWorkspacePath(/**String*/ path) {
+	return new RegExp("^/").test(path);
+}
+
+mSiteMappingsTable.Model = (function() {
+	function Model(rootPath, fetchItems, items) {
+		this.rootPath = rootPath;
+		this.fetchItems = fetchItems;
+		this.items = items;
+	}
+	Model.prototype = new mExplorer.ExplorerFlatModel();
+	mixin(Model.prototype, {
+		getId: function(/**Object*/ item) {
+			var result;
+			if (item === this.root) {
+				result = this.rootId;
+			} else {
+				result = "mapping_" + this.items.indexOf(item);
+			}
+			return result;
+		}});
+	return Model;
+}());
+
+/**
+ * @param {Function} options.onchange Callback with signature: <code>function(item, fieldName, newValue, event)</code>
+ */
+mSiteMappingsTable.Renderer = (function() {
+	function Renderer(options, explorer) {
+		this._init(options);
+		this.options = options;
+		this.explorer = explorer;
+	}
+	Renderer.prototype = new mExplorer.SelectionRenderer();
+	mixin(Renderer.prototype, {
+		getCellHeaderElement: function(/**Number*/ col_no) {
+			var col;
+			switch(col_no){
+				case 0:
+					col = dojo.create("th", {innerHTML: " "});
+					dojo.addClass(col, "isValidColumn");
+					return col;
+				case 1: return dojo.create("th", {innerHTML: "Path"});
+				case 2: return dojo.create("th", {innerHTML: "Mount at (server path)"});
+				case 3: return dojo.create("th", {innerHTML: "Actions"});
+			}
+		},
+		getCellElement: function(/**Number*/ col_no, /**Object*/ item, /**HTMLTableRowElement*/ tableRow) {
+			var col, input;
+			switch(col_no) {
+				case 0:
+					return this.getIsValidCell(col_no, item, tableRow);
+				case 1: // Path
+					col = dojo.create("td");
+					input = dojo.create("input");
+					dojo.addClass(input, "pathInput");
+					input.value = item.FriendlyPath;
+					input.onchange = dojo.hitch(this, function(event) {
+							this.options.onchange(item, "FriendlyPath", event.target.value, event);
+						});
+					dojo.place(input, col);
+					return col;
+				case 2: // Mount at
+					col = dojo.create("td");
+					input = dojo.create("input");
+					dojo.addClass(input, "serverPathInput");
+					input.value = item.Source;
+					input.onchange = dojo.hitch(this, function(event) {
+							this.options.onchange(item, "Source", event.target.value, event);
+						});
+					dojo.place(input, col);
+					return col;
+				case 3: // Actions
+					return this.getActionsColumn(item, tableRow);
+			}
+		},
+		getIsValidCell: function(/**Number*/ col_no, /**Object*/ item, /**HTMLTableRowElement*/ tableRow) {
+			var target = item.Target;
+			var col = document.createElement("td");
+			var href, result;
+			if (isWorkspacePath(target)) {
+				var location = mSiteUtils.makeFullFilePath(target);
+				href = mUtil.safeText(location);
+				col.innerHTML = "<span class=\"validating\">&#8230;</span>";
+				// TODO: should use fileClient here, but we don't want its retrying or error dialog
+				//this._fileClient.fetchChildren(location)
+				dojo.xhrGet({
+					url: location,
+					headers: { "Orion-Version": "1" },
+					handleAs: "json"
+				}).then(
+					function(children) {
+						col.innerHTML = '<a href="' + href + '" target="_new"><img src="/images/folder.gif" title="Workspace folder ' + href + '"/></a>';
+					}, function(error) {
+						col.innerHTML = '<a href="' + href + '" target="_new"><img src="/images/error.gif" title="Workspace folder not found: ' + href + '"/></a>';
+					});
+			} else {
+				href = mUtil.safeText(target);
+				col.innerHTML = '<a href="' + href + '" target="_new"><img src="/images/link.gif" title="External link to ' + href + '"/></a>';
+			}
+			dojo.addClass(col, "isValidCell");
+			return col;
+		}
+	});
+	return Renderer;
+}());
+
+/**
+ * @name orion.sites.MappingsTable
+ */
+mSiteMappingsTable.MappingsTable = (function() {
+	function MappingsTable(serviceRegistry, selection, parentId, siteConfiguration, /**dojo.Deferred*/ projectsPromise) {
+		this.registry = serviceRegistry;
+		serviceRegistry.getService("orion.page.command").then(dojo.hitch(this, function(commandService) {
+			this.commandService = commandService;
+			this.registerCommands();
+		}));
+		this.parentId = parentId;
+		this.selection = selection;
+		this.renderer = new mSiteMappingsTable.Renderer({
+				checkbox: false, /*TODO make true when we have selection-based commands*/
+				onchange: dojo.hitch(this, this.fieldChanged)
+			}, this);
+		this.myTree = null;
+		this.siteConfiguration = siteConfiguration;
+		this.projectsPromise = projectsPromise;
+		this.setDirty(false);
+	}
+	MappingsTable.prototype = new mExplorer.Explorer();
+	mixin(MappingsTable.prototype, /** @lends orion.sites.MappingsTable.prototype */ {
+		startup: function() {
+			var fetchItems = dojo.hitch(this, function() {
+				var d = new dojo.Deferred();
+				d.callback(this.siteConfiguration.Mappings);
+				return d;
+			});
+			dojo.when(this.projectsPromise, dojo.hitch(this, function(projects) {
+				this.projects = projects;
+				// Make FriendlyPath
+				this.siteConfiguration.Mappings = dojo.map(this.siteConfiguration.Mappings, dojo.hitch(this, function(mapping) {
+					return this.createMappingObject(mapping.Source, mapping.Target);
+				}));
+				// Build visuals
+				this.createTree(this.parentId, new mSiteMappingsTable.Model(null, fetchItems, this.siteConfiguration.Mappings));
+			}));
+		},
+		render: function() {
+			this.changedItem(this.siteConfiguration.Mappings, this.siteConfiguration.Mappings);
+		},
+		registerCommands: function() {
+			var deleteMappingCommand = new mCommands.Command({
+				name: "Delete",
+				image: "/images/delete.gif",
+				id: "eclipse.site.mappings.remove",
+				visibleWhen: function(item) {
+					// Only show on a Mappings object
+					return item.Source && item.Target;
+				},
+				callback: dojo.hitch(this, function(item) {
+					//table._hideTooltip();
+					this.deleteMapping(item);
+					this.render();
+					this.setDirty(true);
+				})});
+			this.commandService.addCommand(deleteMappingCommand , "object");
+			this.commandService.registerCommandContribution("eclipse.site.mappings.remove", 0);
+			
+			var moveUpCommand = new mCommands.Command({
+				name: "Move Up",
+				image: "/images/move_up.gif",
+				id: "eclipse.site.mappings.moveUp",
+				visibleWhen: dojo.hitch(this, function(item) {
+					return item.Source && item.Target;
+				}),
+				callback: dojo.hitch(this, function(item) {
+					var index = this.getItemIndex(item);
+					if (index === 0) { return; }
+					var temp = this.siteConfiguration.Mappings[index-1];
+					this.siteConfiguration.Mappings[index-1] = item;
+					this.siteConfiguration.Mappings[index] = temp;
+					this.render();
+					this.setDirty(true);
+				})});
+			this.commandService.addCommand(moveUpCommand, "object");
+			this.commandService.registerCommandContribution("eclipse.site.mappings.moveUp", 1);
+			
+			var moveDownCommand = new mCommands.Command({
+				name: "Move Down",
+				image: "/images/move_down.gif",
+				id: "eclipse.site.mappings.moveDown",
+				visibleWhen: dojo.hitch(this, function(item) {
+					return item.Source && item.Target;
+				}),
+				callback: dojo.hitch(this, function(item) {
+//					this._hideTooltip();
+					var index = this.getItemIndex(item);
+					if (index === this.siteConfiguration.Mappings.length - 1) { return; }
+					var temp = this.siteConfiguration.Mappings[index+1];
+					this.siteConfiguration.Mappings[index+1] = item;
+					this.siteConfiguration.Mappings[index] = temp;
+					this.render();
+					this.setDirty(true);
+				})});
+			this.commandService.addCommand(moveDownCommand, "object");
+			this.commandService.registerCommandContribution("eclipse.site.mappings.moveDown", 2);
+		},
+		getItemIndex: function(item) {
+			return this.siteConfiguration.Mappings.indexOf(item);
+		},
+		// TODO make this a proper Command
+		// Also we should use makeNewItemPlaceHolder()
+		addMapping: function(/**String*/ source, /**String*/ target, /**String*/ friendlyPath) {
+			source = typeof(source) === "string" ? source : this.getNextMountPoint(friendlyPath);
+			target = typeof(target) === "string" ? target : "/";
+			
+			var newItem = this.createMappingObject(source, target);
+			this.siteConfiguration.Mappings.push(newItem);
+			
+			this.render();
+			this.setDirty(true);
+		},
+		getNextMountPoint: function(/**String*/ friendlyPath) {
+			// Try root first
+			var mappings = this.siteConfiguration.Mappings;
+			var hasRoot = false;
+			for (var i=0; i < mappings.length; i++) {
+				if (mappings[i].Source === "/") {
+					hasRoot = true;
+					break;
+				}
+			}
+			if (!hasRoot) {
+				return "/";
+			}
+			// Else base it on friendlyPath
+			if (!friendlyPath) {
+				return "/web/somePath";
+			}
+			var segments = friendlyPath.split("/");
+			for (i = segments.length-1; i >= 0; i--) {
+				if (!/\s+/.test(segments[i])) {
+					return "/" + segments[i];
+				}
+			}
+			return "/web/somePath";
+		},
+		createMappingObject: function(source, target) {
+			return {Source: source, Target: target, FriendlyPath: this.toFriendlyPath(target)};
+		},
+		deleteMapping: function(/**Object*/ mapping) {
+			var index = this.siteConfiguration.Mappings.indexOf(mapping);
+			if (index !== -1) {
+				this.siteConfiguration.Mappings.splice(index, 1);
+			}
+		},
+		fieldChanged: function(/**Object*/ item, /**String*/ fieldName, /**String*/ newValue, /**Event*/ event) {
+			var oldValue = item[fieldName];
+			if (oldValue !== newValue) {
+				item[fieldName] = newValue;
+				if (fieldName === "FriendlyPath") {
+					this.propagate(newValue, item);
+				}
+				this.render();
+				this.setDirty(true);
+			}
+		},
+		toFriendlyPath: function(target) {
+			var friendlyPath;
+			if (isWorkspacePath(target)) {
+				for (var i=0; i < this.projects.length; i++) {
+					var project = this.projects[i];
+					var name = "/" + project.Name;
+					var location = mSiteUtils.makeRelativeFilePath(project.Location);
+					if (this.pathsMatch(target, location)) {
+						friendlyPath = name + target.substring(location.length);
+						break;
+					}
+				}
+			}
+			return friendlyPath || target;
+		},
+		/** Rewrites item's FriendlyPath using the project shortname and sets the result into the Target */
+		propagate: function(friendlyPath, item) {
+			if (isWorkspacePath(friendlyPath)) {
+				var found = false;
+				for (var i=0; i < this.projects.length; i++) {
+					var project = this.projects[i];
+					var name = "/" + project.Name;
+					var location = mSiteUtils.makeRelativeFilePath(project.Location);
+					if (this.pathsMatch(friendlyPath, name)) {
+						var rest = friendlyPath.substring(name.length);
+						found = true;
+						item.Target = location + rest;
+					}
+				}
+				if (!found) {
+					// Bogus path
+					item.Target = friendlyPath;
+				}
+			}
+		},
+		/** @returns true if b is a sub-path of a */
+		pathsMatch: function(a, b) {
+			return a === b || a.indexOf(b + "/") === 0;
+		},
+		setDirty: function(value) {
+			this._isDirty = value;
+		},
+		isDirty: function() {
+			return this._isDirty;
+		}
+	});
+	return MappingsTable;
+}());
+
+return mSiteMappingsTable;
+});
\ No newline at end of file