← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~rharding/launchpad/productjs into lp:launchpad

 

Richard Harding has proposed merging lp:~rharding/launchpad/productjs into lp:launchpad with lp:~rharding/launchpad/pp_register as a prerequisite.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~rharding/launchpad/productjs/+merge/123803

TBD
-- 
https://code.launchpad.net/~rharding/launchpad/productjs/+merge/123803
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~rharding/launchpad/productjs into lp:launchpad.
=== added file 'lib/lp/registry/javascript/product-new.js'
--- lib/lp/registry/javascript/product-new.js	1970-01-01 00:00:00 +0000
+++ lib/lp/registry/javascript/product-new.js	2012-09-11 18:14:22 +0000
@@ -0,0 +1,369 @@
+/* Copyright 2012 Canonical Ltd.  This software is licensed under the
+ * GNU Affero General Public License version 3 (see the file LICENSE).
+ *
+ * Support the UI for registering a new project.
+ *
+ * @module Y.lp.registry.product.new
+ * @requires base, node, effects, lp.app.choice
+ */
+YUI.add('lp.registry.product.new', function (Y) {
+    var ns = Y.namespace('lp.registry.product.new');
+
+    /**
+     * Handle setting up the JS for the new project View
+     *
+     * @class lp.registry.product.new.View
+     * @extends Y.Base
+     */
+    ns.View = Y.Base.create('registry.product.new.view', Y.Base, [], {
+        // These two regexps serve slightly different purposes.  The first
+        // finds the leftmost run of valid url characters for the autofill
+        // operation.  The second validates the entire string, used for
+        // explicit entry into the URL field.  These are simple enough to keep
+        // in sync so it doesn't bother me that we repeat most of it.  Note
+        // that while both ignore case, only the first one should be global in
+        // order to utilize the RegExp.lastIndex behavior of .exec().
+        valid_urls: new RegExp('^[a-z0-9][-.+a-z0-9]*', 'ig'),
+        valid_char: new RegExp('^[a-z0-9][-.+a-z0-9]*$', 'i'),
+
+        /**
+         * Process binding the UI for the information type choice widget.
+         *
+         * @method _bind_information-type
+         * @private
+         */
+        _bind_inforation_type: function () {
+            Y.lp.app.choice.addPopupChoiceForRadioButtons(
+                'information_type', LP.cache.information_type_data, true);
+        },
+
+        /**
+         * Bind the url/name field interaction
+         *
+         * @method _bind_name_field
+         * @private
+         */
+        _bind_name_field: function () {
+            var url_field = Y.one('input[id="field.name"]');
+            var name_field = Y.one('input[id="field.displayname"]');
+            name_field.on('keyup', this._url_autofill, this);
+
+            // Explicitly typing into the URL field disables autofilling.
+            url_field.on('keyup', function(e) {
+                if (url_field.get('value') === '') {
+                    // The user cleared the URL field; turn on autofill.
+                    name_field.on('keyup', this._url_autofill);
+                } else {
+                    /* Honor the user's URL; turn off autofill. */
+                    name_field.detach('keyup', this._url_autofill);
+                }
+            }, this);
+
+            // Prevent invalid characters from being input into the URL field.
+            url_field.on('keypress', function(e) {
+                // Handling key events is madness.  For a glimpse, see
+                // http://unixpapa.com/js/key.html
+                //
+                // Additional spice for the insanity stew is given by the
+                // rhino book, page 428.  This code is basically a rip and
+                // remix of those two texts.
+                var event = e || window.event;
+                var code = e.charCode || e.keyCode;
+
+                if (/* Check for special characters. */
+                    e.which === 0 || e.which === null ||
+                    /* Check for function keys (Firefox only). */
+                    e.charCode === 0 ||
+                    /* Check for ctrl or alt held down. */
+                    e.ctrlKey || e.altKey ||
+                    /* Check for ASCII control character */
+                    32 > code)
+                {
+                    return true;
+                }
+                var char = String.fromCharCode(code);
+                var new_value = url_field.get('value') + char;
+                if (new_value.search(this.valid_char) >= 0) {
+                    /* The character is valid. */
+                    return true;
+                }
+                e.preventDefault();
+                e.returnValue = false;
+                return false;
+            }, this);
+        },
+
+        /**
+         * When the 'No' button is clicked, we swap in the registration
+         * details for the search results.  It really doesn't look good
+         * to leave the search results there.
+         *
+         * @method _complete_registration
+         * @param {Event}
+         * @private
+         */
+        _complete_registration: function(ev) {
+            var that = this;
+            ev.halt();
+            var step_title = Y.one('#step-title');
+            var expander = Y.one('#search-results-expander');
+
+            /* Slide in the search results and hide them under a link. */
+            expander.removeClass('hidden');
+            expander.on('click', function(e) {
+                e.halt();
+
+                var arrow = Y.one('#search-results-arrow');
+                if (arrow.getAttribute('src') === '/@@/treeCollapsed') {
+                    // The search results are currently hidden.  Slide them
+                    // out and turn the arrow to point downward.
+                    arrow.setAttribute('src', '/@@/treeExpanded');
+                    arrow.setAttribute('title', 'Hide search results');
+                    arrow.setAttribute('alt', 'Hide search results');
+                    Y.lazr.effects.slide_out(this.get('search_results')).run();
+                    that._show_separator(true);
+                }
+                else {
+                    // The search results are currently displayed.  Slide them
+                    // in and turn the arrow to point rightward.
+                    arrow.setAttribute('src', '/@@/treeCollapsed');
+                    arrow.setAttribute('title', 'Show search results');
+                    arrow.setAttribute('alt', 'Show search results');
+                    Y.lazr.effects.slide_in(this.get('search_results')).run();
+                    that._show_separator(false);
+                }
+           });
+
+           // Hide the 'No' button, but slide out the search results, so the
+           // user has a clue that Something Is Happening.
+           this.get('details_buttons').addClass('hidden');
+
+           // Slide out the registration details widgets, but add an 'end'
+           // event handler so that the height style left by lazr.effects is
+           // removed when the animation is done.  We're never going to slide
+           // the form widgets back in, and the height style causes the
+           // licence widget to dive under the Complete Registration button.
+           // See bug 391138 for details.
+           var anim = Y.lazr.effects.slide_out(this.get('form_widgets'));
+           anim.on('end', function() {
+               that.get('form_widgets').setStyle('height', null);
+           });
+           anim.run();
+
+           // Toggle the visibility of the various other widgets.
+           this.get('form_actions').removeClass('hidden');
+           this.get('title').removeClass('hidden');
+
+           // Set the H2 title to something more appropriate for the
+           // selected task.
+           step_title.set('innerHTML', 'Step 2 (of 2) Registration details');
+
+           var reset_height = this.get('search_results').getComputedStyle('height');
+           this.get('search_results').setStyle('height', reset_height);
+           Y.lazr.effects.slide_in(this.get('search_results')).run();
+
+           // Append a special marker to the hidden state widget. See
+           // ProjectAddStepTwo.search_results_count() for details.
+           var steps = this.get('marker').getAttribute('value');
+           if (0 > steps.search(new RegExp('hidesearch'))) {
+               this.get('marker').setAttribute('value', steps + "|hidesearch");
+           }
+        },
+
+        /**
+         * Handle the reveals when there are search results.
+         *
+         * @method _show_separator
+         * @param {Boolean}
+         * @private
+         */
+        _show_separator: function (flag) {
+            var separator = Y.one('#registration-separator');
+            if (!separator) {
+                // The separator is not on the page, because there were no
+                // search results.
+                return;
+            }
+            if (flag) {
+                separator.removeClass('hidden');
+            } else {
+                separator.addClass('hidden');
+            }
+        },
+
+        /**
+         * Generate a url for the project based on the name.
+         *
+         * @method _url_autofill
+         * @param {Event}
+         * @private
+         */
+        _url_autofill: function (e) {
+            var url_field = Y.one('input[id="field.name"]');
+            var name_value = e.target.get('value');
+            if (name_value === '') {
+                /* When Name is empty, clear URL. */
+                url_field.set('value', '');
+            } else {
+                // Fill the URL field with as much of the left part of the
+                // string as matches the regexp.  If the regexp doesn't
+                // match (say because there's illegal stuff at the front),
+                // don't change the current URL field.  We have to reset
+                // lastIndex each time we get here so that search begins
+                // at the front of the string.
+                this.valid_urls.lastIndex = 0;
+                var match = this.valid_urls.exec(name_value);
+                if (match) {
+                    var slice = name_value.slice(0, this.valid_urls.lastIndex);
+                    url_field.set('value', slice);
+                }
+            }
+        },
+
+        /**
+         * Bind the UI interactions that will be tracked through the View
+         * lifecycle.
+         *
+         * @method bindUI
+         */
+        bindUI: function () {
+            if (Y.one('input[name="field.information_type"]')) {
+                this._bind_information_type();
+            }
+
+            if(Y.one('input[id="field.name"]')) {
+                this._bind_name_field();
+            }
+
+            if (this.get('details_buttons')) {
+                this.get('details_buttons').on('click',
+                                               this._complete_registration,
+                                               this);
+            }
+        },
+
+        /**
+         * Standard YUI init.
+         *
+         * @method initialize
+         * @param {Object}
+         */
+        initialize: function (cfg) {
+            // The details button is only visible when JavaScript is enabled, but
+            // the H3 separator is only visible when JavaScript is disabled.
+            // Neither is displayed on the step 1 page.
+            this._show_separator(false);
+        },
+
+        /**
+         * Render the view by binding to the current DOM.
+         *
+         * @method render
+         */
+        render: function () {
+            this.bindUI();
+
+            if (this.get('details_buttons')) {
+                this.get('details_buttons').removeClass('hidden');
+            }
+
+            // If there are search results, hide the registration details.
+            if (this.get('search_results')) {
+                this.get('form_widgets').addClass('hidden');
+                this.get('form_actions').addClass('hidden');
+                this.get('title').addClass('hidden');
+            }
+
+            // If we've been here before (e.g. there was an error in
+            // submitting step 2), jump to continuing the registration.
+            var marker = this.get('marker');
+            if (marker && marker.getAttribute('value').search(/hidesearch/) >= 0) {
+                this._complete_registration(null);
+            }
+        }
+    }, {
+        ATTRS: {
+            /**
+             * Lazy load the found node for use through out the View.
+             *
+             * @attribute details_buttons
+             * @default Node
+             * @type Node
+             */
+            details_buttons: {
+                valueFn: function (val) {
+                    return Y.one('#registration-details-buttons');
+                }
+            },
+
+            /**
+             * Lazy load the found node for use through out the View.
+             *
+             * @attribute form_actions
+             * @default Node
+             * @type Node
+             */
+            form_actions: {
+                valueFn: function (va) {
+                    return Y.one('#launchpad-form-actions');
+                }
+            },
+
+            /**
+             * Lazy load the found node for use through out the View.
+             *
+             * @attribute form_widgets
+             * @default Node
+             * @type Node
+             */
+            form_widgets: {
+                valueFn: function (val) {
+                    return Y.one('#launchpad-form-widgets');
+                }
+            },
+
+            /**
+             * Lazy load the found node for use through out the View.
+             * This is the magic hidden widget used by the MultiStepView.
+             *
+             * @attribute marker
+             * @default Node
+             * @type Node
+             */
+            marker: {
+                valueFn: function (val) {
+                    return Y.one(Y.DOM.byId('field.__visited_steps__'));
+                }
+            },
+
+            /**
+             * Lazy load the found node for use through out the View.
+             *
+             * @attribute search_results
+             * @default Node
+             * @type Node
+             */
+            search_results: {
+                valueFn: function (val) {
+                    return Y.one('#search-results');
+                }
+            },
+
+            /**
+             * Lazy load the found node for use through out the View.
+             *
+             * @attribute title
+             * @default Node
+             * @type Node
+             */
+            title: {
+                valueFn: function (val) {
+                    return Y.one('#registration-details-title');
+                }
+            }
+        }
+    });
+
+}, '0.1', {
+    'requires': ['base', 'node', 'lazr.effects', 'lp.app.choice']
+});

