From 024b44bbc2ad4e7b0350c4170e3eeb6c59e97562 Mon Sep 17 00:00:00 2001
From: buczekp <buczekp>
Date: Tue, 7 Jun 2011 12:58:34 +0000
Subject: [PATCH] [LMS-2281] bulk table updates

SVN: 21609
---
 .../web/client/ICommonClientService.java      |  10 +-
 .../web/client/ICommonClientServiceAsync.java |   8 +
 .../application/IGenericImageBundle.java      |   8 +-
 .../client/web/client/application/cancel.gif  | Bin 0 -> 949 bytes
 .../client/web/client/application/confirm.gif | Bin 0 -> 1016 bytes
 .../ui/grid/AbstractBrowserGrid.java          | 217 +++++++++++++++++-
 .../ui/grid/AbstractEntityBrowserGrid.java    |  22 +-
 .../ui/grid/AbstractEntityGrid.java           |  37 +--
 .../ui/grid/ColumnSettingsChooser.java        |  12 +-
 .../ui/grid/IBrowserGridActionInvoker.java    |   5 +
 .../managed_property/ManagedPropertyGrid.java |  10 +
 .../ManagedPropertyGridActionDialog.java      |   3 +-
 .../web/client/application/util/GWTUtils.java |  38 ++-
 .../web/client/dto/EntityPropertyUpdates.java |  88 +++++++
 .../dto/EntityPropertyUpdatesResult.java      |  56 +++++
 .../web/server/CommonClientService.java       |  62 ++++-
 .../openbis/generic/server/CommonServer.java  |  28 +--
 .../generic/server/CommonServerLogger.java    |  32 +--
 .../openbis/generic/shared/ICommonServer.java |  10 +-
 .../shared/ICommonServer.java.expected        |  10 +-
 20 files changed, 565 insertions(+), 91 deletions(-)
 create mode 100644 openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/cancel.gif
 create mode 100644 openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/confirm.gif
 create mode 100644 openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/dto/EntityPropertyUpdates.java
 create mode 100644 openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/dto/EntityPropertyUpdatesResult.java

diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/ICommonClientService.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/ICommonClientService.java
index aedbfd1c3bb..70ec303b5bb 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/ICommonClientService.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/ICommonClientService.java
@@ -25,6 +25,8 @@ import ch.systemsx.cisd.openbis.generic.client.web.client.dto.DefaultResultSetCo
 import ch.systemsx.cisd.openbis.generic.client.web.client.dto.DisplayedCriteriaOrSelectedEntityHolder;
 import ch.systemsx.cisd.openbis.generic.client.web.client.dto.DisplayedOrSelectedDatasetCriteria;
 import ch.systemsx.cisd.openbis.generic.client.web.client.dto.DisplayedOrSelectedIdHolderCriteria;
+import ch.systemsx.cisd.openbis.generic.client.web.client.dto.EntityPropertyUpdates;
+import ch.systemsx.cisd.openbis.generic.client.web.client.dto.EntityPropertyUpdatesResult;
 import ch.systemsx.cisd.openbis.generic.client.web.client.dto.IResultSetConfig;
 import ch.systemsx.cisd.openbis.generic.client.web.client.dto.ListExperimentsCriteria;
 import ch.systemsx.cisd.openbis.generic.client.web.client.dto.ListMaterialDisplayCriteria;
@@ -1043,11 +1045,17 @@ public interface ICommonClientService extends IClientService
      * Returns script evaluation result.
      */
     public String evaluate(DynamicPropertyEvaluationInfo info) throws UserFailureException;
-    
+
     /**
      * Updates specified property for specified entity of specified kind.
      */
     public void updateProperty(EntityKind kind, TechId entityId, String propertyColumnName,
             String value) throws UserFailureException;
 
+    /**
+     * Updates specified properties of an entity.
+     */
+    public EntityPropertyUpdatesResult updateProperties(EntityPropertyUpdates updates)
+            throws UserFailureException;
+
 }
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/ICommonClientServiceAsync.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/ICommonClientServiceAsync.java
index 29599849453..ab5124d4901 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/ICommonClientServiceAsync.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/ICommonClientServiceAsync.java
@@ -27,6 +27,8 @@ import ch.systemsx.cisd.openbis.generic.client.web.client.dto.DefaultResultSetCo
 import ch.systemsx.cisd.openbis.generic.client.web.client.dto.DisplayedCriteriaOrSelectedEntityHolder;
 import ch.systemsx.cisd.openbis.generic.client.web.client.dto.DisplayedOrSelectedDatasetCriteria;
 import ch.systemsx.cisd.openbis.generic.client.web.client.dto.DisplayedOrSelectedIdHolderCriteria;
+import ch.systemsx.cisd.openbis.generic.client.web.client.dto.EntityPropertyUpdates;
+import ch.systemsx.cisd.openbis.generic.client.web.client.dto.EntityPropertyUpdatesResult;
 import ch.systemsx.cisd.openbis.generic.client.web.client.dto.IResultSetConfig;
 import ch.systemsx.cisd.openbis.generic.client.web.client.dto.ListExperimentsCriteria;
 import ch.systemsx.cisd.openbis.generic.client.web.client.dto.ListMaterialDisplayCriteria;
@@ -938,4 +940,10 @@ public interface ICommonClientServiceAsync extends IClientServiceAsync
      */
     public void updateProperty(EntityKind kind, TechId entityId, String propertyColumnName,
             String value, AsyncCallback<Void> callback);
+
+    /**
+     * @see ICommonClientService#updateProperties(EntityPropertyUpdates)
+     */
+    public void updateProperties(EntityPropertyUpdates entityPropertyUpdates,
+            AsyncCallback<EntityPropertyUpdatesResult> callback);
 }
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/IGenericImageBundle.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/IGenericImageBundle.java
index 10d99e1d660..40b403217a4 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/IGenericImageBundle.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/IGenericImageBundle.java
@@ -52,8 +52,14 @@ public interface IGenericImageBundle extends ClientBundle
      */
     @Source("home.png")
     public ImageResource getHomeIcon();
-    
+
     @Source("editable.gif")
     public ImageResource getEditableIcon();
 
