Implemented single-cell editing.

git-svn-id: http://google-refine.googlecode.com/svn/trunk@210 7d457c2a-affb-35e4-300a-418c747d4874
This commit is contained in:
David Huynh 2010-03-05 08:11:48 +00:00
parent 808fe61c8c
commit b3ac945c33
7 changed files with 302 additions and 21 deletions

View File

@ -16,6 +16,7 @@ import com.metaweb.gridworks.commands.edit.AnnotateRowsCommand;
import com.metaweb.gridworks.commands.edit.ApplyOperationsCommand; import com.metaweb.gridworks.commands.edit.ApplyOperationsCommand;
import com.metaweb.gridworks.commands.edit.CreateProjectCommand; import com.metaweb.gridworks.commands.edit.CreateProjectCommand;
import com.metaweb.gridworks.commands.edit.DoTextTransformCommand; import com.metaweb.gridworks.commands.edit.DoTextTransformCommand;
import com.metaweb.gridworks.commands.edit.EditOneCellCommand;
import com.metaweb.gridworks.commands.edit.FacetBasedEditCommand; import com.metaweb.gridworks.commands.edit.FacetBasedEditCommand;
import com.metaweb.gridworks.commands.edit.JoinMultiValueCellsCommand; import com.metaweb.gridworks.commands.edit.JoinMultiValueCellsCommand;
import com.metaweb.gridworks.commands.edit.RemoveColumnCommand; import com.metaweb.gridworks.commands.edit.RemoveColumnCommand;
@ -75,6 +76,7 @@ public class GridworksServlet extends HttpServlet {
_commands.put("compute-facets", new ComputeFacetsCommand()); _commands.put("compute-facets", new ComputeFacetsCommand());
_commands.put("do-text-transform", new DoTextTransformCommand()); _commands.put("do-text-transform", new DoTextTransformCommand());
_commands.put("facet-based-edit", new FacetBasedEditCommand()); _commands.put("facet-based-edit", new FacetBasedEditCommand());
_commands.put("edit-one-cell", new EditOneCellCommand());
_commands.put("add-column", new AddColumnCommand()); _commands.put("add-column", new AddColumnCommand());
_commands.put("remove-column", new RemoveColumnCommand()); _commands.put("remove-column", new RemoveColumnCommand());

View File

@ -0,0 +1,115 @@
package com.metaweb.gridworks.commands.edit;
import java.io.IOException;
import java.io.Serializable;
import java.util.Properties;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.json.JSONWriter;
import com.metaweb.gridworks.commands.Command;
import com.metaweb.gridworks.expr.ExpressionUtils;
import com.metaweb.gridworks.history.Change;
import com.metaweb.gridworks.history.HistoryEntry;
import com.metaweb.gridworks.model.Cell;
import com.metaweb.gridworks.model.Column;
import com.metaweb.gridworks.model.Project;
import com.metaweb.gridworks.model.Recon;
import com.metaweb.gridworks.model.changes.CellChange;
import com.metaweb.gridworks.process.QuickHistoryEntryProcess;
import com.metaweb.gridworks.util.ParsingUtilities;
public class EditOneCellCommand extends Command {
@Override
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
try {
Project project = getProject(request);
int rowIndex = Integer.parseInt(request.getParameter("row"));
int cellIndex = Integer.parseInt(request.getParameter("cell"));
String type = request.getParameter("type");
String valueString = request.getParameter("value");
Serializable value = null;
if ("number".equals(type)) {
value = Double.parseDouble(valueString);
} else if ("boolean".equals(type)) {
value = "true".equalsIgnoreCase(valueString);
} else if ("date".equals(type)) {
value = ParsingUtilities.stringToDate(valueString);
} else {
value = valueString;
}
EditOneCellProcess process = new EditOneCellProcess(
project,
"Edit single cell",
rowIndex,
cellIndex,
value
);
boolean done = project.processManager.queueProcess(process);
if (done) {
JSONWriter writer = new JSONWriter(response.getWriter());
writer.object();
writer.key("code"); writer.value("ok");
writer.key("cell"); process.newCell.write(writer, new Properties());
writer.endObject();
} else {
respond(response, "{ \"code\" : \"pending\" }");
}
} catch (Exception e) {
respondException(response, e);
}
}
protected class EditOneCellProcess extends QuickHistoryEntryProcess {
final int rowIndex;
final int cellIndex;
final Serializable value;
Cell newCell;
EditOneCellProcess(
Project project,
String briefDescription,
int rowIndex,
int cellIndex,
Serializable value
) {
super(project, briefDescription);
this.rowIndex = rowIndex;
this.cellIndex = cellIndex;
this.value = value;
}
protected HistoryEntry createHistoryEntry() throws Exception {
Cell cell = _project.rows.get(rowIndex).getCell(cellIndex);
Column column = _project.columnModel.getColumnByCellIndex(cellIndex);
if (column == null) {
throw new Exception("No such column");
}
newCell = new Cell(
value,
cell != null ? cell.recon : null
);
String description =
"Edit single cell on row " + (rowIndex + 1) +
", column " + column.getHeaderLabel();
Change change = new CellChange(rowIndex, cellIndex, cell, newCell);
return new HistoryEntry(
_project, description, null, change);
}
}
}