=== added file 'lib/lp/registry/javascript/tests/test_product_new.html'
--- lib/lp/registry/javascript/tests/test_product_new.html	1970-01-01 00:00:00 +0000
+++ lib/lp/registry/javascript/tests/test_product_new.html	2012-09-11 18:14:22 +0000
@@ -0,0 +1,60 @@
+<!DOCTYPE html>
+<!--
+Copyright 2012 Canonical Ltd.  This software is licensed under the
+GNU Affero General Public License version 3 (see the file LICENSE).
+-->
+
+<html>
+  <head>
+      <title>Product New Tests</title>
+
+      <!-- YUI and test setup -->
+      <script type="text/javascript"
+              src="../../../../../build/js/yui/yui/yui.js">
+      </script>
+      <link rel="stylesheet"
+      href="../../../../../build/js/yui/console/assets/console-core.css" />
+      <link rel="stylesheet"
+      href="../../../../../build/js/yui/console/assets/skins/sam/console.css" />
+      <link rel="stylesheet"
+      href="../../../../../build/js/yui/test/assets/skins/sam/test.css" />
+
+      <script type="text/javascript"
+              src="../../../../../build/js/lp/app/testing/testrunner.js"></script>
+      <script type="text/javascript"
+              src="../../../../build/js/lp/app/testing/helpers.js"></script>
+
+      <link rel="stylesheet" href="../../../app/javascript/testing/test.css" />
+
+      <!-- Dependencies -->
+      <script type="text/javascript" src="../../../../../build/js/lp/app/client.js"></script>
+      <script type="text/javascript" src="../../../../../build/js/lp/app/choice.js"></script>
+      <script type="text/javascript" src="../../../../../build/js/lp/app/ellipsis.js"></script>
+      <script type="text/javascript" src="../../../../../build/js/lp/app/expander.js"></script>
+      <script type="text/javascript" src="../../../../../build/js/lp/app/errors.js"></script>
+      <script type="text/javascript" src="../../../../../build/js/lp/app/lp.js"></script>
+      <script type="text/javascript" src="../../../../../build/js/lp/app/anim/anim.js"></script>
+      <script type="text/javascript" src="../../../../../build/js/lp/app/extras/extras.js"></script>
+      <script type="text/javascript" src="../../../../../build/js/lp/app/choiceedit/choiceedit.js"></script>
+      <script type="text/javascript" src="../../../../../build/js/lp/app/effects/effects.js"></script>
+      <script type="text/javascript" src="../../../../../build/js/lp/app/formoverlay/formoverlay.js"></script>
+      <script type="text/javascript" src="../../../../../build/js/lp/app/formwidgets/resizing_textarea.js"></script>
+      <script type="text/javascript" src="../../../../../build/js/lp/app/inlineedit/editor.js"></script>
+      <script type="text/javascript" src="../../../../../build/js/lp/app/testing/helpers.js"></script>
+      <script type="text/javascript" src="../../../../../build/js/lp/app/overlay/overlay.js"></script>
+      <script type="text/javascript" src="../../../../../build/js/lp/app/ui/ui.js"></script>
+
+      <!-- The module under test. -->
+      <script type="text/javascript" src="../product-new.js"></script>
+
+      <!-- The test suite -->
+      <script type="text/javascript" src="test_product_new.js"></script>
+
+    </head>
+    <body class="yui3-skin-sam">
+        <ul id="suites">
+            <li>lp.registry.product.new.test</li>
+        </ul>
+        <div id="testdom"></div>
+    </body>
+</html>

