summaryrefslogtreecommitdiffstats
path: root/server
diff options
context:
space:
mode:
authorHenrik Paul <henrik@vaadin.com>2014-08-18 12:43:03 +0300
committerHenrik Paul <henrik@vaadin.com>2014-08-27 06:48:38 +0000
commit5ba20eaf2582655d2609740236672dd2cbc51ddc (patch)
treebd8f56e48121aceee0a16925c8654445a9ca96f8 /server
parentec2ecdf0e79ee3c13f0e16192c8000930e525e8a (diff)
downloadvaadin-framework-5ba20eaf2582655d2609740236672dd2cbc51ddc.tar.gz
vaadin-framework-5ba20eaf2582655d2609740236672dd2cbc51ddc.zip
Server-side editor row (#13334)
Change-Id: Ia84c8f0a00549318e35e2c844b6ec6c419cfa4f3
Diffstat (limited to 'server')
-rw-r--r--server/src/com/vaadin/ui/components/grid/EditorRow.java373
-rw-r--r--server/src/com/vaadin/ui/components/grid/Grid.java21
-rw-r--r--server/tests/src/com/vaadin/tests/server/component/grid/EditorRowTests.java234
3 files changed, 628 insertions, 0 deletions
diff --git a/server/src/com/vaadin/ui/components/grid/EditorRow.java b/server/src/com/vaadin/ui/components/grid/EditorRow.java
new file mode 100644
index 0000000000..5eb38156e2
--- /dev/null
+++ b/server/src/com/vaadin/ui/components/grid/EditorRow.java
@@ -0,0 +1,373 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.ui.components.grid;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+
+import com.vaadin.data.Container;
+import com.vaadin.data.Item;
+import com.vaadin.data.fieldgroup.FieldGroup;
+import com.vaadin.data.fieldgroup.FieldGroup.BindException;
+import com.vaadin.data.fieldgroup.FieldGroup.CommitException;
+import com.vaadin.data.fieldgroup.FieldGroupFieldFactory;
+import com.vaadin.ui.Field;
+
+/**
+ * A class for configuring the editor row in a grid.
+ *
+ * @since
+ * @author Vaadin Ltd
+ * @see Grid
+ */
+public class EditorRow implements Serializable {
+ private final Container container;
+
+ private boolean isEnabled;
+ private FieldGroup fieldGroup = new FieldGroup();
+ private Object editedItemId = null;
+
+ private boolean isDetached = false;
+
+ private HashSet<Object> uneditableProperties = new HashSet<Object>();
+
+ /**
+ * Constructs a new editor row bound to a particular container.
+ *
+ * @param container
+ * the container this editor row is bound to
+ */
+ EditorRow(Container container) {
+ this.container = container;
+ }
+
+ /**
+ * Checks whether the editor row feature is enabled for the grid or not.
+ *
+ * @return <code>true</code> iff the editor row feature is enabled for the
+ * grid
+ * @see #getEditedItemId()
+ */
+ public boolean isEnabled() {
+ checkDetached();
+ return isEnabled;
+ }
+
+ /**
+ * Sets whether or not the editor row feature is enabled for the grid.
+ *
+ * @param isEnabled
+ * <code>true</code> to enable the feature, <code>false</code>
+ * otherwise
+ * @throws IllegalStateException
+ * if an item is currently being edited
+ * @see #getEditedItemId()
+ */
+ public void setEnabled(boolean isEnabled) throws IllegalStateException {
+ checkDetached();
+ if (getEditedItemId() != null) {
+ throw new IllegalStateException("Cannot disable the editor row "
+ + "while an item (" + getEditedItemId()
+ + ") is being edited.");
+ }
+ this.isEnabled = isEnabled;
+ }
+
+ /**
+ * Gets the field group that is backing this editor row.
+ *
+ * @return the backing field group
+ */
+ public FieldGroup getFieldGroup() {
+ checkDetached();
+ return fieldGroup;
+ }
+
+ /**
+ * Sets the field group that is backing this editor row.
+ *
+ * @param fieldGroup
+ * the backing field group
+ */
+ public void setFieldGroup(FieldGroup fieldGroup) {
+ checkDetached();
+ this.fieldGroup = fieldGroup;
+ if (editedItemId != null) {
+ this.fieldGroup.setItemDataSource(container.getItem(editedItemId));
+ }
+ }
+
+ /**
+ * Builds a field using the given caption and binds it to the given property
+ * id using the field binder. Ensures the new field is of the given type.
+ * <p>
+ * <em>Note:</em> This is a pass-through call to the backing field group.
+ *
+ * @param propertyId
+ * The property id to bind to. Must be present in the field
+ * finder
+ * @param fieldType
+ * The type of field that we want to create
+ * @throws BindException
+ * If the field could not be created
+ * @return The created and bound field. Can be any type of {@link Field}.
+ */
+ public <T extends Field<?>> T buildAndBind(Object propertyId,
+ Class<T> fieldComponent) throws BindException {
+ checkDetached();
+ return fieldGroup.buildAndBind(null, propertyId, fieldComponent);
+ }
+
+ /**
+ * Binds the field with the given propertyId from the current item. If an
+ * item has not been set then the binding is postponed until the item is set
+ * using {@link #editItem(Object)}.
+ * <p>
+ * This method also adds validators when applicable.
+ * <p>
+ * <em>Note:</em> This is a pass-through call to the backing field group.
+ *
+ * @param field
+ * The field to bind
+ * @param propertyId
+ * The propertyId to bind to the field
+ * @throws BindException
+ * If the property id is already bound to another field by this
+ * field binder
+ */
+ public void bind(Object propertyId, Field<?> field) throws BindException {
+ checkDetached();
+ fieldGroup.bind(field, propertyId);
+ }
+
+ /**
+ * Sets the field factory for the {@link FieldGroup}. The field factory is
+ * only used when {@link FieldGroup} creates a new field.
+ * <p>
+ * <em>Note:</em> This is a pass-through call to the backing field group.
+ *
+ * @param fieldFactory
+ * The field factory to use
+ */
+ public void setFieldFactory(FieldGroupFieldFactory factory) {
+ checkDetached();
+ fieldGroup.setFieldFactory(factory);
+ }
+
+ /**
+ * Gets the field component that represents a property.
+ * <p>
+ * If the property is not yet bound to a field, it will be bound during this
+ * call. Otherwise the previously bound field will be used.
+ *
+ * @param propertyId
+ * the property id of the property for which to find the field
+ * @see #setPropertyUneditable(Object)
+ */
+ public Field<?> getField(Object propertyId) {
+ checkDetached();
+
+ final Field<?> field;
+ if (fieldGroup.getUnboundPropertyIds().contains(propertyId)) {
+ field = fieldGroup.buildAndBind(propertyId);
+ } else {
+ field = fieldGroup.getField(propertyId);
+ }
+
+ if (field != null) {
+ boolean readonly = fieldGroup.isReadOnly()
+ || field.getPropertyDataSource().isReadOnly()
+ || !isPropertyEditable(propertyId);
+ field.setReadOnly(readonly);
+ }
+
+ return field;
+ }
+
+ /**
+ * Sets a property editable or not.
+ * <p>
+ * In order for a user to edit a particular value with a Field, it needs to
+ * be both non-readonly and editable.
+ * <p>
+ * The difference between read-only and uneditable is that the read-only
+ * state is propagated back into the property, while the editable property
+ * is internal metadata for the editor row.
+ *
+ * @param propertyId
+ * the id of the property to set as editable state
+ * @param editable
+ * whether or not {@code propertyId} chould be editable
+ */
+ public void setPropertyEditable(Object propertyId, boolean editable) {
+ checkDetached();
+ checkPropertyExists(propertyId);
+ if (editable) {
+ uneditableProperties.remove(propertyId);
+ } else {
+ uneditableProperties.add(propertyId);
+ }
+ }
+
+ /**
+ * Checks whether a property is uneditable or not.
+ * <p>
+ * This only checks whether the property is configured as uneditable in this
+ * editor row. The property's or field's readonly status will ultimately
+ * decide whether the value can be edited or not.
+ *
+ * @param propertyId
+ * the id of the property to check for editable status
+ * @return <code>true</code> iff the property is editable according to this
+ * editor row
+ */
+ public boolean isPropertyEditable(Object propertyId) {
+ checkDetached();
+ checkPropertyExists(propertyId);
+ return !uneditableProperties.contains(propertyId);
+ }
+
+ /**
+ * Commits all changes done to the bound fields.
+ * <p>
+ * <em>Note:</em> This is a pass-through call to the backing field group.
+ *
+ * @throws CommitException
+ * If the commit was aborted
+ */
+ public void commit() throws CommitException {
+ checkDetached();
+ fieldGroup.commit();
+ }
+
+ /**
+ * Discards all changes done to the bound fields.
+ * <p>
+ * <em>Note:</em> This is a pass-through call to the backing field group.
+ */
+ public void discard() {
+ checkDetached();
+ fieldGroup.discard();
+ }
+
+ /**
+ * Internal method to inform the editor row that it is no longer attached to
+ * a Grid.
+ */
+ void detach() {
+ checkDetached();
+ isDetached = true;
+ }
+
+ /**
+ * Sets an item as editable.
+ *
+ * @param itemId
+ * the id of the item to edit
+ * @throws IllegalStateException
+ * if the editor row is not enabled
+ * @throws IllegalArgumentException
+ * if the {@code itemId} is not in the backing container
+ * @see #setEnabled(boolean)
+ */
+ public void editItem(Object itemId) throws IllegalStateException,
+ IllegalArgumentException {
+ checkDetached();
+
+ if (!isEnabled()) {
+ throw new IllegalStateException("This "
+ + getClass().getSimpleName() + " is not enabled");
+ }
+
+ Item item = container.getItem(itemId);
+ if (item == null) {
+ throw new IllegalArgumentException("Item with id " + itemId
+ + " not found in current container");
+ }
+
+ fieldGroup.setItemDataSource(item);
+ editedItemId = itemId;
+ }
+
+ /**
+ * Gets the id of the item that is currently being edited.
+ *
+ * @return the id of the item that is currently being edited, or
+ * <code>null</code> if no item is being edited at the moment
+ */
+ public Object getEditedItemId() {
+ checkDetached();
+ return editedItemId;
+ }
+
+ /**
+ * Gets a collection of all fields represented by this editor row.
+ * <p>
+ * All non-editable fields (either readonly or uneditable) are in read-only
+ * mode.
+ *
+ * @return a collection of all the fields represented by this editor row
+ */
+ Collection<Field<?>> getFields() {
+ checkDetached();
+
+ /*
+ * Maybe this isn't the best idea, however. Maybe the components should
+ * always be transferred over the wire, to increase up-front load-time
+ * and decrease on-demand load-time.
+ */
+ if (!isEnabled()) {
+ return Collections.emptySet();
+ }
+
+ for (Object propertyId : fieldGroup.getUnboundPropertyIds()) {
+ fieldGroup.buildAndBind(propertyId);
+ }
+
+ /*
+ * We'll collect this ourselves instead of asking fieldGroup.getFields()
+ * because we might have marked something as uneditable even though it
+ * might not read-only.
+ */
+ ArrayList<Field<?>> fields = new ArrayList<Field<?>>();
+ for (Object propertyId : container.getContainerPropertyIds()) {
+ Field<?> field = getField(propertyId);
+ if (field != null) {
+ fields.add(field);
+ }
+ }
+
+ return fields;
+ }
+
+ private void checkDetached() throws IllegalStateException {
+ if (isDetached) {
+ throw new IllegalStateException("The method cannot be "
+ + "processed as this " + getClass().getSimpleName()
+ + " has become detached.");
+ }
+ }
+
+ private void checkPropertyExists(Object propertyId) {
+ if (!container.getContainerPropertyIds().contains(propertyId)) {
+ throw new IllegalArgumentException("Property with id " + propertyId
+ + " is not in the current Container");
+ }
+ }
+}
diff --git a/server/src/com/vaadin/ui/components/grid/Grid.java b/server/src/com/vaadin/ui/components/grid/Grid.java
index 3c115f9241..f6a1231f43 100644
--- a/server/src/com/vaadin/ui/components/grid/Grid.java
+++ b/server/src/com/vaadin/ui/components/grid/Grid.java
@@ -257,6 +257,8 @@ public class Grid extends AbstractComponent implements SelectionChangeNotifier,
private final GridHeader header = new GridHeader(this);
private final GridFooter footer = new GridFooter(this);
+ private EditorRow editorRow;
+
private static final Method SELECTION_CHANGE_METHOD = ReflectTools
.findMethod(SelectionChangeListener.class, "selectionChange",
SelectionChangeEvent.class);
@@ -420,6 +422,15 @@ public class Grid extends AbstractComponent implements SelectionChangeNotifier,
datasource = container;
+ /*
+ * This is null when this method is called the first time in the
+ * constructor
+ */
+ if (editorRow != null) {
+ editorRow.detach();
+ }
+ editorRow = new EditorRow(datasource);
+
//
// Adjust sort order
//
@@ -1302,6 +1313,16 @@ public class Grid extends AbstractComponent implements SelectionChangeNotifier,
}
}
+ componentList.addAll(getEditorRow().getFields());
return componentList.iterator();
}
+
+ /**
+ * Gets the editor row configuration object.
+ *
+ * @return the editor row configuration object
+ */
+ public EditorRow getEditorRow() {
+ return editorRow;
+ }
}
diff --git a/server/tests/src/com/vaadin/tests/server/component/grid/EditorRowTests.java b/server/tests/src/com/vaadin/tests/server/component/grid/EditorRowTests.java
new file mode 100644
index 0000000000..36c541c99c
--- /dev/null
+++ b/server/tests/src/com/vaadin/tests/server/component/grid/EditorRowTests.java
@@ -0,0 +1,234 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.tests.server.component.grid;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import com.vaadin.data.Item;
+import com.vaadin.data.util.IndexedContainer;
+import com.vaadin.ui.Field;
+import com.vaadin.ui.TextField;
+import com.vaadin.ui.components.grid.EditorRow;
+import com.vaadin.ui.components.grid.Grid;
+
+public class EditorRowTests {
+
+ private static final Object PROPERTY_NAME = "name";
+ private static final Object PROPERTY_AGE = "age";
+ private static final Object ITEM_ID = new Object();
+
+ private Grid grid;
+ private EditorRow row;
+
+ @Before
+ @SuppressWarnings("unchecked")
+ public void setup() {
+ IndexedContainer container = new IndexedContainer();
+ container.addContainerProperty(PROPERTY_NAME, String.class, "[name]");
+ container.addContainerProperty(PROPERTY_AGE, Integer.class,
+ Integer.valueOf(-1));
+
+ Item item = container.addItem(ITEM_ID);
+ item.getItemProperty(PROPERTY_NAME).setValue("Some Valid Name");
+ item.getItemProperty(PROPERTY_AGE).setValue(Integer.valueOf(25));
+
+ grid = new Grid(container);
+ row = grid.getEditorRow();
+ }
+
+ @Test
+ public void initAssumptions() throws Exception {
+ assertNotNull(row);
+ assertFalse(row.isEnabled());
+ assertNull(row.getEditedItemId());
+ assertNotNull(row.getFieldGroup());
+ }
+
+ @Test
+ public void setEnabled() throws Exception {
+ assertFalse(row.isEnabled());
+ row.setEnabled(true);
+ assertTrue(row.isEnabled());
+ }
+
+ @Test
+ public void setDisabled() throws Exception {
+ assertFalse(row.isEnabled());
+ row.setEnabled(true);
+ row.setEnabled(false);
+ assertFalse(row.isEnabled());
+ }
+
+ @Test
+ public void setReEnabled() throws Exception {
+ assertFalse(row.isEnabled());
+ row.setEnabled(true);
+ row.setEnabled(false);
+ row.setEnabled(true);
+ assertTrue(row.isEnabled());
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void detached() throws Exception {
+ EditorRow oldEditorRow = row;
+ grid.setContainerDataSource(new IndexedContainer());
+ oldEditorRow.isEnabled();
+ }
+
+ @Test
+ public void propertyUneditable() throws Exception {
+ row.setPropertyEditable(PROPERTY_NAME, false);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void nonexistentPropertyUneditable() throws Exception {
+ row.setPropertyEditable(new Object(), false);
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void disabledEditItem() throws Exception {
+ row.editItem(ITEM_ID);
+ }
+
+ @Test
+ public void editItem() throws Exception {
+ startEdit();
+ assertEquals(ITEM_ID, row.getEditedItemId());
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void nonexistentEditItem() throws Exception {
+ row.setEnabled(true);
+ row.editItem(new Object());
+ }
+
+ @Test
+ public void getField() throws Exception {
+ startEdit();
+
+ assertNotNull(row.getField(PROPERTY_NAME));
+ }
+
+ @Test
+ public void getFieldWithoutItem() throws Exception {
+ row.setEnabled(true);
+ assertNull(row.getField(PROPERTY_NAME));
+ }
+
+ @Test
+ public void getFieldAfterReSettingFieldAsEditable() throws Exception {
+ startEdit();
+
+ row.setPropertyEditable(PROPERTY_NAME, false);
+ row.setPropertyEditable(PROPERTY_NAME, true);
+ assertNotNull(row.getField(PROPERTY_NAME));
+ }
+
+ @Test
+ public void isEditable() {
+ assertTrue(row.isPropertyEditable(PROPERTY_NAME));
+ }
+
+ @Test
+ public void isUneditable() {
+ row.setPropertyEditable(PROPERTY_NAME, false);
+ assertFalse(row.isPropertyEditable(PROPERTY_NAME));
+ }
+
+ @Test
+ public void isEditableAgain() {
+ row.setPropertyEditable(PROPERTY_NAME, false);
+ row.setPropertyEditable(PROPERTY_NAME, true);
+ assertTrue(row.isPropertyEditable(PROPERTY_NAME));
+ }
+
+ @Test
+ public void isUneditableAgain() {
+ row.setPropertyEditable(PROPERTY_NAME, false);
+ row.setPropertyEditable(PROPERTY_NAME, true);
+ row.setPropertyEditable(PROPERTY_NAME, false);
+ assertFalse(row.isPropertyEditable(PROPERTY_NAME));
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void isNonexistentEditable() {
+ row.isPropertyEditable(new Object());
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void setNonexistentUneditable() {
+ row.setPropertyEditable(new Object(), false);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void setNonexistentEditable() {
+ row.setPropertyEditable(new Object(), true);
+ }
+
+ @Test
+ public void customBinding() {
+ startEdit();
+
+ TextField textField = new TextField();
+ row.bind(PROPERTY_NAME, textField);
+ assertSame(textField, row.getField(PROPERTY_NAME));
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void disableWhileEditing() {
+ startEdit();
+ row.setEnabled(false);
+ }
+
+ @Test
+ public void fieldIsNotReadonly() {
+ startEdit();
+
+ Field<?> field = row.getField(PROPERTY_NAME);
+ assertFalse(field.isReadOnly());
+ }
+
+ @Test
+ public void fieldIsReadonlyWhenFieldGroupIsReadonly() {
+ startEdit();
+
+ row.getFieldGroup().setReadOnly(true);
+ Field<?> field = row.getField(PROPERTY_NAME);
+ assertTrue(field.isReadOnly());
+ }
+
+ @Test
+ public void fieldIsReadonlyWhenPropertyIsNotEditable() {
+ startEdit();
+
+ row.setPropertyEditable(PROPERTY_NAME, false);
+ Field<?> field = row.getField(PROPERTY_NAME);
+ assertTrue(field.isReadOnly());
+ }
+
+ private void startEdit() {
+ row.setEnabled(true);
+ row.editItem(ITEM_ID);
+ }
+}