View File

@ -2,6 +2,7 @@ package com.metaweb.gridworks.model;
import java.io.Serializable; import java.io.Serializable;
import java.util.Calendar; import java.util.Calendar;
import java.util.Date;
import java.util.Properties; import java.util.Properties;
import org.json.JSONException; import org.json.JSONException;
@ -40,10 +41,16 @@ public class Cell implements Serializable, HasFields, Jsonizable {
writer.value(((EvalError) value).message); writer.value(((EvalError) value).message);
} else { } else {
writer.key("v"); writer.key("v");
if (value != null && value instanceof Calendar) { if (value != null) {
writer.value(ParsingUtilities.dateToString(((Calendar) value).getTime())); if (value instanceof Calendar) {
writer.value(ParsingUtilities.dateToString(((Calendar) value).getTime()));
} else if (value instanceof Date) {
writer.value(ParsingUtilities.dateToString((Date) value));
} else {
writer.value(value);
}
} else { } else {
writer.value(value); writer.value(null);
} }
} }

View File

@ -1 +1 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <title>Gridworks</title> <link type="text/css" rel="stylesheet" href="externals/suggest/css/suggest-1.0.3.min.css" /> <link type="text/css" rel="stylesheet" href="externals/jquery-ui/css/ui-lightness/jquery-ui-1.7.2.custom.css" /> <link rel="stylesheet" href="/styles/common.css" /> <link rel="stylesheet" href="/styles/menu.css" /> <link rel="stylesheet" href="/styles/dialog.css" /> <link rel="stylesheet" href="/styles/project.css" /> <link rel="stylesheet" href="/styles/data-table-view.css" /> <link rel="stylesheet" href="/styles/history.css" /> <link rel="stylesheet" href="/styles/browsing.css" /> <link rel="stylesheet" href="/styles/process.css" /> <link rel="stylesheet" href="/styles/menu-bar.css" /> <link rel="stylesheet" href="/styles/expression-preview-dialog.css" /> <link rel="stylesheet" href="/styles/schema-alignment-dialog.css" /> <link rel="stylesheet" href="/styles/recon-dialog.css" /> <link rel="stylesheet" href="/styles/facet-based-edit-dialog.css" /> <script type="text/javascript" src="externals/jquery-1.4.1.min.js"></script> <script type="text/javascript" src="externals/suggest/suggest-1.0.3.min.js"></script> <script type="text/javascript" src="externals/jquery-ui/jquery-ui-1.7.2.custom.min.js"></script> <script type="text/javascript" src="scripts/util/misc.js"></script> <script type="text/javascript" src="scripts/util/url.js"></script> <script type="text/javascript" src="scripts/util/string.js"></script> <script type="text/javascript" src="scripts/util/ajax.js"></script> <script type="text/javascript" src="scripts/util/menu.js"></script> <script type="text/javascript" src="scripts/util/dialog.js"></script> <script type="text/javascript" src="scripts/util/dom.js"></script> <script type="text/javascript" src="scripts/util/property-suggest.js"></script> <script type="text/javascript" src="scripts/project.js"></script> <script type="text/javascript" src="scripts/project/history-widget.js"></script> <script type="text/javascript" src="scripts/project/process-widget.js"></script> <script type="text/javascript" src="scripts/project/menu-bar.js"></script> <script type="text/javascript" src="scripts/project/browsing-engine.js"></script> <script type="text/javascript" src="scripts/project/scripting.js"></script> <script type="text/javascript" src="scripts/facets/list-facet.js"></script> <script type="text/javascript" src="scripts/facets/range-facet.js"></script> <script type="text/javascript" src="scripts/facets/text-search-facet.js"></script> <script type="text/javascript" src="scripts/views/data-table-view.js"></script> <script type="text/javascript" src="scripts/views/data-table-cell-ui.js"></script> <script type="text/javascript" src="scripts/views/data-table-column-header-ui.js"></script> <script type="text/javascript" src="scripts/dialogs/recon-dialog.js"></script> <script type="text/javascript" src="scripts/dialogs/expression-preview-dialog.js"></script> <script type="text/javascript" src="scripts/dialogs/facet-based-edit-dialog.js"></script> <script type="text/javascript" src="scripts/protograph/schema-alignment.js"></script> <script type="text/javascript" src="scripts/protograph/schema-alignment-ui-node.js"></script> <script type="text/javascript" src="scripts/protograph/schema-alignment-ui-link.js"></script> </head> <body> <div id="header"> <div id="path"><a class="app-path-section" href="./index.html">Gridworks</a> &raquo; </div> </div> <div id="body"> <div id="loading-message"><img src="images/large-spinner.gif" /> starting up ...</div> </div> </body> </html> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <title>Gridworks</title> <link type="text/css" rel="stylesheet" href="externals/suggest/css/suggest-1.0.3.min.css" /> <link type="text/css" rel="stylesheet" href="externals/jquery-ui/css/ui-lightness/jquery-ui-1.7.2.custom.css" /> <link rel="stylesheet" href="/styles/common.css" /> <link rel="stylesheet" href="/styles/menu.css" /> <link rel="stylesheet" href="/styles/dialog.css" /> <link rel="stylesheet" href="/styles/project.css" /> <link rel="stylesheet" href="/styles/data-table-view.css" /> <link rel="stylesheet" href="/styles/history.css" /> <link rel="stylesheet" href="/styles/browsing.css" /> <link rel="stylesheet" href="/styles/process.css" /> <link rel="stylesheet" href="/styles/menu-bar.css" /> <link rel="stylesheet" href="/styles/expression-preview-dialog.css" /> <link rel="stylesheet" href="/styles/schema-alignment-dialog.css" /> <link rel="stylesheet" href="/styles/recon-dialog.css" /> <link rel="stylesheet" href="/styles/facet-based-edit-dialog.css" /> <script type="text/javascript" src="externals/jquery-1.4.1.min.js"></script> <script type="text/javascript" src="externals/suggest/suggest-1.0.3.min.js"></script> <script type="text/javascript" src="externals/jquery-ui/jquery-ui-1.7.2.custom.min.js"></script> <script type="text/javascript" src="externals/date.js"></script> <script type="text/javascript" src="scripts/util/misc.js"></script> <script type="text/javascript" src="scripts/util/url.js"></script> <script type="text/javascript" src="scripts/util/string.js"></script> <script type="text/javascript" src="scripts/util/ajax.js"></script> <script type="text/javascript" src="scripts/util/menu.js"></script> <script type="text/javascript" src="scripts/util/dialog.js"></script> <script type="text/javascript" src="scripts/util/dom.js"></script> <script type="text/javascript" src="scripts/util/property-suggest.js"></script> <script type="text/javascript" src="scripts/project.js"></script> <script type="text/javascript" src="scripts/project/history-widget.js"></script> <script type="text/javascript" src="scripts/project/process-widget.js"></script> <script type="text/javascript" src="scripts/project/menu-bar.js"></script> <script type="text/javascript" src="scripts/project/browsing-engine.js"></script> <script type="text/javascript" src="scripts/project/scripting.js"></script> <script type="text/javascript" src="scripts/facets/list-facet.js"></script> <script type="text/javascript" src="scripts/facets/range-facet.js"></script> <script type="text/javascript" src="scripts/facets/text-search-facet.js"></script> <script type="text/javascript" src="scripts/views/data-table-view.js"></script> <script type="text/javascript" src="scripts/views/data-table-cell-ui.js"></script> <script type="text/javascript" src="scripts/views/data-table-column-header-ui.js"></script> <script type="text/javascript" src="scripts/dialogs/recon-dialog.js"></script> <script type="text/javascript" src="scripts/dialogs/expression-preview-dialog.js"></script> <script type="text/javascript" src="scripts/dialogs/facet-based-edit-dialog.js"></script> <script type="text/javascript" src="scripts/protograph/schema-alignment.js"></script> <script type="text/javascript" src="scripts/protograph/schema-alignment-ui-node.js"></script> <script type="text/javascript" src="scripts/protograph/schema-alignment-ui-link.js"></script> </head> <body> <div id="header"> <div id="path"><a class="app-path-section" href="./index.html">Gridworks</a> &raquo; </div> </div> <div id="body"> <div id="loading-message"><img src="images/large-spinner.gif" /> starting up ...</div> </div> </body> </html>