=== added file 'lib/lp/registry/javascript/tests/test_product_new.js'
--- lib/lp/registry/javascript/tests/test_product_new.js	1970-01-01 00:00:00 +0000
+++ lib/lp/registry/javascript/tests/test_product_new.js	2012-09-11 18:14:22 +0000
@@ -0,0 +1,74 @@
+/* Copyright (c) 2012 Canonical Ltd. All rights reserved. */
+
+YUI.add('lp.registry.product.new.test', function (Y) {
+
+    var tests = Y.namespace('lp.registry.product.new.test');
+    tests.suite = new Y.Test.Suite('registry.product.new Tests');
+
+    tests.suite.add(new Y.Test.Case({
+        name: 'registry.product.new_tests',
+
+        setUp: function () {},
+        tearDown: function () {
+            Y.one('#testdom').empty();
+        },
+
+        _setup_url_fields: function () {
+            Y.one('#testdom').setContent(
+                '<input type="text" id="field.name" name="field.name" />' +
+                '<input type="text" id="field.displayname" name="field.displayname" />'
+            );
+        },
+
+        test_library_exists: function () {
+            Y.Assert.isObject(Y.lp.registry.product.new,
+                "Could not locate the lp.registry.product.new module");
+        },
+
+        test_url_autofill_sync: function () {
+            this._setup_url_fields();
+            var view = new Y.lp.registry.product.new.View();
+            view.render();
+
+            var name_field = Y.one('input[id="field.displayname"]');
+            name_field.set('value', 'test');
+            name_field.simulate('keyup');
+
+            Y.Assert.areEqual(
+                'test',
+                Y.one('input[name="field.name"]').get('value'),
+                'The url field should be updated based on the display name');
+        },
+
+        test_url_autofill_disables: function () {
+            this._setup_url_fields();
+            var view = new Y.lp.registry.product.new.View();
+            view.render();
+
+            var name_field = Y.one('input[id="field.displayname"]');
+            var url_field = Y.one('input[id="field.name"]');
+            name_field.set('value', 'test');
+            name_field.simulate('keyup');
+            Y.Assert.areEqual( 'test', url_field.get('value'),
+                'The url field should be updated based on the display name');
+
+            // Now setting the url field manually should detach the event for
+            // the sync.
+            url_field.set('value', 'test2');
+            url_field.simulate('keyup');
+
+            // Changing the value back should fail horribly.
+            name_field.set('value', 'test');
+            name_field.simulate('keyup');
+
+            Y.Assert.areEqual(
+                'test2',
+                url_field.get('value'),
+                'The url field should not be updated.');
+        }
+    }));
+
+}, '0.1', {
+    requires: ['test', 'event-simulate', 'lp.testing.helpers', 'console',
+               'lp.registry.product.new']
+});

