Commits

Peter Suter committed ef191cf

Add clearer batch modify UI for list fields with explicit mode selection.

Comments (0)

Files changed (4)

trac/htdocs/js/query.js

   
   window.initializeBatch = function(){
     // Create the appropriate input for the property.
-    function createBatchInput(inputName, property){
+    function createBatchInput(propertyName, property){
       var td = $('<td class="batchmod_property">');
+      var inputName = getBatchInputName(propertyName);
       switch(property.type){
         case 'select':
           td.append(createSelect(inputName, property.options, true,
             .append(" ").append(createLabel(_("no"), inputName + "_off"));
           break;
         case 'text':
-          td.append(createText(inputName, 42));
+          if ($.inArray(propertyName, batch_list_properties) {
+            appendBatchListControls(td, inputName);
+          } else {
+            td.append(createText(inputName, 42));
+          }
           break;
         case 'time':
           td.append(createText(inputName, 42).addClass("time"));
       return td;
     }
     
+    function appendBatchListControls(td, inputName) {
+      var modeSelect = createSelect(inputName + "_mode", batch_list_modes);
+      var text1 = createText(inputName, 42);
+      var text2 = createText(inputName + "_secondary", 42);
+      
+      td.append(modeSelect);
+      td.append(text1);
+      
+      // Only mode 2 ("Add / remove") has a second  text field:
+      $(modeSelect).change(function() {
+        if (this.selectedIndex == 2) {
+          td.append(text2);
+        } else {
+          if (td.has(text2).length) {
+            text2.remove();
+          }
+        }
+      });
+    }
+    
     function getBatchInputName(propertyName){
       return 'batchmod_value_' + propertyName;
     }
       );
       
       // Add the input element.
-      tr.append(createBatchInput(getBatchInputName(propertyName), property));
+      tr.append(createBatchInput(propertyName, property));
       
       // New rows are added in the same order as listed in the dropdown.
       // This is the same behavior as the filters.
 from trac.ticket.notification import BatchTicketNotifyEmail
 from trac.util.datefmt import utc
 from trac.util.text import exception_to_unicode, to_unicode
-from trac.util.translation import tag_
+from trac.util.translation import _, tag_
 from trac.web import IRequestHandler
 from trac.web.chrome import add_warning, add_script_data
 
         data['batch_modify'] = True
         data['query_href'] = req.session['query_href'] or req.href.query()
         data['action_controls'] = self._get_action_controls(req, tickets)
-        add_script_data(req, batch_modify=True)
+        batch_list_modes = [
+            {'name': _("add"), 'value': "+"},
+            {'name': _("remove"), 'value': "-"},
+            {'name': _("add / remove"), 'value': "+-"},
+            {'name': _("assign"), 'value': "="},
+        ]
+        add_script_data(req, batch_modify=True,
+                             batch_list_modes=batch_list_modes,
+                             batch_list_properties=self.fields_as_list)
 
     def _get_action_controls(self, req, tickets):
         action_controls = []
                 _values = new_values.copy()
                 for field in self.fields_as_list:
                     if field in new_values:
+                        old = t.values[field] if field in t.values else ''
                         new = new_values[field]
-                        old = t.values[field] if field in t.values else ''
-                        _values[field] = self._change_list(old, new)
+                        mode = req.args.get('batchmod_value_' + field +
+                                            '_mode')
+                        new2 = req.args.get('batchmod_value_' + field +
+                                            '_secondary', '')
+                        _values[field] = self._change_list(old, new, new2,
+                                                           mode)
                 controllers = list(self._get_action_controllers(req, t,
                                                                 action))
                 for controller in controllers:
                                   "notifications: %(message)s",
                                   message=to_unicode(e)))
     
-    def _change_list(self, old_list, new_list):
-        """Any entries prefixed with '+' or '-' will be added or removed.
-        
-        These operators carry over to entries without one.
-        If the first entry has no operator, the new list replaces the old one.
-        """
-        
+    def _change_list(self, old_list, new_list, new_list2, mode):
         changed_list = [k.strip()
                         for k in self.list_separator_re.split(old_list)
                         if k]
         new_list = [k.strip()
                     for k in self.list_separator_re.split(new_list)
                     if k]
+        new_list2 = [k.strip()
+                     for k in self.list_separator_re.split(new_list2)
+                     if k]
         
-        if not new_list:
-            return ''
-        
-        op = ''
-        for entry in new_list:
-            if entry.startswith('+') or entry.startswith('-'):
-                op = entry[0]
-                entry = entry[1:]
-            if op == '+':
+        if mode == '=':
+            changed_list = new_list
+        elif mode ==  '+':
+            for entry in new_list:
                 if entry not in changed_list:
                     changed_list.append(entry)
-            elif op == '-':
+        elif mode == '-':
+            for entry in new_list:
                 while entry in changed_list:
                     changed_list.remove(entry)
-            else:
-                changed_list = [entry,]
-                op = '+'
-        
+        elif mode == '+-':
+            for entry in new_list:
+                if entry not in changed_list:
+                    changed_list.append(entry)
+            for entry in new_list2:
+                while entry in changed_list:
+                    changed_list.remove(entry)
         return self.list_connector_string.join(changed_list)

trac/ticket/tests/batch.py

         field_change = [c for c in changes if c[2] == field][0]
         self.assertEqual(field_change[4], new_value)
     
-    def _change_list_test_helper(self, original, new):
+    def _change_list_test_helper(self, original, new, new2, mode):
         batch = BatchModifyModule(self.env)
-        return batch._change_list(original, new)
+        return batch._change_list(original, new, new2, mode)
+        
+    def _add_list_test_helper(self, original, to_add):
+        return self._change_list_test_helper(original, to_add, '', '+')
+        
+    def _remove_list_test_helper(self, original, to_remove):
+        return self._change_list_test_helper(original, to_remove, '', '-')
+        
+    def _add_remove_list_test_helper(self, original, to_add, to_remove):
+        return self._change_list_test_helper(original, to_add, to_remove,
+                                             '+-')
+        
+    def _assign_list_test_helper(self, original, new):
+        return self._change_list_test_helper(original, new, '', '=')        
     
     def _insert_ticket(self, summary, **kw):
         """Helper for inserting a ticket into the database"""
         selected_tickets = batch._get_selected_tickets(self.req)
         self.assertEqual(selected_tickets, [])
 
-    # Replace list items
+    # Assign list items
     
     def test_change_list_replace_empty_with_single(self):
         """Replace emtpy field with single item."""
-        changed = self._change_list_test_helper('', 'alice')
+        changed = self._assign_list_test_helper('', 'alice')
         self.assertEqual(changed, 'alice')
         
     def test_change_list_replace_empty_with_items(self):
         """Replace emtpy field with items."""
-        changed = self._change_list_test_helper('', 'alice, bob')
+        changed = self._assign_list_test_helper('', 'alice, bob')
         self.assertEqual(changed, 'alice, bob')
         
     def test_change_list_replace_item(self):
         """Replace item with a different item."""
-        changed = self._change_list_test_helper('alice', 'bob')
+        changed = self._assign_list_test_helper('alice', 'bob')
         self.assertEqual(changed, 'bob')
         
     def test_change_list_replace_item_with_items(self):
         """Replace item with different items."""
-        changed = self._change_list_test_helper('alice', 'bob, carol')
+        changed = self._assign_list_test_helper('alice', 'bob, carol')
         self.assertEqual(changed, 'bob, carol')
         
     def test_change_list_replace_items_with_item(self):
         """Replace items with a different item."""
-        changed = self._change_list_test_helper('alice, bob', 'carol')
+        changed = self._assign_list_test_helper('alice, bob', 'carol')
         self.assertEqual(changed, 'carol')
         
     def test_change_list_replace_items(self):
         """Replace items with different items."""
-        changed = self._change_list_test_helper('alice, bob', 'carol, dave')
+        changed = self._assign_list_test_helper('alice, bob', 'carol, dave')
         self.assertEqual(changed, 'carol, dave')
         
     def test_change_list_replace_items_partial(self):
         """Replace items with different (or not) items."""
-        changed = self._change_list_test_helper('alice, bob', 'bob, dave')
+        changed = self._assign_list_test_helper('alice, bob', 'bob, dave')
         self.assertEqual(changed, 'bob, dave')
         
     def test_change_list_clear(self):
         """Clear field."""
-        changed = self._change_list_test_helper('alice bob', '')
+        changed = self._assign_list_test_helper('alice bob', '')
         self.assertEqual(changed, '')
 
     # Add / remove list items
     
     def test_change_list_add_item(self):
         """Append additional item."""
-        changed = self._change_list_test_helper('alice', '+bob')
+        changed = self._add_list_test_helper('alice', 'bob')
         self.assertEqual(changed, 'alice, bob')
         
     def test_change_list_add_items(self):
         """Append additional items."""
-        changed = self._change_list_test_helper('alice, bob', '+carol, +dave')
+        changed = self._add_list_test_helper('alice, bob', 'carol, dave')
         self.assertEqual(changed, 'alice, bob, carol, dave')
         
     def test_change_list_remove_item(self):
         """Remove existing item."""
-        changed = self._change_list_test_helper('alice, bob', '-bob')
+        changed = self._remove_list_test_helper('alice, bob', 'bob')
         self.assertEqual(changed, 'alice')
         
     def test_change_list_remove_items(self):
         """Remove existing items."""
-        changed = self._change_list_test_helper('alice, bob, carol',
-                                                '-alice, -carol')
+        changed = self._remove_list_test_helper('alice, bob, carol',
+                                                'alice, carol')
         self.assertEqual(changed, 'bob')
         
     def test_change_list_remove_idempotent(self):
         """Ignore missing item to be removed."""
-        changed = self._change_list_test_helper('alice', '-bob')
+        changed = self._remove_list_test_helper('alice', 'bob')
         self.assertEqual(changed, 'alice')
         
     def test_change_list_remove_mixed(self):
         """Ignore only missing item to be removed."""
-        changed = self._change_list_test_helper('alice, bob', '-bob, -carol')
+        changed = self._remove_list_test_helper('alice, bob', 'bob, carol')
         self.assertEqual(changed, 'alice')
         
     def test_change_list_add_remove(self):
         """Remove existing item and append additional item."""
-        changed = self._change_list_test_helper('alice, bob',
-                                                '-alice, +carol')
+        changed = self._add_remove_list_test_helper('alice, bob', 'carol',
+                                                'alice')
         self.assertEqual(changed, 'bob, carol')
         
     def test_change_list_add_no_duplicates(self):
         """Existing items are not duplicated."""
-        changed = self._change_list_test_helper('alice, bob', '+bob, carol')
+        changed = self._add_list_test_helper('alice, bob', 'bob, carol')
         self.assertEqual(changed, 'alice, bob, carol')
         
     def test_change_list_remove_all_duplicates(self):
         """Remove all duplicates."""
-        changed = self._change_list_test_helper('alice, bob, alice', '-alice')
-        self.assertEqual(changed, 'bob')
-        
-    # Add / remove multiple list items with one operator
-
-    def test_change_list_multiadd(self):
-        """Also add following items without oeprator."""
-        changed = self._change_list_test_helper('alice', '+bob, carol, dave')
-        self.assertEqual(changed, 'alice, bob, carol, dave')
-        
-    def test_change_list_multiremove(self):
-        """Also remove following items without operator."""
-        changed = self._change_list_test_helper('alice, bob, carol',
-                                                '-alice, carol')
-        self.assertEqual(changed, 'bob')
-        
-    def test_change_list_multiadd_multiremove(self):
-        """Also add or remove following items without operator."""
-        changed = self._change_list_test_helper('alice, bob',
-                                                '+carol, dave, -alice, bob')
-        self.assertEqual(changed, 'carol, dave')
-        
-    # Replace and add / remove list items at the same time (not very useful)
-    
-    def test_change_list_replace_then_add(self):
-        """Replace item with different item, then add additional item."""
-        changed = self._change_list_test_helper('alice', 'bob, +carol')
-        self.assertEqual(changed, 'bob, carol')
-        
-    def test_change_list_replace_then_remove(self):
-        """Replace item with different item, then ignore item to be removed,
-        as it was already replaced."""
-        changed = self._change_list_test_helper('alice', 'bob, -alice')
+        changed = self._remove_list_test_helper('alice, bob, alice', 'alice')
         self.assertEqual(changed, 'bob')
     
     # Save

trac/wiki/default-pages/TracBatchModify

 
 == List fields
 
-The `Keywords` and `Cc` fields offer some special list management features. The new field value can consist of multiple items (i.e. multiple keywords or cc addresses).
-
-If all new items are prepended with a '+' or '-', they will get merged with the old items:
-* Items prepended with a '+' are appended to the old list. (No duplicates are added.)
-* Items prepended with a '-' are removed from the old llist. (All occurrences are removed.)
-
-Otherwise the old list gets replaced by the new item(s).
-
-=== Examples
-
-First a few examples where the entire list is replaced (because no '+' or '-' appears anywhere):
-
-||=Old Cc's          ||=Batch Cc field   ||=New Cc's                ||= Summary ||
-||                   || alice            || alice                   || Replace emtpy field with single item. ||
-||                   || alice, bob       || alice, bob              || Replace emtpy field with two items. ||
-|| alice             || bob              || bob                     || Replace item with a different item. ||
-|| alice             || bob, carol       || bob, carol              || Replace item with different items. ||
-|| alice, bob        || carol            || carol                   || Replace items with a different item. ||
-|| alice, bob        || carol, dave      || carol, dave             || Replace items with different items. ||
-|| alice, bob        || bob, dave        || bob, dave               || Replace items with different (or not) items. ||
-|| alice, bob        ||                  ||                         || Replace items with emtpy field. ||
-
-Now a few simple examples with '+' and '-' only:
-
-||=Old Cc's          ||=Batch Cc field   ||=New Cc's                ||= Summary ||
-|| alice             || +bob             || alice, bob              || Append additional item. ||
-|| alice, bob        || +carol, +dave    || alice, bob, carol, dave || Append additional items. ||
-|| alice, bob        || -bob             || alice                   || Remove existing item. ||
-|| alice, bob, carol || -alice, -carol   || bob                     || Remove existing items. ||
-|| alice             || -bob             || alice                   || Ignore missing item to be removed. ||
-|| alice, bob        || -bob, -carol     || alice                   || Ignore only missing item to be removed. ||
-|| alice, bob        || -alice, +carol   || bob, carol              || Remove existing item and append additional item. ||
-
-'+' and '-' carry over to items with no operator:
-
-||=Old Cc's          ||=Batch Cc field   ||=New Cc's                ||= Summary ||
-|| alice             || +carol, bob      || alice, carol, bob       || Add both items. ||
-|| alice, bob        || -alice, bob      ||                         || Remove both items. ||
-|| alice, bob        || +carol, dave, -alice, bob || carol, dave    || Add and remove items. ||
-
-Mixing replacement with '+' and '-' is possible, but not very useful:
-||=Old Cc's          ||=Batch Cc field   ||=New Cc's                ||= Summary ||
-|| alice             || bob, +carol      || bob, carol              || Replace item with different item, then add additional item. ||
-|| alice             || bob, -alice      || bob                     || Replace item with different item, then ignore item to be removed, as it was already replaced. ||
+The `Keywords` and `Cc` fields are treated as lists, where list items can be added and / or removed in addition of replacing the entire list value. All list field controls accept multiple items (i.e. multiple keywords or cc addresses).