View File

@ -21,8 +21,6 @@ MenuSystem.showMenu = function(elmt, onDismiss) {
var level = MenuSystem._layers.length; var level = MenuSystem._layers.length;
elmt.click(MenuSystem.dismissAll);
return level; return level;
}; };
@ -53,14 +51,38 @@ MenuSystem.createMenuItem = function() {
MenuSystem.positionMenuAboveBelow = function(menu, elmt) { MenuSystem.positionMenuAboveBelow = function(menu, elmt) {
var offset = elmt.offset(); var offset = elmt.offset();
menu.css("left", offset.left + "px") var windowWidth = $(document.body).innerWidth();
.css("top", (offset.top + elmt.outerHeight()) + "px"); var windowHeight = $(document.body).innerHeight();
if (offset.top + elmt.outerHeight() - document.body.scrollTop + menu.outerHeight() > windowHeight - 10) {
menu.css("top", (offset.top - menu.outerHeight()) + "px");
} else {
menu.css("top", (offset.top + elmt.outerHeight()) + "px");
}
if (offset.left - document.body.scrollLeft + menu.outerWidth() > windowWidth - 10) {
menu.css("left", (offset.left + elmt.outerWidth() - menu.outerWidth()) + "px");
} else {
menu.css("left", offset.left + "px");
}
}; };
MenuSystem.positionMenuLeftRight = function(menu, elmt) { MenuSystem.positionMenuLeftRight = function(menu, elmt) {
var offset = elmt.offset(); var offset = elmt.offset();
menu.css("top", offset.top + "px") var windowWidth = $(document.body).innerWidth();
.css("left", (offset.left + elmt.outerWidth()) + "px"); var windowHeight = $(document.body).innerHeight();
if (offset.top - document.body.scrollTop + menu.outerHeight() > windowHeight - 10) {
menu.css("top", (offset.top + elmt.outerHeight() - menu.outerHeight()) + "px");
} else {
menu.css("top", offset.top + "px");
}
if (offset.left + elmt.outerWidth() - document.body.scrollLeft + menu.outerWidth() > windowWidth - 10) {
menu.css("left", (offset.left - menu.outerWidth()) + "px");
} else {
menu.css("left", (offset.left + elmt.outerWidth()) + "px");
}
}; };
MenuSystem.createAndShowStandardMenu = function(items, elmt, options) { MenuSystem.createAndShowStandardMenu = function(items, elmt, options) {
@ -100,7 +122,10 @@ MenuSystem.createAndShowStandardMenu = function(items, elmt, options) {
); );
}); });
} else { } else {
menuItem.html(item.label).click(item.click); menuItem.html(item.label).click(function(evt) {
item.click.call(this, evt);
MenuSystem.dismissAll();
});
if ("tooltip" in item) { if ("tooltip" in item) {
menuItem.attr("title", item.tooltip); menuItem.attr("title", item.tooltip);
} }

View File

@ -12,21 +12,31 @@ DataTableCellUI.prototype._render = function() {
var self = this; var self = this;
var cell = this._cell; var cell = this._cell;
$(this._td).empty(); var divContent = $('<div/>')
var divContent = $('<div></div>').appendTo(this._td); .addClass("data-table-cell-content");
var editLink = $('<a href="javascript:{}" />')
.addClass("data-table-cell-edit")
.text("edit")
.appendTo(divContent)
.click(function() { self._startEdit(this); });
$(this._td).empty()
.unbind()
.mouseenter(function() { editLink.css("visibility", "visible"); })
.mouseleave(function() { editLink.css("visibility", "hidden"); });
if (cell == null || ("v" in cell && cell.v == null)) { if (cell == null || ("v" in cell && cell.v == null)) {
$(divContent).html("&nbsp;"); $('<span>').html("&nbsp;").appendTo(divContent);
} else if ("e" in cell) { } else if ("e" in cell) {
$('<span>').addClass("data-table-error").text(cell.e).appendTo(divContent); $('<span>').addClass("data-table-error").text(cell.e).appendTo(divContent);
} else if (!("r" in cell) || cell.r == null) { } else if (!("r" in cell) || cell.r == null) {
$(divContent).text(cell.v); $('<span>').text(cell.v).appendTo(divContent);
} else { } else {
var r = cell.r; var r = cell.r;
if (r.j == "new") { if (r.j == "new") {
$(divContent).text(cell.v + " (new topic)"); $('<span>').text(cell.v + " (new topic) ").appendTo(divContent);
$('<span> </span>').appendTo(divContent);
$('<a href="javascript:{}">re-match</a>') $('<a href="javascript:{}">re-match</a>')
.addClass("data-table-recon-action") .addClass("data-table-recon-action")
.appendTo(divContent).click(function(evt) { .appendTo(divContent).click(function(evt) {
@ -35,19 +45,21 @@ DataTableCellUI.prototype._render = function() {
} else if (r.j == "matched" && "m" in r && r.m != null) { } else if (r.j == "matched" && "m" in r && r.m != null) {
var match = cell.r.m; var match = cell.r.m;
$('<a></a>') $('<a></a>')
.text(match.name)
.attr("href", "http://www.freebase.com/view" + match.id) .attr("href", "http://www.freebase.com/view" + match.id)
.attr("target", "_blank") .attr("target", "_blank")
.text(match.name)
.appendTo(divContent); .appendTo(divContent);
$('<span> </span>').appendTo(divContent); $('<span> </span>').appendTo(divContent);
$('<a href="javascript:{}">re-match</a>') $('<a href="javascript:{}"></a>')
.text("re-match")
.addClass("data-table-recon-action") .addClass("data-table-recon-action")
.appendTo(divContent).click(function(evt) { .appendTo(divContent)
.click(function(evt) {
self._doRematch(); self._doRematch();
}); });
} else { } else {
$(divContent).text(cell.v); $('<span>').text(cell.v).appendTo(divContent);
if (this._dataTableView._showRecon) { if (this._dataTableView._showRecon) {
var ul = $('<div></div>').addClass("data-table-recon-candidates").appendTo(divContent); var ul = $('<div></div>').addClass("data-table-recon-candidates").appendTo(divContent);
@ -118,6 +130,8 @@ DataTableCellUI.prototype._render = function() {
} }
} }
} }
divContent.appendTo(this._td)
}; };
DataTableCellUI.prototype._doRematch = function() { DataTableCellUI.prototype._doRematch = function() {
@ -270,3 +284,87 @@ DataTableCellUI.prototype._previewCandidateTopic = function(id, elmt) {
MenuSystem.showMenu(fakeMenu, function(){}); MenuSystem.showMenu(fakeMenu, function(){});
MenuSystem.positionMenuLeftRight(fakeMenu, $(elmt)); MenuSystem.positionMenuLeftRight(fakeMenu, $(elmt));
}; };
DataTableCellUI.prototype._startEdit = function(elmt) {
self = this;
var menu = MenuSystem.createMenu().width("300px");
menu.html(
'<textarea class="data-table-cell-edit-editor" bind="textarea" />' +
'<table class="data-table-cell-edit-layout">' +
'<tr>' +
'<td>' +
'<input type="radio" name="data-table-cell-edit-type" value="text" checked /> text ' +
'<input type="radio" name="data-table-cell-edit-type" value="number" /> number ' +
'<input type="radio" name="data-table-cell-edit-type" value="boolean" /> boolean' +
'<input type="radio" name="data-table-cell-edit-type" value="date" /> date' +
'</td>' +
'<td width="1%">' +
'<button bind="okButton">&nbsp;&nbsp;OK&nbsp;&nbsp;</button>' +
'</td>' +
'</tr>' +
'</table>'
);
var elmts = DOM.bind(menu);
MenuSystem.showMenu(menu, function(){});
MenuSystem.positionMenuLeftRight(menu, $(elmt));
var commit = function() {
var type = $('input["data-table-cell-edit-type"]:checked')[0].value;
var text = elmts.textarea[0].value;
var value = text;
if (type == "number") {
value = parseFloat(text);
if (isNaN(value)) {
alert("Not a valid number.");
return;
}
} else if (type == "boolean") {
value = ("true" == text);
} else if (type == "date") {
value = Date.parse(text);
if (value == null) {
alert("Not a valid date.");
return;
}
value = value.toString("yyyy-MM-ddTHH:mm:ssZ");
}
MenuSystem.dismissAll();
var params = {
row: self._rowIndex,
cell: self._cellIndex,
value: value,
type: type
};
Gridworks.postProcess(
"edit-one-cell",
params,
null,
{},
{
onDone: function(o) {
self._cell = o.cell;
self._render();
}
}
);
};
elmts.okButton.click(commit);
elmts.textarea
.text(this._cell == null || ("v" in this._cell && this._cell.v == null) ? "" : this._cell.v)
.keydown(function(evt) {
if (evt.keyCode == 13) {
commit();
} else if (evt.keyCode == 27) {
MenuSystem.dismissAll();
}
})
.select()
.focus();
};

View File

@ -59,6 +59,25 @@ img.column-header-menu {
margin: 1em 0; margin: 1em 0;
} }
div.data-table-cell-content {
position: relative;
}
a.data-table-cell-edit {
position: absolute;
top: 0px;
right: 0px;
padding: 2px 4px;
font-size: 80%;
font-weight: bold;
text-decoration: none;
background: #aaf;
color: #eee;
visibility: hidden;
}
a.data-table-cell-edit:hover {
background: #88f;
color: white;
}
.data-table-error { .data-table-error {
color: red; color: red;
@ -143,3 +162,18 @@ a.data-table-star-on {
a.data-table-star-off { a.data-table-star-off {
background-position: -17px 0px; background-position: -17px 0px;
} }
table.data-table-cell-edit-layout {
width: 100%;
}
table.data-table-cell-edit-layout td {
padding: 5px;
}
textarea.data-table-cell-edit-editor {
display: block;
width: 92%;
padding: 2%;
margin: 2%;
height: 3em;
}