← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~wallyworld/launchpad/long-text-truncate-multi-line into lp:launchpad

 

Ian Booth has proposed merging lp:~wallyworld/launchpad/long-text-truncate-multi-line into lp:launchpad with lp:~wallyworld/launchpad/long-text-truncate as a prerequisite.

Requested reviews:
  Curtis Hovey (sinzui)
Related bugs:
  Bug #249848 in Launchpad itself: "Watermark/title displays poorly with very long name"
  https://bugs.launchpad.net/launchpad/+bug/249848

For more details, see:
https://code.launchpad.net/~wallyworld/launchpad/long-text-truncate-multi-line/+merge/119081

== Implementation ==

The previous version of this branch used css to add truncation to long text items. However, only single lines were supported due to limitations in css. This branch adds some 3rd party Javascript to support multi-line truncation.

The way it works is that the base-layout TAL loads the ellipsis yui module which augments the yui Node class to provide an ellipsis() method. The TextLineEditorWidget has been enhanced to invoke the ellipsis method on the relevant text display node. There is a new truncated_lines parameter which can be used to set the number of lines to display. Bugtask and Product titles have 2, the other things have 1.

The existing css rule selector for single line truncation was changed from .ellipsis to .ellipsis.single-line. This allows arbitrary nodes to have the ellipsis applied without using the new yui module, albeit single line only (eg bug picker stuff). The extra selector was needed because the white-space property required for native css ellipsis support enforced single line and thus was incompatible with the new javascript approach.

I had to add a change to the 3rd party code. On firefox at least, yEl.getComputedStyle('width') returns 'auto' if the element width is not explicitly set, even though it is supposed to return the rendered width similar to yEl.getComputedStyle('lineHeight') etc. So if the result is 'auto', I call node.offsetWidth which returns the correct value, even though this is nominally used to support IE.

Separately to the 3rd party code, I added css rules so that when the mouse hovers over the truncated text, the full text is displayed. I used the content: attr(xxx) property to read the data element value set to be the original text. But for some reason I can't figure out, it doesn't work. It works if I set the content property to another attribute like 'id'. It's not a show stopper but it would be nice to know how to fix it.

The diff is lying - there is no conflict in team.py

== QA ==

I've tested on latest firefox and chrome. Perhaps we should test on IE?

== Tests ==

I enhanced the lazr-js-widgets test

== Lint ==

There's a fair bit of lint in the 2rd party code but I don't want to mess with that.
-- 
https://code.launchpad.net/~wallyworld/launchpad/long-text-truncate-multi-line/+merge/119081
Your team Launchpad code reviewers is subscribed to branch lp:launchpad.
=== modified file 'lib/canonical/launchpad/icing/css/modifiers.css'
--- lib/canonical/launchpad/icing/css/modifiers.css	2012-08-09 04:56:41 +0000
+++ lib/canonical/launchpad/icing/css/modifiers.css	2012-08-10 06:01:31 +0000
@@ -112,7 +112,7 @@
     height: 10px;
     }
 
