Merge lp:~yellow/launchpad/accordion-client-1 into lp:launchpad
- accordion-client-1
- Merge into devel
Status: | Merged |
---|---|
Approved by: | Benji York |
Approved revision: | no longer in the source branch. |
Merged at revision: | 12682 |
Proposed branch: | lp:~yellow/launchpad/accordion-client-1 |
Merge into: | lp:launchpad |
Diff against target: |
2488 lines (+2223/-25) 17 files modified
buildout-templates/bin/combine-css.in (+5/-1) lib/lp/app/javascript/lp.js (+43/-13) lib/lp/app/javascript/tests/test_accordionoverlay.html (+26/-0) lib/lp/bugs/browser/structuralsubscription.py (+4/-1) lib/lp/bugs/browser/tests/test_expose.py (+12/-1) lib/lp/bugs/templates/bug-subscription-list.pt (+17/-2) lib/lp/bugs/templates/bugtarget-subscription-list.pt (+17/-2) lib/lp/contrib/javascript/yui3-gallery/gallery-accordion/assets/skins/sam/gallery-accordion.css (+0/-1) lib/lp/registry/browser/product.py (+17/-1) lib/lp/registry/help/structural-subscription-name.html (+30/-0) lib/lp/registry/help/structural-subscription-tags.html (+28/-0) lib/lp/registry/javascript/structural-subscription.js (+1189/-0) lib/lp/registry/javascript/tests/test_structural_subscription.html (+63/-0) lib/lp/registry/javascript/tests/test_structural_subscription.js (+751/-0) lib/lp/registry/templates/product-index.pt (+14/-0) lib/lp/registry/templates/product-portlet-license-missing.pt (+1/-1) lib/lp/services/inlinehelp/javascript/inlinehelp.js (+6/-2) |
To merge this branch: | bzr merge lp:~yellow/launchpad/accordion-client-1 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Graham Binns (community) | code | Approve | |
Review via email: mp+54715@code.launchpad.net |
Commit message
[r=gmb][no-qa] Add client-side changes for the new structural subscription functionality.
Description of the change
This merges the current state of the yellow-squad's client-side work into devel. These changes are feature-flagged.
A UI review will happen later, based on the feature-flagged experience, after we get more a bit more polish done.
= Small changes =
buildout-
lib/lp/
* Clean up wrapper_div to code to put pertinent code in a self-contained function
* toggle_collapsible : add a UI clean up to close any collapsable siblings when you open one.
lib/lp/
(Removed) lib/lp/
lib/lp/
= Accordion overlay =
lib/lp/
lib/lp/
lib/lp/
lib/lp/
Note that we have no Windmill integration tests between the client and server. Do we need those to land? If so, I suspect we'll need some training. Bac writes on IRC:
"""regarding windmill, i think we need something. for instance, the unit tests in test_subscripti
== Help test ==
lib/lp/
lib/lp/
lib/lp/
lib/lp/
These are the help texts for the accordion overlay. We use the overlay in the main site and the bugs site, so we have symlinks to keep them text in sync.
== Show structural subscriptions for a bug ==
This is part of the "unsubscribe in anger" story.
lib/lp/
lib/lp/
lib/lp/
= Edit your structural subscriptions for a given target =
lib/lp/
See previous section for the other files.
= Add feature-flag protected bug link to product page =
lib/lp/
lib/lp/
= lib/lp/
As the comment says, we want this function to to be idempotent so that we can hook up help links after the form overlay has been rendered, which may be after the normal mochikit event handler has run.
Benji York (benji) wrote : | # |
> Whew, done. There are 20-odd items that need addressing but most of them
> are quite small.
[snip]
> One thing that I did notice was that certain things (like the
> trailing-
> picked up by our JS linter (which I think runs as part of `make lint.`
> Could you re-run that for me and, if they're not being picked up, file a
> bug?
The JS linter was indeed not functioning. I filed bug 742619 which has
already provoked a pocketlint fix to be released.
In response to the broken linter I used Crockford's JSLint to guide me
in revision 12660.
A large number of missing semicolons and extra commas were
inserted/removed.
All == and != were changed do === and !==, respectively.
Most uses of string subscripts (foo['bar']) were changed to attribute
access (foo.bar).
Loops over arrays were changed from using the for-in syntax to instead
use traditional indexed for loops.
Added a few missing "var" declarations.
Instead of writing up an exhaustive justification for these changes I'll
let Mr. Crockford explain: http://
I'll address each of the non-lint comments in a subsequent note.
Graham Binns (gmb) wrote : | # |
I'm happy with the lint changes.
Benji York (benji) wrote : | # |
Revision 12662 has the below-mentioned changes.
> [5]
>
> 479 +namespace.
>
> This seems to be a common pattern. Why not just make the function part
> of the namespace in the first place:
>
> namespace.
> ///...
> };
>
> (This is a common pattern elsewhere in the LP JS, though I don't know if
> it's considered a standard). I'll not bother highlighting the other
> cases; they're easy enough to find.
This was done to decrease the identifier length. Otherwise instead of
just referencing "list_contains" in the body of the module, we'd have to
use "namespace.
is needed outside of the namespace is for testing so the one-liner to
expose it to tests seems a reasonable trade-off.
> [10]
>
> 633 +function edit_subscripti
>
> This function needs some documentation commentary.
Done.
>
> [11]
>
> 649 +function create_
> 650 + submit_callback) {
Done.
> [12]
>
> 691 + alert('"Subscribe to bug mail" link not found.');
>
> We shouldn't be using the alert() here any more. Y.fail would seem more
> appropriate.
Fixed.
> [13]
>
> 698 + e.preventDefault();
>
> There's a been a bug about the use of e.preventDefault() and
> e.stopPropagation() not working properly with some browsers (probably IE
> but ISTR some WebKit browsers had problems too). For safety we should
> use e.halt(), since in all the cases where one of {preventDefault,
> stopPropagation} fails, the other works fine, and e.halt() calls both.
> It's probably worth replacing all instances of preventDefault() in this
> JS with halt().
Fixed.
> [14]
>
> 708 +function clear_overlay(
>
> This needs documentation.
Done.
> [15]
>
> 902 + * @param node The node to collapse.
> 903 + */
> 904 +function collapse_node(node, user_cfg) {
>
> user_cfg needs to be documented here, too.
Gary and I have been following the (self-imposed) guideline that only
functions meant for "public" consumption require the javadoc-like
documentation so I fixed the asymmetry in parameter documentation by
removing the docs for the other parameter.
> [16]
>
> 939 +/**
> 940 + * Expand the node and set its arrow to 'collapsed'
> 941 + * @param node The node to collapse.
> 942 + */
> 943 +function expand_node(node, user_cfg) {
>
> Same as [15].
Same fix as [15].
> [17]
>
> 977 +/**
> 978 + * Construct the overlay and populate it with the add/edit form.
> 979 + *
> 980 + * @method setup_overlay
> 981 + * @param {String} content_box_id Id of the element on the page where
> 982 + * the overlay is anchored.
> 983 + * @return {String} overlay_id Id of the constructed overlay element.
> 984 + */
> 985 +function setup_overlay(
>
> There's no overlay_id parameter, so you can drop the documentation for
> it.
Same fix as [15].
> [18]
>
> 1242 + // if we want to allow editing of the recipient, do this
> instead
...
Preview Diff
1 | === modified file 'buildout-templates/bin/combine-css.in' | |||
2 | --- buildout-templates/bin/combine-css.in 2011-03-18 22:47:24 +0000 | |||
3 | +++ buildout-templates/bin/combine-css.in 2011-03-28 19:31:27 +0000 | |||
4 | @@ -11,6 +11,9 @@ | |||
5 | 11 | from lazr.js.build import ComboFile | 11 | from lazr.js.build import ComboFile |
6 | 12 | from lazr.js.combo import combine_files | 12 | from lazr.js.combo import combine_files |
7 | 13 | 13 | ||
8 | 14 | # This constant helps us meet maximum line-length goals. | ||
9 | 15 | GALLERY_ACCORDION = 'yui3-gallery/gallery-accordion/assets/' | ||
10 | 16 | |||
11 | 14 | 17 | ||
12 | 15 | root = os.path.abspath('.') | 18 | root = os.path.abspath('.') |
13 | 16 | root = os.path.normpath(${buildout:directory|path-repr}) | 19 | root = os.path.normpath(${buildout:directory|path-repr}) |
14 | @@ -34,7 +37,8 @@ | |||
15 | 34 | 'lazr/build/picker/assets/skins/sam/picker.css', | 37 | 'lazr/build/picker/assets/skins/sam/picker.css', |
16 | 35 | 'lazr/build/activator/assets/skins/sam/activator.css', | 38 | 'lazr/build/activator/assets/skins/sam/activator.css', |
17 | 36 | 'lazr/build/choiceedit/assets/choiceedit-core.css', | 39 | 'lazr/build/choiceedit/assets/choiceedit-core.css', |
19 | 37 | 'yui3-gallery/gallery-accordion/assets/skins/sam/gallery-accordion.css', | 40 | GALLERY_ACCORDION + 'gallery-accordion-core.css', |
20 | 41 | GALLERY_ACCORDION + 'skins/sam/gallery-accordion-skin.css', | ||
21 | 38 | 'build/sprite.css', | 42 | 'build/sprite.css', |
22 | 39 | # This one goes at the end because it's our main stylesheet and should | 43 | # This one goes at the end because it's our main stylesheet and should |
23 | 40 | # take precedence over the others. | 44 | # take precedence over the others. |
24 | 41 | 45 | ||
25 | === modified file 'lib/lp/app/javascript/lp.js' | |||
26 | --- lib/lp/app/javascript/lp.js 2010-11-10 15:33:47 +0000 | |||
27 | +++ lib/lp/app/javascript/lp.js 2011-03-28 19:31:27 +0000 | |||
28 | @@ -62,15 +62,20 @@ | |||
29 | 62 | Y.lp.toggle_collapsible = function(collapsible) { | 62 | Y.lp.toggle_collapsible = function(collapsible) { |
30 | 63 | // Find the collapse icon and wrapper div for this collapsible. | 63 | // Find the collapse icon and wrapper div for this collapsible. |
31 | 64 | var icon = collapsible.one('.collapseIcon'); | 64 | var icon = collapsible.one('.collapseIcon'); |
41 | 65 | var wrapper_div = collapsible.one('.collapseWrapper'); | 65 | |
42 | 66 | 66 | function get_wrapper_div(node) { | |
43 | 67 | // If either the wrapper or the icon is null, raise an error. | 67 | var wrapper_div = node.one('.collapseWrapper'); |
44 | 68 | if (wrapper_div === null) { | 68 | |
45 | 69 | Y.fail("Collapsible has no wrapper div."); | 69 | // If either the wrapper or the icon is null, raise an error. |
46 | 70 | } | 70 | if (wrapper_div === null) { |
47 | 71 | if (icon === null) { | 71 | Y.fail("Collapsible has no wrapper div."); |
48 | 72 | Y.fail("Collapsible has no icon."); | 72 | } |
49 | 73 | } | 73 | if (icon === null) { |
50 | 74 | Y.fail("Collapsible has no icon."); | ||
51 | 75 | } | ||
52 | 76 | return wrapper_div; | ||
53 | 77 | } | ||
54 | 78 | var wrapper_div = get_wrapper_div(collapsible); | ||
55 | 74 | 79 | ||
56 | 75 | // Work out the target icon and animation based on the state of | 80 | // Work out the target icon and animation based on the state of |
57 | 76 | // the collapse wrapper. We ignore the current state of the icon | 81 | // the collapse wrapper. We ignore the current state of the icon |
58 | @@ -80,12 +85,16 @@ | |||
59 | 80 | // state. | 85 | // state. |
60 | 81 | var target_icon; | 86 | var target_icon; |
61 | 82 | var target_anim; | 87 | var target_anim; |
62 | 88 | var expanding; | ||
63 | 83 | if (wrapper_div.hasClass('lazr-closed')) { | 89 | if (wrapper_div.hasClass('lazr-closed')) { |
65 | 84 | // The wrapper is collapsed. | 90 | // The wrapper is collapsed; expand it and collapse all its |
66 | 91 | // siblings if it's in an accordion. | ||
67 | 92 | expanding = true; | ||
68 | 85 | target_anim = Y.lazr.effects.slide_out(wrapper_div); | 93 | target_anim = Y.lazr.effects.slide_out(wrapper_div); |
69 | 86 | target_icon = "/@@/treeExpanded"; | 94 | target_icon = "/@@/treeExpanded"; |
70 | 87 | } else { | 95 | } else { |
72 | 88 | // The wrapper is open. | 96 | // The wrapper is open; just collapse it. |
73 | 97 | expanding = false; | ||
74 | 89 | target_anim = Y.lazr.effects.slide_in(wrapper_div); | 98 | target_anim = Y.lazr.effects.slide_in(wrapper_div); |
75 | 90 | target_icon = "/@@/treeCollapsed"; | 99 | target_icon = "/@@/treeCollapsed"; |
76 | 91 | } | 100 | } |
77 | @@ -93,6 +102,27 @@ | |||
78 | 93 | // Run the animation and set the icon src correctly. | 102 | // Run the animation and set the icon src correctly. |
79 | 94 | target_anim.run(); | 103 | target_anim.run(); |
80 | 95 | icon.set('src', target_icon); | 104 | icon.set('src', target_icon); |
81 | 105 | |||
82 | 106 | // Work out if the collapsible is in an accordion and process | ||
83 | 107 | // the siblings accordingly if the current collapsible is being | ||
84 | 108 | // expanded. | ||
85 | 109 | var parent_node = collapsible.get('parentNode'); | ||
86 | 110 | var in_accordion = parent_node.hasClass('accordion'); | ||
87 | 111 | if (in_accordion && expanding) { | ||
88 | 112 | var sibling_target_icon = "/@@/treeCollapsed"; | ||
89 | 113 | Y.each(parent_node.all('.collapsible'), function(sibling) { | ||
90 | 114 | // We only actually collapse the sibling if it's not our | ||
91 | 115 | // current collapsible. | ||
92 | 116 | if (sibling != collapsible) { | ||
93 | 117 | var sibling_wrapper_div = get_wrapper_div(sibling); | ||
94 | 118 | var sibling_icon = sibling.one('.collapseIcon'); | ||
95 | 119 | var sibling_target_anim = Y.lazr.effects.slide_in( | ||
96 | 120 | sibling_wrapper_div); | ||
97 | 121 | sibling_target_anim.run(); | ||
98 | 122 | sibling_icon.set('src', sibling_target_icon); | ||
99 | 123 | } | ||
100 | 124 | }); | ||
101 | 125 | } | ||
102 | 96 | }; | 126 | }; |
103 | 97 | 127 | ||
104 | 98 | /** | 128 | /** |
105 | @@ -107,8 +137,8 @@ | |||
106 | 107 | // Try to grab the legend in the usual way. | 137 | // Try to grab the legend in the usual way. |
107 | 108 | var legend = collapsible.one('legend'); | 138 | var legend = collapsible.one('legend'); |
108 | 109 | if (legend === null) { | 139 | if (legend === null) { |
111 | 110 | // If it's null, this might be a collapsible div, not fieldset, | 140 | // If it's null, this might be a collapsible div, not |
112 | 111 | // so try to grap the div's "legend". | 141 | // fieldset, so try to grap the div's "legend". |
113 | 112 | legend = collapsible.one('.config-options'); | 142 | legend = collapsible.one('.config-options'); |
114 | 113 | } | 143 | } |
115 | 114 | if (legend === null || | 144 | if (legend === null || |
116 | 115 | 145 | ||
117 | === added file 'lib/lp/app/javascript/tests/test_accordionoverlay.html' | |||
118 | --- lib/lp/app/javascript/tests/test_accordionoverlay.html 1970-01-01 00:00:00 +0000 | |||
119 | +++ lib/lp/app/javascript/tests/test_accordionoverlay.html 2011-03-28 19:31:27 +0000 | |||
120 | @@ -0,0 +1,26 @@ | |||
121 | 1 | <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> | ||
122 | 2 | <html> | ||
123 | 3 | <head> | ||
124 | 4 | <title>Launchpad accordionoverlay</title> | ||
125 | 5 | |||
126 | 6 | <!-- YUI 3.0 Setup --> | ||
127 | 7 | <script type="text/javascript" src="../../../../canonical/launchpad/icing/yui/yui/yui.js"></script> | ||
128 | 8 | <script type="text/javascript" src="../../../../canonical/launchpad/icing/lazr/build/lazr.js"></script> | ||
129 | 9 | <link rel="stylesheet" href="../../../../canonical/launchpad/icing/yui/cssreset/reset.css"/> | ||
130 | 10 | <link rel="stylesheet" href="../../../../canonical/launchpad/icing/yui/cssfonts/fonts.css"/> | ||
131 | 11 | <link rel="stylesheet" href="../../../../canonical/launchpad/icing/yui/cssbase/base.css"/> | ||
132 | 12 | <link rel="stylesheet" href="../../../../canonical/launchpad/javascript/test.css" /> | ||
133 | 13 | |||
134 | 14 | <!-- The module under test --> | ||
135 | 15 | <script type="text/javascript" src="../accordionoverlay.js"></script> | ||
136 | 16 | |||
137 | 17 | <!-- The test suite --> | ||
138 | 18 | <script type="text/javascript" src="test_accordionoverlay.js"></script> | ||
139 | 19 | </head> | ||
140 | 20 | |||
141 | 21 | <body class="yui3-skin-sam"> | ||
142 | 22 | <div id="form_overlay_example"></div> | ||
143 | 23 | <div id="log"></div> | ||
144 | 24 | </body> | ||
145 | 25 | |||
146 | 26 | </html> | ||
147 | 0 | 27 | ||
148 | === modified file 'lib/lp/bugs/browser/structuralsubscription.py' | |||
149 | --- lib/lp/bugs/browser/structuralsubscription.py 2011-03-23 19:19:43 +0000 | |||
150 | +++ lib/lp/bugs/browser/structuralsubscription.py 2011-03-28 19:31:27 +0000 | |||
151 | @@ -402,7 +402,8 @@ | |||
152 | 402 | record = info.get(target) | 402 | record = info.get(target) |
153 | 403 | if record is None: | 403 | if record is None: |
154 | 404 | record = dict(target_title=target.title, | 404 | record = dict(target_title=target.title, |
156 | 405 | target_url=absoluteURL(target, request), | 405 | target_url=canonical_url( |
157 | 406 | target, rootsite='mainsite'), | ||
158 | 406 | filters=[]) | 407 | filters=[]) |
159 | 407 | info[target] = record | 408 | info[target] = record |
160 | 408 | subscriber = subscription.subscriber | 409 | subscriber = subscription.subscriber |
161 | @@ -413,6 +414,8 @@ | |||
162 | 413 | record['filters'].append(dict( | 414 | record['filters'].append(dict( |
163 | 414 | filter=filter, | 415 | filter=filter, |
164 | 415 | subscriber_link=absoluteURL(subscriber, api_request), | 416 | subscriber_link=absoluteURL(subscriber, api_request), |
165 | 417 | subscriber_url = canonical_url( | ||
166 | 418 | subscriber, rootsite='mainsite'), | ||
167 | 416 | subscriber_title=subscriber.title, | 419 | subscriber_title=subscriber.title, |
168 | 417 | subscriber_is_team=is_team, | 420 | subscriber_is_team=is_team, |
169 | 418 | user_is_team_admin=user_is_team_admin,)) | 421 | user_is_team_admin=user_is_team_admin,)) |
170 | 419 | 422 | ||
171 | === modified file 'lib/lp/bugs/browser/tests/test_expose.py' | |||
172 | --- lib/lp/bugs/browser/tests/test_expose.py 2011-03-23 19:15:41 +0000 | |||
173 | +++ lib/lp/bugs/browser/tests/test_expose.py 2011-03-28 19:31:27 +0000 | |||
174 | @@ -18,6 +18,7 @@ | |||
175 | 18 | from zope.interface import implements | 18 | from zope.interface import implements |
176 | 19 | from zope.traversing.browser import absoluteURL | 19 | from zope.traversing.browser import absoluteURL |
177 | 20 | 20 | ||
178 | 21 | from canonical.launchpad.webapp.publisher import canonical_url | ||
179 | 21 | from canonical.launchpad.webapp.servers import LaunchpadTestRequest | 22 | from canonical.launchpad.webapp.servers import LaunchpadTestRequest |
180 | 22 | from canonical.testing.layers import DatabaseFunctionalLayer | 23 | from canonical.testing.layers import DatabaseFunctionalLayer |
181 | 23 | from lp.bugs.browser.structuralsubscription import ( | 24 | from lp.bugs.browser.structuralsubscription import ( |
182 | @@ -150,7 +151,8 @@ | |||
183 | 150 | target_info = info[0] | 151 | target_info = info[0] |
184 | 151 | self.assertEqual(target_info['target_title'], target.title) | 152 | self.assertEqual(target_info['target_title'], target.title) |
185 | 152 | self.assertEqual( | 153 | self.assertEqual( |
187 | 153 | target_info['target_url'], absoluteURL(target, request)) | 154 | target_info['target_url'], canonical_url( |
188 | 155 | target, rootsite='mainsite')) | ||
189 | 154 | self.assertEqual(len(target_info['filters']), 1) # One filter. | 156 | self.assertEqual(len(target_info['filters']), 1) # One filter. |
190 | 155 | filter_info = target_info['filters'][0] | 157 | filter_info = target_info['filters'][0] |
191 | 156 | self.assertEqual(filter_info['filter'], sub.bug_filters[0]) | 158 | self.assertEqual(filter_info['filter'], sub.bug_filters[0]) |
192 | @@ -160,6 +162,9 @@ | |||
193 | 160 | self.assertEqual( | 162 | self.assertEqual( |
194 | 161 | filter_info['subscriber_link'], | 163 | filter_info['subscriber_link'], |
195 | 162 | absoluteURL(team, IWebServiceClientRequest(request))) | 164 | absoluteURL(team, IWebServiceClientRequest(request))) |
196 | 165 | self.assertEqual( | ||
197 | 166 | filter_info['subscriber_url'], | ||
198 | 167 | canonical_url(team, rootsite='mainsite')) | ||
199 | 163 | 168 | ||
200 | 164 | def test_team_member_subscription(self): | 169 | def test_team_member_subscription(self): |
201 | 165 | # Make a team subscription where the user is not an admin, and | 170 | # Make a team subscription where the user is not an admin, and |
202 | @@ -179,6 +184,9 @@ | |||
203 | 179 | self.assertEqual( | 184 | self.assertEqual( |
204 | 180 | filter_info['subscriber_link'], | 185 | filter_info['subscriber_link'], |
205 | 181 | absoluteURL(team, IWebServiceClientRequest(request))) | 186 | absoluteURL(team, IWebServiceClientRequest(request))) |
206 | 187 | self.assertEqual( | ||
207 | 188 | filter_info['subscriber_url'], | ||
208 | 189 | canonical_url(team, rootsite='mainsite')) | ||
209 | 182 | 190 | ||
210 | 183 | def test_self_subscription(self): | 191 | def test_self_subscription(self): |
211 | 184 | # Make a subscription directly for the user and see what we record. | 192 | # Make a subscription directly for the user and see what we record. |
212 | @@ -195,3 +203,6 @@ | |||
213 | 195 | self.assertEqual( | 203 | self.assertEqual( |
214 | 196 | filter_info['subscriber_link'], | 204 | filter_info['subscriber_link'], |
215 | 197 | absoluteURL(user, IWebServiceClientRequest(request))) | 205 | absoluteURL(user, IWebServiceClientRequest(request))) |
216 | 206 | self.assertEqual( | ||
217 | 207 | filter_info['subscriber_url'], | ||
218 | 208 | canonical_url(user, rootsite='mainsite')) | ||
219 | 198 | 209 | ||
220 | === added symlink 'lib/lp/bugs/help/structural-subscription-name.html' | |||
221 | === target is u'../../registry/help/structural-subscription-name.html' | |||
222 | === added symlink 'lib/lp/bugs/help/structural-subscription-tags.html' | |||
223 | === target is u'../../registry/help/structural-subscription-tags.html' | |||
224 | === modified file 'lib/lp/bugs/templates/bug-subscription-list.pt' | |||
225 | --- lib/lp/bugs/templates/bug-subscription-list.pt 2011-02-22 22:05:16 +0000 | |||
226 | +++ lib/lp/bugs/templates/bug-subscription-list.pt 2011-03-28 19:31:27 +0000 | |||
227 | @@ -10,16 +10,31 @@ | |||
228 | 10 | i18n:domain="malone" | 10 | i18n:domain="malone" |
229 | 11 | > | 11 | > |
230 | 12 | 12 | ||
231 | 13 | <head> | ||
232 | 14 | <tal:head-epilogue metal:fill-slot="head_epilogue"> | ||
233 | 15 | <script type="text/javascript"> | ||
234 | 16 | LPS.use('lp.registry.structural_subscription', function(Y) { | ||
235 | 17 | module = Y.lp.registry.structural_subscription; | ||
236 | 18 | Y.on('domready', function() { | ||
237 | 19 | module.setup_bug_subscriptions( | ||
238 | 20 | {content_box: "#structural-subscription-content-box"}) | ||
239 | 21 | }); | ||
240 | 22 | }); | ||
241 | 23 | </script> | ||
242 | 24 | |||
243 | 25 | </tal:head-epilogue> | ||
244 | 26 | </head> | ||
245 | 13 | <body> | 27 | <body> |
246 | 14 | <div metal:fill-slot="main"> | 28 | <div metal:fill-slot="main"> |
247 | 15 | 29 | ||
248 | 16 | <div id="maincontent"> | 30 | <div id="maincontent"> |
249 | 17 | <div id="nonportlets" class="readable"> | 31 | <div id="nonportlets" class="readable"> |
250 | 32 | <div id="subscription-listing"></div> | ||
251 | 33 | |||
252 | 34 | <div id="structural-subscription-content-box"></div> | ||
253 | 18 | 35 | ||
254 | 19 | </div> | 36 | </div> |
255 | 20 | </div> | 37 | </div> |
256 | 21 | |||
257 | 22 | </div> | 38 | </div> |
258 | 23 | |||
259 | 24 | </body> | 39 | </body> |
260 | 25 | </html> | 40 | </html> |
261 | 26 | 41 | ||
262 | === modified file 'lib/lp/bugs/templates/bugtarget-subscription-list.pt' | |||
263 | --- lib/lp/bugs/templates/bugtarget-subscription-list.pt 2011-03-11 21:31:10 +0000 | |||
264 | +++ lib/lp/bugs/templates/bugtarget-subscription-list.pt 2011-03-28 19:31:27 +0000 | |||
265 | @@ -10,16 +10,31 @@ | |||
266 | 10 | i18n:domain="malone" | 10 | i18n:domain="malone" |
267 | 11 | > | 11 | > |
268 | 12 | 12 | ||
269 | 13 | <head> | ||
270 | 14 | <tal:head-epilogue metal:fill-slot="head_epilogue"> | ||
271 | 15 | <script type="text/javascript"> | ||
272 | 16 | LPS.use('lp.registry.structural_subscription', function(Y) { | ||
273 | 17 | module = Y.lp.registry.structural_subscription; | ||
274 | 18 | Y.on('domready', function() { | ||
275 | 19 | module.setup_bug_subscriptions( | ||
276 | 20 | {content_box: "#structural-subscription-content-box"}) | ||
277 | 21 | }); | ||
278 | 22 | }); | ||
279 | 23 | </script> | ||
280 | 24 | |||
281 | 25 | </tal:head-epilogue> | ||
282 | 26 | </head> | ||
283 | 13 | <body> | 27 | <body> |
284 | 14 | <div metal:fill-slot="main"> | 28 | <div metal:fill-slot="main"> |
285 | 15 | 29 | ||
286 | 16 | <div id="maincontent"> | 30 | <div id="maincontent"> |
287 | 17 | <div id="nonportlets" class="readable"> | 31 | <div id="nonportlets" class="readable"> |
288 | 32 | <div id="subscription-listing"></div> | ||
289 | 33 | |||
290 | 34 | <div id="structural-subscription-content-box"></div> | ||
291 | 18 | 35 | ||
292 | 19 | </div> | 36 | </div> |
293 | 20 | </div> | 37 | </div> |
294 | 21 | |||
295 | 22 | </div> | 38 | </div> |
296 | 23 | |||
297 | 24 | </body> | 39 | </body> |
298 | 25 | </html> | 40 | </html> |
299 | 26 | 41 | ||
300 | === removed file 'lib/lp/contrib/javascript/yui3-gallery/gallery-accordion/assets/skins/sam/gallery-accordion.css' | |||
301 | --- lib/lp/contrib/javascript/yui3-gallery/gallery-accordion/assets/skins/sam/gallery-accordion.css 2011-02-14 20:14:47 +0000 | |||
302 | +++ lib/lp/contrib/javascript/yui3-gallery/gallery-accordion/assets/skins/sam/gallery-accordion.css 1970-01-01 00:00:00 +0000 | |||
303 | @@ -1,1 +0,0 @@ | |||
304 | 1 | .yui3-accordion{width:100%;height:100%;overflow:hidden;position:relative;}.yui3-accordion-item{position:relative;cursor:pointer;width:100%;}.yui3-accordion-item .yui3-widget-hd{overflow:hidden;}.yui3-accordion-item .yui3-widget-bd{cursor:default;overflow:hidden;position:relative;}.yui3-accordion-item-icons{position:relative;float:right;overflow:hidden;padding:1px;height:25px;}.yui3-accordion-item-icon,.yui3-accordion-item-iconexpanded,.yui3-accordion-item-iconalwaysvisible,.yui3-accordion-item-iconclose{width:22px;height:22px;}.yui3-accordion-item-icon,.yui3-accordion-item-label{float:left;}.yui3-accordion-item-label{position:relative;top:4px;_height:22px;}.yui3-accordion-item-iconexpanded,.yui3-accordion-item-iconalwaysvisible,.yui3-accordion-item-iconclose{float:left;}.yui3-accordion-item-iconclose-hidden{display:none;}.yui3-skin-sam .yui3-accordion{border:1px solid #93B2CC;}.yui3-skin-sam .yui3-accordion-item .yui3-widget-hd{background-image:url(accordion_sprite.png);background-position:0 0;border:1px solid #93B2CC;height:25px;}.yui3-skin-sam .yui3-accordion-item-icon,.yui3-skin-sam .yui3-accordion-item-iconexpanded,.yui3-skin-sam .yui3-accordion-item-iconalwaysvisible,.yui3-skin-sam .yui3-accordion-item-iconclose{background-repeat:no-repeat;}.yui3-skin-sam .yui3-accordion-item-icon{background-image:url(accordion_sprite.png);background-position:center -25px;_background-position:center -27px;}.yui3-skin-sam .yui3-accordion-item-label{color:#444;}.yui3-skin-sam .yui3-accordion-item-label{text-decoration:none;background:transparent;overflow:hidden;color:#444;font-weight:bold;}.yui3-skin-sam .yui3-accordion-item-label:hover{text-decoration:underline;}.yui3-skin-sam .yui3-accordion-item-iconalwaysvisible,.yui3-skin-sam .yui3-accordion-item-iconalwaysvisible-off{background-image:url(accordion_sprite.png);background-position:0 -85px;_background-position:0 -87px;}.yui3-skin-sam .yui3-accordion-item-iconalwaysvisible-on{background-image:url(accordion_sprite.png);background-position:0 -55px;_background-position:0 -57px;}.yui3-skin-sam .yui3-accordion-item-iconexpanded,.yui3-skin-sam .yui3-accordion-item-iconexpanded-off{background-image:url(accordion_sprite.png);background-position:0 -175px;_background-position:0 -177px;}.yui3-skin-sam .yui3-accordion-item-iconexpanded-off:hover{background-image:url(accordion_sprite.png);background-position:0 -205px;}.yui3-skin-sam .yui3-accordion-item-iconexpanded-on{background-image:url(accordion_sprite.png);background-position:0 -115px;_background-position:0 -117px;}.yui3-skin-sam .yui3-accordion-item-iconexpanded-on:hover{background-image:url(accordion_sprite.png);background-position:0 -145px;}.yui3-skin-sam .yui3-accordion-item-iconexpanded-expanding{background-image:url(wait_expand.gif);background-position:0 center;}.yui3-skin-sam .yui3-accordion-item-iconexpanded-collapsing{background-image:url(wait_collapse.gif);background-position:0 center;}.yui3-skin-sam .yui3-accordion-item-iconclose{background-image:url(accordion_sprite.png);background-position:0 -235px;_background-position:0 -237px;}.yui3-skin-sam .yui3-accordion-proxyel-visible{border-color:blue;color:white;font-weight:bold;background-color:red;opacity:.7;filter:alpha(opacity = 70);} | ||
305 | 2 | 0 | ||
306 | === modified file 'lib/lp/registry/browser/product.py' | |||
307 | --- lib/lp/registry/browser/product.py 2011-03-22 19:17:42 +0000 | |||
308 | +++ lib/lp/registry/browser/product.py 2011-03-28 19:31:27 +0000 | |||
309 | @@ -193,6 +193,7 @@ | |||
310 | 193 | from lp.registry.interfaces.productseries import IProductSeries | 193 | from lp.registry.interfaces.productseries import IProductSeries |
311 | 194 | from lp.registry.interfaces.series import SeriesStatus | 194 | from lp.registry.interfaces.series import SeriesStatus |
312 | 195 | from lp.registry.interfaces.sourcepackagename import ISourcePackageNameSet | 195 | from lp.registry.interfaces.sourcepackagename import ISourcePackageNameSet |
313 | 196 | from lp.services import features | ||
314 | 196 | from lp.services.fields import ( | 197 | from lp.services.fields import ( |
315 | 197 | PillarAliases, | 198 | PillarAliases, |
316 | 198 | PublicPersonChoice, | 199 | PublicPersonChoice, |
317 | @@ -583,7 +584,22 @@ | |||
318 | 583 | usedfor = IProductActionMenu | 584 | usedfor = IProductActionMenu |
319 | 584 | facet = 'overview' | 585 | facet = 'overview' |
320 | 585 | title = 'Actions' | 586 | title = 'Actions' |
322 | 586 | links = ('edit', 'review_license', 'administer', 'subscribe') | 587 | |
323 | 588 | @property | ||
324 | 589 | def links(self): | ||
325 | 590 | links = ['edit', 'review_license', 'administer'] | ||
326 | 591 | use_advanced_features = features.getFeatureFlag( | ||
327 | 592 | 'advanced-structural-subscriptions.enabled') | ||
328 | 593 | if use_advanced_features: | ||
329 | 594 | links.append('subscribe_to_bug_mail') | ||
330 | 595 | else: | ||
331 | 596 | links.append('subscribe') | ||
332 | 597 | return links | ||
333 | 598 | |||
334 | 599 | @enabled_with_permission('launchpad.AnyPerson') | ||
335 | 600 | def subscribe_to_bug_mail(self): | ||
336 | 601 | text = 'Subscribe to bug mail' | ||
337 | 602 | return Link('#', text, icon='add', hidden=True) | ||
338 | 587 | 603 | ||
339 | 588 | 604 | ||
340 | 589 | class ProductOverviewMenu(ApplicationMenu, ProductEditLinksMixin, | 605 | class ProductOverviewMenu(ApplicationMenu, ProductEditLinksMixin, |
341 | 590 | 606 | ||
342 | === added file 'lib/lp/registry/help/structural-subscription-name.html' | |||
343 | --- lib/lp/registry/help/structural-subscription-name.html 1970-01-01 00:00:00 +0000 | |||
344 | +++ lib/lp/registry/help/structural-subscription-name.html 2011-03-28 19:31:27 +0000 | |||
345 | @@ -0,0 +1,30 @@ | |||
346 | 1 | <html> | ||
347 | 2 | <head> | ||
348 | 3 | <title>Why do I need a "Subscription name"?</title> | ||
349 | 4 | <link rel="stylesheet" type="text/css" | ||
350 | 5 | href="/+icing/yui/cssreset/reset.css" /> | ||
351 | 6 | <link rel="stylesheet" type="text/css" | ||
352 | 7 | href="/+icing/yui/cssfonts/fonts.css" /> | ||
353 | 8 | <link rel="stylesheet" type="text/css" | ||
354 | 9 | href="/+icing/yui/cssbase/base.css" /> | ||
355 | 10 | </head> | ||
356 | 11 | <body> | ||
357 | 12 | <h1>Why do I need a "Subscription name"?</h1> | ||
358 | 13 | |||
359 | 14 | <p> | ||
360 | 15 | You may have more than one structural subscription per project, each | ||
361 | 16 | with different criteria. You may provide a name in order to identify | ||
362 | 17 | them in the future in order to edit or delete them. | ||
363 | 18 | </p> | ||
364 | 19 | <p> | ||
365 | 20 | Also, each email generated by this subscription will quote the name | ||
366 | 21 | you choose, both in the email body and as a header | ||
367 | 22 | (<code>X-Launchpad-Subscription</code>), making it easy to filter | ||
368 | 23 | your bug mail. | ||
369 | 24 | </p> | ||
370 | 25 | <p> | ||
371 | 26 | So, please provide a short, descriptive name to help you remember this | ||
372 | 27 | subscription. | ||
373 | 28 | </p> | ||
374 | 29 | </body> | ||
375 | 30 | </html> | ||
376 | 0 | 31 | ||
377 | === added file 'lib/lp/registry/help/structural-subscription-tags.html' | |||
378 | --- lib/lp/registry/help/structural-subscription-tags.html 1970-01-01 00:00:00 +0000 | |||
379 | +++ lib/lp/registry/help/structural-subscription-tags.html 2011-03-28 19:31:27 +0000 | |||
380 | @@ -0,0 +1,28 @@ | |||
381 | 1 | <html> | ||
382 | 2 | <head> | ||
383 | 3 | <title>Help for filtering on tags</title> | ||
384 | 4 | <link rel="stylesheet" type="text/css" | ||
385 | 5 | href="/+icing/yui/cssreset/reset.css" /> | ||
386 | 6 | <link rel="stylesheet" type="text/css" | ||
387 | 7 | href="/+icing/yui/cssfonts/fonts.css" /> | ||
388 | 8 | <link rel="stylesheet" type="text/css" | ||
389 | 9 | href="/+icing/yui/cssbase/base.css" /> | ||
390 | 10 | </head> | ||
391 | 11 | <body> | ||
392 | 12 | <h1>Help for filtering on tags</h1> | ||
393 | 13 | |||
394 | 14 | <p> | ||
395 | 15 | Bugs can be tagged to help the project categorise the bug report. You | ||
396 | 16 | can filter your subscription based on one or more tags. If you choose | ||
397 | 17 | to <em>Match all tags</em> then your filter will only match bugs that | ||
398 | 18 | have each of those tags you enter here. | ||
399 | 19 | </p> | ||
400 | 20 | <p> | ||
401 | 21 | If you select <em>Match any tags</em> then bugs that have one or more of | ||
402 | 22 | the tags you specify will be found. | ||
403 | 23 | </p> | ||
404 | 24 | <p> | ||
405 | 25 | Enter the tags as a space-separated list. | ||
406 | 26 | </p> | ||
407 | 27 | </body> | ||
408 | 28 | </html> | ||
409 | 0 | 29 | ||
410 | === added file 'lib/lp/registry/javascript/structural-subscription.js' | |||
411 | --- lib/lp/registry/javascript/structural-subscription.js 1970-01-01 00:00:00 +0000 | |||
412 | +++ lib/lp/registry/javascript/structural-subscription.js 2011-03-28 19:31:27 +0000 | |||
413 | @@ -0,0 +1,1189 @@ | |||
414 | 1 | /* Copyright 2011 Canonical Ltd. This software is licensed under the | ||
415 | 2 | * GNU Affero General Public License version 3 (see the file LICENSE). | ||
416 | 3 | * | ||
417 | 4 | * Form overlay widgets and subscriber handling for structural subscriptions. | ||
418 | 5 | * | ||
419 | 6 | * @module registry | ||
420 | 7 | * @submodule structural_subscription | ||
421 | 8 | */ | ||
422 | 9 | |||
423 | 10 | YUI.add('lp.registry.structural_subscription', function(Y) { | ||
424 | 11 | |||
425 | 12 | var namespace = Y.namespace('lp.registry.structural_subscription'); | ||
426 | 13 | |||
427 | 14 | var INNER_HTML = 'innerHTML', | ||
428 | 15 | VALUE = 'value'; | ||
429 | 16 | |||
430 | 17 | var FILTER_COMMENTS = 'filter-comments', | ||
431 | 18 | FILTER_WRAPPER = 'filter-wrapper', | ||
432 | 19 | ACCORDION_WRAPPER = 'accordion-wrapper', | ||
433 | 20 | ADDED_OR_CLOSED = 'added-or-closed', | ||
434 | 21 | ADDED_OR_CHANGED = 'added-or-changed', | ||
435 | 22 | ADVANCED_FILTER = 'advanced-filter', | ||
436 | 23 | MATCH_ALL = 'match-all', | ||
437 | 24 | MATCH_ANY = 'match-any', | ||
438 | 25 | SS_COLLAPSIBLE = 'ss-collapsible' | ||
439 | 26 | ; | ||
440 | 27 | |||
441 | 28 | var add_subscription_overlay; | ||
442 | 29 | var cancel_button_html = | ||
443 | 30 | '<button type="button" name="field.actions.cancel" ' + | ||
444 | 31 | 'class="lazr-neg lazr-btn" >Cancel</button>'; | ||
445 | 32 | |||
446 | 33 | namespace.lp_client = undefined; | ||
447 | 34 | |||
448 | 35 | /* | ||
449 | 36 | * An object representing the global actions portlet. | ||
450 | 37 | * | ||
451 | 38 | */ | ||
452 | 39 | var PortletTarget = function() {}; | ||
453 | 40 | Y.augment(PortletTarget, Y.Event.Target); | ||
454 | 41 | namespace.portlet = new PortletTarget(); | ||
455 | 42 | |||
456 | 43 | function subscription_success() { | ||
457 | 44 | // TODO Should there be some success notification? | ||
458 | 45 | add_subscription_overlay.hide(); | ||
459 | 46 | } | ||
460 | 47 | |||
461 | 48 | var overlay_error_handler = new Y.lp.client.ErrorHandler(); | ||
462 | 49 | overlay_error_handler.showError = function(error_msg) { | ||
463 | 50 | add_subscription_overlay.showError(error_msg); | ||
464 | 51 | }; | ||
465 | 52 | |||
466 | 53 | /** | ||
467 | 54 | * Does the list contain the target? | ||
468 | 55 | * | ||
469 | 56 | * @private | ||
470 | 57 | * @method list_contains | ||
471 | 58 | * @param {List} list The list to search. | ||
472 | 59 | * @param {String} target The target of interest. | ||
473 | 60 | */ | ||
474 | 61 | function list_contains(list, target) { | ||
475 | 62 | // The list may be undefined in some cases. | ||
476 | 63 | return Y.Lang.isArray(list) && list.indexOf(target) !== -1; | ||
477 | 64 | }; | ||
478 | 65 | |||
479 | 66 | // Expose to tests. | ||
480 | 67 | namespace._list_contains = list_contains; | ||
481 | 68 | |||
482 | 69 | /** | ||
483 | 70 | * Reformat the data returned from the add/edit form into something acceptable | ||
484 | 71 | * to send as a PATCH. | ||
485 | 72 | */ | ||
486 | 73 | function extract_form_data(form_data) { | ||
487 | 74 | if (form_data === 'this is a test') { | ||
488 | 75 | // This is a short-circuit to make testing easier. | ||
489 | 76 | return {}; | ||
490 | 77 | } | ||
491 | 78 | var patch_data = { | ||
492 | 79 | description: Y.Lang.trim(form_data.name[0]), | ||
493 | 80 | tags: [], | ||
494 | 81 | find_all_tags: false, | ||
495 | 82 | importances: [], | ||
496 | 83 | statuses: [], | ||
497 | 84 | }; | ||
498 | 85 | |||
499 | 86 | // Set the notification level. | ||
500 | 87 | var added_or_closed = list_contains(form_data.events, ADDED_OR_CLOSED); | ||
501 | 88 | var filter_comments = list_contains(form_data.filters, FILTER_COMMENTS); | ||
502 | 89 | |||
503 | 90 | // Chattiness: Lifecycle < Details < Discussion. | ||
504 | 91 | if (added_or_closed) { | ||
505 | 92 | patch_data.bug_notification_level = 'Lifecycle'; | ||
506 | 93 | } else if (!filter_comments) { | ||
507 | 94 | patch_data.bug_notification_level = 'Discussion'; | ||
508 | 95 | } else { | ||
509 | 96 | patch_data.bug_notification_level = 'Details'; | ||
510 | 97 | } | ||
511 | 98 | |||
512 | 99 | // Set the tags, importances, and statuses. Only do this if | ||
513 | 100 | // ADDED_OR_CHANGED and ADVANCED_FILTER are selected. | ||
514 | 101 | var advanced_filter = (!added_or_closed && | ||
515 | 102 | list_contains(form_data.filters, ADVANCED_FILTER)); | ||
516 | 103 | if (advanced_filter) { | ||
517 | 104 | // Tags are a list with one element being a space-separated string. | ||
518 | 105 | var tags = form_data.tags[0]; | ||
519 | 106 | if (Y.Lang.isValue(tags) && tags !== '') { | ||
520 | 107 | patch_data.tags = tags.toLowerCase().split(' '); | ||
521 | 108 | } | ||
522 | 109 | patch_data.find_all_tags = | ||
523 | 110 | list_contains(form_data.tag_match, MATCH_ALL); | ||
524 | 111 | if (form_data.importances.length > 0) { | ||
525 | 112 | patch_data.importances = form_data.importances; | ||
526 | 113 | } | ||
527 | 114 | if (form_data.statuses.length > 0) { | ||
528 | 115 | patch_data.statuses = form_data.statuses; | ||
529 | 116 | } | ||
530 | 117 | } else { | ||
531 | 118 | // clear out the tags, statuses, and importances in case this is an | ||
532 | 119 | // edit. | ||
533 | 120 | patch_data.tags = patch_data.importances = patch_data.statuses = []; | ||
534 | 121 | } | ||
535 | 122 | return patch_data; | ||
536 | 123 | } | ||
537 | 124 | |||
538 | 125 | // Expose in the namespace for testing purposes. | ||
539 | 126 | namespace._extract_form_data = extract_form_data; | ||
540 | 127 | |||
541 | 128 | /** | ||
542 | 129 | * Given a bug filter, update it with information extracted from a form. | ||
543 | 130 | * | ||
544 | 131 | * @private | ||
545 | 132 | * @method patch_bug_filter | ||
546 | 133 | * @param {Object} bug_filter The bug filter. | ||
547 | 134 | * @param {Object} form_data The data returned from the form submission. | ||
548 | 135 | * @param {Object} on Event handlers to override the defaults. | ||
549 | 136 | */ | ||
550 | 137 | function patch_bug_filter(bug_filter, form_data, on) { | ||
551 | 138 | var patch_data = extract_form_data(form_data); | ||
552 | 139 | |||
553 | 140 | var config = { | ||
554 | 141 | on: Y.merge({ | ||
555 | 142 | success: subscription_success, | ||
556 | 143 | failure: overlay_error_handler.getFailureHandler() | ||
557 | 144 | }, on) | ||
558 | 145 | }; | ||
559 | 146 | namespace.lp_client.patch(bug_filter.self_link, patch_data, config); | ||
560 | 147 | } | ||
561 | 148 | namespace.patch_bug_filter = patch_bug_filter; | ||
562 | 149 | |||
563 | 150 | /** | ||
564 | 151 | * Delete the given filter | ||
565 | 152 | */ | ||
566 | 153 | function delete_filter(filter) { | ||
567 | 154 | var y_config = { | ||
568 | 155 | method: "POST", | ||
569 | 156 | headers: {'X-HTTP-Method-Override': 'DELETE'}, | ||
570 | 157 | on: {failure: overlay_error_handler.getFailureHandler()} | ||
571 | 158 | }; | ||
572 | 159 | Y.io(filter.self_link, y_config); | ||
573 | 160 | } | ||
574 | 161 | |||
575 | 162 | // Exported for testing. | ||
576 | 163 | namespace._delete_filter = delete_filter; | ||
577 | 164 | |||
578 | 165 | /** | ||
579 | 166 | * Create a new structural subscription filter. | ||
580 | 167 | * | ||
581 | 168 | * @method create_structural_subscription filter | ||
582 | 169 | * @param {Object} who Link to the user or team to be subscribed. | ||
583 | 170 | * @param {Object} form_data The data returned from the form submission. | ||
584 | 171 | */ | ||
585 | 172 | function add_bug_filter(who, form_data) { | ||
586 | 173 | var config = { | ||
587 | 174 | on: {success: function (bug_filter) { | ||
588 | 175 | // If we fail to PATCH the new bug filter, DELETE it. | ||
589 | 176 | var on = {failure: function () { | ||
590 | 177 | // We use the namespace binding so tests can override | ||
591 | 178 | // these functions. | ||
592 | 179 | namespace._delete_filter(bug_filter); | ||
593 | 180 | // Call the failure handler to report the original | ||
594 | 181 | // error to the user. | ||
595 | 182 | overlay_error_handler.getFailureHandler() | ||
596 | 183 | .apply(this, arguments); | ||
597 | 184 | }}; | ||
598 | 185 | patch_bug_filter(bug_filter.getAttrs(), form_data, on); | ||
599 | 186 | }, | ||
600 | 187 | failure: overlay_error_handler.getFailureHandler() | ||
601 | 188 | }, | ||
602 | 189 | parameters: { | ||
603 | 190 | subscriber: who | ||
604 | 191 | } | ||
605 | 192 | }; | ||
606 | 193 | |||
607 | 194 | namespace.lp_client.named_post(LP.cache.context.self_link, | ||
608 | 195 | 'addBugSubscriptionFilter', config); | ||
609 | 196 | } | ||
610 | 197 | |||
611 | 198 | // Exported for testing. | ||
612 | 199 | namespace._add_bug_filter = add_bug_filter; | ||
613 | 200 | |||
614 | 201 | /** | ||
615 | 202 | * Given the form data from a user, save the subscription. | ||
616 | 203 | * | ||
617 | 204 | * @private | ||
618 | 205 | * @method save_subscription | ||
619 | 206 | * @param {Object} form_data The data generated by the form submission. | ||
620 | 207 | */ | ||
621 | 208 | |||
622 | 209 | function save_subscription(form_data) { | ||
623 | 210 | var who; | ||
624 | 211 | if (form_data.recipient[0] === 'user') { | ||
625 | 212 | who = LP.links.me; | ||
626 | 213 | } else { | ||
627 | 214 | // There can be only one. | ||
628 | 215 | who = form_data.team[0]; | ||
629 | 216 | } | ||
630 | 217 | add_bug_filter(who, form_data); | ||
631 | 218 | } | ||
632 | 219 | namespace.save_subscription = save_subscription; | ||
633 | 220 | |||
634 | 221 | /** | ||
635 | 222 | * Handle the activation of the edit subscription link. | ||
636 | 223 | */ | ||
637 | 224 | function edit_subscription_handler(context, form_data) { | ||
638 | 225 | var on = {success: function (new_data) { | ||
639 | 226 | var filter = new_data.getAttrs(); | ||
640 | 227 | var description_node = Y.one( | ||
641 | 228 | '#filter-description-'+context.filter_id.toString()); | ||
642 | 229 | description_node.set( | ||
643 | 230 | INNER_HTML, render_filter_description(filter)); | ||
644 | 231 | var name_node = Y.one( | ||
645 | 232 | '#filter-name-'+context.filter_id.toString()); | ||
646 | 233 | name_node.set( | ||
647 | 234 | INNER_HTML, render_filter_name(context.filter_info, filter)); | ||
648 | 235 | add_subscription_overlay.hide(); | ||
649 | 236 | }}; | ||
650 | 237 | patch_bug_filter(context.filter_info.filter, form_data, on); | ||
651 | 238 | } | ||
652 | 239 | |||
653 | 240 | /** | ||
654 | 241 | * Populate the overlay element with the contents of the add/edit form. | ||
655 | 242 | */ | ||
656 | 243 | function create_overlay(content_box_id, overlay_id, submit_button, | ||
657 | 244 | submit_callback) { | ||
658 | 245 | // Create the overlay. | ||
659 | 246 | add_subscription_overlay = new Y.lazr.FormOverlay({ | ||
660 | 247 | headerContent: | ||
661 | 248 | '<h2 id="subscription-overlay-title">Add a mail subscription '+ | ||
662 | 249 | 'for '+LP.cache.context.title + ' bugs</h2>', | ||
663 | 250 | form_content: Y.one(overlay_id), | ||
664 | 251 | centered: true, | ||
665 | 252 | visible: false, | ||
666 | 253 | form_submit_button: submit_button, | ||
667 | 254 | form_cancel_button: Y.Node.create(cancel_button_html), | ||
668 | 255 | form_submit_callback: submit_callback | ||
669 | 256 | }); | ||
670 | 257 | add_subscription_overlay.render(content_box_id); | ||
671 | 258 | // Prevent cruft from hanging around upon closing. | ||
672 | 259 | function clean_up(e) { | ||
673 | 260 | var filter_wrapper = Y.one('#' + FILTER_WRAPPER); | ||
674 | 261 | filter_wrapper.hide(); | ||
675 | 262 | collapse_node(filter_wrapper); | ||
676 | 263 | } | ||
677 | 264 | add_subscription_overlay.get('form_cancel_button').on( | ||
678 | 265 | 'click', clean_up); | ||
679 | 266 | add_subscription_overlay.get('form_submit_button').on( | ||
680 | 267 | 'click', clean_up); | ||
681 | 268 | add_subscription_overlay.on('cancel', clean_up); | ||
682 | 269 | } | ||
683 | 270 | |||
684 | 271 | /* | ||
685 | 272 | * Modify the DOM to insert a link or two into the global actions portlet. | ||
686 | 273 | * If structural subscriptions already exist then a 'modify' link is | ||
687 | 274 | * added. Otherwise, just the 'add' link is put into the portlet. | ||
688 | 275 | * | ||
689 | 276 | * @method setup_subscription_links | ||
690 | 277 | * @param {String} overlay_id Id of the overlay element. | ||
691 | 278 | * @param {String} content_box_id Id of the element on the page where | ||
692 | 279 | * the overlay is anchored. | ||
693 | 280 | */ | ||
694 | 281 | function setup_subscription_links(overlay_id, content_box_id) { | ||
695 | 282 | // Modify the menu-link-subscribe-to-bug-mail link to be visible. | ||
696 | 283 | var link = Y.one('.menu-link-subscribe_to_bug_mail'); | ||
697 | 284 | if (!Y.Lang.isValue(link)) { | ||
698 | 285 | Y.fail('"Subscribe to bug mail" link not found.'); | ||
699 | 286 | } | ||
700 | 287 | link.removeClass('invisible-link'); | ||
701 | 288 | link.addClass('visible-link'); | ||
702 | 289 | link.on('click', function(e) { | ||
703 | 290 | // Only proceed if the form content is already available. | ||
704 | 291 | if (add_subscription_overlay) { | ||
705 | 292 | e.halt(); | ||
706 | 293 | // We always set up the overlay as a blank canvas, in case it was | ||
707 | 294 | // used before. | ||
708 | 295 | clear_overlay(Y.one(content_box_id)); | ||
709 | 296 | add_subscription_overlay.show(); | ||
710 | 297 | } | ||
711 | 298 | }); | ||
712 | 299 | link.addClass('js-action'); | ||
713 | 300 | } // setup_subscription_links | ||
714 | 301 | |||
715 | 302 | /** | ||
716 | 303 | * Reset the overlay form to initial values. | ||
717 | 304 | */ | ||
718 | 305 | function clear_overlay(content_node) { | ||
719 | 306 | set_recipient(content_node, false, undefined); | ||
720 | 307 | content_node.one('[name="name"]').set('value', ''); | ||
721 | 308 | set_checkboxes( | ||
722 | 309 | content_node, LP.cache.statuses, LP.cache.statuses); | ||
723 | 310 | set_checkboxes( | ||
724 | 311 | content_node, LP.cache.importances, LP.cache.importances); | ||
725 | 312 | content_node.one('[name="tags"]').set('value', ''); | ||
726 | 313 | set_radio_buttons( | ||
727 | 314 | content_node, [MATCH_ALL, MATCH_ANY], MATCH_ALL); | ||
728 | 315 | set_radio_buttons( | ||
729 | 316 | content_node, [ADDED_OR_CLOSED, ADDED_OR_CHANGED], ADDED_OR_CLOSED); | ||
730 | 317 | set_checkboxes( | ||
731 | 318 | content_node, [FILTER_COMMENTS, ADVANCED_FILTER], []); | ||
732 | 319 | collapse_node(Y.one('#' + ACCORDION_WRAPPER), {duration: 0}); | ||
733 | 320 | collapse_node(Y.one('#' + FILTER_WRAPPER), {duration: 0}); | ||
734 | 321 | } | ||
735 | 322 | |||
736 | 323 | /** | ||
737 | 324 | * Make a table cell. | ||
738 | 325 | * | ||
739 | 326 | * @private | ||
740 | 327 | * @method make_cell | ||
741 | 328 | * @param {Object} item Item to be placed in the cell. | ||
742 | 329 | * @param {String} name Name of the control. | ||
743 | 330 | */ | ||
744 | 331 | function make_cell(item, name) { | ||
745 | 332 | return '<td style="padding-left:3px"><label><input type="checkbox" ' + | ||
746 | 333 | 'name="' + name +'" ' + | ||
747 | 334 | 'value="' + item + '" checked="checked">' + | ||
748 | 335 | item + '</label><td>'; | ||
749 | 336 | } | ||
750 | 337 | /** | ||
751 | 338 | * Make a table. | ||
752 | 339 | * | ||
753 | 340 | * @private | ||
754 | 341 | * @method make_table | ||
755 | 342 | * @param {Object} list List of items to be put in the table. | ||
756 | 343 | * @param {String} name Name of the control. | ||
757 | 344 | * @param {Int} num_cols The number of columns for the table to use. | ||
758 | 345 | */ | ||
759 | 346 | function make_table(list, name, num_cols) { | ||
760 | 347 | var html = '<table>'; | ||
761 | 348 | var i; | ||
762 | 349 | for (i=0; i<list.length; i++) { | ||
763 | 350 | if (i % num_cols === 0) { | ||
764 | 351 | if (i !== 0) { | ||
765 | 352 | html += '</tr>'; | ||
766 | 353 | } | ||
767 | 354 | html += '<tr>'; | ||
768 | 355 | } | ||
769 | 356 | html += make_cell(list[i], name); | ||
770 | 357 | } | ||
771 | 358 | html += '</tr></table>'; | ||
772 | 359 | return html; | ||
773 | 360 | } | ||
774 | 361 | |||
775 | 362 | /** | ||
776 | 363 | * Make selector controls, the links for 'Select all' and | ||
777 | 364 | * 'Select none' that appear within elements with many checkboxes. | ||
778 | 365 | * | ||
779 | 366 | * @private | ||
780 | 367 | * @method make_selector_controls | ||
781 | 368 | * @param {String} parent Name of the parent. | ||
782 | 369 | * @return {Object} Hash with 'all_name', 'none_name', and 'html' keys. | ||
783 | 370 | */ | ||
784 | 371 | function make_selector_controls(parent) { | ||
785 | 372 | var rv = {}; | ||
786 | 373 | rv.all_name = parent + '-select-all'; | ||
787 | 374 | rv.none_name = parent + '-select-none'; | ||
788 | 375 | rv.html = '<div id="'+ parent + '-selectors" '+ | ||
789 | 376 | 'style="margin-left: 10px;margin-bottom: 10px">' + | ||
790 | 377 | ' <a href="#" id="' + rv.all_name + | ||
791 | 378 | '">Select all</a> ' + | ||
792 | 379 | ' <a href="#" id="' + rv.none_name + | ||
793 | 380 | '">Select none</a>' + | ||
794 | 381 | '</div>'; | ||
795 | 382 | |||
796 | 383 | return rv; | ||
797 | 384 | } | ||
798 | 385 | namespace.make_selector_controls = make_selector_controls; | ||
799 | 386 | |||
800 | 387 | /** | ||
801 | 388 | * Construct a handler closure for select all/none links. | ||
802 | 389 | */ | ||
803 | 390 | function make_select_handler(node, all, checked_value) { | ||
804 | 391 | return function(e) { | ||
805 | 392 | e.halt(); | ||
806 | 393 | Y.each(all, function(value) { | ||
807 | 394 | get_input_by_value(node, value).set('checked', checked_value); | ||
808 | 395 | }); | ||
809 | 396 | }; | ||
810 | 397 | } | ||
811 | 398 | |||
812 | 399 | /** | ||
813 | 400 | * Create the accordion. | ||
814 | 401 | * | ||
815 | 402 | * @method create_accordion | ||
816 | 403 | * @param {String} overlay_id Id of the overlay element. | ||
817 | 404 | * @param {Object} content_node Node where the overlay is anchored. | ||
818 | 405 | * @return {Object} accordion The accordion just created. | ||
819 | 406 | */ | ||
820 | 407 | function create_accordion(overlay_id, content_node) { | ||
821 | 408 | var accordion = new Y.Accordion({ | ||
822 | 409 | useAnimation: true, | ||
823 | 410 | collapseOthersOnExpand: true, | ||
824 | 411 | visible: false | ||
825 | 412 | }); | ||
826 | 413 | |||
827 | 414 | accordion.render(overlay_id); | ||
828 | 415 | |||
829 | 416 | var statuses_ai, | ||
830 | 417 | importances_ai, | ||
831 | 418 | tags_ai; | ||
832 | 419 | |||
833 | 420 | // Build tags pane. | ||
834 | 421 | tags_ai = new Y.AccordionItem( { | ||
835 | 422 | label: "Tags", | ||
836 | 423 | expanded: false, | ||
837 | 424 | alwaysVisible: false, | ||
838 | 425 | id: "tags_ai", | ||
839 | 426 | contentHeight: {method: "auto"} | ||
840 | 427 | } ); | ||
841 | 428 | |||
842 | 429 | tags_ai.set("bodyContent", | ||
843 | 430 | '<div>\n' + | ||
844 | 431 | '<div>\n' + | ||
845 | 432 | ' <input type="radio" name="tag_match" value="' + | ||
846 | 433 | MATCH_ALL + '" checked> Match all tags\n' + | ||
847 | 434 | ' <input type="radio" name="tag_match" value="' + | ||
848 | 435 | MATCH_ANY + '"> Match any tags\n' + | ||
849 | 436 | '</div>\n' + | ||
850 | 437 | '<div style="padding-bottom:10px;">\n' + | ||
851 | 438 | ' <input type="text" name="tags" size="60"/>\n' + | ||
852 | 439 | ' <a target="help"'+ | ||
853 | 440 | ' href="/+help/structural-subscription-tags.html" ' + | ||
854 | 441 | ' class="sprite maybe"> '+ | ||
855 | 442 | '<span class="invisible-link">Structural subscription tags '+ | ||
856 | 443 | ' help</span></a>\n ' + | ||
857 | 444 | '</div>\n' + | ||
858 | 445 | '</div>\n'); | ||
859 | 446 | |||
860 | 447 | accordion.addItem(tags_ai); | ||
861 | 448 | |||
862 | 449 | // Build importances pane. | ||
863 | 450 | importances_ai = new Y.AccordionItem( { | ||
864 | 451 | label: "Importances", | ||
865 | 452 | expanded: false, | ||
866 | 453 | alwaysVisible: false, | ||
867 | 454 | id: "importances_ai", | ||
868 | 455 | contentHeight: {method: "auto"} | ||
869 | 456 | } ); | ||
870 | 457 | var importances = LP.cache.importances; | ||
871 | 458 | var selectors = make_selector_controls('importances'); | ||
872 | 459 | var importances_html = '<div id="importances-wrapper">' + | ||
873 | 460 | selectors.html + | ||
874 | 461 | make_table(importances, 'importances', 4) + | ||
875 | 462 | '</div>'; | ||
876 | 463 | importances_ai.set("bodyContent", importances_html); | ||
877 | 464 | accordion.addItem(importances_ai); | ||
878 | 465 | // Wire up the 'all' and 'none' selectors. | ||
879 | 466 | var all_link = content_node.one('#' + selectors.all_name); | ||
880 | 467 | var none_link = Y.one('#' + selectors.none_name); | ||
881 | 468 | var node = content_node.one('#importances-wrapper'); | ||
882 | 469 | var select_all_handler = make_select_handler(node, importances, true); | ||
883 | 470 | var select_none_handler = make_select_handler(node, importances, false); | ||
884 | 471 | all_link.on('click', select_all_handler); | ||
885 | 472 | none_link.on('click', select_none_handler); | ||
886 | 473 | |||
887 | 474 | // Build statuses pane. | ||
888 | 475 | statuses_ai = new Y.AccordionItem( { | ||
889 | 476 | label: "Statuses", | ||
890 | 477 | expanded: false, | ||
891 | 478 | alwaysVisible: false, | ||
892 | 479 | id: "statuses_ai", | ||
893 | 480 | contentHeight: {method: "auto"} | ||
894 | 481 | } ); | ||
895 | 482 | var statuses = LP.cache.statuses; | ||
896 | 483 | selectors = make_selector_controls('statuses'); | ||
897 | 484 | var status_html = '<div id="statuses-wrapper">' + | ||
898 | 485 | selectors.html + make_table(statuses, 'statuses', 3)+ | ||
899 | 486 | '</div>'; | ||
900 | 487 | statuses_ai.set("bodyContent", status_html); | ||
901 | 488 | accordion.addItem(statuses_ai); | ||
902 | 489 | all_link = content_node.one('#' + selectors.all_name); | ||
903 | 490 | none_link = Y.one('#' + selectors.none_name); | ||
904 | 491 | node = content_node.one('#statuses-wrapper'); | ||
905 | 492 | select_all_handler = make_select_handler(node, statuses, true); | ||
906 | 493 | select_none_handler = make_select_handler(node, statuses, false); | ||
907 | 494 | all_link.on('click', select_all_handler); | ||
908 | 495 | none_link.on('click', select_none_handler); | ||
909 | 496 | |||
910 | 497 | return accordion; | ||
911 | 498 | } | ||
912 | 499 | |||
913 | 500 | /** | ||
914 | 501 | * Collapse the node and set its arrow to 'collapsed' | ||
915 | 502 | */ | ||
916 | 503 | function collapse_node(node, user_cfg) { | ||
917 | 504 | if (user_cfg && user_cfg.duration === 0) { | ||
918 | 505 | node.setStyles({ | ||
919 | 506 | height: 0, | ||
920 | 507 | visibility: 'hidden', | ||
921 | 508 | overflow: 'hidden' | ||
922 | 509 | // Don't set display: none because then the node won't be taken | ||
923 | 510 | // into account and the rendering will sometimes jiggle | ||
924 | 511 | // horizontally when the node is opened. | ||
925 | 512 | }); | ||
926 | 513 | node.addClass('lazr-closed').removeClass('lazr-opened'); | ||
927 | 514 | return; | ||
928 | 515 | } | ||
929 | 516 | var anim = Y.lazr.effects.slide_in(node, user_cfg); | ||
930 | 517 | // XXX: BradCrittenden 2011-03-03 bug=728457 : This fix for | ||
931 | 518 | // resizing needs to be incorporated into lazr.effects. When that | ||
932 | 519 | // is done it should be removed from here. | ||
933 | 520 | anim.on("start", function() { | ||
934 | 521 | node.setStyles({ | ||
935 | 522 | visibility: 'visible' | ||
936 | 523 | }); | ||
937 | 524 | }); | ||
938 | 525 | anim.on("end", function() { | ||
939 | 526 | node.setStyles({ | ||
940 | 527 | height: 0, | ||
941 | 528 | visibility: 'hidden', | ||
942 | 529 | display: null | ||
943 | 530 | // Don't set display: none because then the node won't be taken | ||
944 | 531 | // into account and the rendering will sometimes jiggle | ||
945 | 532 | // horizontally when the node is opened. | ||
946 | 533 | }); | ||
947 | 534 | }); | ||
948 | 535 | anim.run(); | ||
949 | 536 | } | ||
950 | 537 | |||
951 | 538 | /** | ||
952 | 539 | * Expand the node and set its arrow to 'collapsed' | ||
953 | 540 | */ | ||
954 | 541 | function expand_node(node, user_cfg) { | ||
955 | 542 | if (user_cfg && user_cfg.duration === 0) { | ||
956 | 543 | node.setStyles({ | ||
957 | 544 | height: 'auto', | ||
958 | 545 | visibility: 'visible', | ||
959 | 546 | overflow: null, // Inherit. | ||
960 | 547 | display: null // Inherit. | ||
961 | 548 | }); | ||
962 | 549 | node.addClass('lazr-opened').removeClass('lazr-closed'); | ||
963 | 550 | return; | ||
964 | 551 | } | ||
965 | 552 | // Set the node to 'hidden' so that the proper size can be found. | ||
966 | 553 | node.setStyles({ | ||
967 | 554 | visibility: 'hidden' | ||
968 | 555 | }); | ||
969 | 556 | var anim = Y.lazr.effects.slide_out(node, user_cfg); | ||
970 | 557 | // XXX: BradCrittenden 2011-03-03 bug=728457 : This fix for | ||
971 | 558 | // resizing needs to be incorporated into lazr.effects. When that | ||
972 | 559 | // is done it should be removed from here. | ||
973 | 560 | anim.on("start", function() { | ||
974 | 561 | // Set the node to 'visible' for the beginning of the animation. | ||
975 | 562 | node.setStyles({ | ||
976 | 563 | visibility: 'visible' | ||
977 | 564 | }); | ||
978 | 565 | }); | ||
979 | 566 | anim.on("end", function() { | ||
980 | 567 | // Change the height to auto when the animation completes. | ||
981 | 568 | node.setStyles({ | ||
982 | 569 | height: 'auto' | ||
983 | 570 | }); | ||
984 | 571 | }); | ||
985 | 572 | anim.run(); | ||
986 | 573 | } | ||
987 | 574 | |||
988 | 575 | /** | ||
989 | 576 | * Construct the overlay and populate it with the add/edit form. | ||
990 | 577 | */ | ||
991 | 578 | function setup_overlay(content_box_id, hide_recipient_picker) { | ||
992 | 579 | var content_node = Y.one(content_box_id); | ||
993 | 580 | var container = Y.Node.create('<div id="overlay-container"></div>'); | ||
994 | 581 | var accordion_overlay_id = 'accordion-overlay'; | ||
995 | 582 | var teams = LP.cache.administratedTeams; | ||
996 | 583 | var no_recipient_picker = | ||
997 | 584 | ' <input type="hidden" name="recipient" value="user">\n' + | ||
998 | 585 | ' <span>Yourself</span>\n', | ||
999 | 586 | recipient_picker = | ||
1000 | 587 | ' <input type="radio" name="recipient" value="user"\n'+ | ||
1001 | 588 | ' id="structural-subscription-recipient-user" checked>\n'+ | ||
1002 | 589 | ' <label for="structural-subscription-recipient-user">\n'+ | ||
1003 | 590 | ' Yourself</label><br>\n' + | ||
1004 | 591 | ' <input type="radio" name="recipient"\n'+ | ||
1005 | 592 | ' id="structural-subscription-recipient-team"\n'+ | ||
1006 | 593 | ' value="team">\n'+ | ||
1007 | 594 | ' <label for="structural-subscription-recipient-team">One of\n'+ | ||
1008 | 595 | ' the teams you administer</label><br>\n' + | ||
1009 | 596 | ' <dl style="margin-left:25px;">\n' + | ||
1010 | 597 | ' <dt></dt>\n' + | ||
1011 | 598 | ' <dd>\n' + | ||
1012 | 599 | ' <select name="team" id="structural-subscription-teams">\n'+ | ||
1013 | 600 | ' </select>\n' + | ||
1014 | 601 | ' </dd>\n' + | ||
1015 | 602 | ' </dl>\n', | ||
1016 | 603 | control_code = | ||
1017 | 604 | '<dl>\n' + | ||
1018 | 605 | ' <dt>Bug mail recipient</dt>\n' + | ||
1019 | 606 | ' <dd>\n' + | ||
1020 | 607 | ((!hide_recipient_picker && teams.length > 0) ? | ||
1021 | 608 | recipient_picker : no_recipient_picker) + | ||
1022 | 609 | ' </dd>\n' + | ||
1023 | 610 | ' <dt>Subscription name</dt>\n' + | ||
1024 | 611 | ' <dd>\n' + | ||
1025 | 612 | ' <input type="text" name="name">\n' + | ||
1026 | 613 | ' <a target="help" class="sprite maybe"\n' + | ||
1027 | 614 | ' href="/+help/structural-subscription-name.html"> \n' + | ||
1028 | 615 | ' <span class="invisible-link">Structural subscription\n'+ | ||
1029 | 616 | ' description help</span></a>\n ' + | ||
1030 | 617 | ' </dd>\n' + | ||
1031 | 618 | ' <dt>Receive mail for bugs affecting\n'+ | ||
1032 | 619 | ' <span id="structural-subscription-context-title">\n'+ | ||
1033 | 620 | ' '+LP.cache.context.title+'</span> that</dt>\n' + | ||
1034 | 621 | ' <dd>\n' + | ||
1035 | 622 | ' <div id="events">\n' + | ||
1036 | 623 | ' <input type="radio" name="events"\n' + | ||
1037 | 624 | ' value="' + ADDED_OR_CLOSED + '"\n'+ | ||
1038 | 625 | ' id="' + ADDED_OR_CLOSED + '" checked>\n'+ | ||
1039 | 626 | ' <label for="'+ADDED_OR_CLOSED+'">are added or '+ | ||
1040 | 627 | ' closed</label>\n'+ | ||
1041 | 628 | ' <br>\n' + | ||
1042 | 629 | ' <input type="radio" name="events"\n'+ | ||
1043 | 630 | ' value="' + ADDED_OR_CHANGED + '"\n' + | ||
1044 | 631 | ' id="' + ADDED_OR_CHANGED + '">\n'+ | ||
1045 | 632 | ' <label for="'+ADDED_OR_CHANGED+'">are added or changed in\n'+ | ||
1046 | 633 | ' any way\n'+ | ||
1047 | 634 | ' <em id="'+ADDED_OR_CHANGED+'-more">(more options...)</em>\n'+ | ||
1048 | 635 | ' </label>\n' + | ||
1049 | 636 | ' </div>\n' + | ||
1050 | 637 | ' <div id="' + FILTER_WRAPPER + '" class="ss-collapsible">\n' + | ||
1051 | 638 | ' <dl style="margin-left:25px;">\n' + | ||
1052 | 639 | ' <dt></dt>\n' + | ||
1053 | 640 | ' <dd>\n' + | ||
1054 | 641 | ' <input type="checkbox" name="filters"\n' + | ||
1055 | 642 | ' value="' + FILTER_COMMENTS + '"\n'+ | ||
1056 | 643 | ' id="'+FILTER_COMMENTS+'">\n' + | ||
1057 | 644 | ' <label for="'+FILTER_COMMENTS+'">Don\'t send mail about\n'+ | ||
1058 | 645 | ' comments</label><br>\n' + | ||
1059 | 646 | ' <input type="checkbox" name="filters"\n' + | ||
1060 | 647 | ' value="' + ADVANCED_FILTER + '"\n' + | ||
1061 | 648 | ' id="' + ADVANCED_FILTER + '">\n' + | ||
1062 | 649 | ' <label for="'+ADVANCED_FILTER+'">Bugs must match this\n'+ | ||
1063 | 650 | ' filter <em id="'+ADVANCED_FILTER+'-more">(...)</em>\n'+ | ||
1064 | 651 | ' </label><br>\n' + | ||
1065 | 652 | ' <div id="' + ACCORDION_WRAPPER + '" \n' + | ||
1066 | 653 | ' class="' + SS_COLLAPSIBLE + '">\n' + | ||
1067 | 654 | ' <dl>\n' + | ||
1068 | 655 | ' <dt></dt>\n' + | ||
1069 | 656 | ' <dd style="margin-left:25px;">\n' + | ||
1070 | 657 | ' <div id="' + accordion_overlay_id + '"\n' + | ||
1071 | 658 | ' style="position:relative; '+ | ||
1072 | 659 | 'overflow:hidden;"></div>\n' + | ||
1073 | 660 | ' </dd>\n' + | ||
1074 | 661 | ' </dl>\n' + | ||
1075 | 662 | ' </div> \n' + | ||
1076 | 663 | ' </dd>\n' + | ||
1077 | 664 | ' </dl>\n' + | ||
1078 | 665 | ' </div> \n' + | ||
1079 | 666 | ' </dd>\n' + | ||
1080 | 667 | ' <dt></dt>\n' + | ||
1081 | 668 | '</dl>'; | ||
1082 | 669 | |||
1083 | 670 | content_node.appendChild(container); | ||
1084 | 671 | container.appendChild(Y.Node.create(control_code)); | ||
1085 | 672 | |||
1086 | 673 | var accordion = create_accordion( | ||
1087 | 674 | '#' + accordion_overlay_id, content_node); | ||
1088 | 675 | |||
1089 | 676 | // Set up click handlers for the events radio buttons. | ||
1090 | 677 | var radio_group = Y.all('#events input'); | ||
1091 | 678 | radio_group.on( | ||
1092 | 679 | 'change', | ||
1093 | 680 | function() {handle_change(ADDED_OR_CHANGED, FILTER_WRAPPER);}); | ||
1094 | 681 | |||
1095 | 682 | // And a listener for advanced filter selection. | ||
1096 | 683 | var advanced_filter = Y.one('#' + ADVANCED_FILTER); | ||
1097 | 684 | advanced_filter.on( | ||
1098 | 685 | 'change', | ||
1099 | 686 | function() {handle_change(ADVANCED_FILTER, ACCORDION_WRAPPER);}); | ||
1100 | 687 | // Populate the team drop down from LP.cache data, if appropriate. | ||
1101 | 688 | if (!hide_recipient_picker && teams.length > 0) { | ||
1102 | 689 | var select = Y.one('#structural-subscription-teams'); | ||
1103 | 690 | var i; | ||
1104 | 691 | var team; | ||
1105 | 692 | for (i=0; i<teams.length; i++) { | ||
1106 | 693 | team = teams[i]; | ||
1107 | 694 | var option = Y.Node.create('<option></option>'); | ||
1108 | 695 | option.set(INNER_HTML, team.title); | ||
1109 | 696 | option.set(VALUE, team.link); | ||
1110 | 697 | select.appendChild(option); | ||
1111 | 698 | } | ||
1112 | 699 | select.on( | ||
1113 | 700 | 'focus', | ||
1114 | 701 | function () { | ||
1115 | 702 | Y.one('input[value="team"][name="recipient"]').set( | ||
1116 | 703 | 'checked', true); | ||
1117 | 704 | } | ||
1118 | 705 | ); | ||
1119 | 706 | } | ||
1120 | 707 | return '#' + container._node.id; | ||
1121 | 708 | } // setup_overlay | ||
1122 | 709 | // Expose in the namespace for testing purposes. | ||
1123 | 710 | namespace._setup_overlay = setup_overlay; | ||
1124 | 711 | |||
1125 | 712 | function handle_change(control_name, div_name, user_cfg) { | ||
1126 | 713 | // Expand or collapse the node depending on the control. | ||
1127 | 714 | // user_cfg is passed to expand_node or collapse_node, and is | ||
1128 | 715 | // useful to set the duration. | ||
1129 | 716 | var ctl = Y.one('#' + control_name); | ||
1130 | 717 | var more = Y.one('#' + control_name + '-more'); | ||
1131 | 718 | var div = Y.one('#' + div_name); | ||
1132 | 719 | var checked = ctl.get('checked'); | ||
1133 | 720 | if (checked) { | ||
1134 | 721 | expand_node(div, user_cfg); | ||
1135 | 722 | more.setStyle('display', 'none'); | ||
1136 | 723 | } else { | ||
1137 | 724 | collapse_node(div, user_cfg); | ||
1138 | 725 | more.setStyle('display', null); | ||
1139 | 726 | } | ||
1140 | 727 | } | ||
1141 | 728 | |||
1142 | 729 | /* | ||
1143 | 730 | * Create the LP client. | ||
1144 | 731 | * | ||
1145 | 732 | * @method setup_client | ||
1146 | 733 | */ | ||
1147 | 734 | function setup_client() { | ||
1148 | 735 | namespace.lp_client = new Y.lp.client.Launchpad(); | ||
1149 | 736 | } // setup_client | ||
1150 | 737 | |||
1151 | 738 | /* | ||
1152 | 739 | * External entry point for configuring the structual subscription. | ||
1153 | 740 | * @method setup_bug_subscriptions | ||
1154 | 741 | * @param {Object} config Object literal of config name/value pairs. | ||
1155 | 742 | * config.content_box is the name of an element on the page where | ||
1156 | 743 | * the overlay will be anchored. | ||
1157 | 744 | */ | ||
1158 | 745 | namespace.setup_bug_subscriptions = function(config) { | ||
1159 | 746 | validate_config(config); | ||
1160 | 747 | Y.on('domready', function() { | ||
1161 | 748 | if (Y.Lang.isValue(config.lp_client)) { | ||
1162 | 749 | // Tests can specify an lp_client if they want to. | ||
1163 | 750 | namespace.lp_client = config.lp_client; | ||
1164 | 751 | } else { | ||
1165 | 752 | // Setup the Launchpad client. | ||
1166 | 753 | setup_client(); | ||
1167 | 754 | } | ||
1168 | 755 | |||
1169 | 756 | var overlay_id = setup_overlay(config.content_box, true); | ||
1170 | 757 | var submit_button = Y.Node.create( | ||
1171 | 758 | '<button type="submit" name="field.actions.create" ' + | ||
1172 | 759 | 'value="Save Changes" class="lazr-pos lazr-btn" '+ | ||
1173 | 760 | '>OK</button>'); | ||
1174 | 761 | // This is a bit of an odd approach, but it lets us retrofit code | ||
1175 | 762 | // without a large refactoring. When edit_subscription_handler is | ||
1176 | 763 | // called, context.filter_info will have the information about the | ||
1177 | 764 | // filter that is being edited. | ||
1178 | 765 | var context = {}; | ||
1179 | 766 | create_overlay(config.content_box, overlay_id, submit_button, | ||
1180 | 767 | function (form_data) { | ||
1181 | 768 | return edit_subscription_handler(context, form_data);}); | ||
1182 | 769 | fill_in_bug_subscriptions(config, context); | ||
1183 | 770 | // We need to initialize the help links. They may have already been | ||
1184 | 771 | // initialized except for the ones we added, so setupHelpTrigger | ||
1185 | 772 | // is idempotent. Notice that this is old MochiKit code. | ||
1186 | 773 | forEach(findHelpLinks(), setupHelpTrigger); | ||
1187 | 774 | }, window); | ||
1188 | 775 | }; | ||
1189 | 776 | |||
1190 | 777 | function get_input_by_value(node, value) { | ||
1191 | 778 | // XXX broken: this should also care about input name because some values | ||
1192 | 779 | // repeat in other areas of the form | ||
1193 | 780 | return node.one('input[value="'+value+'"]'); | ||
1194 | 781 | } | ||
1195 | 782 | |||
1196 | 783 | |||
1197 | 784 | /** | ||
1198 | 785 | * Set the value of a set of checkboxes to the provided values. | ||
1199 | 786 | */ | ||
1200 | 787 | function set_checkboxes(node, all, checked) { | ||
1201 | 788 | // Clear all the checkboxes. | ||
1202 | 789 | Y.each(all, function (value) { | ||
1203 | 790 | get_input_by_value(node, value).set('checked', false); | ||
1204 | 791 | }); | ||
1205 | 792 | // Check the checkboxes that are supposed to be checked. | ||
1206 | 793 | Y.each(checked, function (value) { | ||
1207 | 794 | get_input_by_value(node, value).set('checked', true); | ||
1208 | 795 | }); | ||
1209 | 796 | } | ||
1210 | 797 | |||
1211 | 798 | /** | ||
1212 | 799 | * Set the value of a select box to the provided value. | ||
1213 | 800 | */ | ||
1214 | 801 | function set_options(node, name, value) { | ||
1215 | 802 | var select = node.one('select[name="team"]'); | ||
1216 | 803 | Y.each(select.get('options'), function (option) { | ||
1217 | 804 | option.set('selected', option.get('value')===value); | ||
1218 | 805 | }); | ||
1219 | 806 | } | ||
1220 | 807 | |||
1221 | 808 | /** | ||
1222 | 809 | * Set the value of a set of radio buttons to the provided value. | ||
1223 | 810 | */ | ||
1224 | 811 | function set_radio_buttons(node, all, value) { | ||
1225 | 812 | set_checkboxes(node, all, [value]); | ||
1226 | 813 | } | ||
1227 | 814 | |||
1228 | 815 | /** | ||
1229 | 816 | * Set the values of the recipient select box and radio buttons. | ||
1230 | 817 | */ | ||
1231 | 818 | function set_recipient(node, is_team, team_link) { | ||
1232 | 819 | if (LP.cache.administratedTeams.length > 0) { | ||
1233 | 820 | get_input_by_value(node, 'user').set('checked', !is_team); | ||
1234 | 821 | get_input_by_value(node, 'team').set('checked', is_team); | ||
1235 | 822 | set_options(node, 'teams', | ||
1236 | 823 | team_link || LP.cache.administratedTeams[0].link); | ||
1237 | 824 | } | ||
1238 | 825 | } | ||
1239 | 826 | |||
1240 | 827 | /** | ||
1241 | 828 | * Return an edit handler for the specified filter. | ||
1242 | 829 | */ | ||
1243 | 830 | function make_edit_handler(subscription, filter_info, filter_id, | ||
1244 | 831 | config, context) { | ||
1245 | 832 | // subscription is the filter's subscription. | ||
1246 | 833 | // filter_info is the filter's information (from subscription.filters). | ||
1247 | 834 | // filter_id is the numerical id for the filter, unique on the page. | ||
1248 | 835 | // config is the configuration object used for the entire assembly of the | ||
1249 | 836 | // page. | ||
1250 | 837 | // context is a way to communicate to the shared edit handler what filter | ||
1251 | 838 | // should be updated. | ||
1252 | 839 | return function(e) { | ||
1253 | 840 | // Only proceed if the form content is already available. | ||
1254 | 841 | if (add_subscription_overlay) { | ||
1255 | 842 | e.halt(); | ||
1256 | 843 | var content_node = Y.one(config.content_box), | ||
1257 | 844 | teams = LP.cache.administratedTeams, | ||
1258 | 845 | filter = filter_info.filter, | ||
1259 | 846 | is_lifecycle = filter.bug_notification_level==='Lifecycle', | ||
1260 | 847 | recipient_label = content_node.one( | ||
1261 | 848 | 'input[name="recipient"] + span'), | ||
1262 | 849 | statuses = filter.statuses, | ||
1263 | 850 | importances = filter.importances; | ||
1264 | 851 | if (filter_info.subscriber_is_team) { | ||
1265 | 852 | var i; | ||
1266 | 853 | for (i=0; i<teams.length; i++) { | ||
1267 | 854 | if (teams[i].link === filter_info.subscriber_link){ | ||
1268 | 855 | recipient_label.set(INNER_HTML, teams[i].title); | ||
1269 | 856 | break; | ||
1270 | 857 | } | ||
1271 | 858 | } | ||
1272 | 859 | } else { | ||
1273 | 860 | recipient_label.set(INNER_HTML, 'Yourself'); | ||
1274 | 861 | } | ||
1275 | 862 | content_node.one('[name="name"]').set('value',filter.description); | ||
1276 | 863 | if (is_lifecycle) { | ||
1277 | 864 | statuses = LP.cache.statuses; | ||
1278 | 865 | importances = LP.cache.importances; | ||
1279 | 866 | } else { | ||
1280 | 867 | // An absence of values is equivalent to all values. | ||
1281 | 868 | if (statuses.length === 0) { | ||
1282 | 869 | statuses = LP.cache.statuses; | ||
1283 | 870 | } | ||
1284 | 871 | if (importances.length === 0) { | ||
1285 | 872 | importances = LP.cache.importances; | ||
1286 | 873 | } | ||
1287 | 874 | } | ||
1288 | 875 | set_checkboxes(content_node, LP.cache.statuses, statuses); | ||
1289 | 876 | set_checkboxes( | ||
1290 | 877 | content_node, LP.cache.importances, importances); | ||
1291 | 878 | content_node.one('[name="tags"]').set( | ||
1292 | 879 | 'value', is_lifecycle ? '' : filter.tags.join(' ')); | ||
1293 | 880 | set_radio_buttons( | ||
1294 | 881 | content_node, [MATCH_ALL, MATCH_ANY], | ||
1295 | 882 | filter.find_all_tags ? MATCH_ALL : MATCH_ANY); | ||
1296 | 883 | var has_advanced_filters = !is_lifecycle && ( | ||
1297 | 884 | filter.statuses.length || | ||
1298 | 885 | filter.importances.length || | ||
1299 | 886 | filter.tags.length) > 0, | ||
1300 | 887 | filters = has_advanced_filters ? [ADVANCED_FILTER] : [], | ||
1301 | 888 | event = ADDED_OR_CHANGED; | ||
1302 | 889 | // Chattiness: Lifecycle < Details < Discussion. | ||
1303 | 890 | switch (filter.bug_notification_level) { | ||
1304 | 891 | case 'Lifecycle': | ||
1305 | 892 | event = ADDED_OR_CLOSED; | ||
1306 | 893 | filters = []; | ||
1307 | 894 | break; | ||
1308 | 895 | case 'Details': | ||
1309 | 896 | filters.push(FILTER_COMMENTS); | ||
1310 | 897 | break; | ||
1311 | 898 | // case 'Discussion': This is already handled/the default. | ||
1312 | 899 | // default: If we get here then it is a programmer error. | ||
1313 | 900 | } | ||
1314 | 901 | set_radio_buttons( | ||
1315 | 902 | content_node, [ADDED_OR_CLOSED, ADDED_OR_CHANGED], event); | ||
1316 | 903 | set_checkboxes( | ||
1317 | 904 | content_node, [FILTER_COMMENTS, ADVANCED_FILTER], filters); | ||
1318 | 905 | handle_change(ADDED_OR_CHANGED, FILTER_WRAPPER, {duration: 0}); | ||
1319 | 906 | handle_change(ADVANCED_FILTER, ACCORDION_WRAPPER, {duration: 0}); | ||
1320 | 907 | context.filter_info = filter_info; | ||
1321 | 908 | context.filter_id = filter_id; | ||
1322 | 909 | var title = subscription.target_title; | ||
1323 | 910 | Y.one('#structural-subscription-context-title').set( | ||
1324 | 911 | INNER_HTML, title); | ||
1325 | 912 | Y.one('#subscription-overlay-title').set( | ||
1326 | 913 | INNER_HTML, 'Edit subscription for '+title+' bugs'); | ||
1327 | 914 | add_subscription_overlay.show(); | ||
1328 | 915 | } | ||
1329 | 916 | }; | ||
1330 | 917 | } | ||
1331 | 918 | |||
1332 | 919 | /** | ||
1333 | 920 | * Construct a handler for an unsubscribe link. | ||
1334 | 921 | */ | ||
1335 | 922 | function make_delete_handler(filter, filter_id, subscriber_id) { | ||
1336 | 923 | var error_handler = new Y.lp.client.ErrorHandler(); | ||
1337 | 924 | error_handler.showError = function(error_msg) { | ||
1338 | 925 | var unsubscribe_node = Y.one('#unsubscribe-'+filter_id.toString()); | ||
1339 | 926 | Y.lp.app.errors.display_error(unsubscribe_node, error_msg); | ||
1340 | 927 | }; | ||
1341 | 928 | return function() { | ||
1342 | 929 | var y_config = { | ||
1343 | 930 | method: "POST", | ||
1344 | 931 | headers: {'X-HTTP-Method-Override': 'DELETE'}, | ||
1345 | 932 | on: {success: function(transactionid, response, args){ | ||
1346 | 933 | var subscriber = Y.one( | ||
1347 | 934 | '#subscription-'+subscriber_id.toString()), | ||
1348 | 935 | node = subscriber, | ||
1349 | 936 | filters = subscriber.all('.subscription-filter'); | ||
1350 | 937 | if (!filters.isEmpty()) { | ||
1351 | 938 | node = Y.one( | ||
1352 | 939 | '#subscription-filter-'+filter_id.toString()); | ||
1353 | 940 | } | ||
1354 | 941 | collapse_node(node); | ||
1355 | 942 | }, | ||
1356 | 943 | failure: error_handler.getFailureHandler() | ||
1357 | 944 | } | ||
1358 | 945 | }; | ||
1359 | 946 | Y.io(filter.self_link, y_config); | ||
1360 | 947 | }; | ||
1361 | 948 | } | ||
1362 | 949 | |||
1363 | 950 | /** | ||
1364 | 951 | * Attach activation (click) handlers to all of the edit links on the page. | ||
1365 | 952 | */ | ||
1366 | 953 | function wire_up_edit_links(config, context) { | ||
1367 | 954 | var listing = Y.one('#subscription-listing'); | ||
1368 | 955 | var subscription_info = LP.cache.subscription_info; | ||
1369 | 956 | var filter_id = 0; | ||
1370 | 957 | var i; | ||
1371 | 958 | var j; | ||
1372 | 959 | for (i=0; i<subscription_info.length; i++) { | ||
1373 | 960 | var sub = subscription_info[i]; | ||
1374 | 961 | for (j=0; j<sub.filters.length; j++) { | ||
1375 | 962 | var filter_info = sub.filters[j]; | ||
1376 | 963 | if (!filter_info.subscriber_is_team || | ||
1377 | 964 | filter_info.user_is_team_admin) { | ||
1378 | 965 | var edit_link = Y.one('#edit-'+filter_id.toString()); | ||
1379 | 966 | var edit_handler = make_edit_handler( | ||
1380 | 967 | sub, filter_info, filter_id, config, context); | ||
1381 | 968 | edit_link.on('click', edit_handler); | ||
1382 | 969 | var delete_link = Y.one('#unsubscribe-'+filter_id.toString()); | ||
1383 | 970 | var delete_handler = make_delete_handler( | ||
1384 | 971 | filter_info.filter, filter_id, i); | ||
1385 | 972 | delete_link.on('click', delete_handler); | ||
1386 | 973 | } | ||
1387 | 974 | filter_id += 1; | ||
1388 | 975 | } | ||
1389 | 976 | } | ||
1390 | 977 | } | ||
1391 | 978 | |||
1392 | 979 | /** | ||
1393 | 980 | * Populate the subscription list DOM element with subscription descriptions. | ||
1394 | 981 | */ | ||
1395 | 982 | function fill_in_bug_subscriptions(config, context) { | ||
1396 | 983 | validate_config(config); | ||
1397 | 984 | var listing = Y.one('#subscription-listing'); | ||
1398 | 985 | var subscription_info = LP.cache.subscription_info; | ||
1399 | 986 | var html = '<div class="yui-g"><div id="structural-subscriptions">'; | ||
1400 | 987 | var filter_id = 0; | ||
1401 | 988 | var i; | ||
1402 | 989 | var j; | ||
1403 | 990 | for (i=0; i<subscription_info.length; i++) { | ||
1404 | 991 | var sub = subscription_info[i]; | ||
1405 | 992 | html += | ||
1406 | 993 | '<div style="margin-top: 2em; padding: 0 1em 1em 1em; '+ | ||
1407 | 994 | ' border: 1px solid #ddd;"'+ | ||
1408 | 995 | ' id="subscription-'+i.toString()+'">'+ | ||
1409 | 996 | ' <span style="float: left; margin-top: -0.6em; padding: 0 1ex;'+ | ||
1410 | 997 | ' background-color: #fff;">Subscriptions to'+ | ||
1411 | 998 | ' <a href="'+sub.target_url+'">'+sub.target_title+'</a>'+ | ||
1412 | 999 | ' </span>'; | ||
1413 | 1000 | |||
1414 | 1001 | for (j=0; j<sub.filters.length; j++) { | ||
1415 | 1002 | var filter = sub.filters[j].filter; | ||
1416 | 1003 | // We put the filters in the cache so that the patch mechanism | ||
1417 | 1004 | // can automatically find them and update them on a successful | ||
1418 | 1005 | // edit. This makes it possible to open up a filter after an edit | ||
1419 | 1006 | // and see the information you expect to see. | ||
1420 | 1007 | LP.cache['structural-subscription-filter-'+filter_id.toString()] = | ||
1421 | 1008 | filter; | ||
1422 | 1009 | html += | ||
1423 | 1010 | '<div style="margin: 1em 0em 0em 1em"'+ | ||
1424 | 1011 | ' id="subscription-filter-'+filter_id.toString()+'"'+ | ||
1425 | 1012 | ' class="subscription-filter">'+ | ||
1426 | 1013 | ' <div style="margin-top: 1em">'+ | ||
1427 | 1014 | ' <strong id="filter-name-'+ | ||
1428 | 1015 | filter_id.toString()+'">'+ | ||
1429 | 1016 | render_filter_name(sub.filters[j], filter)+'</strong>'; | ||
1430 | 1017 | if (!sub.filters[j].subscriber_is_team || | ||
1431 | 1018 | sub.filters[j].user_is_team_admin) { | ||
1432 | 1019 | // User can edit the subscription. | ||
1433 | 1020 | html += | ||
1434 | 1021 | '<span style="float: right">'+ | ||
1435 | 1022 | '<a href="#" class="sprite modify edit js-action"'+ | ||
1436 | 1023 | ' id="edit-'+filter_id.toString()+'">'+ | ||
1437 | 1024 | ' Edit this subscription</a> or '+ | ||
1438 | 1025 | '<a href="#" class="sprite modify remove js-action"'+ | ||
1439 | 1026 | ' id="unsubscribe-'+filter_id.toString()+'">'+ | ||
1440 | 1027 | ' Unsubscribe</a></span>'; | ||
1441 | 1028 | } else { | ||
1442 | 1029 | // User cannot edit the subscription, because this is a | ||
1443 | 1030 | // team and the user does not have admin privileges. | ||
1444 | 1031 | html += | ||
1445 | 1032 | '<span style="float: right"><em>'+ | ||
1446 | 1033 | 'You do not have privileges to change this subscription'+ | ||
1447 | 1034 | '</em></span>'; | ||
1448 | 1035 | } | ||
1449 | 1036 | html += '</div>'; | ||
1450 | 1037 | html += | ||
1451 | 1038 | '<div style="padding-left: 1em"'+ | ||
1452 | 1039 | ' id="filter-description-'+filter_id.toString()+'">'+ | ||
1453 | 1040 | render_filter_description(filter)+'</div>'; | ||
1454 | 1041 | |||
1455 | 1042 | html += '</div>'; | ||
1456 | 1043 | filter_id += 1; | ||
1457 | 1044 | } | ||
1458 | 1045 | |||
1459 | 1046 | // We can remove this once we enforce at least one filter per | ||
1460 | 1047 | // subscription. | ||
1461 | 1048 | if (subscription_info[i].filters.length === 0) { | ||
1462 | 1049 | html += '<strong>All messages</strong>'; | ||
1463 | 1050 | } | ||
1464 | 1051 | html += '</div>'; | ||
1465 | 1052 | } | ||
1466 | 1053 | html += '</div></div>'; | ||
1467 | 1054 | listing.appendChild(Y.Node.create(html)); | ||
1468 | 1055 | |||
1469 | 1056 | wire_up_edit_links(config, context); | ||
1470 | 1057 | } | ||
1471 | 1058 | |||
1472 | 1059 | /** | ||
1473 | 1060 | * Construct a one-line textual description of a filter's name. | ||
1474 | 1061 | */ | ||
1475 | 1062 | function render_filter_name(filter_info, filter) { | ||
1476 | 1063 | var description; | ||
1477 | 1064 | if (filter.description) { | ||
1478 | 1065 | description = '"'+filter.description+'"'; | ||
1479 | 1066 | } else { | ||
1480 | 1067 | description = '(unnamed)'; | ||
1481 | 1068 | } | ||
1482 | 1069 | if (filter_info.subscriber_is_team) { | ||
1483 | 1070 | return '<a href="'+filter_info.subscriber_url+'">'+ | ||
1484 | 1071 | filter_info.subscriber_title+"</a> subscription: "+description; | ||
1485 | 1072 | } else { | ||
1486 | 1073 | return 'Your subscription: '+description; | ||
1487 | 1074 | } | ||
1488 | 1075 | } | ||
1489 | 1076 | |||
1490 | 1077 | /** | ||
1491 | 1078 | * Construct a textual description of all of filter's properties. | ||
1492 | 1079 | */ | ||
1493 | 1080 | function render_filter_description(filter) { | ||
1494 | 1081 | var html = ''; | ||
1495 | 1082 | var filter_items = ''; | ||
1496 | 1083 | // Format status conditions. | ||
1497 | 1084 | if (filter.statuses.length !== 0) { | ||
1498 | 1085 | filter_items += '<li> have status ' + | ||
1499 | 1086 | filter.statuses.join(', '); | ||
1500 | 1087 | } | ||
1501 | 1088 | |||
1502 | 1089 | // Format importance conditions. | ||
1503 | 1090 | if (filter.importances.length !== 0) { | ||
1504 | 1091 | filter_items += '<li> are of importance ' + | ||
1505 | 1092 | filter.importances.join(', '); | ||
1506 | 1093 | } | ||
1507 | 1094 | |||
1508 | 1095 | // Format tag conditions. | ||
1509 | 1096 | if (filter.tags.length !== 0) { | ||
1510 | 1097 | filter_items += '<li> are tagged with '; | ||
1511 | 1098 | if (filter.find_all_tags) { | ||
1512 | 1099 | filter_items += '<strong>all</strong>'; | ||
1513 | 1100 | } else { | ||
1514 | 1101 | filter_items += '<strong>any</strong>'; | ||
1515 | 1102 | } | ||
1516 | 1103 | filter_items += ' of these tags: ' + | ||
1517 | 1104 | filter.tags.join(', '); | ||
1518 | 1105 | } | ||
1519 | 1106 | |||
1520 | 1107 | // If there were any conditions to list, stich them in with an | ||
1521 | 1108 | // intro. | ||
1522 | 1109 | if (filter_items !== '') { | ||
1523 | 1110 | html += 'You are subscribed to bugs that'+ | ||
1524 | 1111 | '<ul class="bulleted">'+filter_items+'</ul>'; | ||
1525 | 1112 | } | ||
1526 | 1113 | |||
1527 | 1114 | // Format event details. | ||
1528 | 1115 | if (filter.bug_notification_level === 'Discussion') { | ||
1529 | 1116 | html += 'You will recieve an email when any change '+ | ||
1530 | 1117 | 'is made or a comment is added.'; | ||
1531 | 1118 | } else if (filter.bug_notification_level === 'Details') { | ||
1532 | 1119 | html += 'You will recieve an email when any changes '+ | ||
1533 | 1120 | 'are made to the bug. Bug comments will not be sent.'; | ||
1534 | 1121 | } else if (filter.bug_notification_level === 'Lifecycle') { | ||
1535 | 1122 | html += 'You will recieve an email when bugs are '+ | ||
1536 | 1123 | 'opened or closed.'; | ||
1537 | 1124 | } | ||
1538 | 1125 | return html; | ||
1539 | 1126 | } | ||
1540 | 1127 | |||
1541 | 1128 | /** | ||
1542 | 1129 | * Check the configuration for obvious faults. | ||
1543 | 1130 | */ | ||
1544 | 1131 | function validate_config(config) { | ||
1545 | 1132 | if (!Y.Lang.isValue(config)) { | ||
1546 | 1133 | throw new Error( | ||
1547 | 1134 | 'Missing config for structural_subscription.'); | ||
1548 | 1135 | } | ||
1549 | 1136 | if (!Y.Lang.isValue(config.content_box)) { | ||
1550 | 1137 | throw new Error( | ||
1551 | 1138 | 'Structural_subscription configuration has ' + | ||
1552 | 1139 | 'undefined properties.'); | ||
1553 | 1140 | } | ||
1554 | 1141 | } | ||
1555 | 1142 | |||
1556 | 1143 | // Expose in the namespace for testing purposes. | ||
1557 | 1144 | namespace._validate_config = validate_config; | ||
1558 | 1145 | |||
1559 | 1146 | /* | ||
1560 | 1147 | * External entry point for configuring the structual subscription. | ||
1561 | 1148 | * @method setup | ||
1562 | 1149 | * @param {Object} config Object literal of config name/value pairs. | ||
1563 | 1150 | * config.content_box is the name of an element on the page where | ||
1564 | 1151 | * the overlay will be anchored. | ||
1565 | 1152 | */ | ||
1566 | 1153 | namespace.setup = function(config) { | ||
1567 | 1154 | validate_config(config); | ||
1568 | 1155 | |||
1569 | 1156 | // If the user is not logged in, then we need to defer to the | ||
1570 | 1157 | // default behaviour. | ||
1571 | 1158 | if (LP.links.me === undefined) { | ||
1572 | 1159 | return; | ||
1573 | 1160 | } | ||
1574 | 1161 | if (Y.Lang.isValue(config.lp_client)) { | ||
1575 | 1162 | // Tests can specify an lp_client if they want to. | ||
1576 | 1163 | namespace.lp_client = config.lp_client; | ||
1577 | 1164 | } else { | ||
1578 | 1165 | // Setup the Launchpad client. | ||
1579 | 1166 | setup_client(); | ||
1580 | 1167 | } | ||
1581 | 1168 | // Create the overlay. | ||
1582 | 1169 | var overlay_id = setup_overlay(config.content_box); | ||
1583 | 1170 | // Create the subscription links on the page. | ||
1584 | 1171 | setup_subscription_links(overlay_id, config.content_box); | ||
1585 | 1172 | |||
1586 | 1173 | var submit_button = Y.Node.create( | ||
1587 | 1174 | '<button type="submit" name="field.actions.create" ' + | ||
1588 | 1175 | 'value="Create subscription" class="lazr-pos lazr-btn" '+ | ||
1589 | 1176 | '>OK</button>'); | ||
1590 | 1177 | create_overlay(config.content_box, overlay_id, submit_button, | ||
1591 | 1178 | save_subscription); | ||
1592 | 1179 | // We need to initialize the help links. They may have already been | ||
1593 | 1180 | // initialized except for the ones we added, so setupHelpTrigger | ||
1594 | 1181 | // is idempotent. Notice that this is old MochiKit code. | ||
1595 | 1182 | forEach(findHelpLinks(), setupHelpTrigger); | ||
1596 | 1183 | |||
1597 | 1184 | }; // setup | ||
1598 | 1185 | |||
1599 | 1186 | }, '0.1', {requires: [ | ||
1600 | 1187 | 'dom', 'node', 'lazr.anim', 'lazr.formoverlay', 'lazr.overlay', | ||
1601 | 1188 | 'lazr.effects', 'lp.app.errors', 'lp.client', 'gallery-accordion' | ||
1602 | 1189 | ]}); | ||
1603 | 0 | 1190 | ||
1604 | === added file 'lib/lp/registry/javascript/tests/test_structural_subscription.html' | |||
1605 | --- lib/lp/registry/javascript/tests/test_structural_subscription.html 1970-01-01 00:00:00 +0000 | |||
1606 | +++ lib/lp/registry/javascript/tests/test_structural_subscription.html 2011-03-28 19:31:27 +0000 | |||
1607 | @@ -0,0 +1,63 @@ | |||
1608 | 1 | <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> | ||
1609 | 2 | <html> | ||
1610 | 3 | <head> | ||
1611 | 4 | <title>Structural Subscription Overlay</title> | ||
1612 | 5 | |||
1613 | 6 | <!-- YUI 3.0 Setup --> | ||
1614 | 7 | <script type="text/javascript" src="../../../../canonical/launchpad/icing/yui/yui/yui.js"></script> | ||
1615 | 8 | <link rel="stylesheet" href="../../../../canonical/launchpad/icing/yui/cssreset/reset.css"/> | ||
1616 | 9 | <link rel="stylesheet" href="../../../../canonical/launchpad/icing/yui/cssfonts/fonts.css"/> | ||
1617 | 10 | <link rel="stylesheet" href="../../../../canonical/launchpad/icing/yui/cssbase/base.css"/> | ||
1618 | 11 | <link rel="stylesheet" | ||
1619 | 12 | href="../../../../canonical/launchpad/icing/lazr/build/testing/assets/testlogger.css"/> | ||
1620 | 13 | |||
1621 | 14 | <!-- Dependency --> | ||
1622 | 15 | <script type="text/javascript" | ||
1623 | 16 | src="../../../../canonical/launchpad/icing/lazr/build/lazr.js"></script> | ||
1624 | 17 | <script type="text/javascript" | ||
1625 | 18 | src="../../../../canonical/launchpad/icing/lazr/build/testing/testing.js"></script> | ||
1626 | 19 | <script type="text/javascript" | ||
1627 | 20 | src="../../../../canonical/launchpad/icing/lazr/build/testing/mockio.js"></script> | ||
1628 | 21 | <script type="text/javascript" | ||
1629 | 22 | src="../../../../canonical/launchpad/icing/lazr/build/overlay/overlay.js"> | ||
1630 | 23 | </script> | ||
1631 | 24 | <script type="text/javascript" | ||
1632 | 25 | src="../../../../canonical/launchpad/icing/lazr/build/formoverlay/formoverlay.js"> | ||
1633 | 26 | </script> | ||
1634 | 27 | <script type="text/javascript" | ||
1635 | 28 | src="../../../../canonical/launchpad/icing/lazr/build/choiceedit/choiceedit.js"> | ||
1636 | 29 | </script> | ||
1637 | 30 | <script type="text/javascript" | ||
1638 | 31 | src="../../../app/javascript/client.js"></script> | ||
1639 | 32 | <script type="text/javascript" | ||
1640 | 33 | src="../../../contrib/javascript/yui3-gallery/gallery-accordion/gallery-accordion.js"> | ||
1641 | 34 | </script> | ||
1642 | 35 | <script type="text/javascript" | ||
1643 | 36 | src="../../../../canonical/launchpad/icing/MochiKit.js"></script> | ||
1644 | 37 | <script type="text/javascript" | ||
1645 | 38 | src="../../../services/inlinehelp/javascript/inlinehelp.js"></script> | ||
1646 | 39 | |||
1647 | 40 | |||
1648 | 41 | <!-- The module under test --> | ||
1649 | 42 | <script type="text/javascript" src="../structural-subscription.js"></script> | ||
1650 | 43 | |||
1651 | 44 | <!-- The test suite --> | ||
1652 | 45 | <script type="text/javascript" src="test_structural_subscription.js"></script> | ||
1653 | 46 | |||
1654 | 47 | <!-- Test layout --> | ||
1655 | 48 | <link rel="stylesheet" | ||
1656 | 49 | href="../../../../canonical/launchpad/javascript/test.css" /> | ||
1657 | 50 | <style type="text/css"> | ||
1658 | 51 | /* CSS classes specific to this test */ | ||
1659 | 52 | .unseen { display: none; } | ||
1660 | 53 | </style> | ||
1661 | 54 | </head> | ||
1662 | 55 | <body class="yui3-skin-sam"> | ||
1663 | 56 | <div> | ||
1664 | 57 | This exists to stop tests from breaking. | ||
1665 | 58 | <a href="#" class="menu-link-subscribe_to_bug_mail" | ||
1666 | 59 | >A link, a link, my kingdom for a link</a> | ||
1667 | 60 | </div> | ||
1668 | 61 | <div id="log"></div> | ||
1669 | 62 | </body> | ||
1670 | 63 | </html> | ||
1671 | 0 | 64 | ||
1672 | === added file 'lib/lp/registry/javascript/tests/test_structural_subscription.js' | |||
1673 | --- lib/lp/registry/javascript/tests/test_structural_subscription.js 1970-01-01 00:00:00 +0000 | |||
1674 | +++ lib/lp/registry/javascript/tests/test_structural_subscription.js 2011-03-28 19:31:27 +0000 | |||
1675 | @@ -0,0 +1,751 @@ | |||
1676 | 1 | /* Copyright (c) 2011, Canonical Ltd. All rights reserved. */ | ||
1677 | 2 | |||
1678 | 3 | YUI({ | ||
1679 | 4 | base: '../../../../canonical/launchpad/icing/yui/', | ||
1680 | 5 | filter: 'raw', | ||
1681 | 6 | combine: false, | ||
1682 | 7 | fetchCSS: false | ||
1683 | 8 | }).use('test', 'console', 'node', 'lp.client', | ||
1684 | 9 | 'lp.registry.structural_subscription', function(Y) { | ||
1685 | 10 | |||
1686 | 11 | var suite = new Y.Test.Suite("Structural subscription overlay tests"); | ||
1687 | 12 | |||
1688 | 13 | var context; | ||
1689 | 14 | var test_case; | ||
1690 | 15 | |||
1691 | 16 | // Local aliases | ||
1692 | 17 | var Assert = Y.Assert, | ||
1693 | 18 | ArrayAssert = Y.ArrayAssert, | ||
1694 | 19 | module = Y.lp.registry.structural_subscription; | ||
1695 | 20 | |||
1696 | 21 | // Expected content box. | ||
1697 | 22 | var content_box_name = 'ss-content-box'; | ||
1698 | 23 | var content_box_id = '#' + content_box_name; | ||
1699 | 24 | |||
1700 | 25 | var target_link_class = '.menu-link-subscribe_to_bug_mail'; | ||
1701 | 26 | |||
1702 | 27 | function array_compare(a,b) { | ||
1703 | 28 | if (a.length != b.length) | ||
1704 | 29 | return false; | ||
1705 | 30 | a.sort(); | ||
1706 | 31 | b.sort(); | ||
1707 | 32 | for (i in a) { | ||
1708 | 33 | if (a[i] != b[i]) | ||
1709 | 34 | return false; | ||
1710 | 35 | } | ||
1711 | 36 | return true; | ||
1712 | 37 | } | ||
1713 | 38 | |||
1714 | 39 | function create_test_node() { | ||
1715 | 40 | return Y.Node.create( | ||
1716 | 41 | '<div id="test-content">' + | ||
1717 | 42 | ' <div id="' + content_box_name + '"></div>' + | ||
1718 | 43 | '</div>'); | ||
1719 | 44 | } | ||
1720 | 45 | |||
1721 | 46 | function remove_test_node() { | ||
1722 | 47 | Y.one('body').removeChild(Y.one('#test-content')); | ||
1723 | 48 | } | ||
1724 | 49 | |||
1725 | 50 | function test_checked(list, expected) { | ||
1726 | 51 | var item, i; | ||
1727 | 52 | var length = list.size(); | ||
1728 | 53 | for (i=0; i < length; i++) { | ||
1729 | 54 | item = list.item(i); | ||
1730 | 55 | if (item.get('checked') != expected) | ||
1731 | 56 | return false; | ||
1732 | 57 | } | ||
1733 | 58 | return true; | ||
1734 | 59 | } | ||
1735 | 60 | |||
1736 | 61 | test_case = new Y.Test.Case({ | ||
1737 | 62 | name: 'structural_subscription_overlay', | ||
1738 | 63 | |||
1739 | 64 | _should: { | ||
1740 | 65 | error: { | ||
1741 | 66 | test_setup_config_none: new Error( | ||
1742 | 67 | 'Missing config for structural_subscription.'), | ||
1743 | 68 | test_setup_config_no_content_box: new Error( | ||
1744 | 69 | 'Structural_subscription configuration has undefined '+ | ||
1745 | 70 | 'properties.') | ||
1746 | 71 | } | ||
1747 | 72 | }, | ||
1748 | 73 | |||
1749 | 74 | setUp: function() { | ||
1750 | 75 | // Monkeypatch LP to avoid network traffic and to allow | ||
1751 | 76 | // insertion of test data. | ||
1752 | 77 | window.LP = { | ||
1753 | 78 | links: {}, | ||
1754 | 79 | cache: {} | ||
1755 | 80 | }; | ||
1756 | 81 | LP.cache.context = { | ||
1757 | 82 | title: 'Test Project', | ||
1758 | 83 | self_link: 'https://launchpad.dev/api/test_project' | ||
1759 | 84 | }; | ||
1760 | 85 | LP.cache.administratedTeams = []; | ||
1761 | 86 | LP.cache.importances = []; | ||
1762 | 87 | LP.cache.statuses = []; | ||
1763 | 88 | |||
1764 | 89 | this.configuration = { | ||
1765 | 90 | content_box: content_box_id, | ||
1766 | 91 | }; | ||
1767 | 92 | this.content_node = create_test_node(); | ||
1768 | 93 | Y.one('body').appendChild(this.content_node); | ||
1769 | 94 | }, | ||
1770 | 95 | |||
1771 | 96 | tearDown: function() { | ||
1772 | 97 | //delete this.configuration; | ||
1773 | 98 | remove_test_node(); | ||
1774 | 99 | delete this.content_node; | ||
1775 | 100 | delete this.configuration.lp_client; | ||
1776 | 101 | delete this.content_node; | ||
1777 | 102 | }, | ||
1778 | 103 | |||
1779 | 104 | test_setup_config_none: function() { | ||
1780 | 105 | // The config passed to setup may not be null. | ||
1781 | 106 | module.setup(); | ||
1782 | 107 | }, | ||
1783 | 108 | |||
1784 | 109 | test_setup_config_no_content_box: function() { | ||
1785 | 110 | // The config passed to setup must contain a content_box. | ||
1786 | 111 | module.setup({}); | ||
1787 | 112 | }, | ||
1788 | 113 | |||
1789 | 114 | test_anonymous: function() { | ||
1790 | 115 | // The link should not be shown to anonymous users so | ||
1791 | 116 | // 'setup' should not do anything in that case. If it | ||
1792 | 117 | // were successful, the lp_client would be defined after | ||
1793 | 118 | // setup is called. | ||
1794 | 119 | LP.links.me = undefined; | ||
1795 | 120 | Assert.isUndefined(module.lp_client); | ||
1796 | 121 | module.setup(this.configuration); | ||
1797 | 122 | Assert.isUndefined(module.lp_client); | ||
1798 | 123 | }, | ||
1799 | 124 | |||
1800 | 125 | test_logged_in_user: function() { | ||
1801 | 126 | // If there is a logged-in user, setup is successful | ||
1802 | 127 | LP.links.me = 'https://launchpad.dev/api/~someone'; | ||
1803 | 128 | Assert.isUndefined(module.lp_client); | ||
1804 | 129 | module.setup(this.configuration); | ||
1805 | 130 | Assert.isNotUndefined(module.lp_client); | ||
1806 | 131 | }, | ||
1807 | 132 | |||
1808 | 133 | test_list_contains: function() { | ||
1809 | 134 | // Validate that the list_contains function actually reports | ||
1810 | 135 | // whether or not an element is in a list. | ||
1811 | 136 | var list = ['a', 'b', 'c']; | ||
1812 | 137 | Assert.isTrue(module._list_contains(list, 'b')); | ||
1813 | 138 | Assert.isFalse(module._list_contains(list, 'd')); | ||
1814 | 139 | Assert.isFalse(module._list_contains([], 'a')); | ||
1815 | 140 | Assert.isTrue(module._list_contains(['a', 'a'], 'a')); | ||
1816 | 141 | Assert.isFalse(module._list_contains([], '')); | ||
1817 | 142 | Assert.isFalse(module._list_contains([], null)); | ||
1818 | 143 | Assert.isFalse(module._list_contains(['a'], null)); | ||
1819 | 144 | Assert.isFalse(module._list_contains([])); | ||
1820 | 145 | }, | ||
1821 | 146 | |||
1822 | 147 | test_make_selector_controls: function() { | ||
1823 | 148 | // Verify the creation of select all/none controls. | ||
1824 | 149 | var selectors = module.make_selector_controls('sharona'); | ||
1825 | 150 | Assert.areEqual('sharona-select-all', selectors['all_name']); | ||
1826 | 151 | Assert.areEqual('sharona-select-none', selectors['none_name']); | ||
1827 | 152 | Assert.areEqual( | ||
1828 | 153 | '<div id="sharona-selectors"', | ||
1829 | 154 | selectors['html'].slice(0, 27)); | ||
1830 | 155 | } | ||
1831 | 156 | }); | ||
1832 | 157 | suite.add(test_case); | ||
1833 | 158 | |||
1834 | 159 | test_case = new Y.Test.Case({ | ||
1835 | 160 | name: 'Structural Subscription Overlay save_subscription', | ||
1836 | 161 | |||
1837 | 162 | _should: { | ||
1838 | 163 | error: {} | ||
1839 | 164 | }, | ||
1840 | 165 | |||
1841 | 166 | setUp: function() { | ||
1842 | 167 | // Monkeypatch LP to avoid network traffic and to allow | ||
1843 | 168 | // insertion of test data. | ||
1844 | 169 | window.LP = { | ||
1845 | 170 | links: {}, | ||
1846 | 171 | cache: {} | ||
1847 | 172 | }; | ||
1848 | 173 | Y.lp.client.Launchpad = function() {}; | ||
1849 | 174 | Y.lp.client.Launchpad.prototype.named_post = | ||
1850 | 175 | function(url, func, config) { | ||
1851 | 176 | context.url = url; | ||
1852 | 177 | context.func = func; | ||
1853 | 178 | context.config = config; | ||
1854 | 179 | // No need to call the on.success handler. | ||
1855 | 180 | }; | ||
1856 | 181 | LP.cache.context = { | ||
1857 | 182 | title: 'Test Project', | ||
1858 | 183 | self_link: 'https://launchpad.dev/api/test_project' | ||
1859 | 184 | }; | ||
1860 | 185 | LP.links.me = 'https://launchpad.dev/api/~someone'; | ||
1861 | 186 | LP.cache.administratedTeams = []; | ||
1862 | 187 | LP.cache.importances = []; | ||
1863 | 188 | LP.cache.statuses = []; | ||
1864 | 189 | |||
1865 | 190 | this.configuration = { | ||
1866 | 191 | content_box: content_box_id | ||
1867 | 192 | }; | ||
1868 | 193 | this.content_node = create_test_node(); | ||
1869 | 194 | Y.one('body').appendChild(this.content_node); | ||
1870 | 195 | |||
1871 | 196 | this.bug_filter = { | ||
1872 | 197 | lp_original_uri: | ||
1873 | 198 | '/api/devel/firefox/+subscription/mark/+filter/28' | ||
1874 | 199 | }; | ||
1875 | 200 | this.form_data = { | ||
1876 | 201 | recipient: ['user'] | ||
1877 | 202 | }; | ||
1878 | 203 | context = {}; | ||
1879 | 204 | }, | ||
1880 | 205 | |||
1881 | 206 | tearDown: function() { | ||
1882 | 207 | delete this.configuration; | ||
1883 | 208 | remove_test_node(); | ||
1884 | 209 | delete this.content_node; | ||
1885 | 210 | }, | ||
1886 | 211 | |||
1887 | 212 | test_user_recipient: function() { | ||
1888 | 213 | // When the user selects themselves as the recipient, the current | ||
1889 | 214 | // user's URI is used as the recipient value. | ||
1890 | 215 | module.setup(this.configuration); | ||
1891 | 216 | this.form_data.recipient = ['user']; | ||
1892 | 217 | module.save_subscription(this.form_data); | ||
1893 | 218 | Assert.areEqual( | ||
1894 | 219 | LP.links.me, | ||
1895 | 220 | context.config.parameters.subscriber); | ||
1896 | 221 | }, | ||
1897 | 222 | |||
1898 | 223 | test_team_recipient: function() { | ||
1899 | 224 | // When the user selects a team as the recipient, the selected | ||
1900 | 225 | // team's URI is used as the recipient value. | ||
1901 | 226 | module.setup(this.configuration); | ||
1902 | 227 | this.form_data.recipient = ['team']; | ||
1903 | 228 | this.form_data.team = ['https://launchpad.dev/api/~super-team']; | ||
1904 | 229 | module.save_subscription(this.form_data); | ||
1905 | 230 | Assert.areEqual( | ||
1906 | 231 | this.form_data.team[0], | ||
1907 | 232 | context.config.parameters.subscriber); | ||
1908 | 233 | } | ||
1909 | 234 | }); | ||
1910 | 235 | suite.add(test_case); | ||
1911 | 236 | |||
1912 | 237 | test_case = new Y.Test.Case({ | ||
1913 | 238 | name: 'Structural Subscription interaction tests', | ||
1914 | 239 | |||
1915 | 240 | _should: { | ||
1916 | 241 | error: { | ||
1917 | 242 | } | ||
1918 | 243 | }, | ||
1919 | 244 | |||
1920 | 245 | setUp: function() { | ||
1921 | 246 | // Monkeypatch LP to avoid network traffic and to allow | ||
1922 | 247 | // insertion of test data. | ||
1923 | 248 | window.LP = { | ||
1924 | 249 | links: {}, | ||
1925 | 250 | cache: {} | ||
1926 | 251 | }; | ||
1927 | 252 | |||
1928 | 253 | LP.cache.context = { | ||
1929 | 254 | title: 'Test Project', | ||
1930 | 255 | self_link: 'https://launchpad.dev/api/test_project' | ||
1931 | 256 | }; | ||
1932 | 257 | LP.cache.administratedTeams = []; | ||
1933 | 258 | LP.cache.importances = []; | ||
1934 | 259 | LP.cache.statuses = []; | ||
1935 | 260 | LP.links.me = 'https://launchpad.dev/api/~someone'; | ||
1936 | 261 | |||
1937 | 262 | var lp_client = function() {}; | ||
1938 | 263 | this.configuration = { | ||
1939 | 264 | content_box: content_box_id, | ||
1940 | 265 | lp_client: lp_client | ||
1941 | 266 | }; | ||
1942 | 267 | |||
1943 | 268 | this.content_node = create_test_node(); | ||
1944 | 269 | Y.one('body').appendChild(this.content_node); | ||
1945 | 270 | }, | ||
1946 | 271 | |||
1947 | 272 | tearDown: function() { | ||
1948 | 273 | remove_test_node(); | ||
1949 | 274 | delete this.content_node; | ||
1950 | 275 | }, | ||
1951 | 276 | |||
1952 | 277 | test_setup_overlay: function() { | ||
1953 | 278 | // At the outset there should be no overlay. | ||
1954 | 279 | var overlay = Y.one('#accordion-overlay'); | ||
1955 | 280 | Assert.isNull(overlay); | ||
1956 | 281 | module.setup(this.configuration); | ||
1957 | 282 | // After the setup the overlay should be in the DOM. | ||
1958 | 283 | overlay = Y.one('#accordion-overlay'); | ||
1959 | 284 | Assert.isNotNull(overlay); | ||
1960 | 285 | var header = Y.one(content_box_id).one('h2'); | ||
1961 | 286 | Assert.areEqual( | ||
1962 | 287 | 'Add a mail subscription for Test Project bugs', | ||
1963 | 288 | header.get('text')); | ||
1964 | 289 | }, | ||
1965 | 290 | |||
1966 | 291 | test_initial_state: function() { | ||
1967 | 292 | // When initialized the <div> elements for the filter | ||
1968 | 293 | // wrapper and the accordion wrapper should be collapsed. | ||
1969 | 294 | module.setup(this.configuration); | ||
1970 | 295 | // Simulate a click on the link to open the overlay. | ||
1971 | 296 | var link = Y.one('.menu-link-subscribe_to_bug_mail'); | ||
1972 | 297 | Y.Event.simulate( | ||
1973 | 298 | Y.Node.getDOMNode(link), 'click'); | ||
1974 | 299 | var filter_wrapper = Y.one('#filter-wrapper'); | ||
1975 | 300 | var accordion_wrapper = Y.one('#accordion-wrapper'); | ||
1976 | 301 | Assert.isTrue(filter_wrapper.hasClass('lazr-closed')); | ||
1977 | 302 | Assert.isTrue(accordion_wrapper.hasClass('lazr-closed')); | ||
1978 | 303 | }, | ||
1979 | 304 | |||
1980 | 305 | test_added_or_changed_toggles: function() { | ||
1981 | 306 | // Test that the filter wrapper opens and closes in | ||
1982 | 307 | // response to the added_or_changed radio button. | ||
1983 | 308 | module.setup(this.configuration); | ||
1984 | 309 | // Simulate a click on the link to open the overlay. | ||
1985 | 310 | var link = Y.one('.menu-link-subscribe_to_bug_mail'); | ||
1986 | 311 | Y.Event.simulate( | ||
1987 | 312 | Y.Node.getDOMNode(link), 'click'); | ||
1988 | 313 | var added_changed = Y.one('#added-or-changed'); | ||
1989 | 314 | Assert.isFalse(added_changed.get('checked')); | ||
1990 | 315 | var filter_wrapper = Y.one('#filter-wrapper'); | ||
1991 | 316 | // Initially closed. | ||
1992 | 317 | Assert.isTrue(filter_wrapper.hasClass('lazr-closed')); | ||
1993 | 318 | // Opens when selected. | ||
1994 | 319 | Y.Event.simulate(Y.Node.getDOMNode(added_changed), 'click'); | ||
1995 | 320 | this.wait(function() { | ||
1996 | 321 | Assert.isTrue(filter_wrapper.hasClass('lazr-opened')); | ||
1997 | 322 | }, 500); | ||
1998 | 323 | // Closes when deselected. | ||
1999 | 324 | Y.Event.simulate( | ||
2000 | 325 | Y.Node.getDOMNode(Y.one('#added-or-closed')), 'click'); | ||
2001 | 326 | this.wait(function() { | ||
2002 | 327 | Assert.isTrue(filter_wrapper.hasClass('lazr-closed')); | ||
2003 | 328 | }, 500); | ||
2004 | 329 | }, | ||
2005 | 330 | |||
2006 | 331 | test_advanced_filter_toggles: function() { | ||
2007 | 332 | // Test that the accordion wrapper opens and closes in | ||
2008 | 333 | // response to the advanced filter check box. | ||
2009 | 334 | module.setup(this.configuration); | ||
2010 | 335 | // Simulate a click on the link to open the overlay. | ||
2011 | 336 | var link = Y.one('.menu-link-subscribe_to_bug_mail'); | ||
2012 | 337 | Y.Event.simulate( | ||
2013 | 338 | Y.Node.getDOMNode(link), 'click'); | ||
2014 | 339 | var added_changed = Y.one('#added-or-changed'); | ||
2015 | 340 | added_changed.set('checked', true); | ||
2016 | 341 | |||
2017 | 342 | // Initially closed. | ||
2018 | 343 | var advanced_filter = Y.one('#advanced-filter'); | ||
2019 | 344 | Assert.isFalse(advanced_filter.get('checked')); | ||
2020 | 345 | var accordion_wrapper = Y.one('#accordion-wrapper'); | ||
2021 | 346 | this.wait(function() { | ||
2022 | 347 | Assert.isTrue(accordion_wrapper.hasClass('lazr-closed')); | ||
2023 | 348 | }, 500); | ||
2024 | 349 | // Opens when selected. | ||
2025 | 350 | advanced_filter.set('checked') = true; | ||
2026 | 351 | this.wait(function() { | ||
2027 | 352 | Assert.isTrue(accordion_wrapper.hasClass('lazr-opened')); | ||
2028 | 353 | }, 500); | ||
2029 | 354 | // Closes when deselected. | ||
2030 | 355 | advanced_filter.set('checked') = false; | ||
2031 | 356 | this.wait(function() { | ||
2032 | 357 | Assert.isTrue(accordion_wrapper.hasClass('lazr-closed')); | ||
2033 | 358 | }, 500); | ||
2034 | 359 | }, | ||
2035 | 360 | |||
2036 | 361 | test_importances_select_all_none: function() { | ||
2037 | 362 | // Test the select all/none functionality for the importances | ||
2038 | 363 | // accordion pane. | ||
2039 | 364 | module.setup(this.configuration); | ||
2040 | 365 | var checkboxes = Y.all('input[name="importances"]'); | ||
2041 | 366 | var select_all = Y.one('#importances-select-all'); | ||
2042 | 367 | var select_none = Y.one('#importances-select-none'); | ||
2043 | 368 | Assert.isTrue(test_checked(checkboxes, true)); | ||
2044 | 369 | // Simulate a click on the select_none control. | ||
2045 | 370 | Y.Event.simulate(Y.Node.getDOMNode(select_none), 'click'); | ||
2046 | 371 | Assert.isTrue(test_checked(checkboxes, false)); | ||
2047 | 372 | // Simulate a click on the select_all control. | ||
2048 | 373 | Y.Event.simulate(Y.Node.getDOMNode(select_all), 'click'); | ||
2049 | 374 | Assert.isTrue(test_checked(checkboxes, true)); | ||
2050 | 375 | }, | ||
2051 | 376 | |||
2052 | 377 | test_statuses_select_all_none: function() { | ||
2053 | 378 | // Test the select all/none functionality for the statuses | ||
2054 | 379 | // accordion pane. | ||
2055 | 380 | module.setup(this.configuration); | ||
2056 | 381 | var checkboxes = Y.all('input[name="statuses"]'); | ||
2057 | 382 | var select_all = Y.one('#statuses-select-all'); | ||
2058 | 383 | var select_none = Y.one('#statuses-select-none'); | ||
2059 | 384 | Assert.isTrue(test_checked(checkboxes, true)); | ||
2060 | 385 | // Simulate a click on the select_none control. | ||
2061 | 386 | Y.Event.simulate(Y.Node.getDOMNode(select_none), 'click'); | ||
2062 | 387 | Assert.isTrue(test_checked(checkboxes, false)); | ||
2063 | 388 | // Simulate a click on the select_all control. | ||
2064 | 389 | Y.Event.simulate(Y.Node.getDOMNode(select_all), 'click'); | ||
2065 | 390 | Assert.isTrue(test_checked(checkboxes, true)); | ||
2066 | 391 | } | ||
2067 | 392 | |||
2068 | 393 | }); | ||
2069 | 394 | suite.add(test_case); | ||
2070 | 395 | |||
2071 | 396 | test_case = new Y.Test.Case({ | ||
2072 | 397 | // Test the setup method. | ||
2073 | 398 | name: 'Structural Subscription error handling', | ||
2074 | 399 | |||
2075 | 400 | _should: { | ||
2076 | 401 | error: { | ||
2077 | 402 | } | ||
2078 | 403 | }, | ||
2079 | 404 | |||
2080 | 405 | setUp: function() { | ||
2081 | 406 | // Monkeypatch LP to avoid network traffic and to allow | ||
2082 | 407 | // insertion of test data. | ||
2083 | 408 | window.LP = { | ||
2084 | 409 | links: {}, | ||
2085 | 410 | cache: {} | ||
2086 | 411 | }; | ||
2087 | 412 | |||
2088 | 413 | LP.cache.context = { | ||
2089 | 414 | title: 'Test Project', | ||
2090 | 415 | self_link: 'https://launchpad.dev/api/test_project' | ||
2091 | 416 | }; | ||
2092 | 417 | LP.cache.administratedTeams = []; | ||
2093 | 418 | LP.cache.importances = ['Unknown', 'Critical', 'High', 'Medium', | ||
2094 | 419 | 'Low', 'Wishlist', 'Undecided']; | ||
2095 | 420 | LP.cache.statuses = ['New', 'Incomplete', 'Opinion', | ||
2096 | 421 | 'Invalid', 'Won\'t Fix', 'Expired', | ||
2097 | 422 | 'Confirmed', 'Triaged', 'In Progress', | ||
2098 | 423 | 'Fix Committed', 'Fix Released', 'Unknown']; | ||
2099 | 424 | LP.links.me = 'https://launchpad.dev/api/~someone'; | ||
2100 | 425 | |||
2101 | 426 | var lp_client = function() {}; | ||
2102 | 427 | this.configuration = { | ||
2103 | 428 | content_box: content_box_id, | ||
2104 | 429 | lp_client: lp_client | ||
2105 | 430 | }; | ||
2106 | 431 | |||
2107 | 432 | this.content_node = create_test_node(); | ||
2108 | 433 | Y.one('body').appendChild(this.content_node); | ||
2109 | 434 | }, | ||
2110 | 435 | |||
2111 | 436 | tearDown: function() { | ||
2112 | 437 | remove_test_node(); | ||
2113 | 438 | delete this.content_node; | ||
2114 | 439 | }, | ||
2115 | 440 | |||
2116 | 441 | test_overlay_error_handling_adding: function() { | ||
2117 | 442 | // Verify that errors generated during adding of a filter are | ||
2118 | 443 | // displayed to the user. | ||
2119 | 444 | this.configuration.lp_client.named_post = | ||
2120 | 445 | function(url, func, config) { | ||
2121 | 446 | config.on.failure(true, true); | ||
2122 | 447 | }; | ||
2123 | 448 | module.setup(this.configuration); | ||
2124 | 449 | // After the setup the overlay should be in the DOM. | ||
2125 | 450 | overlay = Y.one('#accordion-overlay'); | ||
2126 | 451 | Assert.isNotNull(overlay); | ||
2127 | 452 | submit_button = Y.one('.yui3-lazr-formoverlay-actions button'); | ||
2128 | 453 | Y.Event.simulate(Y.Node.getDOMNode(submit_button), 'click'); | ||
2129 | 454 | |||
2130 | 455 | var error_box = Y.one('.yui3-lazr-formoverlay-errors'); | ||
2131 | 456 | Assert.areEqual( | ||
2132 | 457 | 'The following errors were encountered: ', | ||
2133 | 458 | error_box.get('text')); | ||
2134 | 459 | }, | ||
2135 | 460 | |||
2136 | 461 | test_overlay_error_handling_patching: function() { | ||
2137 | 462 | // Verify that errors generated during patching of a filter are | ||
2138 | 463 | // displayed to the user. | ||
2139 | 464 | var original_delete_filter = module._delete_filter; | ||
2140 | 465 | module._delete_filter = function() {}; | ||
2141 | 466 | this.configuration.lp_client.patch = | ||
2142 | 467 | function(bug_filter, data, config) { | ||
2143 | 468 | config.on.failure(true, true); | ||
2144 | 469 | }; | ||
2145 | 470 | var bug_filter = { | ||
2146 | 471 | 'getAttrs': function() { return {}; } | ||
2147 | 472 | }; | ||
2148 | 473 | this.configuration.lp_client.named_post = | ||
2149 | 474 | function(url, func, config) { | ||
2150 | 475 | config.on.success(bug_filter); | ||
2151 | 476 | }; | ||
2152 | 477 | module.setup(this.configuration); | ||
2153 | 478 | // After the setup the overlay should be in the DOM. | ||
2154 | 479 | overlay = Y.one('#accordion-overlay'); | ||
2155 | 480 | Assert.isNotNull(overlay); | ||
2156 | 481 | submit_button = Y.one('.yui3-lazr-formoverlay-actions button'); | ||
2157 | 482 | Y.Event.simulate(Y.Node.getDOMNode(submit_button), 'click'); | ||
2158 | 483 | |||
2159 | 484 | // Put this stubbed function back. | ||
2160 | 485 | module._delete_filter = original_delete_filter; | ||
2161 | 486 | |||
2162 | 487 | var error_box = Y.one('.yui3-lazr-formoverlay-errors'); | ||
2163 | 488 | Assert.areEqual( | ||
2164 | 489 | 'The following errors were encountered: ', | ||
2165 | 490 | error_box.get('text')); | ||
2166 | 491 | } | ||
2167 | 492 | |||
2168 | 493 | }); | ||
2169 | 494 | suite.add(test_case); | ||
2170 | 495 | |||
2171 | 496 | suite.add(new Y.Test.Case({ | ||
2172 | 497 | name: 'Structural Subscription: deleting failed filters', | ||
2173 | 498 | |||
2174 | 499 | _should: {error: {}}, | ||
2175 | 500 | |||
2176 | 501 | setUp: function() { | ||
2177 | 502 | // Monkeypatch LP to avoid network traffic and to allow | ||
2178 | 503 | // insertion of test data. | ||
2179 | 504 | this.original_lp = window.LP; | ||
2180 | 505 | window.LP = { | ||
2181 | 506 | links: {}, | ||
2182 | 507 | cache: {} | ||
2183 | 508 | }; | ||
2184 | 509 | LP.cache.context = { | ||
2185 | 510 | self_link: 'https://launchpad.dev/api/test_project' | ||
2186 | 511 | }; | ||
2187 | 512 | LP.links.me = 'https://launchpad.dev/api/~someone'; | ||
2188 | 513 | LP.cache.administratedTeams = []; | ||
2189 | 514 | }, | ||
2190 | 515 | |||
2191 | 516 | tearDown: function() { | ||
2192 | 517 | window.LP = this.original_lp; | ||
2193 | 518 | }, | ||
2194 | 519 | |||
2195 | 520 | test_delete_on_patch_failure: function() { | ||
2196 | 521 | // Creating a filter is a two step process. First it is created | ||
2197 | 522 | // and then patched. If the PATCH fails, then we should DELETE | ||
2198 | 523 | // the undifferentiated filter. | ||
2199 | 524 | |||
2200 | 525 | // First we inject our own delete_filter implementation that just | ||
2201 | 526 | // tells us that it was called. | ||
2202 | 527 | var original_delete_filter = module._delete_filter; | ||
2203 | 528 | var delete_called = false; | ||
2204 | 529 | module._delete_filter = function() { | ||
2205 | 530 | delete_called = true; | ||
2206 | 531 | }; | ||
2207 | 532 | var patch_failed = false; | ||
2208 | 533 | |||
2209 | 534 | var TestBugFilter = function() {}; | ||
2210 | 535 | TestBugFilter.prototype = { | ||
2211 | 536 | 'getAttrs': function () { | ||
2212 | 537 | return {}; | ||
2213 | 538 | }, | ||
2214 | 539 | }; | ||
2215 | 540 | |||
2216 | 541 | // Now we need an lp_client that will appear to succesfully create | ||
2217 | 542 | // the filter but then fail to patch it. | ||
2218 | 543 | var TestClient = function() {}; | ||
2219 | 544 | TestClient.prototype = { | ||
2220 | 545 | 'named_post': function (uri, operation_name, config) { | ||
2221 | 546 | if (operation_name === 'addBugSubscriptionFilter') { | ||
2222 | 547 | config.on.success(new TestBugFilter()); | ||
2223 | 548 | } else { | ||
2224 | 549 | throw new Error('unexpected operation'); | ||
2225 | 550 | } | ||
2226 | 551 | }, | ||
2227 | 552 | 'patch': function(uri, representation, config, headers) { | ||
2228 | 553 | config.on.failure(true, {'status':400}); | ||
2229 | 554 | patch_failed = true; | ||
2230 | 555 | }, | ||
2231 | 556 | }; | ||
2232 | 557 | module.lp_client = new TestClient(); | ||
2233 | 558 | |||
2234 | 559 | // OK, we're ready to add the bug filter and let the various | ||
2235 | 560 | // handlers be called. | ||
2236 | 561 | module._add_bug_filter(LP.links.me, 'this is a test'); | ||
2237 | 562 | // Put some functions back. | ||
2238 | 563 | module._delete_filter = original_delete_filter; | ||
2239 | 564 | |||
2240 | 565 | // Delete should have been called and the patch has failed. | ||
2241 | 566 | Assert.isTrue(delete_called); | ||
2242 | 567 | Assert.isTrue(patch_failed); | ||
2243 | 568 | }, | ||
2244 | 569 | |||
2245 | 570 | })); | ||
2246 | 571 | |||
2247 | 572 | suite.add(new Y.Test.Case({ | ||
2248 | 573 | name: 'Structural Subscription validate_config', | ||
2249 | 574 | |||
2250 | 575 | _should: { | ||
2251 | 576 | error: { | ||
2252 | 577 | test_setup_config_none: new Error( | ||
2253 | 578 | 'Missing config for structural_subscription.'), | ||
2254 | 579 | test_setup_config_no_content_box: new Error( | ||
2255 | 580 | 'Structural_subscription configuration has undefined '+ | ||
2256 | 581 | 'properties.') | ||
2257 | 582 | } | ||
2258 | 583 | }, | ||
2259 | 584 | |||
2260 | 585 | // Included in _should/error above. | ||
2261 | 586 | test_setup_config_none: function() { | ||
2262 | 587 | // The config passed to setup may not be null. | ||
2263 | 588 | module._validate_config(); | ||
2264 | 589 | }, | ||
2265 | 590 | |||
2266 | 591 | // Included in _should/error above. | ||
2267 | 592 | test_setup_config_no_content_box: function() { | ||
2268 | 593 | // The config passed to setup must contain a content_box. | ||
2269 | 594 | module._validate_config({}); | ||
2270 | 595 | } | ||
2271 | 596 | })); | ||
2272 | 597 | |||
2273 | 598 | suite.add(new Y.Test.Case({ | ||
2274 | 599 | name: 'Structural Subscription extract_form_data', | ||
2275 | 600 | |||
2276 | 601 | // Verify that all the different values of the structural subscription | ||
2277 | 602 | // add/edit form are correctly extracted by the extract_form_data | ||
2278 | 603 | // function. | ||
2279 | 604 | |||
2280 | 605 | _should: { | ||
2281 | 606 | error: { | ||
2282 | 607 | } | ||
2283 | 608 | }, | ||
2284 | 609 | |||
2285 | 610 | test_extract_description: function() { | ||
2286 | 611 | var form_data = { | ||
2287 | 612 | name: ['filter description'], | ||
2288 | 613 | events: [], | ||
2289 | 614 | filters: [], | ||
2290 | 615 | }; | ||
2291 | 616 | var patch_data = module._extract_form_data(form_data); | ||
2292 | 617 | Assert.areEqual(patch_data.description, form_data.name[0]); | ||
2293 | 618 | }, | ||
2294 | 619 | |||
2295 | 620 | test_extract_description_trim: function() { | ||
2296 | 621 | // Any leading or trailing whitespace is stripped from the | ||
2297 | 622 | // description. | ||
2298 | 623 | var form_data = { | ||
2299 | 624 | name: [' filter description '], | ||
2300 | 625 | events: [], | ||
2301 | 626 | filters: [], | ||
2302 | 627 | }; | ||
2303 | 628 | var patch_data = module._extract_form_data(form_data); | ||
2304 | 629 | Assert.areEqual('filter description', patch_data.description); | ||
2305 | 630 | }, | ||
2306 | 631 | |||
2307 | 632 | test_extract_chattiness_lifecycle: function() { | ||
2308 | 633 | var form_data = { | ||
2309 | 634 | name: [], | ||
2310 | 635 | events: ['added-or-closed'], | ||
2311 | 636 | filters: [], | ||
2312 | 637 | }; | ||
2313 | 638 | var patch_data = module._extract_form_data(form_data); | ||
2314 | 639 | Assert.areEqual( | ||
2315 | 640 | patch_data.bug_notification_level, 'Lifecycle'); | ||
2316 | 641 | }, | ||
2317 | 642 | |||
2318 | 643 | test_extract_chattiness_discussion: function() { | ||
2319 | 644 | var form_data = { | ||
2320 | 645 | name: [], | ||
2321 | 646 | events: [], | ||
2322 | 647 | filters: ['filter-comments'], | ||
2323 | 648 | }; | ||
2324 | 649 | var patch_data = module._extract_form_data(form_data); | ||
2325 | 650 | Assert.areEqual( | ||
2326 | 651 | patch_data.bug_notification_level, 'Details'); | ||
2327 | 652 | }, | ||
2328 | 653 | |||
2329 | 654 | test_extract_chattiness_details: function() { | ||
2330 | 655 | var form_data = { | ||
2331 | 656 | name: [], | ||
2332 | 657 | events: [], | ||
2333 | 658 | filters: [], | ||
2334 | 659 | }; | ||
2335 | 660 | var patch_data = module._extract_form_data(form_data); | ||
2336 | 661 | Assert.areEqual( | ||
2337 | 662 | patch_data.bug_notification_level, 'Discussion'); | ||
2338 | 663 | }, | ||
2339 | 664 | |||
2340 | 665 | test_extract_tags: function() { | ||
2341 | 666 | var form_data = { | ||
2342 | 667 | name: [], | ||
2343 | 668 | events: [], | ||
2344 | 669 | filters: ['advanced-filter'], | ||
2345 | 670 | tags: ['one two THREE'], | ||
2346 | 671 | tag_match: [''], | ||
2347 | 672 | importances: [], | ||
2348 | 673 | statuses: [], | ||
2349 | 674 | }; | ||
2350 | 675 | var patch_data = module._extract_form_data(form_data); | ||
2351 | 676 | // Note that the tags are converted to lower case. | ||
2352 | 677 | ArrayAssert.itemsAreEqual( | ||
2353 | 678 | patch_data.tags, ['one', 'two', 'three']); | ||
2354 | 679 | }, | ||
2355 | 680 | |||
2356 | 681 | test_extract_find_all_tags_true: function() { | ||
2357 | 682 | var form_data = { | ||
2358 | 683 | name: [], | ||
2359 | 684 | events: [], | ||
2360 | 685 | filters: ['advanced-filter'], | ||
2361 | 686 | tags: ['tag'], | ||
2362 | 687 | tag_match: ['match-all'], | ||
2363 | 688 | importances: [], | ||
2364 | 689 | statuses: [], | ||
2365 | 690 | }; | ||
2366 | 691 | var patch_data = module._extract_form_data(form_data); | ||
2367 | 692 | Assert.isTrue(patch_data.find_all_tags); | ||
2368 | 693 | }, | ||
2369 | 694 | |||
2370 | 695 | test_extract_find_all_tags_false: function() { | ||
2371 | 696 | var form_data = { | ||
2372 | 697 | name: [], | ||
2373 | 698 | events: [], | ||
2374 | 699 | filters: ['advanced-filter'], | ||
2375 | 700 | tags: ['tag'], | ||
2376 | 701 | tag_match: [], | ||
2377 | 702 | importances: [], | ||
2378 | 703 | statuses: [], | ||
2379 | 704 | }; | ||
2380 | 705 | var patch_data = module._extract_form_data(form_data); | ||
2381 | 706 | Assert.isFalse(patch_data.find_all_tags); | ||
2382 | 707 | }, | ||
2383 | 708 | |||
2384 | 709 | test_all_values_set: function() { | ||
2385 | 710 | // We need all the values to be set (even if empty) because | ||
2386 | 711 | // PATCH expects a set of changes to make and any unspecified | ||
2387 | 712 | // attributes will retain the previous value. | ||
2388 | 713 | var form_data = { | ||
2389 | 714 | name: [], | ||
2390 | 715 | events: [], | ||
2391 | 716 | filters: [], | ||
2392 | 717 | tags: ['tag'], | ||
2393 | 718 | tag_match: ['match-all'], | ||
2394 | 719 | importances: ['importance1'], | ||
2395 | 720 | statuses: ['status1'], | ||
2396 | 721 | }; | ||
2397 | 722 | var patch_data = module._extract_form_data(form_data); | ||
2398 | 723 | // Since advanced-filter isn't set, all the advanced values should | ||
2399 | 724 | // be empty/false despite the form values. | ||
2400 | 725 | Assert.isFalse(patch_data.find_all_tags); | ||
2401 | 726 | ArrayAssert.isEmpty(patch_data.tags) | ||
2402 | 727 | ArrayAssert.isEmpty(patch_data.importances) | ||
2403 | 728 | ArrayAssert.isEmpty(patch_data.statuses) | ||
2404 | 729 | }, | ||
2405 | 730 | |||
2406 | 731 | })); | ||
2407 | 732 | |||
2408 | 733 | // Lock, stock, and two smoking barrels. | ||
2409 | 734 | var handle_complete = function(data) { | ||
2410 | 735 | var status_node = Y.Node.create( | ||
2411 | 736 | '<p id="complete">Test status: complete</p>'); | ||
2412 | 737 | Y.one('body').appendChild(status_node); | ||
2413 | 738 | }; | ||
2414 | 739 | Y.Test.Runner.on('complete', handle_complete); | ||
2415 | 740 | Y.Test.Runner.add(suite); | ||
2416 | 741 | |||
2417 | 742 | // The following two lines may be commented out for debugging but | ||
2418 | 743 | // must be restored before being checked in or the tests will fail | ||
2419 | 744 | // in the test runner. | ||
2420 | 745 | var console = new Y.Console({newestOnTop: false}); | ||
2421 | 746 | console.render('#log'); | ||
2422 | 747 | |||
2423 | 748 | Y.on('domready', function() { | ||
2424 | 749 | Y.Test.Runner.run(); | ||
2425 | 750 | }); | ||
2426 | 751 | }); | ||
2427 | 0 | 752 | ||
2428 | === modified file 'lib/lp/registry/templates/product-index.pt' | |||
2429 | --- lib/lp/registry/templates/product-index.pt 2011-03-14 15:31:05 +0000 | |||
2430 | +++ lib/lp/registry/templates/product-index.pt 2011-03-28 19:31:27 +0000 | |||
2431 | @@ -32,6 +32,16 @@ | |||
2432 | 32 | </style> | 32 | </style> |
2433 | 33 | </noscript> | 33 | </noscript> |
2434 | 34 | 34 | ||
2435 | 35 | <script type="text/javascript" | ||
2436 | 36 | tal:condition=" | ||
2437 | 37 | request/features/advanced-structural-subscriptions.enabled"> | ||
2438 | 38 | LPS.use('lp.registry.structural_subscription', function(Y) { | ||
2439 | 39 | module = Y.lp.registry.structural_subscription; | ||
2440 | 40 | Y.on('domready', function() { | ||
2441 | 41 | module.setup({content_box: "#structural-subscription-content-box"}); | ||
2442 | 42 | }); | ||
2443 | 43 | }); | ||
2444 | 44 | </script> | ||
2445 | 35 | </tal:head-epilogue> | 45 | </tal:head-epilogue> |
2446 | 36 | </head> | 46 | </head> |
2447 | 37 | 47 | ||
2448 | @@ -246,6 +256,10 @@ | |||
2449 | 246 | 256 | ||
2450 | 247 | <div tal:content="structure context/@@+portlet-coming-sprints" /> | 257 | <div tal:content="structure context/@@+portlet-coming-sprints" /> |
2451 | 248 | </div> | 258 | </div> |
2452 | 259 | <div class="yui-u"> | ||
2453 | 260 | <div id="structural-subscription-content-box"></div> | ||
2454 | 261 | </div> | ||
2455 | 262 | |||
2456 | 249 | </div> | 263 | </div> |
2457 | 250 | </tal:main> | 264 | </tal:main> |
2458 | 251 | 265 | ||
2459 | 252 | 266 | ||
2460 | === modified file 'lib/lp/registry/templates/product-portlet-license-missing.pt' | |||
2461 | --- lib/lp/registry/templates/product-portlet-license-missing.pt 2009-08-11 12:43:24 +0000 | |||
2462 | +++ lib/lp/registry/templates/product-portlet-license-missing.pt 2011-03-28 19:31:27 +0000 | |||
2463 | @@ -13,7 +13,7 @@ | |||
2464 | 13 | src="/@@/expiration-large" /> | 13 | src="/@@/expiration-large" /> |
2465 | 14 | This project needs to have its licensing information entered. | 14 | This project needs to have its licensing information entered. |
2466 | 15 | <br />Select the appropriate license checkboxes from | 15 | <br />Select the appropriate license checkboxes from |
2468 | 16 | <a tal:content="structure context/menu:overview/edit/fmt:link-icon" />. | 16 | <a tal:replace="structure context/menu:overview/edit/fmt:link-icon" />. |
2469 | 17 | </div> | 17 | </div> |
2470 | 18 | </div> | 18 | </div> |
2471 | 19 | </tal:root> | 19 | </tal:root> |
2472 | 20 | 20 | ||
2473 | === modified file 'lib/lp/services/inlinehelp/javascript/inlinehelp.js' | |||
2474 | --- lib/lp/services/inlinehelp/javascript/inlinehelp.js 2010-05-10 22:21:41 +0000 | |||
2475 | +++ lib/lp/services/inlinehelp/javascript/inlinehelp.js 2011-03-28 19:31:27 +0000 | |||
2476 | @@ -50,8 +50,12 @@ | |||
2477 | 50 | 'class="help"' attribute if it is missing, and connect the | 50 | 'class="help"' attribute if it is missing, and connect the |
2478 | 51 | necessary event handlers. | 51 | necessary event handlers. |
2479 | 52 | */ | 52 | */ |
2482 | 53 | addElementClass(elem, 'help'); | 53 | // We want this to be idempotent, so we treat the 'help' class as a |
2483 | 54 | connect(elem, 'onclick', handleClickOnHelp); | 54 | // marker. |
2484 | 55 | if (!hasElementClass(elem, 'help')) { | ||
2485 | 56 | addElementClass(elem, 'help'); | ||
2486 | 57 | connect(elem, 'onclick', handleClickOnHelp); | ||
2487 | 58 | } | ||
2488 | 55 | } | 59 | } |
2489 | 56 | 60 | ||
2490 | 57 | 61 |
Hi Gary,
Whew, done. There are 20-odd items that need addressing but most of them
are quite small. Bear in mind that some of these may be addressed in the
next branch, but I thought it better to make note of them now just in
case. I'm marking the branch as r=me as I think all of these can be
addressed fairly simply and I don't want to block the landing. If any
major revisions are made someone else can add their vote beside mine.
One thing that I did notice was that certain things (like the comma-before- closing- brace problem, see [6]) should have been
trailing-
picked up by our JS linter (which I think runs as part of `make lint.`
Could you re-run that for me and, if they're not being picked up, file a
bug?
[1]
236 + Y.on('domready', function() { setup_bug_ subscriptions( subscription- content- box"})
237 + module.
238 + {content_box: "#structural-
This line needs a semicolon.
239 + });
And this need to be outdented by four spaces to avoid it looking like
it's closing the above JS Object.
[2]
236 + Y.on('domready', function() { setup_bug_ subscriptions( subscription- content- box"})
237 + module.
238 + {content_box: "#structural-
239 + });
Same comments as [1].
[3]
395 + Bugs can be tagged to help the project categorise the bug report. You
396 + can filter your subscription based on one or more tags. If you can choose
397 + to <em>Match all tags</em> then your filter will only match bugs that
398 + have each of those tags you enter here.
Should the end of line 396 read "If you choose" rather than "If you can
choose..."?
[4]
476 + // The list may be undefined in some cases. target) != -1;
477 + return list && list.indexOf(
We should use isArray() explicitly here:
return Y.Lang. isArray( list) && list.indexOf( target) != -1;
[5]
479 +namespace. list_contains = list_contains;
This seems to be a common pattern. Why not just make the function part
of the namespace in the first place:
namespace. list_contains = function(list, target) {
///...
};
(This is a common pattern elsewhere in the LP JS, though I don't know if
it's considered a standard). I'll not bother highlighting the other
cases; they're easy enough to find.
[6]
495 + statuses: [],
496 + };
The comma before the closing brace on an object literal causes problems
with some browsers (Opera and some versions of WebKit, IIRC); it should
be removed.
[7]
569 + on: {failure: overlay_ error_handler. getFailureHandl er()},
The trailing comma needs to die for the same reason as in [6].
[8]
575 +namespace. _delete_ filter = delete_filter
Needs a semicolon.
[9]
611 +namespace. _add_bug_ filter = add_bug_filter
So does this.
[10]
633 +function edit_subscripti on_handler( context, form_data) {
This function needs some documentation commentary.
[11]
649 +function create_ overlay( content_ box_id, overlay_id, submit_button,
650 + submit_callback) {
As does this one.
[12]
691 + alert('"Subscribe to bug mail" link not found.');
We shouldn't be using the alert() here any more. Y.fail would seem more
appropriate.
[13]...