+    @Source("confirm.gif")
+    public ImageResource getConfirmIcon();
+
+    @Source("cancel.gif")
+    public ImageResource getCancelIcon();
+
 }
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/cancel.gif b/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/cancel.gif
new file mode 100644
index 0000000000000000000000000000000000000000..08d083355ff1b4e99b9ef8139f28ede1485b50cf
GIT binary patch
literal 949
zcmZ?wbhEHb6krfwXlGzJz`$^jh2@BVz)=yA<1#WQRaDNJn4Gt<xe^d?H6-L_O3JN_
zj5`GdcS}m1OqlR|&YYJ^mb~7u;q8tc@Am9@f8fA}BS$`-IPvMsnXfl)e1GuZ$CD>N
zU%dGJ;lrOVU;h61@&EsShEXsY0)sdN6o0ZXGcd?A=z!b^$`cG6lNjtdWNtJvwem3w
z^YtV!G#qAN*V6d2fsv7ciC4iUL4l!xsfAfr@4<wCW@k2TMw1yY1RNT;nRrDQEH^A|
zW8}0eVR+z>=-tS}R<nR1!RgR^CV7Sv3JWuQ+c~EuJvb1oaH-KT`P?3@4{7K41*4c|
p%y^LM(XPUt<-x%4^gx%mao(FBg-VB~38?z1XdG;6?*v+G4FKH^bqD|e

literal 0
HcmV?d00001

diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/confirm.gif b/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/confirm.gif
new file mode 100644
index 0000000000000000000000000000000000000000..8aacb307e89d690f46853e01f5c4726bd5d94e31
GIT binary patch
literal 1016
zcmZ?wbhEHb6krfwXlGzhFH%vfSJo_7)vQuAsWC9EH&km;*6LR^?KiY<wYKlJw(qsJ
zpJ`*gP~B&`t<yqdmxacj8`J~0>xFJMjooS=wa?sdwqwu&r?{0KDI0upwuR+x56{~g
zkq<(VSvvztwnvw2k15z6Ua%vwaA$PU&gkM@F@^i$%l9PIZcnS(l~TJWt#)5}{f^9-
z1J*HzZPSi=W*zp-IqIEx!mH#^WYOu+{6mTPhZFOT08vuj(d7JNDFp|U3y&lh98WDi
zo>p==rRYRP$%%~86B%VEGs{k8RUS;KJD6E_Jiqc}cGa2O`cnnX`*Pb46}28MZ8%lj
zaHgpFTzUJ+%FZKY-6tw0oU5O>vwy;#zG=ssCm!gZcDil)nbs*M`lp@kn035;#_6_M
zr`l(nX`gwvYwo%3nHRffUg(*1rFZuAiSsW_n15;F+#8b?UYok``qahOr>(v;d-dhn
ztL{u+dw=%2>kHRkU$E}Z()D+iZN9m5<K5+3ZZ6$&YuTpTE4JQVvE}ZnE%(=Ky}xGL
zg{|8zZry%!>#o~d_ub#R;qm;f57%vfxPJS?4f`H%+y8jS!N=PUJlT2r&He)i4xD~_
z;M%)OH{V=&_T};0@2@}p{P5-1r$2vx|NZy(|Ns9CqkyasQ2fcl%)rpgpaaqk$`cG6
zR~e)^Wjr=4aC9<_3F%-wzQDoVIAhB~=k&AfoLyW-Re?t*%+d(FBC_aGf`Fq$D3_+D
zkjse)Dz(dOBqZEh6jdE-UYxkdEGT3zv4dmE!Dl=ZWi9e<M3iU>%{1g;@!G-s^!P$|
z8==@$A<sHwn3<HXM1D%Sr0N;HdqRStdV>R3<{5^GPA?~^>Pma%d|c$9FpH<AXHEf=
zXAc{n;-`omMgmU4wi-S?A1*Mq%oSth;IcZ9;y*)FYu^J;Muvcc4jg(7m5<rFnVITk
U%ua1eXlP&wOL?s(A;4e_0Qp#8fdBvi

literal 0
HcmV?d00001

diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/ui/grid/AbstractBrowserGrid.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/ui/grid/AbstractBrowserGrid.java
index 37ddd67394a..e4985d1fbc2 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/ui/grid/AbstractBrowserGrid.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/ui/grid/AbstractBrowserGrid.java
@@ -17,10 +17,13 @@
 package ch.systemsx.cisd.openbis.generic.client.web.client.application.ui.grid;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.HashMap;
 import java.util.HashSet;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Map.Entry;
 import java.util.Set;
 
 import com.extjs.gxt.ui.client.GXT;
@@ -49,6 +52,7 @@ import com.extjs.gxt.ui.client.widget.ContentPanel;
 import com.extjs.gxt.ui.client.widget.Dialog;
 import com.extjs.gxt.ui.client.widget.Info;
 import com.extjs.gxt.ui.client.widget.InfoConfig;
+import com.extjs.gxt.ui.client.widget.Label;
 import com.extjs.gxt.ui.client.widget.LayoutContainer;
 import com.extjs.gxt.ui.client.widget.MessageBox;
 import com.extjs.gxt.ui.client.widget.button.Button;
@@ -66,13 +70,17 @@ import com.extjs.gxt.ui.client.widget.menu.Menu;
 import com.extjs.gxt.ui.client.widget.toolbar.PagingToolBar;
 import com.extjs.gxt.ui.client.widget.toolbar.SeparatorToolItem;
 import com.extjs.gxt.ui.client.widget.toolbar.ToolBar;
+import com.google.gwt.core.client.GWT;
 import com.google.gwt.user.client.Element;
 import com.google.gwt.user.client.rpc.AsyncCallback;
+import com.google.gwt.user.client.ui.AbstractImagePrototype;
 
+import ch.systemsx.cisd.common.shared.basic.utils.StringUtils;
 import ch.systemsx.cisd.openbis.generic.client.web.client.ICommonClientServiceAsync;
 import ch.systemsx.cisd.openbis.generic.client.web.client.application.AbstractAsyncCallback;
 import ch.systemsx.cisd.openbis.generic.client.web.client.application.Dict;
 import ch.systemsx.cisd.openbis.generic.client.web.client.application.GenericConstants;
+import ch.systemsx.cisd.openbis.generic.client.web.client.application.IGenericImageBundle;
 import ch.systemsx.cisd.openbis.generic.client.web.client.application.IViewContext;
 import ch.systemsx.cisd.openbis.generic.client.web.client.application.VoidAsyncCallback;
 import ch.systemsx.cisd.openbis.generic.client.web.client.application.framework.AbstractTabItemFactory;
@@ -96,11 +104,14 @@ import ch.systemsx.cisd.openbis.generic.client.web.client.application.ui.columns
 import ch.systemsx.cisd.openbis.generic.client.web.client.application.ui.grid.BrowserGridPagingToolBar.PagingToolBarButtonKind;
 import ch.systemsx.cisd.openbis.generic.client.web.client.application.ui.grid.expressions.filter.FilterToolbar;
 import ch.systemsx.cisd.openbis.generic.client.web.client.application.ui.widget.IDataRefreshCallback;
+import ch.systemsx.cisd.openbis.generic.client.web.client.application.util.GWTUtils;
+import ch.systemsx.cisd.openbis.generic.client.web.client.application.util.GWTUtils.DisplayInfoTime;
 import ch.systemsx.cisd.openbis.generic.client.web.client.application.util.IDelegatedAction;
 import ch.systemsx.cisd.openbis.generic.client.web.client.application.util.IMessageProvider;
 import ch.systemsx.cisd.openbis.generic.client.web.client.application.util.WindowUtils;
 import ch.systemsx.cisd.openbis.generic.client.web.client.dto.Constants;
 import ch.systemsx.cisd.openbis.generic.client.web.client.dto.DefaultResultSetConfig;
+import ch.systemsx.cisd.openbis.generic.client.web.client.dto.EntityPropertyUpdatesResult;
 import ch.systemsx.cisd.openbis.generic.client.web.client.dto.GridCustomColumnInfo;
 import ch.systemsx.cisd.openbis.generic.client.web.client.dto.GridFilters;
 import ch.systemsx.cisd.openbis.generic.client.web.client.dto.GridRowModels;
@@ -208,6 +219,8 @@ public abstract class AbstractBrowserGrid<T/* Entity */, M extends BaseEntityMod
     // used to change displayed filter widgets
     private final FilterToolbar<T> filterToolbar;
 
+    private final ToolBar modificationsToolbar;
+
     private final IDisplayTypeIDGenerator displayTypeIDGenerator;
 
     // --------- private non-final fields
@@ -235,6 +248,9 @@ public abstract class AbstractBrowserGrid<T/* Entity */, M extends BaseEntityMod
         this(viewContext, gridId, false, displayTypeIDGenerator);
     }
 
+    private static final IGenericImageBundle IMAGE_BUNDLE = GWT
+            .<IGenericImageBundle> create(IGenericImageBundle.class);
+
     /**
      * @param refreshAutomatically should the data be automatically loaded when the grid is rendered
      *            for the first time?
@@ -264,6 +280,30 @@ public abstract class AbstractBrowserGrid<T/* Entity */, M extends BaseEntityMod
         pagingToolbar.bind(pagingLoader);
         this.filterToolbar =
                 new FilterToolbar<T>(viewContext, gridId, this, createApplyFiltersDelagator());
+        this.modificationsToolbar = new ToolBar();
+        modificationsToolbar.add(new Label("Table Modifications:"));
+        AbstractImagePrototype confirmIcon =
+                AbstractImagePrototype.create(IMAGE_BUNDLE.getConfirmIcon());
+        AbstractImagePrototype cancelIcon =
+                AbstractImagePrototype.create(IMAGE_BUNDLE.getCancelIcon());
+        modificationsToolbar.add(new Button("Save", confirmIcon,
+                new SelectionListener<ButtonEvent>()
+                    {
+                        @Override
+                        public void componentSelected(ButtonEvent be)
+                        {
+                            asActionInvoker().saveModifications();
+                        }
+                    }));
+        modificationsToolbar.add(new Button("Cancel", cancelIcon,
+                new SelectionListener<ButtonEvent>()
+                    {
+                        @Override
+                        public void componentSelected(ButtonEvent be)
+                        {
+                            asActionInvoker().cancelModifications();
+                        }
+                    }));
 
         this.contentPanel = createEmptyContentPanel();
         bottomToolbars = createBottomToolbars(contentPanel, pagingToolbar);
@@ -942,7 +982,7 @@ public abstract class AbstractBrowserGrid<T/* Entity */, M extends BaseEntityMod
                     if (show)
                     {
                         int logId = log("adding filters");
-                        showFiltersBar();
+                        delegate.showFiltersBar();
                         viewContext.logStop(logId);
                     } else
                     {
@@ -952,12 +992,57 @@ public abstract class AbstractBrowserGrid<T/* Entity */, M extends BaseEntityMod
                         viewContext.logStop(logId);
                     }
                 }
+
+                public void saveModifications()
+                {
+                    finishedModifications = 0;
+                    for (Entry<M, List<IModification>> entry : modificationsByModel.entrySet())
+                    {
+                        applyModifications(entry.getKey(), entry.getValue());
+                    }
+                }
+
+                public void cancelModifications()
+                {
+                    clearModifications();
+                }
             };
     }
 
+    private void clearModifications()
+    {
+        // allModifications = 0;
+        finishedModifications = 0;
+        failedModifications.clear();
+        modificationsByModel.clear();
+        hideModificationsBar();
+        refresh();
+    }
+
     protected void showFiltersBar()
     {
-        bottomToolbars.insert(filterToolbar, 0);
+        // always show filters under modifications
+        int position = bottomToolbars.getItemCount() > 1 ? 1 : 0;
+        bottomToolbars.insert(filterToolbar, position);
+        bottomToolbars.layout();
+    }
+
+    protected void showModificationsBar()
+    {
+        if (bottomToolbars.getItems().contains(modificationsToolbar) == false)
+        {
+            GWTUtils.displayInfo(
+                    "Table Modification Mode On",
+                    "Use 'Table Modification' toolbar below the table to <b>save</b> or <b>cancel</b> the changes made in table cells.",
+                    DisplayInfoTime.LONG);
+            bottomToolbars.insert(modificationsToolbar, 0);
+            bottomToolbars.layout();
+        }
+    }
+
+    protected void hideModificationsBar()
+    {
+        bottomToolbars.remove(modificationsToolbar);
         bottomToolbars.layout();
     }
 
@@ -1732,13 +1817,15 @@ public abstract class AbstractBrowserGrid<T/* Entity */, M extends BaseEntityMod
                     M model = event.getModel();
                     String columnID = event.getProperty();
                     Object value = event.getValue();
-                    if (value != null && value.equals(event.getStartValue())
-                            || value == event.getStartValue())
+                    String oldValueNotNull = StringUtils.toStringEmptyIfNull(value);
+                    String newValueNotNull = StringUtils.toStringEmptyIfNull(event.getStartValue());
+                    if (oldValueNotNull.equals(newValueNotNull))
                     {
                         event.setCancelled(true);
                     } else
                     {
-                        handleEditingEvent(model, columnID, value == null ? null : value.toString());
+                        showModificationsBar();
+                        handleEditingEvent(model, columnID, StringUtils.toStringOrNull(value));
                     }
                 }
             });
@@ -1775,15 +1862,66 @@ public abstract class AbstractBrowserGrid<T/* Entity */, M extends BaseEntityMod
         return null;
     }
 
+    public interface IModification
+    {
+        String getColumnID();
+
+        String tryGetNewValue();
+    }
+
+    private static class Modification implements IModification
+    {
+        private final String columnID;
+
+        private final String newValueOrNull;
+
+        public Modification(String columnID, String newValueOrNull)
+        {
+            super();
+            this.columnID = columnID;
+            this.newValueOrNull = newValueOrNull;
+        }
+
+        public String getColumnID()
+        {
+            return columnID;
+        }
+
+        public String tryGetNewValue()
+        {
+            return newValueOrNull;
+        }
+    }
+
+    private final Map<M, List<String>> failedModifications = new LinkedHashMap<M, List<String>>();
+
+    private int finishedModifications = 0;
+
+    // private int allModifications = 0;
+
+    private final Map<M, List<IModification>> modificationsByModel =
+            new LinkedHashMap<M, List<IModification>>();
+
+    /** Handle cell editing event. */
+    private void handleEditingEvent(M model, String columnID, String newValueOrNull)
+    {
+        List<IModification> modificationsForModel = modificationsByModel.get(model);
+        if (modificationsForModel == null)
+        {
+            modificationsForModel = new ArrayList<IModification>();
+            modificationsByModel.put(model, modificationsForModel);
+        }
+        modificationsForModel.add(new Modification(columnID, newValueOrNull));
+        // allModifications++;
+    }
+
     /**
-     * Handle cell editing event.
-     * 
-     * @param model Row object corresponding to edited cell.
-     * @param columnID ID of cell's column
-     * @param newValueOrNull New value of the cell.
+     * Apply specified modification to the model. Should be overriden by subclasses. Default
+     * implementation does nothing.
      */
-    protected void handleEditingEvent(M model, String columnID, String newValueOrNull)
+    protected void applyModifications(M model, List<IModification> modifications)
     {
+
     }
 
     /**
@@ -1801,6 +1939,63 @@ public abstract class AbstractBrowserGrid<T/* Entity */, M extends BaseEntityMod
             });
     }
 
+    protected AsyncCallback<EntityPropertyUpdatesResult> createApplyModificationsCallback(
+            final M model, final List<IModification> modifications)
+    {
+        return new AbstractAsyncCallback<EntityPropertyUpdatesResult>(viewContext)
+            {
+                @Override
+                protected void process(EntityPropertyUpdatesResult result)
+                {
+                    finishedModifications++;
+                    if (result.getErrors().size() > 0)
+                    {
+                        handleErrors(result.getErrors());
+                    }
+                    if (isApplyModificationsComplete())
+                    {
+                        onApplyModificationsComplete();
+                    }
+                }
+
+                @Override
+                public void finishOnFailure(Throwable caught)
+                {
+                    finishedModifications++;
+                    handleErrors(Arrays.asList(caught.getMessage()));
+                    if (isApplyModificationsComplete())
+                    {
+                        onApplyModificationsComplete();
+                    }
+                }
+
+                private void handleErrors(List<String> errors)
+                {
+                    failedModifications.put(model, errors);
+                }
+            };
+    }
+
+    private boolean isApplyModificationsComplete()
+    {
+        return finishedModifications == modificationsByModel.size();
+    }
+
+    private void onApplyModificationsComplete()
+    {
+        if (failedModifications.size() > 0)
+        {
+            MessageBox.alert("Operation failed", failedModifications.size()
+                    + " modification(s) couldn't be applied.", null);
+            refresh();
+        } else
+        {
+            GWTUtils.displayInfo("All modifications successfully applied.");
+            refresh();
+        }
+        clearModifications();
+    }
+
     /**
      * Creates a callback object which invokes specified refresh action after server-side editing
      * action took place.
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/ui/grid/AbstractEntityBrowserGrid.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/ui/grid/AbstractEntityBrowserGrid.java
index 21c8e912f5d..5ab38705fdb 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/ui/grid/AbstractEntityBrowserGrid.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/ui/grid/AbstractEntityBrowserGrid.java
@@ -39,6 +39,7 @@ import ch.systemsx.cisd.openbis.generic.client.web.client.application.ui.widget.
 import ch.systemsx.cisd.openbis.generic.client.web.client.application.util.IDelegatedAction;
 import ch.systemsx.cisd.openbis.generic.client.web.client.application.util.SetUtils;
 import ch.systemsx.cisd.openbis.generic.client.web.client.dto.DefaultResultSetConfig;
+import ch.systemsx.cisd.openbis.generic.client.web.client.dto.EntityPropertyUpdates;
 import ch.systemsx.cisd.openbis.generic.shared.basic.TechId;
 import ch.systemsx.cisd.openbis.generic.shared.basic.dto.DatabaseModificationKind;
 import ch.systemsx.cisd.openbis.generic.shared.basic.dto.DatabaseModificationKind.ObjectKind;
@@ -130,7 +131,7 @@ public abstract class AbstractEntityBrowserGrid<T extends IEntityPropertiesHolde
                 }
             };
     }
-    
+
     @Override
     protected boolean isEditable(M model, String columnID)
     {
@@ -145,13 +146,20 @@ public abstract class AbstractEntityBrowserGrid<T extends IEntityPropertiesHolde
     }
 
     @Override
-    protected void handleEditingEvent(M model, String columnID, String newValueOrNull)
+    protected void applyModifications(M model, List<IModification> modifications)
     {
-        EntityKind entityKind = getEntityKind();
-        TechId entityId = new TechId(model.getBaseObject().getId());
-        String propertyName = columnID.substring(EntityPropertyColDef.PROPERTY_PREFIX.length());
-        viewContext.getService().updateProperty(entityKind, entityId, propertyName,
-                newValueOrNull, createPostEditingRefreshCallback());
+        final EntityKind entityKind = getEntityKind();
+        final TechId entityId = new TechId(model.getBaseObject().getId());
+        final EntityPropertyUpdates updates = new EntityPropertyUpdates(entityKind, entityId);
+        for (IModification modification : modifications)
+        {
+            String propertyCode =
+                    modification.getColumnID().substring(
+                            EntityPropertyColDef.PROPERTY_PREFIX.length());
+            updates.addModifiedProperty(propertyCode, modification.tryGetNewValue());
+        }
+        viewContext.getService().updateProperties(updates,
+                createApplyModificationsCallback(model, modifications));
     }
 
     /**
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/ui/grid/AbstractEntityGrid.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/ui/grid/AbstractEntityGrid.java
index 83cae4c12de..51ca5c4682a 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/ui/grid/AbstractEntityGrid.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/ui/grid/AbstractEntityGrid.java
@@ -24,6 +24,7 @@ import ch.systemsx.cisd.openbis.generic.client.web.client.application.IViewConte
 import ch.systemsx.cisd.openbis.generic.client.web.client.application.framework.IDisplayTypeIDGenerator;
 import ch.systemsx.cisd.openbis.generic.client.web.client.application.model.BaseEntityModel;
 import ch.systemsx.cisd.openbis.generic.client.web.client.application.ui.TypedTableGrid;
+import ch.systemsx.cisd.openbis.generic.client.web.client.dto.EntityPropertyUpdates;
 import ch.systemsx.cisd.openbis.generic.client.web.client.dto.SampleGridColumnIDs;
 import ch.systemsx.cisd.openbis.generic.shared.basic.IColumnDefinition;
 import ch.systemsx.cisd.openbis.generic.shared.basic.IEntityInformationHolderWithProperties;
@@ -35,10 +36,11 @@ import ch.systemsx.cisd.openbis.generic.shared.basic.dto.TableModelRowWithObject
 
 /**
  * Abstract super class of {@link TypedTableGrid}-based entity browsers.
- *
+ * 
  * @author Franz-Josef Elmer
  */
-public abstract class AbstractEntityGrid<E extends IEntityInformationHolderWithProperties> extends TypedTableGrid<E>
+public abstract class AbstractEntityGrid<E extends IEntityInformationHolderWithProperties> extends
+        TypedTableGrid<E>
 {
     public AbstractEntityGrid(IViewContext<ICommonClientServiceAsync> viewContext,
             String browserId, boolean refreshAutomatically,
@@ -52,12 +54,11 @@ public abstract class AbstractEntityGrid<E extends IEntityInformationHolderWithP
     {
         super(viewContext, browserId, displayTypeIDGenerator);
     }
-    
+
     protected abstract EntityKind getEntityKind();
 
     @Override
-    protected boolean isEditable(BaseEntityModel<TableModelRowWithObject<E>> model,
-            String columnID)
+    protected boolean isEditable(BaseEntityModel<TableModelRowWithObject<E>> model, String columnID)
     {
         String propertyName = columnID.substring(SampleGridColumnIDs.PROPERTIES_PREFIX.length());
         E sample = model.getBaseObject().getObjectOrNull();
@@ -69,20 +70,26 @@ public abstract class AbstractEntityGrid<E extends IEntityInformationHolderWithP
         }
         List<IColumnDefinition<TableModelRowWithObject<E>>> columnDefinitions =
                 getColumnDefinitions(Arrays.asList(columnID));
-        IColumnDefinition<TableModelRowWithObject<E>> columnDefinition =
-                columnDefinitions.get(0);
+        IColumnDefinition<TableModelRowWithObject<E>> columnDefinition = columnDefinitions.get(0);
         return columnDefinition.tryToGetProperty(entityType.getCode()) != null;
     }
-    
+
     @Override
-    protected void handleEditingEvent(BaseEntityModel<TableModelRowWithObject<E>> model,
-            String columnID, String newValueOrNull)
+    protected void applyModifications(BaseEntityModel<TableModelRowWithObject<E>> model,
+            List<IModification> modifications)
     {
-        E entity = model.getBaseObject().getObjectOrNull();
-        TechId sampleID = new TechId(entity.getId());
-        String propertyName = columnID.substring(SampleGridColumnIDs.PROPERTIES_PREFIX.length());
-        viewContext.getService().updateProperty(getEntityKind(), sampleID, propertyName,
-                newValueOrNull, createPostEditingRefreshCallback());
+        final EntityKind entityKind = getEntityKind();
+        final TechId entityId = new TechId(model.getBaseObject().getObjectOrNull());
+        final EntityPropertyUpdates updates = new EntityPropertyUpdates(entityKind, entityId);
+        for (IModification modification : modifications)
+        {
+            String propertyCode =
+                    modification.getColumnID().substring(
+                            SampleGridColumnIDs.PROPERTIES_PREFIX.length());
+            updates.addModifiedProperty(propertyCode, modification.tryGetNewValue());
+        }
+        viewContext.getService().updateProperties(updates,
+                createApplyModificationsCallback(model, modifications));
     }
 
     @Override
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/ui/grid/ColumnSettingsChooser.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/ui/grid/ColumnSettingsChooser.java
index 868ed811825..2c275f3b3b7 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/ui/grid/ColumnSettingsChooser.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/ui/grid/ColumnSettingsChooser.java
@@ -18,7 +18,6 @@ import com.extjs.gxt.ui.client.store.ListStore;
 import com.extjs.gxt.ui.client.store.Record;
 import com.extjs.gxt.ui.client.widget.Component;
 import com.extjs.gxt.ui.client.widget.ContentPanel;
-import com.extjs.gxt.ui.client.widget.Info;
 import com.extjs.gxt.ui.client.widget.MessageBox;
 import com.extjs.gxt.ui.client.widget.WidgetComponent;
 import com.extjs.gxt.ui.client.widget.button.Button;
@@ -39,6 +38,7 @@ import ch.systemsx.cisd.openbis.generic.client.web.client.application.Dict;
 import ch.systemsx.cisd.openbis.generic.client.web.client.application.IViewContext;
 import ch.systemsx.cisd.openbis.generic.client.web.client.application.renderer.LinkRenderer;
 import ch.systemsx.cisd.openbis.generic.client.web.client.application.util.GWTUtils;
+import ch.systemsx.cisd.openbis.generic.client.web.client.application.util.GWTUtils.DisplayInfoTime;
 
 /**
  * Allows to change visibility and order of the grid columns.
@@ -129,8 +129,9 @@ class ColumnSettingsChooser
                         {
                             if (counter == maxVisibleColumns - 1)
                             {
-                                Info.display(
-                                        createVisibleColumnsLimitReachedMsg(maxVisibleColumns), "");
+                                GWTUtils.displayInfo(
+                                        createVisibleColumnsLimitReachedMsg(maxVisibleColumns), "",
+                                        DisplayInfoTime.SHORT);
                             }
                             if (counter >= maxVisibleColumns)
                             {
@@ -145,8 +146,9 @@ class ColumnSettingsChooser
                         {
                             if (counter == maxVisibleColumns + 1)
                             {
-                                Info.display(
-                                        createVisibleColumnsLimitReachedMsg(maxVisibleColumns), "");
+                                GWTUtils.displayInfo(
+                                        createVisibleColumnsLimitReachedMsg(maxVisibleColumns),
+                                        DisplayInfoTime.SHORT);
                             }
                             r.set(getDataIndex(), !b);
                         }
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/ui/grid/IBrowserGridActionInvoker.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/ui/grid/IBrowserGridActionInvoker.java
index 2d218f7c379..4886040b5c7 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/ui/grid/IBrowserGridActionInvoker.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/ui/grid/IBrowserGridActionInvoker.java
@@ -1,5 +1,6 @@
 package ch.systemsx.cisd.openbis.generic.client.web.client.application.ui.grid;
 
+
 /**
  * Interface to delegate export and refresh actions.
  */
@@ -12,4 +13,8 @@ public interface IBrowserGridActionInvoker
     void configure();
 
     void toggleFilters(boolean show);
+
+    void saveModifications();
+
+    void cancelModifications();
 }
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/ui/managed_property/ManagedPropertyGrid.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/ui/managed_property/ManagedPropertyGrid.java
index 54a01318146..03bd6d4b6dd 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/ui/managed_property/ManagedPropertyGrid.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/ui/managed_property/ManagedPropertyGrid.java
@@ -227,6 +227,16 @@ public class ManagedPropertyGrid extends TypedTableGrid<ReportRowModel>
                 {
                     delegate.configure();
                 }
+
+                public void saveModifications()
+                {
+                    delegate.saveModifications();
+                }
+
+                public void cancelModifications()
+                {
+                    delegate.cancelModifications();
+                }
             };
     }
 
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/ui/managed_property/ManagedPropertyGridActionDialog.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/ui/managed_property/ManagedPropertyGridActionDialog.java
index 52e008e0fc9..d25dcb0fe9b 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/ui/managed_property/ManagedPropertyGridActionDialog.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/ui/managed_property/ManagedPropertyGridActionDialog.java
@@ -35,6 +35,7 @@ import ch.systemsx.cisd.openbis.generic.client.web.client.application.IViewConte
 import ch.systemsx.cisd.openbis.generic.client.web.client.application.ui.field.MultilineVarcharField;
 import ch.systemsx.cisd.openbis.generic.client.web.client.application.ui.widget.AbstractDataConfirmationDialog;
 import ch.systemsx.cisd.openbis.generic.client.web.client.application.ui.widget.FieldUtil;
+import ch.systemsx.cisd.openbis.generic.client.web.client.application.util.GWTUtils;
 import ch.systemsx.cisd.openbis.generic.client.web.client.application.util.lang.StringEscapeUtils;
 import ch.systemsx.cisd.openbis.generic.client.web.client.exception.UserFailureException;
 import ch.systemsx.cisd.openbis.generic.shared.basic.IEntityInformationHolder;
@@ -188,7 +189,7 @@ public final class ManagedPropertyGridActionDialog extends
             {
                 if (viewContext.isLoggingEnabled())
                 {
-                    Info.display("found binding", inputDescription.getLabel() + "->"
+                    GWTUtils.displayInfo("found binding", inputDescription.getLabel() + "->"
                             + boundedColumnTitleOrNull);
                 }
                 TableModelRowWithObject<ReportRowModel> selectedRow = data.get(0);
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/util/GWTUtils.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/util/GWTUtils.java
index d7f2296da70..936db0dfabb 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/util/GWTUtils.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/application/util/GWTUtils.java
@@ -471,17 +471,49 @@ public final class GWTUtils
         return GWT.isScript();
     }
 
+    public enum DisplayInfoTime
+    {
+        /** short time of display (2,5s) */
+        SHORT(2500),
+        /** medium time of display (5s) */
+        MEDIUM(5000),
+        /** long time of display (10s) */
+        LONG(10000);
+
+        private final int time;
+
+        DisplayInfoTime(int time)
+        {
+            this.time = time;
+        }
+
+        public int getTime()
+        {
+            return time;
+        }
+    }
+
     public final static void displayInfo(String title)
     {
-        displayInfo(title, "");
+        displayInfo(title, "", DisplayInfoTime.MEDIUM);
+    }
+
+    public final static void displayInfo(String title, DisplayInfoTime time)
+    {
+        displayInfo(title, "", time.getTime());
     }
 
     public final static void displayInfo(String title, String text)
     {
-        displayInfo(title, text, 10000); // 10s
+        displayInfo(title, text, DisplayInfoTime.MEDIUM);
+    }
+
+    public final static void displayInfo(String title, String text, DisplayInfoTime time)
+    {
+        displayInfo(title, text, time.getTime());
     }
 
-    public final static void displayInfo(String title, String text, int displayTime)
+    private final static void displayInfo(String title, String text, int displayTime)
     {
         InfoConfig config = new InfoConfig(title, text);
         config.display = displayTime;
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/dto/EntityPropertyUpdates.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/dto/EntityPropertyUpdates.java
new file mode 100644
index 00000000000..3bcdc07090b
--- /dev/null
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/dto/EntityPropertyUpdates.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2009 ETH Zuerich, CISD
+ *
+ * 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 ch.systemsx.cisd.openbis.generic.client.web.client.dto;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import ch.systemsx.cisd.openbis.generic.shared.basic.ISerializable;
+import ch.systemsx.cisd.openbis.generic.shared.basic.TechId;
+import ch.systemsx.cisd.openbis.generic.shared.basic.dto.EntityKind;
+import ch.systemsx.cisd.openbis.generic.shared.basic.dto.ServiceVersionHolder;
+
+/**
+ * Updates of properties of an entity.
+ * 
+ * @author Piotr Buczek
+ */
+public class EntityPropertyUpdates implements ISerializable
+{
+    private static final long serialVersionUID = ServiceVersionHolder.VERSION;
+
+    private EntityKind entityKind;
+
+    private TechId entityId;
+
+    private Map<String /* property code */, String /* new value */> modifiedProperties =
+            new LinkedHashMap<String, String>();
+
+    public EntityPropertyUpdates()
+    {
+    }
+
+    public EntityPropertyUpdates(EntityKind entityKind, TechId entityId)
+    {
+        this.entityKind = entityKind;
+        this.entityId = entityId;
+    }
+
+    public EntityKind getEntityKind()
+    {
+        return entityKind;
+    }
+
+    public void setEntityKind(EntityKind entityKind)
+    {
+        this.entityKind = entityKind;
+    }
+
+    public TechId getEntityId()
+    {
+        return entityId;
+    }
+
+    public void setEntityId(TechId entityId)
+    {
+        this.entityId = entityId;
+    }
+
+    public Map<String, String> getModifiedProperties()
+    {
+        return modifiedProperties;
+    }
+
+    public void addModifiedProperty(String propertyCode, String propertyValue)
+    {
+        modifiedProperties.put(propertyCode, propertyValue);
+    }
+
+    public void setModifiedProperties(Map<String, String> modifiedProperties)
+    {
+        this.modifiedProperties = modifiedProperties;
+    }
+
+}
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/dto/EntityPropertyUpdatesResult.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/dto/EntityPropertyUpdatesResult.java
new file mode 100644
index 00000000000..30a222a0fc8
--- /dev/null
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/client/dto/EntityPropertyUpdatesResult.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2009 ETH Zuerich, CISD
+ *
+ * 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 ch.systemsx.cisd.openbis.generic.client.web.client.dto;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import ch.systemsx.cisd.openbis.generic.shared.basic.ISerializable;
+import ch.systemsx.cisd.openbis.generic.shared.basic.dto.ServiceVersionHolder;
+
+/**
+ * Stores result of update of properties of an entity. Currently used only to transfer grouped
+ * errors to the client side.
+ * 
+ * @author Piotr Buczek
+ */
+public class EntityPropertyUpdatesResult implements ISerializable
+{
+    private static final long serialVersionUID = ServiceVersionHolder.VERSION;
+
+    private List<String> errors = new ArrayList<String>();
+
+    public EntityPropertyUpdatesResult()
+    {
+    }
+
+    public void addError(String errorMessage)
+    {
+        errors.add(errorMessage);
+    }
+
+    public List<String> getErrors()
+    {
+        return errors;
+    }
+
+    public void setErrors(List<String> errors)
+    {
+        this.errors = errors;
+    }
+
+}
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/server/CommonClientService.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/server/CommonClientService.java
index 6e0af61fa0e..145f9f0282a 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/server/CommonClientService.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/client/web/server/CommonClientService.java
@@ -24,6 +24,7 @@ import java.util.Collections;
 import java.util.Date;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Map.Entry;
 import java.util.Set;
 
 import javax.servlet.http.HttpSession;
@@ -47,6 +48,8 @@ import ch.systemsx.cisd.openbis.generic.client.web.client.dto.DefaultResultSetCo
 import ch.systemsx.cisd.openbis.generic.client.web.client.dto.DisplayedCriteriaOrSelectedEntityHolder;
 import ch.systemsx.cisd.openbis.generic.client.web.client.dto.DisplayedOrSelectedDatasetCriteria;
 import ch.systemsx.cisd.openbis.generic.client.web.client.dto.DisplayedOrSelectedIdHolderCriteria;
+import ch.systemsx.cisd.openbis.generic.client.web.client.dto.EntityPropertyUpdates;
+import ch.systemsx.cisd.openbis.generic.client.web.client.dto.EntityPropertyUpdatesResult;
 import ch.systemsx.cisd.openbis.generic.client.web.client.dto.GridRowModels;
 import ch.systemsx.cisd.openbis.generic.client.web.client.dto.IResultSetConfig;
 import ch.systemsx.cisd.openbis.generic.client.web.client.dto.ListExperimentsCriteria;
@@ -2195,8 +2198,7 @@ public final class CommonClientService extends AbstractClientService implements
             TableExportCriteria<TableModelRowWithObject<Experiment>> displayedItemsCriteria =
                     experimentCriteria.tryGetDisplayedItems();
             assert displayedItemsCriteria != null : "displayedItemsCriteria is null";
-            rows =
-                    fetchCachedEntities(displayedItemsCriteria).extractOriginalObjects();
+            rows = fetchCachedEntities(displayedItemsCriteria).extractOriginalObjects();
         }
         List<Experiment> experiments = new ArrayList<Experiment>();
         for (TableModelRowWithObject<Experiment> row : rows)
@@ -2760,7 +2762,8 @@ public final class CommonClientService extends AbstractClientService implements
         }
     }
 
-    public ArchivingResult lockDatasets(DisplayedCriteriaOrSelectedEntityHolder<TableModelRowWithObject<Experiment>> criteria)
+    public ArchivingResult lockDatasets(
+            DisplayedCriteriaOrSelectedEntityHolder<TableModelRowWithObject<Experiment>> criteria)
             throws ch.systemsx.cisd.openbis.generic.client.web.client.exception.UserFailureException
     {
         try
@@ -2801,16 +2804,20 @@ public final class CommonClientService extends AbstractClientService implements
             switch (kind)
             {
                 case DATA_SET:
-                    commonServer.updateDataSetProperty(sessionToken, entityId, propertyColumnName, value);
+                    commonServer.updateDataSetProperty(sessionToken, entityId, propertyColumnName,
+                            value);
                     break;
                 case EXPERIMENT:
-                    commonServer.updateExperimentProperty(sessionToken, entityId, propertyColumnName, value);
+                    commonServer.updateExperimentProperty(sessionToken, entityId,
+                            propertyColumnName, value);
                     break;
                 case MATERIAL:
-                    commonServer.updateMaterialProperty(sessionToken, entityId, propertyColumnName, value);
+                    commonServer.updateMaterialProperty(sessionToken, entityId, propertyColumnName,
+                            value);
                     break;
                 case SAMPLE:
-                    commonServer.updateSampleProperty(sessionToken, entityId, propertyColumnName, value);
+                    commonServer.updateSampleProperty(sessionToken, entityId, propertyColumnName,
+                            value);
                     break;
             }
         } catch (final UserFailureException e)
@@ -2819,4 +2826,45 @@ public final class CommonClientService extends AbstractClientService implements
         }
     }
 
+    public EntityPropertyUpdatesResult updateProperties(EntityPropertyUpdates updates)
+            throws ch.systemsx.cisd.openbis.generic.client.web.client.exception.UserFailureException
+    {
+        final String sessionToken = getSessionToken();
+
+        final EntityKind entityKind = updates.getEntityKind();
+        final TechId entityId = updates.getEntityId();
+        final EntityPropertyUpdatesResult result = new EntityPropertyUpdatesResult();
+
+        for (Entry<String, String> entry : updates.getModifiedProperties().entrySet())
+        {
+            try
+            {
+                String propertyCode = entry.getKey();
+                String value = entry.getValue();
+                switch (entityKind)
+                {
+                    case DATA_SET:
+                        commonServer.updateDataSetProperty(sessionToken, entityId, propertyCode,
+                                value);
+                        break;
+                    case EXPERIMENT:
+                        commonServer.updateExperimentProperty(sessionToken, entityId, propertyCode,
+                                value);
+                        break;
+                    case MATERIAL:
+                        commonServer.updateMaterialProperty(sessionToken, entityId, propertyCode,
+                                value);
+                        break;
+                    case SAMPLE:
+                        commonServer.updateSampleProperty(sessionToken, entityId, propertyCode,
+                                value);
+                        break;
+                }
+            } catch (final UserFailureException e)
+            {
+                result.addError(e.getMessage());
+            }
+        }
+        return result;
+    }
 }
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/CommonServer.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/CommonServer.java
index 252568e0ca6..36af0c1598b 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/CommonServer.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/CommonServer.java
@@ -2500,15 +2500,15 @@ public final class CommonServer extends AbstractCommonServer<ICommonServerForInt
         return dataStores.get(0).getDownloadUrl();
     }
 
-    public void updateDataSetProperty(String sessionToken, TechId entityId,
-            String propertyColumnName, String value)
+    public void updateDataSetProperty(String sessionToken, TechId entityId, String propertyName,
+            String value)
     {
         checkSession(sessionToken);
         ExternalData dataSet = getDataSetInfo(sessionToken, entityId);
         DataSetUpdatesDTO updates = new DataSetUpdatesDTO();
         updates.setDatasetId(entityId);
         updates.setVersion(dataSet.getModificationDate());
-        Map<String, String> properties = createPropertiesMap(propertyColumnName, value);
+        Map<String, String> properties = createPropertiesMap(propertyName, value);
         updates.setProperties(EntityHelper.translatePropertiesMapToList(properties));
         Experiment exp = dataSet.getExperiment();
         if (exp != null)
@@ -2528,8 +2528,8 @@ public final class CommonServer extends AbstractCommonServer<ICommonServerForInt
         updateDataSet(sessionToken, updates);
     }
 
-    public void updateExperimentProperty(String sessionToken, TechId entityId,
-            String propertyColumnName, String value)
+    public void updateExperimentProperty(String sessionToken, TechId entityId, String propertyCode,
+            String value)
     {
         checkSession(sessionToken);
         Experiment experiment = getExperimentInfo(sessionToken, entityId);
@@ -2539,34 +2539,34 @@ public final class CommonServer extends AbstractCommonServer<ICommonServerForInt
         updates.setAttachments(Collections.<NewAttachment> emptySet());
         updates.setProjectIdentifier(new ProjectIdentifierFactory(experiment.getProject()
                 .getIdentifier()).createIdentifier());
-        Map<String, String> properties = createPropertiesMap(propertyColumnName, value);
+        Map<String, String> properties = createPropertiesMap(propertyCode, value);
         updates.setProperties(EntityHelper.translatePropertiesMapToList(properties));
         updateExperiment(sessionToken, updates);
     }
 
-    public void updateSampleProperty(String sessionToken, TechId entityId,
-            String propertyColumnName, String value)
+    public void updateSampleProperty(String sessionToken, TechId entityId, String propertyCode,
+            String value)
     {
         checkSession(sessionToken);
-        Map<String, String> properties = createPropertiesMap(propertyColumnName, value);
+        Map<String, String> properties = createPropertiesMap(propertyCode, value);
         EntityHelper.updateSampleProperties(this, sessionToken, entityId, properties);
     }
 
-    public void updateMaterialProperty(String sessionToken, TechId entityId,
-            String propertyColumnName, String value)
+    public void updateMaterialProperty(String sessionToken, TechId entityId, String propertyCode,
+            String value)
     {
         checkSession(sessionToken);
         Date modificationDate =
                 getDAOFactory().getMaterialDAO().tryGetByTechId(entityId).getModificationDate();
-        Map<String, String> properties = createPropertiesMap(propertyColumnName, value);
+        Map<String, String> properties = createPropertiesMap(propertyCode, value);
         updateMaterial(sessionToken, entityId,
                 EntityHelper.translatePropertiesMapToList(properties), modificationDate);
     }
 
-    private Map<String, String> createPropertiesMap(String propertyColumnName, String value)
+    private Map<String, String> createPropertiesMap(String propertyCode, String value)
     {
         Map<String, String> properties = new HashMap<String, String>();
-        properties.put(CodeConverter.getPropertyTypeCode(propertyColumnName), value);
+        properties.put(CodeConverter.getPropertyTypeCode(propertyCode), value);
         return properties;
     }
 
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/CommonServerLogger.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/CommonServerLogger.java
index 30413b4aea1..7e7bfe0c81f 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/CommonServerLogger.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/CommonServerLogger.java
@@ -1080,32 +1080,32 @@ final class CommonServerLogger extends AbstractServerLogger implements ICommonSe
         return null;
     }
 
-    public void updateDataSetProperty(String sessionToken, TechId entityId,
-            String propertyColumnName, String value)
+    public void updateDataSetProperty(String sessionToken, TechId entityId, String propertyCode,
+            String value)
     {
-        logTracking(sessionToken, "updateDataSetProperty",
-                "ID(%s) PROPERTY_COLUMN_NAME(%s) VALUE(%s)", entityId, propertyColumnName, value);
+        logTracking(sessionToken, "updateDataSetProperty", "ID(%s) PROPERTY(%s) VALUE(%s)",
+                entityId, propertyCode, value);
     }
 
-    public void updateExperimentProperty(String sessionToken, TechId entityId,
-            String propertyColumnName, String value)
+    public void updateExperimentProperty(String sessionToken, TechId entityId, String propertyCode,
+            String value)
     {
-        logTracking(sessionToken, "updateExperimentProperty",
-                "ID(%s) PROPERTY_COLUMN_NAME(%s) VALUE(%s)", entityId, propertyColumnName, value);
+        logTracking(sessionToken, "updateExperimentProperty", "ID(%s) PROPERTY(%s) VALUE(%s)",
+                entityId, propertyCode, value);
     }
 
-    public void updateSampleProperty(String sessionToken, TechId entityId,
-            String propertyColumnName, String value)
+    public void updateSampleProperty(String sessionToken, TechId entityId, String propertyCode,
+            String value)
     {
-        logTracking(sessionToken, "updateSampleProperty",
-                "ID(%s) PROPERTY_COLUMN_NAME(%s) VALUE(%s)", entityId, propertyColumnName, value);
+        logTracking(sessionToken, "updateSampleProperty", "ID(%s) PROPERTY(%s) VALUE(%s)",
+                entityId, propertyCode, value);
     }
 
-    public void updateMaterialProperty(String sessionToken, TechId entityId,
-            String propertyColumnName, String value)
+    public void updateMaterialProperty(String sessionToken, TechId entityId, String propertyCode,
+            String value)
     {
-        logTracking(sessionToken, "updateMaterialProperty",
-                "ID(%s) PROPERTY_COLUMN_NAME(%s) VALUE(%s)", entityId, propertyColumnName, value);
+        logTracking(sessionToken, "updateMaterialProperty", "ID(%s) PROPERTY(%s) VALUE(%s)",
+                entityId, propertyCode, value);
     }
 
 }
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/ICommonServer.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/ICommonServer.java
index a1ebedcf39e..09c133053f4 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/ICommonServer.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/ICommonServer.java
@@ -1364,23 +1364,23 @@ public interface ICommonServer extends IServer
     @RolesAllowed(RoleWithHierarchy.SPACE_USER)
     public void updateDataSetProperty(String sessionToken,
             @AuthorizationGuard(guardClass = DataSetTechIdPredicate.class) TechId entityId,
-            String propertyColumnName, String value);
+            String propertyCode, String value);
 
     @Transactional
     @RolesAllowed(RoleWithHierarchy.SPACE_USER)
     public void updateExperimentProperty(String sessionToken,
             @AuthorizationGuard(guardClass = ExperimentTechIdPredicate.class) TechId entityId,
-            String propertyColumnName, String value);
+            String propertyCode, String value);
 
     @Transactional
     @RolesAllowed(RoleWithHierarchy.SPACE_USER)
     public void updateSampleProperty(String sessionToken,
             @AuthorizationGuard(guardClass = SampleTechIdPredicate.class) TechId entityId,
-            String propertyColumnName, String value);
+            String propertyCode, String value);
 
     @Transactional
     @RolesAllowed(RoleWithHierarchy.INSTANCE_ADMIN)
-    public void updateMaterialProperty(String sessionToken, TechId entityId,
-            String propertyColumnName, String value);
+    public void updateMaterialProperty(String sessionToken, TechId entityId, String propertyCode,
+            String value);
 
 }
diff --git a/openbis/sourceTest/java/ch/systemsx/cisd/openbis/generic/shared/ICommonServer.java.expected b/openbis/sourceTest/java/ch/systemsx/cisd/openbis/generic/shared/ICommonServer.java.expected
index a1ebedcf39e..09c133053f4 100644
--- a/openbis/sourceTest/java/ch/systemsx/cisd/openbis/generic/shared/ICommonServer.java.expected
+++ b/openbis/sourceTest/java/ch/systemsx/cisd/openbis/generic/shared/ICommonServer.java.expected
@@ -1364,23 +1364,23 @@ public interface ICommonServer extends IServer
     @RolesAllowed(RoleWithHierarchy.SPACE_USER)
     public void updateDataSetProperty(String sessionToken,
             @AuthorizationGuard(guardClass = DataSetTechIdPredicate.class) TechId entityId,
-            String propertyColumnName, String value);
+            String propertyCode, String value);
 
     @Transactional
     @RolesAllowed(RoleWithHierarchy.SPACE_USER)
     public void updateExperimentProperty(String sessionToken,
             @AuthorizationGuard(guardClass = ExperimentTechIdPredicate.class) TechId entityId,
-            String propertyColumnName, String value);
+            String propertyCode, String value);
 
     @Transactional
     @RolesAllowed(RoleWithHierarchy.SPACE_USER)
     public void updateSampleProperty(String sessionToken,
             @AuthorizationGuard(guardClass = SampleTechIdPredicate.class) TechId entityId,
-            String propertyColumnName, String value);
+            String propertyCode, String value);
 
     @Transactional
     @RolesAllowed(RoleWithHierarchy.INSTANCE_ADMIN)
-    public void updateMaterialProperty(String sessionToken, TechId entityId,
-            String propertyColumnName, String value);
+    public void updateMaterialProperty(String sessionToken, TechId entityId, String propertyCode,
+            String value);
 
 }
-- 
GitLab