From ac3567bc7b1d5a09511fda2c8d6f738f88bf79df Mon Sep 17 00:00:00 2001 From: Shane Tomlinson <stomlinson@mozilla.com> Date: Tue, 17 Jan 2012 13:50:02 +0000 Subject: [PATCH] Grey out buttons and disable the forms whenenver there is an XHR request occuring. * Add the "xhr_disable_form" module - whenever there is an XHR request, it greys out all buttons. * Fix the tests that were borked yesterday. issue #905 issue #888 --- resources/static/css/common.css | 13 +++-- .../static/dialog/controllers/authenticate.js | 2 +- resources/static/dialog/resources/helpers.js | 9 +--- resources/static/dialog/start.js | 2 + resources/static/pages/page_helpers.js | 9 +--- resources/static/pages/start.js | 5 +- resources/static/shared/helpers.js | 16 +++++- .../static/shared/modules/page_module.js | 53 +++++++++++++++---- .../static/shared/modules/xhr_disable_form.js | 30 +++++++++++ resources/static/shared/network.js | 1 + resources/static/test/index.html | 7 ++- .../test/qunit/pages/forgot_unit_test.js | 2 +- .../test/qunit/pages/signin_unit_test.js | 4 +- .../qunit/pages/verify_email_address_test.js | 2 +- .../{ => modules}/page_module_unit_test.js | 25 ++++++--- .../{ => modules}/xhr_delay_unit_test.js | 0 .../modules/xhr_disable_form_unit_test.js | 41 ++++++++++++++ .../static/test/qunit/testHelpers/helpers.js | 5 +- resources/views/dialog_layout.ejs | 1 + resources/views/layout.ejs | 1 + scripts/compress.sh | 4 +- 21 files changed, 182 insertions(+), 50 deletions(-) create mode 100644 resources/static/shared/modules/xhr_disable_form.js rename resources/static/test/qunit/shared/{ => modules}/page_module_unit_test.js (92%) rename resources/static/test/qunit/shared/{ => modules}/xhr_delay_unit_test.js (100%) create mode 100644 resources/static/test/qunit/shared/modules/xhr_disable_form_unit_test.js diff --git a/resources/static/css/common.css b/resources/static/css/common.css index 6a96e10d2..0a8a061ec 100644 --- a/resources/static/css/common.css +++ b/resources/static/css/common.css @@ -195,13 +195,16 @@ button::-moz-focus-inner, .button::-moz-focus-inner { border: 0 } -button[disabled] { - -ms-filter:"progid:DXImageTransform.Microsoft.Alpha(Opacity=50)"; - opacity: .5; +button[disabled], .submit_disabled button, .submit_disabled .button, +.submit_disabled button:focus, .submit_disabled .button:focus, +.submit_disabled button:active, .submit_disabled .button:active { + background-color: #37A6FF; + background-image: -moz-linear-gradient(center top , #76C2FF 0pt, #37A6FF 100%); + background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #76C2FF), color-stop(100%, #37A6FF)); + -ms-filter:"progid:DXImageTransform.Microsoft.Alpha(Opacity=50)"; + opacity: .5; } - - hr { height: 1px; border: none; diff --git a/resources/static/dialog/controllers/authenticate.js b/resources/static/dialog/controllers/authenticate.js index 997a248b0..b4733b69c 100644 --- a/resources/static/dialog/controllers/authenticate.js +++ b/resources/static/dialog/controllers/authenticate.js @@ -14,7 +14,7 @@ BrowserID.Modules.Authenticate = (function() { tooltip = bid.Tooltip, helpers = bid.Helpers, dialogHelpers = helpers.Dialog, - cancelEvent = dialogHelpers.cancelEvent, + cancelEvent = helpers.cancelEvent, dom = bid.DOM, lastEmail = ""; diff --git a/resources/static/dialog/resources/helpers.js b/resources/static/dialog/resources/helpers.js index 3191825e4..bcc103b82 100644 --- a/resources/static/dialog/resources/helpers.js +++ b/resources/static/dialog/resources/helpers.js @@ -125,13 +125,6 @@ } } - function cancelEvent(callback) { - return function(event) { - event && event.preventDefault(); - callback.call(this); - }; - } - helpers.Dialog = helpers.Dialog || {}; helpers.extend(helpers.Dialog, { @@ -140,7 +133,7 @@ createUser: createUser, addEmail: addEmail, resetPassword: resetPassword, - cancelEvent: cancelEvent + cancelEvent: helpers.cancelEvent }); }()); diff --git a/resources/static/dialog/start.js b/resources/static/dialog/start.js index 5da6f8236..9566d978b 100644 --- a/resources/static/dialog/start.js +++ b/resources/static/dialog/start.js @@ -32,9 +32,11 @@ moduleManager.register("primary_user_provisioned", modules.PrimaryUserProvisioned); moduleManager.register("email_chosen", modules.EmailChosen); moduleManager.register("xhr_delay", modules.XHRDelay); + moduleManager.register("xhr_disable_form", modules.XHRDisableForm); moduleManager.start("xhr_delay"); + moduleManager.start("xhr_disable_form"); moduleManager.start("dialog"); } } diff --git a/resources/static/pages/page_helpers.js b/resources/static/pages/page_helpers.js index 638778590..b15cf2eb8 100644 --- a/resources/static/pages/page_helpers.js +++ b/resources/static/pages/page_helpers.js @@ -130,13 +130,6 @@ BrowserID.PageHelpers = (function() { dom.focus("input:visible:eq(0)"); } - function cancelEvent(callback) { - return function(event) { - event && event.preventDefault(); - callback && callback(); - }; - } - function openPrimaryAuth(winchan, email, baseURL, callback) { if(!(email && baseURL)) { throw "cannot verify with primary without an email address and URL" @@ -195,7 +188,7 @@ BrowserID.PageHelpers = (function() { emailSent: emailSent, cancelEmailSent: cancelEmailSent, userValidationComplete: userValidationComplete, - cancelEvent: cancelEvent, + cancelEvent: helpers.cancelEvent, openPrimaryAuth: openPrimaryAuth }; }()); diff --git a/resources/static/pages/start.js b/resources/static/pages/start.js index 132ce6e89..d64a447b6 100644 --- a/resources/static/pages/start.js +++ b/resources/static/pages/start.js @@ -16,11 +16,14 @@ $(function() { user = bid.User, token = pageHelpers.getParameterByName("token"), path = document.location.pathname, - XHRDelay = bid.Modules.XHRDelay; + XHRDelay = bid.Modules.XHRDelay, + XHRDisableForm = bid.Modules.XHRDisableForm; network.init({ time_until_delay: 10 * 1000 }); var xhrDelay = XHRDelay.create({}); xhrDelay.start(); + var xhrDisableForm = XHRDisableForm.create({}); + xhrDisableForm.start(); if (!path || path === "/") { bid.index(); diff --git a/resources/static/shared/helpers.js b/resources/static/shared/helpers.js index 1a94fa11a..edbb0e5b3 100644 --- a/resources/static/shared/helpers.js +++ b/resources/static/shared/helpers.js @@ -132,6 +132,12 @@ return dObj; } + function cancelEvent(callback) { + return function(event) { + event && event.preventDefault(); + callback.call(this); + }; + } extend(helpers, { /** @@ -175,8 +181,14 @@ * @param {Date} date * @returns {string} date relative to now. */ - relativeDate: relativeDate - + relativeDate: relativeDate, + /** + * Return a function that calls preventDefault on the event and then calls + * the callback with the arguments. + * @method cancelEvent + * @param {function} function to call after the event is cancelled. + */ + cancelEvent: cancelEvent }); diff --git a/resources/static/shared/modules/page_module.js b/resources/static/shared/modules/page_module.js index b702f6163..ecefd0751 100644 --- a/resources/static/shared/modules/page_module.js +++ b/resources/static/shared/modules/page_module.js @@ -11,13 +11,12 @@ BrowserID.Modules.PageModule = (function() { bid = BrowserID, dom = bid.DOM, screens = bid.Screens, + helpers = bid.Helpers, + cancelEvent = helpers.cancelEvent, mediator = bid.Mediator; - function onSubmit(event) { - event.stopPropagation(); - event.preventDefault(); - - if (this.validate()) { + function onSubmit() { + if (!dom.hasClass("body", "submit_disabled") && this.validate()) { this.submit(); } return false; @@ -55,8 +54,8 @@ BrowserID.Modules.PageModule = (function() { start: function(options) { var self=this; - self.bind("form", "submit", onSubmit); - self.bind("#thisIsNotMe", "click", self.close.bind(self, "notme")); + self.bind("form", "submit", cancelEvent(onSubmit)); + self.bind("#thisIsNotMe", "click", cancelEvent(self.close.bind(self, "notme"))); }, stop: function() { @@ -69,6 +68,14 @@ BrowserID.Modules.PageModule = (function() { this.stop(); }, + /** + * Bind a dom event + * @method bind + * @param {string} target - css selector + * @param {string} type - event type + * @param {function} callback + * @param {object} [context] - optional context, if not given, use this. + */ bind: function(target, type, callback, context) { var self=this, cb = callback.bind(context || this); @@ -123,10 +130,20 @@ BrowserID.Modules.PageModule = (function() { screens.error.hide(); }, + /** + * Validate the form, if returns false when called, submit will not be + * called on click. + * @method validate. + */ validate: function() { return true; }, + /** + * Submit the form. Can be called to force override the + * disableSubmit function. + * @method submit + */ submit: function() { }, @@ -137,12 +154,25 @@ BrowserID.Modules.PageModule = (function() { } }, + /** + * Publish a message to the mediator. + * @method publish + * @param {string} message + * @param {object} data + */ publish: function(message, data) { mediator.publish(message, data); }, - subscribe: function(message, callback) { - mediator.subscribe(message, callback.bind(this)); + /** + * Subscribe to a message on the mediator. + * @method subscribe + * @param {string} message + * @param {function} callback + * @param {object} [context] - context, if not given, use this. + */ + subscribe: function(message, callback, context) { + mediator.subscribe(message, callback.bind(context || this)); }, /** @@ -161,6 +191,11 @@ BrowserID.Modules.PageModule = (function() { }, lowLevelInfo), onerror); }; } + + // BEGIN TESTING API + , + onSubmit: onSubmit + // END TESTING API }); return Module; diff --git a/resources/static/shared/modules/xhr_disable_form.js b/resources/static/shared/modules/xhr_disable_form.js new file mode 100644 index 000000000..23ba3ebed --- /dev/null +++ b/resources/static/shared/modules/xhr_disable_form.js @@ -0,0 +1,30 @@ +/*globals BrowserID: true */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +BrowserID.Modules.XHRDisableForm = (function() { + "use strict"; + + var bid = BrowserID, + dom = bid.DOM, + sc; + + var Module = bid.Modules.PageModule.extend({ + start: function(options) { + var self=this; + + self.subscribe("xhr_start", + dom.addClass.curry("body", "submit_disabled")); + self.subscribe("xhr_complete", + dom.removeClass.curry("body", "submit_disabled")); + + sc.start.call(self, options); + } + }); + + sc = Module.sc; + + return Module; + +}()); + diff --git a/resources/static/shared/network.js b/resources/static/shared/network.js index f0e007d1b..47f1d9ad3 100644 --- a/resources/static/shared/network.js +++ b/resources/static/shared/network.js @@ -84,6 +84,7 @@ BrowserID.Network = (function() { delayTimeout = setTimeout(xhrDelay.curry(reqInfo), time_until_delay); }; + mediator.publish("xhr_start", reqInfo); xhr.ajax(req); } diff --git a/resources/static/test/index.html b/resources/static/test/index.html index ebea44ef7..08d998779 100644 --- a/resources/static/test/index.html +++ b/resources/static/test/index.html @@ -95,6 +95,7 @@ <script src="/shared/modules/page_module.js"></script> <script src="/shared/modules/xhr_delay.js"></script> + <script src="/shared/modules/xhr_disable_form.js"></script> <script src="/dialog/resources/internal_api.js"></script> <script src="/dialog/resources/helpers.js"></script> @@ -136,8 +137,10 @@ <script src="qunit/shared/storage_unit_test.js"></script> <script src="qunit/shared/network_unit_test.js"></script> <script src="qunit/shared/user_unit_test.js"></script> - <script src="qunit/shared/page_module_unit_test.js"></script> - <script src="qunit/shared/xhr_delay_unit_test.js"></script> + + <script src="qunit/shared/modules/page_module_unit_test.js"></script> + <script src="qunit/shared/modules/xhr_delay_unit_test.js"></script> + <script src="qunit/shared/modules/xhr_disable_form_unit_test.js"></script> <script src="qunit/pages/browserid_unit_test.js"></script> <script src="qunit/pages/page_helpers_unit_test.js"></script> diff --git a/resources/static/test/qunit/pages/forgot_unit_test.js b/resources/static/test/qunit/pages/forgot_unit_test.js index 621d7cecc..22491ef90 100644 --- a/resources/static/test/qunit/pages/forgot_unit_test.js +++ b/resources/static/test/qunit/pages/forgot_unit_test.js @@ -76,7 +76,7 @@ $("#email").val("testuser@testuser.com"); testEmailNotSent(function() { - equal($("#error").is(":visible"), true, "error is visible"); + testHelpers.testErrorVisible(); start(); }); }); diff --git a/resources/static/test/qunit/pages/signin_unit_test.js b/resources/static/test/qunit/pages/signin_unit_test.js index d03a08556..79baa83d2 100644 --- a/resources/static/test/qunit/pages/signin_unit_test.js +++ b/resources/static/test/qunit/pages/signin_unit_test.js @@ -155,9 +155,7 @@ $("#email").val("registered@testuser.com"); $("#password").val("password"); - testUserNotSignedIn(function() { - equal($("#error").is(":visible"), true, "error is visible"); - }); + testUserNotSignedIn(testHelpers.testErrorVisible); }); asyncTest("authWithPrimary opens winchan", function() { diff --git a/resources/static/test/qunit/pages/verify_email_address_test.js b/resources/static/test/qunit/pages/verify_email_address_test.js index f8af66a95..a4c3ef06d 100644 --- a/resources/static/test/qunit/pages/verify_email_address_test.js +++ b/resources/static/test/qunit/pages/verify_email_address_test.js @@ -58,7 +58,7 @@ asyncTest("verifyEmailAddress with emailForVerficationToken XHR failure", function() { xhr.useResult("ajaxError"); bid.verifyEmailAddress("token", function() { - ok($("#error").is(":visible"), "cannot communicate box is visible"); + testHelpers.testErrorVisible(); start(); }); }); diff --git a/resources/static/test/qunit/shared/page_module_unit_test.js b/resources/static/test/qunit/shared/modules/page_module_unit_test.js similarity index 92% rename from resources/static/test/qunit/shared/page_module_unit_test.js rename to resources/static/test/qunit/shared/modules/page_module_unit_test.js index 82b472d88..5b52f0346 100644 --- a/resources/static/test/qunit/shared/page_module_unit_test.js +++ b/resources/static/test/qunit/shared/modules/page_module_unit_test.js @@ -17,7 +17,7 @@ controller.start(); } - module("shared/page_controller", { + module("shared/page_module", { setup: function() { el = $("#controller_head"); bid.TestHelpers.setup(); @@ -51,11 +51,6 @@ var html = el.find("#formWrap .contents").html(); ok(html.length, "with template specified, form text is loaded"); -/* - - var input = el.find("input").eq(0); - ok(input.is(":focus"), "make sure the first input is focused"); -*/ html = el.find("#wait .contents").html(); equal(html, "", "with body template specified, wait text is not loaded"); }); @@ -233,5 +228,23 @@ equal(error, "missing config option: requiredField"); }); + test("form is not submitted when 'submit_disabled' class is added to body", function() { + createController(); + + var submitCalled = false; + controller.submit = function() { + submitCalled = true; + }; + + $("body").addClass("submit_disabled"); + controller.onSubmit(); + + equal(submitCalled, false, "submit was prevented from being called"); + + + $("body").removeClass("submit_disabled"); + controller.onSubmit(); + equal(submitCalled, true, "submit permitted to complete"); + }) }()); diff --git a/resources/static/test/qunit/shared/xhr_delay_unit_test.js b/resources/static/test/qunit/shared/modules/xhr_delay_unit_test.js similarity index 100% rename from resources/static/test/qunit/shared/xhr_delay_unit_test.js rename to resources/static/test/qunit/shared/modules/xhr_delay_unit_test.js diff --git a/resources/static/test/qunit/shared/modules/xhr_disable_form_unit_test.js b/resources/static/test/qunit/shared/modules/xhr_disable_form_unit_test.js new file mode 100644 index 000000000..61a1d9bb1 --- /dev/null +++ b/resources/static/test/qunit/shared/modules/xhr_disable_form_unit_test.js @@ -0,0 +1,41 @@ +/*jshint browsers:true, forin: true, laxbreak: true */ +/*global test: true, start: true, stop: true, module: true, ok: true, equal: true, BrowserID:true */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +(function() { + "use strict"; + + var bid = BrowserID, + Module = bid.Modules.XHRDisableForm, + testHelpers = bid.TestHelpers, + mediator = bid.Mediator, + mod; + + function createModule(options) { + mod = Module.create({}); + mod.start(options); + return mod; + } + + module("shared/xhr_disable_form", { + setup: function() { + testHelpers.setup(); + createModule(); + }, + + teardown: function() { + testHelpers.teardown(); + } + }); + + test("xhr_start adds 'submit_disabled' to class, xhr_complete removes it", function() { + var body = $("body"); + + mediator.publish("xhr_start"); + equal(body.hasClass("submit_disabled"), true, "xhr_start adds submit_disabled"); + + mediator.publish("xhr_complete"); + equal(body.hasClass("submit_disabled"), false, "xhr_complete removes submit_disabled"); + }); +}()); diff --git a/resources/static/test/qunit/testHelpers/helpers.js b/resources/static/test/qunit/testHelpers/helpers.js index 5bfd71489..f7703e275 100644 --- a/resources/static/test/qunit/testHelpers/helpers.js +++ b/resources/static/test/qunit/testHelpers/helpers.js @@ -103,12 +103,15 @@ BrowserID.TestHelpers = (function() { isTriggered: function(message) { return calls[message]; }, + errorVisible: function() { return screens.error.visible; }, + testErrorVisible: function() { - equal(this.errorVisible(), true, "error screen is visible"); + equal(TestHelpers.errorVisible(), true, "error screen is visible"); }, + checkNetworkError: checkNetworkError, unexpectedSuccess: function() { ok(false, "unexpected success"); diff --git a/resources/views/dialog_layout.ejs b/resources/views/dialog_layout.ejs index 18f29c49b..4d7403322 100644 --- a/resources/views/dialog_layout.ejs +++ b/resources/views/dialog_layout.ejs @@ -82,6 +82,7 @@ <script src="/shared/modules/page_module.js"></script> <script src="/shared/modules/xhr_delay.js"></script> + <script src="/shared/modules/xhr_disable_form.js"></script> <script src="/dialog/resources/internal_api.js"></script> <script src="/dialog/resources/helpers.js"></script> diff --git a/resources/views/layout.ejs b/resources/views/layout.ejs index 3e4fbcfb4..9b6475067 100644 --- a/resources/views/layout.ejs +++ b/resources/views/layout.ejs @@ -47,6 +47,7 @@ <script src="/shared/modules/page_module.js"></script> <script src="/shared/modules/xhr_delay.js"></script> + <script src="/shared/modules/xhr_disable_form.js"></script> <script src="/pages/page_helpers.js"></script> <script src="/pages/index.js"></script> diff --git a/scripts/compress.sh b/scripts/compress.sh index 8c63c4fcd..7a3557f0e 100755 --- a/scripts/compress.sh +++ b/scripts/compress.sh @@ -58,7 +58,7 @@ cp templates.js $BUILD_PATH/templates.js cd ../.. # produce the dialog js -cat lib/jquery-1.7.1.min.js lib/winchan.js lib/underscore-min.js lib/vepbundle.js lib/ejs.js shared/browserid.js lib/hub.js lib/dom-jquery.js lib/module.js lib/jschannel.js shared/javascript-extensions.js shared/mediator.js shared/class.js shared/storage.js $BUILD_PATH/templates.js shared/renderer.js shared/error-display.js shared/screens.js shared/tooltip.js shared/validation.js shared/provisioning.js shared/network.js shared/user.js shared/error-messages.js shared/browser-support.js shared/wait-messages.js shared/helpers.js shared/modules/page_module.js shared/modules/xhr_delay.js dialog/resources/internal_api.js dialog/resources/helpers.js dialog/resources/state_machine.js dialog/controllers/code_check.js dialog/controllers/actions.js dialog/controllers/dialog.js dialog/controllers/authenticate.js dialog/controllers/forgot_password.js dialog/controllers/check_registration.js dialog/controllers/pick_email.js dialog/controllers/add_email.js dialog/controllers/required_email.js dialog/controllers/verify_primary_user.js dialog/controllers/provision_primary_user.js dialog/controllers/primary_user_provisioned.js dialog/controllers/email_chosen.js dialog/start.js > $BUILD_PATH/dialog.uncompressed.js +cat lib/jquery-1.7.1.min.js lib/winchan.js lib/underscore-min.js lib/vepbundle.js lib/ejs.js shared/browserid.js lib/hub.js lib/dom-jquery.js lib/module.js lib/jschannel.js shared/javascript-extensions.js shared/mediator.js shared/class.js shared/storage.js $BUILD_PATH/templates.js shared/renderer.js shared/error-display.js shared/screens.js shared/tooltip.js shared/validation.js shared/provisioning.js shared/network.js shared/user.js shared/error-messages.js shared/browser-support.js shared/wait-messages.js shared/helpers.js shared/modules/page_module.js shared/modules/xhr_delay.js shared/modules/xhr_disable_form.js dialog/resources/internal_api.js dialog/resources/helpers.js dialog/resources/state_machine.js dialog/controllers/code_check.js dialog/controllers/actions.js dialog/controllers/dialog.js dialog/controllers/authenticate.js dialog/controllers/forgot_password.js dialog/controllers/check_registration.js dialog/controllers/pick_email.js dialog/controllers/add_email.js dialog/controllers/required_email.js dialog/controllers/verify_primary_user.js dialog/controllers/provision_primary_user.js dialog/controllers/primary_user_provisioned.js dialog/controllers/email_chosen.js dialog/start.js > $BUILD_PATH/dialog.uncompressed.js # produce the dialog css cat css/common.css dialog/css/popup.css dialog/css/m.css > $BUILD_PATH/dialog.uncompressed.css @@ -71,7 +71,7 @@ echo '****Building BrowserID.org HTML, CSS, and JS****' echo '' #produce the main site js -cat lib/vepbundle.js lib/jquery-1.7.1.min.js lib/underscore-min.js lib/ejs.js shared/javascript-extensions.js shared/browserid.js lib/dom-jquery.js lib/jschannel.js lib/winchan.js lib/hub.js $BUILD_PATH/templates.js shared/renderer.js shared/error-display.js shared/screens.js shared/error-messages.js shared/wait-messages.js shared/mediator.js shared/storage.js shared/network.js shared/provisioning.js shared/user.js shared/tooltip.js shared/validation.js shared/helpers.js shared/class.js shared/modules/page_module.js shared/modules/xhr_delay.js pages/page_helpers.js pages/start.js pages/index.js pages/add_email_address.js pages/verify_email_address.js pages/forgot.js pages/manage_account.js pages/signin.js pages/signup.js > $BUILD_PATH/browserid.uncompressed.js +cat lib/vepbundle.js lib/jquery-1.7.1.min.js lib/underscore-min.js lib/ejs.js shared/javascript-extensions.js shared/browserid.js lib/dom-jquery.js lib/jschannel.js lib/winchan.js lib/hub.js $BUILD_PATH/templates.js shared/renderer.js shared/error-display.js shared/screens.js shared/error-messages.js shared/wait-messages.js shared/mediator.js shared/storage.js shared/network.js shared/provisioning.js shared/user.js shared/tooltip.js shared/validation.js shared/helpers.js shared/class.js shared/modules/page_module.js shared/modules/xhr_delay.js shared/modules/xhr_disable_form.js pages/page_helpers.js pages/start.js pages/index.js pages/add_email_address.js pages/verify_email_address.js pages/forgot.js pages/manage_account.js pages/signin.js pages/signup.js > $BUILD_PATH/browserid.uncompressed.js # produce the main site css cat css/common.css css/style.css css/m.css > $BUILD_PATH/browserid.uncompressed.css -- GitLab