From 5412020762b676fdfba5991b59ebec7adc59aeca Mon Sep 17 00:00:00 2001 From: "Luke \"Jared\" Bennett" Date: Tue, 2 May 2017 08:05:43 +0000 Subject: Better Explore Groups view --- app/assets/javascripts/dispatcher.js | 12 ++ app/assets/javascripts/landing.js | 37 +++++ app/assets/stylesheets/framework/blocks.scss | 57 ++++++++ app/assets/stylesheets/framework/common.scss | 5 + app/assets/stylesheets/pages/cycle_analytics.scss | 44 +----- app/assets/stylesheets/pages/groups.scss | 23 +++ app/views/dashboard/_groups_head.html.haml | 6 +- app/views/explore/groups/index.html.haml | 9 ++ .../shared/icons/_icon_explore_groups_splash.svg | 1 + spec/features/explore/groups_list_spec.rb | 28 +++- spec/features/merge_requests/create_new_mr_spec.rb | 1 + spec/javascripts/landing_spec.js | 160 +++++++++++++++++++++ 12 files changed, 335 insertions(+), 48 deletions(-) create mode 100644 app/assets/javascripts/landing.js create mode 100644 app/views/shared/icons/_icon_explore_groups_splash.svg create mode 100644 spec/javascripts/landing_spec.js diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index 15fe87f21ea..0bdce52cc89 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -44,6 +44,7 @@ import GroupsList from './groups_list'; import ProjectsList from './projects_list'; import MiniPipelineGraph from './mini_pipeline_graph_dropdown'; import BlobLinePermalinkUpdater from './blob/blob_line_permalink_updater'; +import Landing from './landing'; import BlobForkSuggestion from './blob/blob_fork_suggestion'; import UserCallout from './user_callout'; import { ProtectedTagCreate, ProtectedTagEditList } from './protected_tags'; @@ -148,8 +149,19 @@ const ShortcutsBlob = require('./shortcuts_blob'); new ProjectsList(); break; case 'dashboard:groups:index': + new GroupsList(); + break; case 'explore:groups:index': new GroupsList(); + + const landingElement = document.querySelector('.js-explore-groups-landing'); + if (!landingElement) break; + const exploreGroupsLanding = new Landing( + landingElement, + landingElement.querySelector('.dismiss-button'), + 'explore_groups_landing_dismissed', + ); + exploreGroupsLanding.toggle(); break; case 'projects:milestones:new': case 'projects:milestones:edit': diff --git a/app/assets/javascripts/landing.js b/app/assets/javascripts/landing.js new file mode 100644 index 00000000000..8c0950ad5d5 --- /dev/null +++ b/app/assets/javascripts/landing.js @@ -0,0 +1,37 @@ +import Cookies from 'js-cookie'; + +class Landing { + constructor(landingElement, dismissButton, cookieName) { + this.landingElement = landingElement; + this.cookieName = cookieName; + this.dismissButton = dismissButton; + this.eventWrapper = {}; + } + + toggle() { + const isDismissed = this.isDismissed(); + + this.landingElement.classList.toggle('hidden', isDismissed); + if (!isDismissed) this.addEvents(); + } + + addEvents() { + this.eventWrapper.dismissLanding = this.dismissLanding.bind(this); + this.dismissButton.addEventListener('click', this.eventWrapper.dismissLanding); + } + + removeEvents() { + this.dismissButton.removeEventListener('click', this.eventWrapper.dismissLanding); + } + + dismissLanding() { + this.landingElement.classList.add('hidden'); + Cookies.set(this.cookieName, 'true', { expires: 365 }); + } + + isDismissed() { + return Cookies.get(this.cookieName) === 'true'; + } +} + +export default Landing; diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss index f3e2a5db0a6..ac1fc0eb8ae 100644 --- a/app/assets/stylesheets/framework/blocks.scss +++ b/app/assets/stylesheets/framework/blocks.scss @@ -254,6 +254,63 @@ padding: 10px 0; } +.landing { + margin-bottom: $gl-padding; + overflow: hidden; + display: flex; + position: relative; + border: 1px solid $blue-300; + border-radius: $border-radius-default; + background-color: $blue-25; + justify-content: center; + + .dismiss-button { + position: absolute; + right: 6px; + top: 6px; + cursor: pointer; + color: $blue-300; + z-index: 1; + border: none; + background-color: transparent; + + &:hover, + &:focus { + border: none; + color: $blue-400; + } + } + + .svg-container { + align-self: center; + } + + .inner-content { + text-align: left; + white-space: nowrap; + + h4 { + color: $gl-text-color; + font-size: 17px; + } + + p { + color: $gl-text-color; + margin-bottom: $gl-padding; + } + } + + @media (max-width: $screen-sm-min) { + flex-direction: column; + + .inner-content { + white-space: normal; + padding: 0 28px; + text-align: center; + } + } +} + .empty-state { margin: 100px 0 0; diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index 638c1403b25..1a6f36d032d 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -424,6 +424,11 @@ table { } } +.bordered-box { + border: 1px solid $border-color; + border-radius: $border-radius-default; +} + .str-truncated { &-60 { @include str-truncated(60%); diff --git a/app/assets/stylesheets/pages/cycle_analytics.scss b/app/assets/stylesheets/pages/cycle_analytics.scss index ad3dbc7ac48..403724cd68a 100644 --- a/app/assets/stylesheets/pages/cycle_analytics.scss +++ b/app/assets/stylesheets/pages/cycle_analytics.scss @@ -93,11 +93,6 @@ top: $gl-padding-top; } - .bordered-box { - border: 1px solid $border-color; - border-radius: $border-radius-default; - } - .content-list { li { padding: 18px $gl-padding $gl-padding; @@ -139,42 +134,9 @@ } } - .landing { - margin-bottom: $gl-padding; - overflow: hidden; - - .dismiss-icon { - position: absolute; - right: $cycle-analytics-box-padding; - cursor: pointer; - color: $cycle-analytics-dismiss-icon-color; - } - - .svg-container { - text-align: center; - - svg { - width: 136px; - height: 136px; - } - } - - .inner-content { - @media (max-width: $screen-xs-max) { - padding: 0 28px; - text-align: center; - } - - h4 { - color: $gl-text-color; - font-size: 17px; - } - - p { - color: $cycle-analytics-box-text-color; - margin-bottom: $gl-padding; - } - } + .landing svg { + width: 136px; + height: 136px; } .fa-spinner { diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss index 73a5889867a..72d73b89a2a 100644 --- a/app/assets/stylesheets/pages/groups.scss +++ b/app/assets/stylesheets/pages/groups.scss @@ -88,3 +88,26 @@ color: $gl-text-color-secondary; margin-top: 10px; } + +.explore-groups.landing { + margin-top: 10px; + + .inner-content { + padding: 0; + + p { + margin: 7px 0 0; + max-width: 480px; + padding: 0 $gl-padding; + + @media (max-width: $screen-sm-min) { + margin: 0 auto; + } + } + } + + svg { + width: 62px; + height: 50px; + } +} diff --git a/app/views/dashboard/_groups_head.html.haml b/app/views/dashboard/_groups_head.html.haml index 0e848386ebb..4594c52b34b 100644 --- a/app/views/dashboard/_groups_head.html.haml +++ b/app/views/dashboard/_groups_head.html.haml @@ -2,10 +2,10 @@ %ul.nav-links = nav_link(page: dashboard_groups_path) do = link_to dashboard_groups_path, title: 'Your groups' do - Your Groups + Your groups = nav_link(page: explore_groups_path) do - = link_to explore_groups_path, title: 'Explore groups' do - Explore Groups + = link_to explore_groups_path, title: 'Explore public groups' do + Explore public groups .nav-controls = render 'shared/groups/search_form' = render 'shared/groups/dropdown' diff --git a/app/views/explore/groups/index.html.haml b/app/views/explore/groups/index.html.haml index bb2cd0d44c8..ffe07b217a7 100644 --- a/app/views/explore/groups/index.html.haml +++ b/app/views/explore/groups/index.html.haml @@ -7,6 +7,15 @@ = render 'explore/head' = render 'nav' +- if cookies[:explore_groups_landing_dismissed] != 'true' + .explore-groups.landing.content-block.js-explore-groups-landing.hidden + %button.dismiss-button{ type: 'button', 'aria-label' => 'Dismiss' }= icon('times') + .svg-container + = custom_icon('icon_explore_groups_splash') + .inner-content + %p Below you will find all the groups that are public. + %p You can easily contribute to them by requesting to join these groups. + - if @groups.present? = render 'groups' - else diff --git a/app/views/shared/icons/_icon_explore_groups_splash.svg b/app/views/shared/icons/_icon_explore_groups_splash.svg new file mode 100644 index 00000000000..79f17872739 --- /dev/null +++ b/app/views/shared/icons/_icon_explore_groups_splash.svg @@ -0,0 +1 @@ + diff --git a/spec/features/explore/groups_list_spec.rb b/spec/features/explore/groups_list_spec.rb index 8e5421a984b..9828cb179a7 100644 --- a/spec/features/explore/groups_list_spec.rb +++ b/spec/features/explore/groups_list_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe 'Explore Groups page', js: true, feature: true do +describe 'Explore Groups page', :js, :feature do let!(:user) { create :user } let!(:group) { create(:group) } let!(:public_group) { create(:group, :public) } @@ -46,19 +46,39 @@ describe 'Explore Groups page', js: true, feature: true do it 'shows non-archived projects count' do # Initially project is not archived expect(find('.js-groups-list-holder .content-list li:first-child .stats span:first-child')).to have_text("1") - + # Archive project empty_project.archive! visit explore_groups_path # Check project count expect(find('.js-groups-list-holder .content-list li:first-child .stats span:first-child')).to have_text("0") - + # Unarchive project empty_project.unarchive! visit explore_groups_path # Check project count - expect(find('.js-groups-list-holder .content-list li:first-child .stats span:first-child')).to have_text("1") + expect(find('.js-groups-list-holder .content-list li:first-child .stats span:first-child')).to have_text("1") + end + + describe 'landing component' do + it 'should show a landing component' do + expect(page).to have_content('Below you will find all the groups that are public.') + end + + it 'should be dismissable' do + find('.dismiss-button').click + + expect(page).not_to have_content('Below you will find all the groups that are public.') + end + + it 'should persistently not show once dismissed' do + find('.dismiss-button').click + + visit explore_groups_path + + expect(page).not_to have_content('Below you will find all the groups that are public.') + end end end diff --git a/spec/features/merge_requests/create_new_mr_spec.rb b/spec/features/merge_requests/create_new_mr_spec.rb index e1353ec71ed..f0fec625108 100644 --- a/spec/features/merge_requests/create_new_mr_spec.rb +++ b/spec/features/merge_requests/create_new_mr_spec.rb @@ -35,6 +35,7 @@ feature 'Create New Merge Request', feature: true, js: true do expect(page).to have_content('Target branch') first('.js-target-branch').click + first('.dropdown-target-branch .dropdown-content') first('.dropdown-target-branch .dropdown-content a', text: 'v1.1.0').click expect(page).to have_content "b83d6e3" diff --git a/spec/javascripts/landing_spec.js b/spec/javascripts/landing_spec.js new file mode 100644 index 00000000000..7916073190a --- /dev/null +++ b/spec/javascripts/landing_spec.js @@ -0,0 +1,160 @@ +import Landing from '~/landing'; +import Cookies from 'js-cookie'; + +describe('Landing', function () { + describe('class constructor', function () { + beforeEach(function () { + this.landingElement = {}; + this.dismissButton = {}; + this.cookieName = 'cookie_name'; + + this.landing = new Landing(this.landingElement, this.dismissButton, this.cookieName); + }); + + it('should set .landing', function () { + expect(this.landing.landingElement).toBe(this.landingElement); + }); + + it('should set .cookieName', function () { + expect(this.landing.cookieName).toBe(this.cookieName); + }); + + it('should set .dismissButton', function () { + expect(this.landing.dismissButton).toBe(this.dismissButton); + }); + + it('should set .eventWrapper', function () { + expect(this.landing.eventWrapper).toEqual({}); + }); + }); + + describe('toggle', function () { + beforeEach(function () { + this.isDismissed = false; + this.landingElement = { classList: jasmine.createSpyObj('classList', ['toggle']) }; + this.landing = { + isDismissed: () => {}, + addEvents: () => {}, + landingElement: this.landingElement, + }; + + spyOn(this.landing, 'isDismissed').and.returnValue(this.isDismissed); + spyOn(this.landing, 'addEvents'); + + Landing.prototype.toggle.call(this.landing); + }); + + it('should call .isDismissed', function () { + expect(this.landing.isDismissed).toHaveBeenCalled(); + }); + + it('should call .classList.toggle', function () { + expect(this.landingElement.classList.toggle).toHaveBeenCalledWith('hidden', this.isDismissed); + }); + + it('should call .addEvents', function () { + expect(this.landing.addEvents).toHaveBeenCalled(); + }); + + describe('if isDismissed is true', function () { + beforeEach(function () { + this.isDismissed = true; + this.landingElement = { classList: jasmine.createSpyObj('classList', ['toggle']) }; + this.landing = { + isDismissed: () => {}, + addEvents: () => {}, + landingElement: this.landingElement, + }; + + spyOn(this.landing, 'isDismissed').and.returnValue(this.isDismissed); + spyOn(this.landing, 'addEvents'); + + this.landing.isDismissed.calls.reset(); + + Landing.prototype.toggle.call(this.landing); + }); + + it('should not call .addEvents', function () { + expect(this.landing.addEvents).not.toHaveBeenCalled(); + }); + }); + }); + + describe('addEvents', function () { + beforeEach(function () { + this.dismissButton = jasmine.createSpyObj('dismissButton', ['addEventListener']); + this.eventWrapper = {}; + this.landing = { + eventWrapper: this.eventWrapper, + dismissButton: this.dismissButton, + dismissLanding: () => {}, + }; + + Landing.prototype.addEvents.call(this.landing); + }); + + it('should set .eventWrapper.dismissLanding', function () { + expect(this.eventWrapper.dismissLanding).toEqual(jasmine.any(Function)); + }); + + it('should call .addEventListener', function () { + expect(this.dismissButton.addEventListener).toHaveBeenCalledWith('click', this.eventWrapper.dismissLanding); + }); + }); + + describe('removeEvents', function () { + beforeEach(function () { + this.dismissButton = jasmine.createSpyObj('dismissButton', ['removeEventListener']); + this.eventWrapper = { dismissLanding: () => {} }; + this.landing = { + eventWrapper: this.eventWrapper, + dismissButton: this.dismissButton, + }; + + Landing.prototype.removeEvents.call(this.landing); + }); + + it('should call .removeEventListener', function () { + expect(this.dismissButton.removeEventListener).toHaveBeenCalledWith('click', this.eventWrapper.dismissLanding); + }); + }); + + describe('dismissLanding', function () { + beforeEach(function () { + this.landingElement = { classList: jasmine.createSpyObj('classList', ['add']) }; + this.cookieName = 'cookie_name'; + this.landing = { landingElement: this.landingElement, cookieName: this.cookieName }; + + spyOn(Cookies, 'set'); + + Landing.prototype.dismissLanding.call(this.landing); + }); + + it('should call .classList.add', function () { + expect(this.landingElement.classList.add).toHaveBeenCalledWith('hidden'); + }); + + it('should call Cookies.set', function () { + expect(Cookies.set).toHaveBeenCalledWith(this.cookieName, 'true', { expires: 365 }); + }); + }); + + describe('isDismissed', function () { + beforeEach(function () { + this.cookieName = 'cookie_name'; + this.landing = { cookieName: this.cookieName }; + + spyOn(Cookies, 'get').and.returnValue('true'); + + this.isDismissed = Landing.prototype.isDismissed.call(this.landing); + }); + + it('should call Cookies.get', function () { + expect(Cookies.get).toHaveBeenCalledWith(this.cookieName); + }); + + it('should return a boolean', function () { + expect(typeof this.isDismissed).toEqual('boolean'); + }); + }); +}); -- cgit v1.2.1