diff --git a/app/assets/javascripts/behaviors/bind_in_out.js.es6 b/app/assets/javascripts/behaviors/bind_in_out.js.es6 new file mode 100644 index 0000000000000000000000000000000000000000..3cb892451e37d2ae954fbfb2b94f8eb5945b2498 --- /dev/null +++ b/app/assets/javascripts/behaviors/bind_in_out.js.es6 @@ -0,0 +1,48 @@ +class BindInOut { + constructor(bindIn, bindOut) { + this.in = bindIn; + this.out = bindOut; + + this.eventWrapper = {}; + this.eventType = /(INPUT|TEXTAREA)/.test(bindIn.tagName) ? 'keyup' : 'change'; + } + + addEvents() { + this.eventWrapper.updateOut = this.updateOut.bind(this); + + this.in.addEventListener(this.eventType, this.eventWrapper.updateOut); + + return this; + } + + updateOut() { + this.out.textContent = this.in.value; + + return this; + } + + removeEvents() { + this.in.removeEventListener(this.eventType, this.eventWrapper.updateOut); + + return this; + } + + static initAll() { + const ins = document.querySelectorAll('*[data-bind-in]'); + + return [].map.call(ins, anIn => BindInOut.init(anIn)); + } + + static init(anIn, anOut) { + const out = anOut || document.querySelector(`*[data-bind-out="${anIn.dataset.bindIn}"]`); + const bindInOut = new BindInOut(anIn, out); + + return bindInOut.addEvents().updateOut(); + } +} + +const global = window.gl || (window.gl = {}); + +global.BindInOut = BindInOut; + +module.exports = BindInOut; diff --git a/app/assets/javascripts/behaviors/toggler_behavior.js b/app/assets/javascripts/behaviors/toggler_behavior.js index a7181904ac9bbfeb257425eff92edcc50f8ce5ea..2d98fc5a545a94a615fdc1e73d95c0a1903debb4 100644 --- a/app/assets/javascripts/behaviors/toggler_behavior.js +++ b/app/assets/javascripts/behaviors/toggler_behavior.js @@ -22,7 +22,6 @@ // %div.js-toggle-content // $('body').on('click', '.js-toggle-button', function(e) { - e.preventDefault(); toggleContainer($(this).closest('.js-toggle-container')); }); diff --git a/app/assets/javascripts/dispatcher.js.es6 b/app/assets/javascripts/dispatcher.js.es6 index 45aa6050aed3453d5a2c7fd1fd8b558e823cd5e8..f7ab9eefab16a6be15b634e34ff12f70702e970d 100644 --- a/app/assets/javascripts/dispatcher.js.es6 +++ b/app/assets/javascripts/dispatcher.js.es6 @@ -216,6 +216,7 @@ const ShortcutsBlob = require('./shortcuts_blob'); case 'admin:groups:edit': case 'admin:groups:new': new GroupAvatar(); + gl.BindInOut.initAll(); break; case 'projects:tree:show': shortcut_handler = new ShortcutsNavigation(); diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index 0e421b915cfdce6d2d98f323c94dff117666a55c..5b1898b0ee17f9331884400aa12d60f26ef340ba 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -81,7 +81,6 @@ class GroupsController < Groups::ApplicationController end def update - byebug if Groups::UpdateService.new(@group, current_user, group_params).execute redirect_to edit_group_path(@group), notice: "Group '#{@group.name}' was successfully updated." else @@ -143,7 +142,7 @@ class GroupsController < Groups::ApplicationController :request_access_enabled, :share_with_group_lock, :visibility_level, - :parent_id + :parent_id, :create_chat_team, :chat_team_name ] diff --git a/app/views/groups/_create_chat_team.html.haml b/app/views/groups/_create_chat_team.html.haml index 1e702c4f20fa78fa9f581a49241b637c0795774d..8b908e233dcbab89825db2fb82f798185ff6ebcc 100644 --- a/app/views/groups/_create_chat_team.html.haml +++ b/app/views/groups/_create_chat_team.html.haml @@ -4,15 +4,13 @@ = custom_icon('icon_mattermost') Mattermost .col-sm-10 - .checkbox + .checkbox.js-toggle-container = f.label :create_chat_team do - = f.check_box(:create_chat_team, { checked: @group.chat_team }, 'true', 'false') - Link the group to a new Mattermost team - -.form-group - = f.label :chat_team, class: 'control-label' do - Chat Team name - .col-sm-10 - = f.text_field :chat_team, placeholder: @group.chat_team, class: 'form-control mattermost-team-name' - %small - Leave blank to match your group name + .js-toggle-button= f.check_box(:create_chat_team, { checked: true }, 'true', 'false') + Create a Mattermost team for this group + %br + %small.light.js-toggle-content + Team URL: + = Settings.mattermost.host + %span> / + %span{ "data-bind-out" => "create_chat_team"} diff --git a/app/views/shared/_group_form.html.haml b/app/views/shared/_group_form.html.haml index efb207b9916c2331b12abd3b78aa75d8c69a19f9..5b8c9a662052675e144190b81d0f6437175f9dd3 100644 --- a/app/views/shared/_group_form.html.haml +++ b/app/views/shared/_group_form.html.haml @@ -18,7 +18,8 @@ = f.text_field :path, placeholder: 'open-source', class: 'form-control', autofocus: local_assigns[:autofocus] || false, required: true, pattern: Gitlab::Regex::NAMESPACE_REGEX_STR_SIMPLE, - title: 'Please choose a group name with no special characters.' + title: 'Please choose a group name with no special characters.', + "data-bind-in" => "#{'create_chat_team' if Gitlab.config.mattermost.enabled}" - if parent = f.hidden_field :parent_id, value: parent.id diff --git a/spec/features/groups_spec.rb b/spec/features/groups_spec.rb index 37b7c20239fcf2a3b3c8f38e7cb56d93534dc564..b4bd192582258febe1daba30dcc8ea8bc073a996 100644 --- a/spec/features/groups_spec.rb +++ b/spec/features/groups_spec.rb @@ -43,6 +43,43 @@ feature 'Group', feature: true do expect(page).to have_namespace_error_message end end + + describe 'Mattermost team creation' do + before do + Settings.mattermost['enabled'] = mattermost_enabled + visit new_group_path + end + + context 'Mattermost enabled' do + let(:mattermost_enabled) { true } + + it 'displays a team creation checkbox' do + expect(page).to have_selector('#group_create_chat_team') + end + + it 'checks the checkbox by default' do + expect(find('#group_create_chat_team')['checked']).to eq(true) + end + + it 'updates the team URL on graph path update', :js do + out_span = find('span[data-bind-out="create_chat_team"]') + + expect(out_span.text).to be_empty + + fill_in('group_path', with: 'test-group') + + expect(out_span.text).to eq('test-group') + end + end + + context 'Mattermost disabled' do + let(:mattermost_enabled) { false } + + it 'doesnt show a team creation checkbox if Mattermost not enabled' do + expect(page).not_to have_selector('#group_create_chat_team') + end + end + end end describe 'create a nested group' do diff --git a/spec/javascripts/behaviors/bind_in_out_spec.js.es6 b/spec/javascripts/behaviors/bind_in_out_spec.js.es6 new file mode 100644 index 0000000000000000000000000000000000000000..69d2460ab1c252bc4e81f25d0df6e33e69c2d0ac --- /dev/null +++ b/spec/javascripts/behaviors/bind_in_out_spec.js.es6 @@ -0,0 +1,189 @@ +const BindInOut = require('~/behaviors/bind_in_out'); +const ClassSpecHelper = require('../helpers/class_spec_helper'); + +describe('BindInOut', function () { + describe('.constructor', function () { + beforeEach(function () { + this.in = {}; + this.out = {}; + + this.bindInOut = new BindInOut(this.in, this.out); + }); + + it('should set .in', function () { + expect(this.bindInOut.in).toBe(this.in); + }); + + it('should set .out', function () { + expect(this.bindInOut.out).toBe(this.out); + }); + + it('should set .eventWrapper', function () { + expect(this.bindInOut.eventWrapper).toEqual({}); + }); + + describe('if .in is an input', function () { + beforeEach(function () { + this.bindInOut = new BindInOut({ tagName: 'INPUT' }); + }); + + it('should set .eventType to keyup ', function () { + expect(this.bindInOut.eventType).toEqual('keyup'); + }); + }); + + describe('if .in is a textarea', function () { + beforeEach(function () { + this.bindInOut = new BindInOut({ tagName: 'TEXTAREA' }); + }); + + it('should set .eventType to keyup ', function () { + expect(this.bindInOut.eventType).toEqual('keyup'); + }); + }); + + describe('if .in is not an input or textarea', function () { + beforeEach(function () { + this.bindInOut = new BindInOut({ tagName: 'SELECT' }); + }); + + it('should set .eventType to change ', function () { + expect(this.bindInOut.eventType).toEqual('change'); + }); + }); + }); + + describe('.addEvents', function () { + beforeEach(function () { + this.in = jasmine.createSpyObj('in', ['addEventListener']); + + this.bindInOut = new BindInOut(this.in); + + this.addEvents = this.bindInOut.addEvents(); + }); + + it('should set .eventWrapper.updateOut', function () { + expect(this.bindInOut.eventWrapper.updateOut).toEqual(jasmine.any(Function)); + }); + + it('should call .addEventListener', function () { + expect(this.in.addEventListener) + .toHaveBeenCalledWith( + this.bindInOut.eventType, + this.bindInOut.eventWrapper.updateOut, + ); + }); + + it('should return the instance', function () { + expect(this.addEvents).toBe(this.bindInOut); + }); + }); + + describe('.updateOut', function () { + beforeEach(function () { + this.in = { value: 'the-value' }; + this.out = { textContent: 'not-the-value' }; + + this.bindInOut = new BindInOut(this.in, this.out); + + this.updateOut = this.bindInOut.updateOut(); + }); + + it('should set .out.textContent to .in.value', function () { + expect(this.out.textContent).toBe(this.in.value); + }); + + it('should return the instance', function () { + expect(this.updateOut).toBe(this.bindInOut); + }); + }); + + describe('.removeEvents', function () { + beforeEach(function () { + this.in = jasmine.createSpyObj('in', ['removeEventListener']); + this.updateOut = () => {}; + + this.bindInOut = new BindInOut(this.in); + this.bindInOut.eventWrapper.updateOut = this.updateOut; + + this.removeEvents = this.bindInOut.removeEvents(); + }); + + it('should call .removeEventListener', function () { + expect(this.in.removeEventListener) + .toHaveBeenCalledWith( + this.bindInOut.eventType, + this.updateOut, + ); + }); + + it('should return the instance', function () { + expect(this.removeEvents).toBe(this.bindInOut); + }); + }); + + describe('.initAll', function () { + beforeEach(function () { + this.ins = [0, 1, 2]; + this.instances = []; + + spyOn(document, 'querySelectorAll').and.returnValue(this.ins); + spyOn(Array.prototype, 'map').and.callThrough(); + spyOn(BindInOut, 'init'); + + this.initAll = BindInOut.initAll(); + }); + + ClassSpecHelper.itShouldBeAStaticMethod(BindInOut, 'initAll'); + + it('should call .querySelectorAll', function () { + expect(document.querySelectorAll).toHaveBeenCalledWith('*[data-bind-in]'); + }); + + it('should call .map', function () { + expect(Array.prototype.map).toHaveBeenCalledWith(jasmine.any(Function)); + }); + + it('should call .init for each element', function () { + expect(BindInOut.init.calls.count()).toEqual(3); + }); + + it('should return an array of instances', function () { + expect(this.initAll).toEqual(jasmine.any(Array)); + }); + }); + + describe('.init', function () { + beforeEach(function () { + spyOn(BindInOut.prototype, 'addEvents').and.callFake(function () { return this; }); + spyOn(BindInOut.prototype, 'updateOut').and.callFake(function () { return this; }); + + this.init = BindInOut.init({}, {}); + }); + + ClassSpecHelper.itShouldBeAStaticMethod(BindInOut, 'init'); + + it('should call .addEvents', function () { + expect(BindInOut.prototype.addEvents).toHaveBeenCalled(); + }); + + it('should call .updateOut', function () { + expect(BindInOut.prototype.updateOut).toHaveBeenCalled(); + }); + + describe('if no anOut is provided', function () { + beforeEach(function () { + this.anIn = { dataset: { bindIn: 'the-data-bind-in' } }; + + spyOn(document, 'querySelector'); + + BindInOut.init(this.anIn); + }); + + it('should call .querySelector', function () { + expect(document.querySelector) + .toHaveBeenCalledWith(`*[data-bind-out="${this.anIn.dataset.bindIn}"]`); + }); + }); + }); +}); diff --git a/spec/javascripts/helpers/class_spec_helper.js.es6 b/spec/javascripts/helpers/class_spec_helper.js.es6 index d3c37d39431950cfd4d8531ba791a5afadaba9eb..61db27a8fccb0d90a14898d91c4ad1af8e22ab96 100644 --- a/spec/javascripts/helpers/class_spec_helper.js.es6 +++ b/spec/javascripts/helpers/class_spec_helper.js.es6 @@ -7,3 +7,5 @@ class ClassSpecHelper { } window.ClassSpecHelper = ClassSpecHelper; + +module.exports = ClassSpecHelper;