=== modified file 'lib/lp/registry/templates/product-new.pt'
--- lib/lp/registry/templates/product-new.pt	2012-09-11 18:14:22 +0000
+++ lib/lp/registry/templates/product-new.pt	2012-09-11 18:14:22 +0000
@@ -9,240 +9,17 @@
   <div metal:fill-slot="main">
 
   <script type="text/javascript">
-/*
- * When step 2 of this wizard has search results, we want to hide the
- * details widgets until the user states that the project they are
- * registering is not a duplicate.
- */
-LPJS.use('node', 'lazr.effects', 'lp.app.choice',  function(Y) {
-    Y.on('domready', function() {
-        // Setup the information choice widget.
-        if (Y.one('input[name="field.information_type"]')) {
-            Y.lp.app.choice.addPopupChoiceForRadioButtons(
-                'information_type', LP.cache.information_type_data, true);
-        }
-
-        /* These two regexps serve slightly different purposes.  The first
-         * finds the leftmost run of valid url characters for the autofill
-         * operation.  The second validates the entire string, used for
-         * explicit entry into the URL field.  These are simple enough to keep
-         * in sync so it doesn't bother me that we repeat most of it.  Note
-         * that while both ignore case, only the first one should be global in
-         * order to utilize the RegExp.lastIndex behavior of .exec().
-         */
-        var valid_urls = new RegExp('^[a-z0-9][-.+a-z0-9]*', 'ig');
-        var valid_char = new RegExp('^[a-z0-9][-.+a-z0-9]*$', 'i');
-
-        /* Handle key presses in the Name field for autofilling the URL
-         * field.  By ensuring that field.name exists, we only do this when
-         * we're on step 1 of the wizard.
-         *
-         * XXX BarryWarsaw 12-May-2009
-         * http://yuilibrary.com/projects/yui3/ticket/2423101
-         * Note that we have to use the more verbose way of getting field.name
-         * because Y.one() doesn't like the dots in the Zope field names.
-         */
-        var url_field = Y.one(Y.DOM.byId('field.name'));
-        if (url_field) {
-            var name_field = Y.one(Y.DOM.byId('field.displayname'));
-            function autofill(e) {
-                var name_value = name_field.get('value');
-                if (name_value == '') {
-                    /* When Name is empty, clear URL. */
-                    url_field.set('value', '');
-                }
-                else {
-                    /* Fill the URL field with as much of the left part of the
-                     * string as matches the regexp.  If the regexp doesn't
-                     * match (say because there's illegal stuff at the front),
-                     * don't change the current URL field.  We have to reset
-                     * lastIndex each time we get here so that search begins
-                     * at the front of the string.
-                     */
-                    valid_urls.lastIndex = 0;
-                    var match = valid_urls.exec(name_value);
-                    if (match) {
-                        var slice = name_value.slice(0, valid_urls.lastIndex);
-                        url_field.set('value', slice);
-                    }
-                }
-            }
-            name_field.on('keyup', autofill);
-            /* Prevent invalid characters from being input into the URL field.
-             */
-            url_field.on('keypress', function(e) {
-                /* Handling key events is madness.  For a glimpse, see
-                 * http://unixpapa.com/js/key.html
-                 *
-                 * Additional spice for the insanity stew is given by the
-                 * rhino book, page 428.  This code is basically a rip and
-                 * remix of those two texts.
-                 */
-                var event = e || window.event;
-                var code = e.charCode || e.keyCode;
-
-                if (/* Check for special characters. */
-                    e.which == 0 || e.which == null ||
-                    /* Check for function keys (Firefox only). */
-                    e.charCode == 0 ||
-                    /* Check for ctrl or alt held down. */
-                    e.ctrlKey || e.altKey ||
-                    /* Check for ASCII control character */
-                    32 > code)
-                {
-                    return true;
-                }
-                var char = String.fromCharCode(code);
-                var new_value = url_field.get('value') + char;
-                if (new_value.search(valid_char) >= 0) {
-                    /* The character is valid. */
-                    return true;
-                }
-                e.preventDefault();
-                e.returnValue = false;
-                return false;
-            });
-            /* Explicitly typing into the URL field disables autofilling. */
-            url_field.on('keyup', function(e) {
-                if (url_field.get('value') == '') {
-                    /* The user cleared the URL field; turn on autofill. */
-                    name_field.on('keyup', autofill);
-                }
-                else {
-                    /* Honor the user's URL; turn off autofill. */
-                    name_field.detach('keyup', autofill);
-                }
-            });
-        }
-
-        /* Handle the reveals when there are search results. */
-        var details_buttons = Y.one('#registration-details-buttons');
-        var form_actions = Y.one('#launchpad-form-actions');
-        var form_widgets = Y.one('#launchpad-form-widgets');
-        var search_results = Y.one('#search-results');
-        var step_title = Y.one('#step-title');
-        var title = Y.one('#registration-details-title');
-
-        /* This is the magic hidden widget used by the MultiStepView. */
-        var marker = Y.one(Y.DOM.byId('field.__visited_steps__'));
-
-        var separator = Y.one('#registration-separator');
-        function show_separator(flag) {
-            if (!separator) {
-                /* The separator is not on the page, because there were no
-                 * search results.
-                 */
-                return;
-            }
-            if (flag) {
-                separator.removeClass('hidden');
-            }
-            else {
-                separator.addClass('hidden');
-            }
-        }
-
-        /* When the 'No' button is clicked, we swap in the registration
-         * details for the search results.  It really doesn't look good
-         * to leave the search results there.
-         */
-        function complete_registration(e) {
-            var expander = Y.one('#search-results-expander');
-
-            /* Slide in the search results and hide them under a link. */
-            expander.removeClass('hidden');
-            expander.on('click', function(e) {
-                e.preventDefault();
-
-                var arrow = Y.one('#search-results-arrow');
-                if (arrow.getAttribute('src') == '/@@/treeCollapsed') {
-                    /* The search results are currently hidden.  Slide them
-                     * out and turn the arrow to point downward.
-                     */
-                    arrow.setAttribute('src', '/@@/treeExpanded');
-                    arrow.setAttribute('title', 'Hide search results');
-                    arrow.setAttribute('alt', 'Hide search results');
-                    Y.lazr.effects.slide_out(search_results).run();
-                    show_separator(true);
-                }
-                else {
-                    /* The search results are currently displayed.  Slide them
-                     * in and turn the arrow to point rightward.
-                     */
-                    arrow.setAttribute('src', '/@@/treeCollapsed');
-                    arrow.setAttribute('title', 'Show search results');
-                    arrow.setAttribute('alt', 'Show search results');
-                    Y.lazr.effects.slide_in(search_results).run();
-                    show_separator(false);
-                }
-            });
-
-            /* Hide the 'No' button, but slide out the search results, so the
-             * user has a clue that Something Is Happening.
-             */
-            details_buttons.addClass('hidden');
-
-            /* Slide out the registration details widgets, but add an 'end'
-             * event handler so that the height style left by lazr.effects is
-             * removed when the animation is done.  We're never going to slide
-             * the form widgets back in, and the height style causes the
-             * licence widget to dive under the Complete Registration button.
-             * See bug 391138 for details.
-             */
-            var anim = Y.lazr.effects.slide_out(form_widgets);
-            anim.on('end', function() {
-                form_widgets.setStyle('height', null);
-            });
-            anim.run();
-
-            /* Toggle the visibility of the various other widgets. */
-            form_actions.removeClass('hidden');
-            title.removeClass('hidden');
-
-            /* Set the H2 title to something more appropriate for the
-             * selected task.
-             */
-            step_title.set('innerHTML', 'Step 2 (of 2) Registration details');
-
-            var reset_height = search_results.getComputedStyle('height');
-            search_results.setStyle('height', reset_height);
-            Y.lazr.effects.slide_in(search_results).run();
-
-            /* Append a special marker to the hidden state widget.  See
-             * ProjectAddStepTwo.search_results_count() for details.
-             */
-            var steps = marker.getAttribute('value');
-            if (0 > steps.search(new RegExp('hidesearch'))) {
-                marker.setAttribute('value', steps + "|hidesearch");
-            }
-        }
-
-        /* The details button is only visible when JavaScript is enabled, but
-         * the H3 separator is only visible when JavaScript is disabled.
-         * Neither is displayed on the step 1 page.
-         */
-        show_separator(false);
-
-        if (details_buttons) {
-            details_buttons.removeClass('hidden');
-            details_buttons.on('click', complete_registration);
-        }
-
-        /* If there are search results, hide the registration details. */
-        if (search_results) {
-            form_widgets.addClass('hidden');
-            form_actions.addClass('hidden');
-            title.addClass('hidden');
-        }
-
-        /* Finally, if we've been here before (e.g. there was an error in
-         * submitting step 2), jump to continuing the registration.
-         */
-         if (marker.getAttribute('value').search(/hidesearch/) >= 0) {
-            complete_registration(null);
-         }
-    })
-});
+      /*
+       * When step 2 of this wizard has search results, we want to hide the
+       * details widgets until the user states that the project they are
+       * registering is not a duplicate.
+       */
+      LPJS.use('lp.registry.product.new',  function(Y) {
+          Y.on('domready', function() {
+              var view = new Y.lp.registry.product.new.View();
+              view.render();
+          });
+      });
   </script>
 
     <div id="staging-message" style="background: #e0f0d0;


Follow ups