-.ellipsis {
+.ellipsis.single-line {
     display: inline-block;
     white-space: nowrap;
     overflow: hidden;
@@ -125,6 +125,15 @@
     max-width: 60em;
     }
 
+.ellipsis:before {
+    content: attr(data-ellipsis-original-text);
+    position: absolute;
+    display: block;
+    }
+.ellipsis:hover:before {
+    display: block;
+    }
+
 .exception {
     color: #cc0000;
     }

=== modified file 'lib/lp/app/browser/lazrjs.py'
--- lib/lp/app/browser/lazrjs.py	2012-08-10 06:01:30 +0000
+++ lib/lp/app/browser/lazrjs.py	2012-08-10 06:01:31 +0000
@@ -155,7 +155,7 @@
 
     def __init__(self, context, exported_field, title, tag, css_class=None,
                  content_box_id=None, edit_view="+edit", edit_url=None,
-                 edit_title='', max_width=None,
+                 edit_title='', max_width=None, truncate_lines=0,
                  default_text=None, initial_value_override=None, width=None):
         """Create a widget wrapper.
 
@@ -167,6 +167,8 @@
         :param css_class: The css class value to use.
         :param max_width: The maximum width of the rendered text before it is
             truncated with an '...'.
+        :param truncate_lines: The maximum number of lines of text to display
+            before any overflow is truncated with an '...'.
         :param content_box_id: The HTML id to use for this widget.
             Defaults to edit-<attribute name>.
         :param edit_view: The view name to use to generate the edit_url if
@@ -186,6 +188,7 @@
         self.tag = tag
         self.css_class = css_class
         self.max_width = max_width
+        self.truncate_lines = truncate_lines
         self.default_text = default_text
         self.initial_value_override = simplejson.dumps(initial_value_override)
         self.width = simplejson.dumps(width)
@@ -199,7 +202,16 @@
             return FormattersAPI(text).obfuscate_email()
 
     @property
-    def css_style(self):
+    def text_css_class(self):
+        clazz = "yui3-editable_text-text"
+        if self.truncate_lines and self.truncate_lines > 0:
+            clazz += ' ellipsis'
+            if self.truncate_lines == 1:
+                clazz += ' single-line'
+        return clazz
+
+    @property
+    def text_css_style(self):
         if self.max_width:
             return 'max-width: %s;' % self.max_width
         return ''

=== modified file 'lib/lp/app/doc/lazr-js-widgets.txt'
--- lib/lp/app/doc/lazr-js-widgets.txt	2012-08-10 06:01:30 +0000
+++ lib/lp/app/doc/lazr-js-widgets.txt	2012-08-10 06:01:31 +0000
@@ -29,15 +29,16 @@
     >>> title_field = IProduct['title']
     >>> title = 'Edit the title'
     >>> widget = TextLineEditorWidget(
-    ...     product, title_field, title, 'h1', max_width='90%')
+    ...     product, title_field, title, 'h1', max_width='90%',
+    ...     truncate_lines=2)
 
 The widget is rendered by executing it, it prints out the attribute
 content.
 
     >>> print widget()
     <h1 id="edit-title">
-    <span class="yui3-editable_text-text ellipsis"
-          style="max-width: 90%;">
+    <span style="max-width: 90%;"
+          class="yui3-editable_text-text ellipsis">
         Widgets &gt; important
     </span>
     </h1>
@@ -49,8 +50,8 @@
     >>> ignored = login_person(product.owner)
     >>> print widget()
     <h1 id="edit-title">
-    <span class="yui3-editable_text-text ellipsis"
-          style="max-width: 90%;">
+    <span style="max-width: 90%;"
+          class="yui3-editable_text-text ellipsis">
         Widgets &gt; important
     </span>
         <a class="yui3-editable_text-trigger sprite edit action-icon"

=== added file 'lib/lp/app/javascript/ellipsis.js'
--- lib/lp/app/javascript/ellipsis.js	1970-01-01 00:00:00 +0000
+++ lib/lp/app/javascript/ellipsis.js	2012-08-10 06:01:31 +0000
@@ -0,0 +1,263 @@
+/**
+* Ellipsis plugin (YUI) - For when text is too l ...
+*
+* @fileOverview A slightly smarter way of truncating text
+* @author Dan Beam <dan@xxxxxxxxxxx>
+* @param {object} conf - configuration objects to override the defaults
+* @return {Node} the Node passed to the method
+*
+* Copyright (c) 2010 Dan Beam
+* Licensed under the MIT License: http://www.opensource.org/licenses/mit-license.php
+*
+* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+* THE SOFTWARE.
+*/
+
+YUI.add('lp.app.ellipsis', function(Y) {
+
+    var // the allowable difference when comparing floating point numbers
+        fp_epsilon = 0.01,
+
+        // floating point comparison
+        fp_equals = function (a, b) { return Math.abs(a - b) <= fp_epsilon; },
+        fp_greater = function (a, b) { return a - b >= fp_epsilon; },
+        fp_lesser = function (a, b) { return a - b <= fp_epsilon; },
+
+        // do a quick feature test to see if native text-overflow: ellipsis is supported
+        nativeRule = false,
+
+        // remember some native styles if we have support
+        nativeStyles = {
+            'white-space' : 'nowrap',
+            'overflow' : 'hidden'
+        },
+
+        // determine whether we want to use currentStyle instead of some buggy .getComputedStyle() results
+        currentStyle = true;
+
+    // add this on all Y.Node instances (but only if imported
+    Y.DOM.ellipsis = function (node, conf) {
+
+        // homogenize conf to object
+        conf = conf || {};
+
+        // augment our conf object with some default settings
+        Y.mix(conf, {
+            // end marker
+            'ellipsis' : '\u2026',
+
+            // for stuff we *really* don't want to wrap, increase this number just in case
+            'fudge' : 3,
+
+            // target number of lines to wrap
+            'lines' : 1,
+
+            // whether or not to remember the original text to able to de-truncate
+            'remember' : true,
+
+            // should we use native browser support when it exists? (on by default)
+            'native' : true
+        });
+
+        // console.log(conf);
+        // console.log(Y.one(node).getComputedStyle('lineHeight'));
+        // console.log(Y.one(node).getComputedStyle('fontSize'));
+
+            // the element we're trying to truncate
+        var yEl = Y.one(node),
+
+            // the name of the field we use to store using .setData()
+            dataAttrName = 'ellipsis-original-text',
+
+            // original text
+            originalText = conf.remember && yEl.getData(dataAttrName) || yEl.get('text'),
+
+            // keep the current length of the text so far
+            currentLength = originalText.length,
+
+            // the number of characters to increment or decrement the text by
+            charIncrement = currentLength,
+
+            // copy the element so we can string length invisibly
+            clone = Y.one(document.createElement(yEl.get('nodeName'))),
+
+            // some current values used to cache .getComputedStyle() accesses and compare to our goals
+            lineHeight, targetHeight, currentHeight, lastKnownGood;
+
+        // @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
+        // @ NOTE: I'm intentionally ignoring padding as .getComputedStyle('height') @
+        // @ NOTE: and .getComputedStyle('width') both ignore this as well. @
+        // @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
+
+        // copy styles to clone object
+
+        // XXX - if no width is explicitly specified, getComputedStyle('width')
+        // may return 'auto' which is not suitable for setting up the cloned
+        // node.
+        var width = currentStyle ? node.offsetWidth : yEl.getComputedStyle('width');
+        if (width === 'auto') {
+            width = node.offsetWidth;
+        }
+
+        clone.setStyles({
+            'overflow' : 'hidden', // only at first
+            'position' : 'absolute',
+            'visibility' : 'hidden',
+            'display' : 'block',
+            'bottom' : '-10px',
+            'left' : '-10px',
+            'width' : width,
+            'fontSize' : currentStyle ? node.currentStyle.fontSize : yEl.getComputedStyle('fontSize'), /* weird IE7 + reset bug */
+            'fontFamily' : yEl.getComputedStyle('fontFamily'),
+            'fontWeight' : yEl.getComputedStyle('fontWeight'),
+            'letterSpacing' : yEl.getComputedStyle('letterSpacing'),
+            'lineHeight' : yEl.getComputedStyle('lineHeight')
+        });
+
+        // insert some text to get the line-height (because .getComputedStyle('lineHeight') can be "normal" sometimes!)
+        clone.set('text', 'some sample text');
+
+        // unfortunately, we must insert into the DOM, :(
+        Y.one('body').append(clone);
+
+        // get the height of the node with only 1 character of text (should be 1 line)
+        lineHeight = parseFloat(clone.getComputedStyle('height'));
+
+        // if we have the native support for text-overflow and we only want 1 line with the same style ellipsis
+        if (Y.DOM.ellipsis.nativeSupport && conf['native'] && 1 == conf.lines && '\u2026' === conf.ellipsis) {
+            // console.log('using native!');
+            // apply the styles
+            yEl.setStyles(nativeStyles);
+            // this is needed to trigger the overflow in some browser (*cough* Opera *cough*)
+            yEl.setStyle('height', lineHeight + 'px');
+            // exit early and clean-up
+            clone.remove();
+            return;
+        }
+
+        // set overflow back to visible
+        clone.setStyle('overflow', 'visible');
+
+        // compute how high the node should be if it's the right number of lines
+        targetHeight = conf.lines * lineHeight;
+
+        // insert the original text, in case we've already truncated
+        clone.set('text', originalText);
+
+        // ok, now that we have a node in the DOM with the right text, measure it's height
+        currentHeight = parseFloat(clone.getComputedStyle('height'));
+
+        // console.log('lineHeight', lineHeight);
+        // console.log('currentHeight', currentHeight);
+        // console.log('targetHeight', targetHeight);
+        // console.log('originalText.length', originalText.length);
+        // console.log('yEl.get(\'text\').length', yEl.get('text').length);
+
+        // quick sanity check
+        if (fp_lesser(currentHeight, targetHeight) && originalText.length === yEl.get('text').length) {
+            // console.log('truncation not necessary!');
+            clone.remove();
+            return;
+        }
+
+        // now, let's start looping through and slicing the text as necessary
+        for (; charIncrement >= 1; ) {
+
+            // increment decays by half every time
+            charIncrement = Math.floor(charIncrement / 2);
+
+            // if the height is too big, remove some chars, else add some
+            currentLength += fp_greater(currentHeight, targetHeight) ? -charIncrement : +charIncrement;
+
+            // try text at current length
+            clone.set('text', originalText.slice(0, currentLength - conf.ellipsis.length) + conf.ellipsis);
+
+            // compute the current height
+            currentHeight = parseFloat(clone.getComputedStyle('height'));
+
+            // we only want to store values that aren't too big
+            if (fp_equals(currentHeight, targetHeight) || fp_lesser(currentHeight, targetHeight)) {
+                lastKnownGood = currentLength;
+            }
+
+            // console.log('currentLength', currentLength);
+            // console.log('currentHeight', currentHeight);
+            // console.log('targetHeight' , targetHeight );
+            // console.log('charIncrement', charIncrement);
+            // console.log('lastKnownGood', lastKnownGood);
+
+        }
+
+        // remove from DOM
+        clone.remove();
+
+        // set the original text if we want to ever want to expand past the current truncation
+        if (conf.remember && !yEl.getData(dataAttrName)) {
+            yEl.setData(dataAttrName, originalText);
+        }
+
+        // console.log('originalText.length', originalText.length);
+        // console.log('clone.get(\'text\').length', clone.get('text').length);
+        // console.log('conf.ellipsis.length', conf.ellipsis.length);
+
+        // if the text matches
+        if (originalText.length === (clone.get('text').length - conf.ellipsis.length)) {
+            // this means we *de-truncated* and can fit fully in the new space
+            // console.log('de-truncated!');
+            yEl.set('text', originalText);
+        }
+        // this should never happen, but it doesn't hurt to check
+        else if ('undefined' !== typeof lastKnownGood) {
+            // do this thing, already!
+            yEl.set('text', originalText.slice(0, lastKnownGood - conf.ellipsis.length - conf.fudge) + conf.ellipsis);
+        }
+        // return myself for chainability
+        return yEl;
+
+    };
+
+    Y.Node.importMethod(Y.DOM, 'ellipsis');
+    Y.NodeList.importMethod(Y.Node.prototype, 'ellipsis');
+
+    // must wait to append hidden node
+    Y.on('domready', function () {
+
+        // create a hidden node and try to style it
+        var cloned,
+            hidden = Y.Node.create('<div style="visibility:hidden;position:absolute;white-space:nowrap;overflow:hidden;"></div>'),
+            rules = ['textOverflow', 'OTextOverflow'];
+
+        // pseudo feature detection to detect browsers with currentStyle but without a more standards-ish implementation (currently IE6-8)
+        currentStyle = !!(document.body.currentStyle && (window.CSSCurrentStyleDeclaration || !window.CSSStyleDeclaration));
+
+        Y.each(rules, function (rule) {
+            hidden.setStyle(rule, 'ellipsis');
+        });
+
+        // add to DOM
+        Y.one('body').appendChild(hidden);
+
+        // deep clone the node (include attributes)
+        cloned = hidden.cloneNode(true);
+
+        Y.some(rules, function (rule) {
+            if ('ellipsis' === cloned.getStyle(rule)) {
+                nativeRule = rule;
+                nativeStyles[nativeRule] = 'ellipsis';
+                Y.DOM.ellipsis.nativeSupport = true;
+                return true;
+            }
+        });
+
+        // clean-up
+        hidden.remove();
+        hidden = cloned = null;
+
+    });
+
+}, "0.1", {"requires": []});

=== modified file 'lib/lp/app/javascript/inlineedit/editor.js'
--- lib/lp/app/javascript/inlineedit/editor.js	2012-03-21 12:07:28 +0000
+++ lib/lp/app/javascript/inlineedit/editor.js	2012-08-10 06:01:31 +0000
@@ -34,6 +34,7 @@
     ACCEPT_EMPTY = 'accept_empty',
     MULTILINE = 'multiline',
     MULTILINE_MIN_SIZE = 60,
+    TRUNCATE_LINES = 'truncate_lines',
 
     TOP_BUTTONS = 'top_buttons',
     BOTTOM_BUTTONS = 'bottom_buttons',
@@ -275,6 +276,20 @@
     },
 
     /**
+     * Determines the maximum number of lines to display before the text is
+     * truncated with an ellipsis. 0 means no truncation.
+     *
+     * @attribute truncate_lines
+     * @default 0
+     */
+    truncate_lines: {
+        value: 0,
+        validator: function(value) {
+            return Y.Lang.isNumber(value) && value >= 0;
+        }
+    },
+
+    /**
      * Determines which sets of buttons should be shown in multi-line
      * mode: "top", "bottom", or "both".
      *
@@ -766,6 +781,20 @@
         // 'down:27' is the decimal value of the Firefox DOM_VK_ESCAPE
         // symbol or U+001B.
         Y.on('key', this.cancel, this.get(INPUT_EL), 'down:27', this);
+
+        // Set up the callback to truncate the displayed text if necessary.
+        var truncate_lines = this.get(TRUNCATE_LINES);
+        if (truncate_lines > 0) {
+            var bounding_box = this.get(BOUNDING_BOX);
+            var ellipsis_nodes = bounding_box.get('parentNode')
+                    .all('.yui3-editable_text-text.ellipsis');
+            Y.on('domready', function() {
+                ellipsis_nodes.ellipsis({lines: truncate_lines});
+                });
+            Y.on('windowresize', function () {
+                ellipsis_nodes.ellipsis({lines: truncate_lines});
+            });
+        }
     },
 
     _bindButtons: function(button_class, method) {
@@ -1380,4 +1409,5 @@
 }, "0.2", {"skinnable": true,
            "requires": ["oop", "anim", "event", "node", "widget",
                         "lp.anim", "lazr.base", "lp.app.errors",
-                        "lp.app.formwidgets.resizing_textarea"]});
+                        "lp.app.formwidgets.resizing_textarea",
+                        "lp.app.ellipsis"]});

=== modified file 'lib/lp/app/templates/base-layout-macros.pt'
--- lib/lp/app/templates/base-layout-macros.pt	2012-07-26 15:43:32 +0000
+++ lib/lp/app/templates/base-layout-macros.pt	2012-08-10 06:01:31 +0000
@@ -167,7 +167,8 @@
             'lp.app.banner.beta', 'lp.app.foldables','lp.app.sorttable',
             'lp.app.inlinehelp', 'lp.app.links', 'lp.app.longpoll',
             'lp.bugs.bugtask_index', 'lp.bugs.subscribers',
-            'lp.code.branchmergeproposal.diff', function(Y) {
+            'lp.app.ellipsis', 'lp.code.branchmergeproposal.diff',
+             function(Y) {
 
             Y.on("domready", function () {
                 if (Y.one(document.body).hasClass('private')) {

=== modified file 'lib/lp/app/templates/text-line-editor.pt'
--- lib/lp/app/templates/text-line-editor.pt	2012-08-10 06:01:30 +0000
+++ lib/lp/app/templates/text-line-editor.pt	2012-08-10 06:01:31 +0000
@@ -1,6 +1,8 @@
 <tal:open-tag replace="structure view/open_tag"/>
-<span class="yui3-editable_text-text ellipsis"
-    tal:attributes="style string:${view/css_style}">
+<span
+    tal:attributes="
+     class string:${view/text_css_class};
+     style string:${view/text_css_style};">
     <tal:text replace="view/value"/>
 </span>
   <a tal:condition="view/can_write"
@@ -17,6 +19,7 @@
                 contentBox: ${view/widget_css_selector},
                 accept_empty: ${view/accept_empty},
                 width: ${view/width},
+                truncate_lines: ${view/truncate_lines},
                 initial_value_override: ${view/initial_value_override}
                 });
             widget.editor.plug({

=== modified file 'lib/lp/blueprints/browser/specification.py'
--- lib/lp/blueprints/browser/specification.py	2012-08-10 06:01:30 +0000
+++ lib/lp/blueprints/browser/specification.py	2012-08-10 06:01:31 +0000
@@ -621,7 +621,8 @@
         field = ISpecification['title']
         title = "Edit the blueprint title"
         return TextLineEditorWidget(
-            self.context, field, title, 'h1', max_width='90%')
+            self.context, field, title, 'h1', max_width='95%',
+            truncate_lines=2)
 
     @property
     def summary_widget(self):

=== modified file 'lib/lp/bugs/browser/bugtask.py'
--- lib/lp/bugs/browser/bugtask.py	2012-08-10 06:01:30 +0000
+++ lib/lp/bugs/browser/bugtask.py	2012-08-10 06:01:31 +0000
@@ -734,7 +734,7 @@
         self.bug_title_edit_widget = TextLineEditorWidget(
             bug, IBug['title'], "Edit this summary", 'h1',
             edit_url=canonical_url(self.context, view_name='+edit'),
-            max_width='90%')
+            max_width='95%', truncate_lines=2)
 
         # XXX 2010-10-05 gmb bug=655597:
         # This line of code keeps the view's query count down,

=== modified file 'lib/lp/bugs/javascript/bug_picker.js'
--- lib/lp/bugs/javascript/bug_picker.js	2012-08-09 04:56:41 +0000
+++ lib/lp/bugs/javascript/bug_picker.js	2012-08-10 06:01:31 +0000
@@ -190,10 +190,11 @@
         '  <div class="buglisting-col2">',
         '  <a href="{{bug_url}}" class="bugtitle sprite new-window" ',
         '  style="padding-top: 3px">',
-        '  <p class="ellipsis"><span class="bugnumber">#{{id}}</span>',
+        '  <p class="ellipsis single-line">',
+        '  <span class="bugnumber">#{{id}}</span>',
         '  &nbsp;{{bug_summary}}</p></a>',
         '  <div class="buginfo-extra">',
-        '      <p class="ellipsis">{{description}}</p></div>',
+        '      <p class="ellipsis single-line">{{description}}</p></div>',
         '  </div>',
         '</div></td></tr>',
         '{{> private_warning}}',

=== modified file 'lib/lp/bugs/javascript/duplicates.js'
--- lib/lp/bugs/javascript/duplicates.js	2012-08-09 04:56:41 +0000
+++ lib/lp/bugs/javascript/duplicates.js	2012-08-10 06:01:31 +0000
@@ -381,7 +381,8 @@
     // table.
     _duplicate_bug_info_message: function(dup_id, dup_title) {
         var info_template = [
-            '<span class="bug-duplicate-details ellipsis wide">',
+            '<span class="bug-duplicate-details ellipsis ',
+            'single-line wide">',
             '<span class="sprite info"></span>',
             'This bug report is a duplicate of:&nbsp;',
             '<a href="/bugs/{dup_id}">Bug #{dup_id} {dup_title}</a></span>',

=== modified file 'lib/lp/bugs/templates/bugtask-index.pt'
--- lib/lp/bugs/templates/bugtask-index.pt	2012-08-09 04:56:41 +0000
+++ lib/lp/bugs/templates/bugtask-index.pt	2012-08-10 06:01:31 +0000
@@ -101,7 +101,7 @@
           <tal:dupe-info
              condition="duplicateof|nothing"
              define="duplicateof context/bug/duplicateof">
-          <span class="bug-duplicate-details ellipsis wide">
+          <span class="bug-duplicate-details ellipsis single-line wide">
             <span class="sprite info"></span>
             This bug report is a duplicate of:&nbsp;
                 <a

=== modified file 'lib/lp/code/browser/sourcepackagerecipe.py'
--- lib/lp/code/browser/sourcepackagerecipe.py	2012-08-10 06:01:30 +0000
+++ lib/lp/code/browser/sourcepackagerecipe.py	2012-08-10 06:01:31 +0000
@@ -302,7 +302,8 @@
         name = ISourcePackageRecipe['name']
         title = "Edit the recipe name"
         return TextLineEditorWidget(
-            self.context, name, title, 'h1', max_width='90%')
+            self.context, name, title, 'h1', max_width='95%',
+            truncate_lines=1)
 
     @property
     def distroseries_widget(self):

=== modified file 'lib/lp/registry/browser/product.py'
--- lib/lp/registry/browser/product.py	2012-08-10 06:01:30 +0000
+++ lib/lp/registry/browser/product.py	2012-08-10 06:01:31 +0000
@@ -960,7 +960,8 @@
         title_field = IProduct['title']
         title = "Edit this title"
         self.title_edit_widget = TextLineEditorWidget(
-            product, title_field, title, 'h1', max_width='90%')
+            product, title_field, title, 'h1', max_width='95%',
+            truncate_lines=2)
         programming_lang = IProduct['programminglang']
         title = 'Edit programming languages'
         additional_arguments = {

=== modified file 'lib/lp/registry/browser/team.py'
--- lib/lp/registry/browser/team.py	2012-08-10 06:01:30 +0000
+++ lib/lp/registry/browser/team.py	2012-08-09 13:23:35 +0000
@@ -1005,13 +1005,8 @@
         'subscriptionpolicy', LaunchpadRadioWidgetWithDescription,
         orientation='vertical')
     custom_widget('teamdescription', TextAreaWidget, height=10, width=30)
-<<<<<<< TREE
     custom_widget('defaultrenewalperiod', IntWidget,
         widget_class='field subordinate')
-=======
-    custom_widget('defaultrenewalperiod', StrippedTextWidget,
-        widget_class='field subordinate')
->>>>>>> MERGE-SOURCE
 
     def setUpFields(self):
         """See `LaunchpadViewForm`.

=== modified file 'lib/lp/registry/browser/tests/private-team-creation-views.txt'
--- lib/lp/registry/browser/tests/private-team-creation-views.txt	2012-08-10 06:01:30 +0000
+++ lib/lp/registry/browser/tests/private-team-creation-views.txt	2012-08-09 13:23:35 +0000
@@ -100,7 +100,7 @@
     ...     'field.displayname': 'Shhhh',
     ...     'field.description': 'my own team description',
     ...     'field.defaultmembershipperiod': '365',
-    ...     'field.defaultrenewalperiod': '',
+    ...     'field.defaultrenewalperiod': '365',
     ...     'field.subscriptionpolicy': 'RESTRICTED',
     ...     'field.renewal_policy': 'ONDEMAND',
     ...     'field.visibility': 'PRIVATE',

=== modified file 'lib/lp/soyuz/browser/archive.py'
--- lib/lp/soyuz/browser/archive.py	2012-08-10 06:01:30 +0000
+++ lib/lp/soyuz/browser/archive.py	2012-08-10 06:01:31 +0000
@@ -901,7 +901,8 @@
         display_name = IArchive['displayname']
         title = "Edit the displayname"
         return TextLineEditorWidget(
-            self.context, display_name, title, 'h1', max_width='90%')
+            self.context, display_name, title, 'h1', max_width='95%',
+            truncate_lines=1)
 
     @property
     def default_series_filter(self):


Follow ups