From 452ec54fc224da754947be2a6032d71dea4e5ad2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADtor=20Avelino?= Date: Tue, 26 Sep 2017 16:33:16 -0300 Subject: [PATCH] teams: migrated index page to vue components --- .../modules/teams/components/new-form.js | 87 +++++++++++++++++++ .../modules/teams/components/table-row.js | 22 +++++ .../modules/teams/components/table.js | 54 ++++++++++++ app/assets/javascripts/modules/teams/index.js | 15 +--- .../edit-team-form.js | 0 .../new-team-form.js | 0 .../new-team-user-form.js | 0 .../team-details.js | 0 .../team-users-panel.js | 0 .../team-users-table.js | 0 .../teams-panel.js | 0 .../javascripts/modules/teams/pages/index.js | 57 +++++++++--- .../javascripts/modules/teams/pages/show.js | 4 +- .../javascripts/modules/teams/service.js | 52 +++++++++++ app/assets/javascripts/modules/teams/store.js | 10 +++ app/controllers/teams_controller.rb | 31 ++----- app/services/teams/create_service.rb | 20 +++++ app/views/teams/components/_form.html.slim | 18 ++++ app/views/teams/components/_table.html.slim | 35 ++++++++ .../teams/components/_table_row.html.slim | 12 +++ app/views/teams/create.js.erb | 10 --- app/views/teams/index.html.slim | 60 ++++--------- app/views/teams/partials/_panel.html.slim | 13 +++ config/routes.rb | 2 +- lib/api/entities.rb | 18 ++++ lib/api/v1/teams.rb | 27 ++++++ spec/api/grape_api/v1/teams_spec.rb | 73 ++++++++++++++++ spec/controllers/teams_controller_spec.rb | 66 -------------- spec/features/teams_spec.rb | 66 +++++++------- spec/services/teams/create_service_spec.rb | 31 +++++++ 30 files changed, 572 insertions(+), 211 deletions(-) create mode 100644 app/assets/javascripts/modules/teams/components/new-form.js create mode 100644 app/assets/javascripts/modules/teams/components/table-row.js create mode 100644 app/assets/javascripts/modules/teams/components/table.js rename app/assets/javascripts/modules/teams/{components => legacy-components}/edit-team-form.js (100%) rename app/assets/javascripts/modules/teams/{components => legacy-components}/new-team-form.js (100%) rename app/assets/javascripts/modules/teams/{components => legacy-components}/new-team-user-form.js (100%) rename app/assets/javascripts/modules/teams/{components => legacy-components}/team-details.js (100%) rename app/assets/javascripts/modules/teams/{components => legacy-components}/team-users-panel.js (100%) rename app/assets/javascripts/modules/teams/{components => legacy-components}/team-users-table.js (100%) rename app/assets/javascripts/modules/teams/{components => legacy-components}/teams-panel.js (100%) create mode 100644 app/assets/javascripts/modules/teams/service.js create mode 100644 app/assets/javascripts/modules/teams/store.js create mode 100644 app/services/teams/create_service.rb create mode 100644 app/views/teams/components/_form.html.slim create mode 100644 app/views/teams/components/_table.html.slim create mode 100644 app/views/teams/components/_table_row.html.slim delete mode 100644 app/views/teams/create.js.erb create mode 100644 app/views/teams/partials/_panel.html.slim create mode 100644 spec/services/teams/create_service_spec.rb diff --git a/app/assets/javascripts/modules/teams/components/new-form.js b/app/assets/javascripts/modules/teams/components/new-form.js new file mode 100644 index 000000000..21eddf21a --- /dev/null +++ b/app/assets/javascripts/modules/teams/components/new-form.js @@ -0,0 +1,87 @@ +import Vue from 'vue'; + +import { required } from 'vuelidate/lib/validators'; + +import EventBus from '~/utils/eventbus'; +import Alert from '~/shared/components/alert'; +import FormMixin from '~/shared/mixins/form'; + +import TeamsService from '../service'; + +const { set } = Vue; + +export default { + template: '#js-new-team-form-tmpl', + + mixins: [FormMixin], + + data() { + return { + team: { + name: '', + }, + timeout: { + name: null, + }, + }; + }, + + methods: { + onSubmit() { + TeamsService.save(this.team).then((response) => { + const team = response.data; + + this.toggleForm(); + this.$v.$reset(); + set(this, 'team', { + name: '', + }); + + Alert.show(`Team '${team.name}' was created successfully`); + EventBus.$emit('teamCreated', team); + }).catch((response) => { + let errors = response.data; + + if (Array.isArray(errors)) { + errors = errors.join('
'); + } + + Alert.show(errors); + }); + }, + }, + + validations: { + team: { + name: { + required, + available(value) { + clearTimeout(this.timeout.name); + + // required already taking care of this + if (value === '') { + return true; + } + + return new Promise((resolve) => { + const searchTeam = () => { + const promise = TeamsService.exists(value); + + promise.then((exists) => { + // leave it for the back-end + if (exists === null) { + resolve(true); + } + + // if it doesn't exist, valid + resolve(!exists); + }); + }; + + this.timeout.name = setTimeout(searchTeam, 1000); + }); + }, + }, + }, + }, +}; diff --git a/app/assets/javascripts/modules/teams/components/table-row.js b/app/assets/javascripts/modules/teams/components/table-row.js new file mode 100644 index 000000000..0a9d143fa --- /dev/null +++ b/app/assets/javascripts/modules/teams/components/table-row.js @@ -0,0 +1,22 @@ +export default { + template: '#js-team-table-row-tmpl', + + props: ['team', 'teamsPath'], + + computed: { + scopeClass() { + return `team_${this.team.id}`; + }, + + teamIcon() { + if (this.team.users_count > 1) { + return 'fa-users'; + } + return 'fa-user'; + }, + + teamPath() { + return `${this.teamsPath}/${this.team.id}`; + }, + }, +}; diff --git a/app/assets/javascripts/modules/teams/components/table.js b/app/assets/javascripts/modules/teams/components/table.js new file mode 100644 index 000000000..3a7bd2e8f --- /dev/null +++ b/app/assets/javascripts/modules/teams/components/table.js @@ -0,0 +1,54 @@ +import getProperty from 'lodash/get'; + +import Comparator from '~/utils/comparator'; + +import TableSortableMixin from '~/shared/mixins/table-sortable'; +import TablePaginatedMixin from '~/shared/mixins/table-paginated'; + +import TeamTableRow from './table-row'; + +export default { + template: '#js-teams-table-tmpl', + + props: { + teams: { + type: Array, + }, + teamsPath: { + type: String, + }, + prefix: { + type: String, + default: 'tm_', + }, + }, + + mixins: [TableSortableMixin, TablePaginatedMixin], + + components: { + TeamTableRow, + }, + + computed: { + filteredTeams() { + const order = this.sorting.asc ? 1 : -1; + const sortedTeams = [...this.teams]; + const sample = sortedTeams[0]; + const value = getProperty(sample, this.sorting.by); + const comparator = Comparator.of(value); + + // sorting + sortedTeams.sort((a, b) => { + const aValue = getProperty(a, this.sorting.by); + const bValue = getProperty(b, this.sorting.by); + + return order * comparator(aValue, bValue); + }); + + // pagination + const slicedTeams = sortedTeams.slice(this.offset, this.limit * this.currentPage); + + return slicedTeams; + }, + }, +}; diff --git a/app/assets/javascripts/modules/teams/index.js b/app/assets/javascripts/modules/teams/index.js index df9c7dfed..2ca43dff9 100644 --- a/app/assets/javascripts/modules/teams/index.js +++ b/app/assets/javascripts/modules/teams/index.js @@ -1,15 +1,2 @@ +import './pages/index'; import './pages/show'; - -import TeamsIndexPage from './pages/index'; - -const TEAMS_INDEX_ROUTE = 'teams/index'; - -$(() => { - const $body = $('body'); - const route = $body.data('route'); - - if (route === TEAMS_INDEX_ROUTE) { - // eslint-disable-next-line - new TeamsIndexPage($body); - } -}); diff --git a/app/assets/javascripts/modules/teams/components/edit-team-form.js b/app/assets/javascripts/modules/teams/legacy-components/edit-team-form.js similarity index 100% rename from app/assets/javascripts/modules/teams/components/edit-team-form.js rename to app/assets/javascripts/modules/teams/legacy-components/edit-team-form.js diff --git a/app/assets/javascripts/modules/teams/components/new-team-form.js b/app/assets/javascripts/modules/teams/legacy-components/new-team-form.js similarity index 100% rename from app/assets/javascripts/modules/teams/components/new-team-form.js rename to app/assets/javascripts/modules/teams/legacy-components/new-team-form.js diff --git a/app/assets/javascripts/modules/teams/components/new-team-user-form.js b/app/assets/javascripts/modules/teams/legacy-components/new-team-user-form.js similarity index 100% rename from app/assets/javascripts/modules/teams/components/new-team-user-form.js rename to app/assets/javascripts/modules/teams/legacy-components/new-team-user-form.js diff --git a/app/assets/javascripts/modules/teams/components/team-details.js b/app/assets/javascripts/modules/teams/legacy-components/team-details.js similarity index 100% rename from app/assets/javascripts/modules/teams/components/team-details.js rename to app/assets/javascripts/modules/teams/legacy-components/team-details.js diff --git a/app/assets/javascripts/modules/teams/components/team-users-panel.js b/app/assets/javascripts/modules/teams/legacy-components/team-users-panel.js similarity index 100% rename from app/assets/javascripts/modules/teams/components/team-users-panel.js rename to app/assets/javascripts/modules/teams/legacy-components/team-users-panel.js diff --git a/app/assets/javascripts/modules/teams/components/team-users-table.js b/app/assets/javascripts/modules/teams/legacy-components/team-users-table.js similarity index 100% rename from app/assets/javascripts/modules/teams/components/team-users-table.js rename to app/assets/javascripts/modules/teams/legacy-components/team-users-table.js diff --git a/app/assets/javascripts/modules/teams/components/teams-panel.js b/app/assets/javascripts/modules/teams/legacy-components/teams-panel.js similarity index 100% rename from app/assets/javascripts/modules/teams/components/teams-panel.js rename to app/assets/javascripts/modules/teams/legacy-components/teams-panel.js diff --git a/app/assets/javascripts/modules/teams/pages/index.js b/app/assets/javascripts/modules/teams/pages/index.js index ec9f92bd3..232b7d59d 100644 --- a/app/assets/javascripts/modules/teams/pages/index.js +++ b/app/assets/javascripts/modules/teams/pages/index.js @@ -1,19 +1,50 @@ -import BaseComponent from '~/base/component'; +import Vue from 'vue'; -import TeamsPanel from '../components/teams-panel'; +import ToggleLink from '~/shared/components/toggle-link'; +import EventBus from '~/utils/eventbus'; -const TEAMS_PANEL = '.teams-wrapper'; +import NewTeamForm from '../components/new-form'; +import TeamsTable from '../components/table'; +import TeamsStore from '../store'; -// TeamsIndexPage component responsible to instantiate -// the teams index page components and handle interactions. -class TeamsIndexPage extends BaseComponent { - elements() { - this.$teamsPanel = this.$el.find(TEAMS_PANEL); - } +const { set } = Vue; - mount() { - this.teamsPanel = new TeamsPanel(this.$teamsPanel); +$(() => { + if (!$('body[data-route="teams/index"]').length) { + return; } -} -export default TeamsIndexPage; + // eslint-disable-next-line no-new + new Vue({ + el: 'body[data-route="teams/index"] .vue-root', + + components: { + ToggleLink, + NewTeamForm, + TeamsTable, + }, + + data() { + return { + state: TeamsStore.state, + teams: window.teams, + }; + }, + + methods: { + onCreate(team) { + const currentTeams = this.teams; + const teams = [ + ...currentTeams, + team, + ]; + + set(this, 'teams', teams); + }, + }, + + mounted() { + EventBus.$on('teamCreated', team => this.onCreate(team)); + }, + }); +}); diff --git a/app/assets/javascripts/modules/teams/pages/show.js b/app/assets/javascripts/modules/teams/pages/show.js index 6df65cd7d..a2f84679f 100644 --- a/app/assets/javascripts/modules/teams/pages/show.js +++ b/app/assets/javascripts/modules/teams/pages/show.js @@ -9,8 +9,8 @@ import NewNamespaceForm from '../../namespaces/components/new-form'; import NamespacesStore from '../../namespaces/store'; // legacy -import TeamDetails from '../components/team-details'; -import TeamUsersPanel from '../components/team-users-panel'; +import TeamDetails from '../legacy-components/team-details'; +import TeamUsersPanel from '../legacy-components/team-users-panel'; const { set } = Vue; diff --git a/app/assets/javascripts/modules/teams/service.js b/app/assets/javascripts/modules/teams/service.js new file mode 100644 index 000000000..7f85cc6fd --- /dev/null +++ b/app/assets/javascripts/modules/teams/service.js @@ -0,0 +1,52 @@ +import Vue from 'vue'; +import VueResource from 'vue-resource'; + +Vue.use(VueResource); + +const customActions = { + teamTypeahead: { + method: 'GET', + url: 'teams/typeahead/{teamName}', + }, +}; + +const resource = Vue.resource('api/v1/teams{/id}', {}, customActions); + +function all(params = {}) { + return resource.get({}, params); +} + +function get(id) { + return resource.get({ id }); +} + +function save(team) { + return resource.save({}, team); +} + +function searchTeam(teamName) { + return resource.teamTypeahead({ teamName }); +} + +function exists(value) { + return searchTeam(value) + .then((response) => { + const collection = response.data; + + if (Array.isArray(collection)) { + return collection.some(e => e.name === value); + } + + // some unexpected response from the api, + // leave it for the back-end validation + return null; + }) + .catch(() => null); +} + +export default { + get, + all, + save, + exists, +}; diff --git a/app/assets/javascripts/modules/teams/store.js b/app/assets/javascripts/modules/teams/store.js new file mode 100644 index 000000000..073aa0c4c --- /dev/null +++ b/app/assets/javascripts/modules/teams/store.js @@ -0,0 +1,10 @@ +class TeamsStore { + constructor() { + this.state = { + newFormVisible: false, + editFormVisible: false, + }; + } +} + +export default new TeamsStore(); diff --git a/app/controllers/teams_controller.rb b/app/controllers/teams_controller.rb index 04cceb855..1b46bdbed 100644 --- a/app/controllers/teams_controller.rb +++ b/app/controllers/teams_controller.rb @@ -8,6 +8,11 @@ class TeamsController < ApplicationController # GET /teams def index @teams = policy_scope(Team).page(params[:page]) + @teams_serialized = API::Entities::Teams.represent( + @teams, + current_user: current_user, + type: :internal + ).to_json respond_with(@teams) end @@ -26,23 +31,6 @@ def show ) end - # POST /teams - # POST /teams.json - def create - @team = fetch_team - authorize @team - @team.owners << current_user - - if @team.save - @team.create_activity(:create, - owner: current_user, - parameters: { team: @team.name }) - respond_with(@team) - else - respond_with @team.errors, status: :unprocessable_entity - end - end - # PATCH/PUT /teams/1 # PATCH/PUT /teams/1.json def update @@ -73,15 +61,6 @@ def all_with_query private - # Fetch the team to be created from the given parameters. - def fetch_team - team = params.require(:team).permit(:name, :description) - - @team = Team.new(name: team["name"]) - @team.description = team["description"] if team["description"] - @team - end - def set_team @team = Team.find(params[:id]) end diff --git a/app/services/teams/create_service.rb b/app/services/teams/create_service.rb new file mode 100644 index 000000000..4d860a658 --- /dev/null +++ b/app/services/teams/create_service.rb @@ -0,0 +1,20 @@ +module Teams + class CreateService < ::BaseService + def execute + @team = Team.new(params) + @team.owners << current_user + + create_activity! if @team.save + + @team + end + + private + + def create_activity! + @team.create_activity(:create, + owner: current_user, + parameters: { team: @team.name }) + end + end +end diff --git a/app/views/teams/components/_form.html.slim b/app/views/teams/components/_form.html.slim new file mode 100644 index 000000000..139c2e44e --- /dev/null +++ b/app/views/teams/components/_form.html.slim @@ -0,0 +1,18 @@ +div ref="form" class="collapse" + = form_for :team, url: teams_path, html: {id: 'new-team-form', class: 'form-horizontal', role: 'form', novalidate: true, "@submit.prevent" => "onSubmit"} do |f| + .form-group :class=="{ 'has-error': $v.team.name.$error }" + = f.label :name, {class: 'control-label col-md-2'} + .col-md-7 + = f.text_field(:name, class: 'form-control', required: true, placeholder: "New team's name", "@input" => "$v.team.name.$touch()", "v-model.trim" => "team.name") + span.help-block + span v-if="!$v.team.name.required" + | Name can't be blank + span v-if="!$v.team.name.available" + | Name has already been taken + .form-group + = f.label :description, {class: 'control-label col-md-2'} + .col-md-7 + = f.text_area(:description, class: 'form-control fixed-size', required: false, placeholder: "A short description of your team", "v-model" => "team.description") + .form-group + .col-md-offset-2.col-md-7 + = f.submit('Add', class: 'btn btn-primary', ":disabled" => "$v.$invalid") diff --git a/app/views/teams/components/_table.html.slim b/app/views/teams/components/_table.html.slim new file mode 100644 index 000000000..8a38f3b59 --- /dev/null +++ b/app/views/teams/components/_table.html.slim @@ -0,0 +1,35 @@ +div + table.table.table-striped.table-hover :class=="{'table-sortable': sortable}" + colgroup + col.col-5 + col.col-30 + col.col-25 + col.col-20 + col.col-20 + thead + tr + th + th @click=="sort('name')" + i.fa.fa-fw.fa-sort{ + :class=="{ + 'fa-sort-amount-asc': sorting.by === 'name' && sorting.asc, + 'fa-sort-amount-desc': sorting.by === 'name' && !sorting.asc, + }" + } + | Team + + th @click=="sort('role')" + i.fa.fa-fw.fa-sort{ + :class=="{ + 'fa-sort-amount-asc': sorting.by === 'role' && sorting.asc, + 'fa-sort-amount-desc': sorting.by === 'role' && !sorting.asc, + }" + } + | Role + th Number of members + th Number of namespaces + tbody + + + + diff --git a/app/views/teams/components/_table_row.html.slim b/app/views/teams/components/_table_row.html.slim new file mode 100644 index 000000000..74473d9d7 --- /dev/null +++ b/app/views/teams/components/_table_row.html.slim @@ -0,0 +1,12 @@ +tr :class="scopeClass" + td.table-icon + i class="fa fa-lg" :class="teamIcon" + td + a :href="teamPath" + | {{ team.name }} + td + | {{ team.role }} + td + | {{ team.users_count }} + td + | {{ team.namespaces_count }} diff --git a/app/views/teams/create.js.erb b/app/views/teams/create.js.erb deleted file mode 100644 index f518182a8..000000000 --- a/app/views/teams/create.js.erb +++ /dev/null @@ -1,10 +0,0 @@ -<% if @team.errors.any? %> - $('#float-alert p').html("<%= escape_javascript(@team.errors.full_messages.join('
')) %>"); -<% else %> - $("<%= escape_javascript(render @team) %>").appendTo("#teams"); - $('#float-alert p').html("Team '<%= escape_javascript(@team.name) %>' was created successfully"); - $('#add_team_form').fadeOut(); - $('#add_team_btn i').addClass("fa-plus-circle") - $('#add_team_btn i').removeClass("fa-minus-circle") -<% end %> -$('#float-alert').fadeIn(setTimeOutAlertDelay()); \ No newline at end of file diff --git a/app/views/teams/index.html.slim b/app/views/teams/index.html.slim index 2daaae58f..88df6b8c4 100644 --- a/app/views/teams/index.html.slim +++ b/app/views/teams/index.html.slim @@ -1,47 +1,17 @@ -.teams-wrapper - #add_team_form.collapse - = form_for :team, url: teams_path, remote: true, html: {id: 'new-team-form', class: 'form-horizontal', role: 'form'} do |f| - .form-group - = f.label :name, {class: 'control-label col-md-2'} - .col-md-7 - = f.text_field(:name, class: 'form-control', required: true, placeholder: "New team's name") - .form-group - = f.label :description, {class: 'control-label col-md-2'} - .col-md-7 - = f.text_area(:description, class: 'form-control fixed-size', required: false, placeholder: "A short description of your team") - .form-group - .col-md-offset-2.col-md-7 - = f.submit('Add', class: 'btn btn-primary') + - .panel.panel-default - .panel-heading - .row - .col-sm-6 - h5 - ' Teams you are member of - .col-sm-6.text-right - - if can_create_team? - a#add_team_btn.btn.btn-xs.btn-link.js-toggle-button[role="button"] - i.fa.fa-plus-circle - | Create new team += render "teams/partials/panel" - .panel-body - .table-responsive - table.table.table-striped.table-hover - colgroup - col.col-5 - col.col-30 - col.col-25 - col.col-20 - col.col-20 - thead - tr - th - th Team - th Role - th Number of members - th Number of namespaces - tbody#teams - - @teams.each do |team| - = render team - .panel-footer= paginate(@teams) +- content_for :js_body do + script#js-new-team-form-tmpl type="text/x-template" + = render "teams/components/form" + + script#js-teams-table-tmpl type="text/x-template" + = render "teams/components/table" + + script#js-team-table-row-tmpl type="text/x-template" + = render "teams/components/table_row" + +- content_for :js_header do + javascript: + window.teams = #{raw @teams_serialized}; diff --git a/app/views/teams/partials/_panel.html.slim b/app/views/teams/partials/_panel.html.slim new file mode 100644 index 000000000..3d2d52b62 --- /dev/null +++ b/app/views/teams/partials/_panel.html.slim @@ -0,0 +1,13 @@ +.panel.panel-default + .panel-heading + .row + .col-sm-6 + h5 + ' Teams you are member of + .col-sm-6.text-right + - if can_create_team? + + + .panel-body + .table-responsive + diff --git a/config/routes.rb b/config/routes.rb index 52ff34d12..c703a711b 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,6 +1,6 @@ Rails.application.routes.draw do resources :errors, only: [:show] - resources :teams, only: [:index, :show, :create, :update] do + resources :teams, only: [:index, :show, :update] do member do get "typeahead/:query" => "teams#typeahead", :defaults => { format: "json" } end diff --git a/lib/api/entities.rb b/lib/api/entities.rb index aef04867b..a5e96946d 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -95,6 +95,24 @@ class Teams < Grape::Entity type: "Boolean", desc: "Whether the team is visible to the final user or not" } + expose :role, documentation: { + type: String, + desc: "The role this of the current user within this team" + }, if: { type: :internal } do |team, options| + user = options[:current_user] + + # TODO: partially taken from TeamsHelper. Avoid duplication! + team_user = team.team_users.find_by(user_id: user.id) + team_user.role.titleize if team_user + end + expose :users_count, documentation: { + type: Integer, + desc: "The number of enabled users that belong to this team" + }, if: { type: :internal } { |t| t.users.enabled.count } + expose :namespaces_count, documentation: { + type: Integer, + desc: "The number of namespaces that belong to this team" + }, if: { type: :internal } { |t| t.namespaces.count } end class Namespaces < Grape::Entity diff --git a/lib/api/v1/teams.rb b/lib/api/v1/teams.rb index c2217e7e4..3f09d5c33 100644 --- a/lib/api/v1/teams.rb +++ b/lib/api/v1/teams.rb @@ -22,6 +22,33 @@ class Teams < Grape::API present policy_scope(Team), with: API::Entities::Teams end + desc "Create a team", + entity: API::Entities::Teams, + failure: [ + [401, "Authentication fails."], + [403, "Authorization fails."] + ] + + params do + requires :name, type: String, documentation: { desc: "Team name." } + optional :description, type: String, documentation: { desc: "Team description" } + end + + post do + authorize Team, :create? + + team = ::Teams::CreateService.new(current_user, permitted_params).execute + + if team.valid? + present team, + with: API::Entities::Teams, + current_user: current_user, + type: current_type + else + error!({ "errors" => team.errors.full_messages }, 422, header) + end + end + route_param :id, type: String, requirements: { id: /.*/ } do resource :namespaces do desc "Returns the list of namespaces for the given team", diff --git a/spec/api/grape_api/v1/teams_spec.rb b/spec/api/grape_api/v1/teams_spec.rb index a66e8435d..d75d328af 100644 --- a/spec/api/grape_api/v1/teams_spec.rb +++ b/spec/api/grape_api/v1/teams_spec.rb @@ -3,6 +3,7 @@ describe API::V1::Teams do let!(:admin) { create(:admin) } let!(:token) { create(:application_token, user: admin) } + let!(:user_token) { create(:application_token, user: create(:user)) } let!(:hidden_team) do create(:team, name: "portus_global_team_1", @@ -12,6 +13,7 @@ before :each do @header = build_token_header(token) + @user_header = build_token_header(user_token) end context "GET /api/v1/teams" do @@ -75,4 +77,75 @@ expect(members.length).to eq(2) end end + + context "POST /api/v1/teams" do + let(:valid_attributes) do + { name: "qa team", description: "short test description" } + end + + let(:invalid_attributes) do + { admin: "not valid" } + end + + it "creates a team" do + expect do + post "/api/v1/teams", valid_attributes, @header + end.to change(Team, :count).by(1) + + team = Team.last + team_parsed = JSON.parse(response.body) + expect(response).to have_http_status(:success) + expect(team_parsed["id"]).to eq(team.id) + expect(team_parsed["name"]).to eq(team.name) + end + + it "creates a team even if feature is disabled and admin" do + APP_CONFIG["user_permission"]["create_team"]["enabled"] = false + + expect do + post "/api/v1/teams", valid_attributes, @header + end.to change(Team, :count).by(1) + + team = Team.last + team_parsed = JSON.parse(response.body) + expect(response).to have_http_status(:success) + expect(team_parsed["id"]).to eq(team.id) + expect(team_parsed["name"]).to eq(team.name) + end + + it "returns 400 if invalid params" do + post "/api/v1/teams", invalid_attributes, @header + + expect(response).to have_http_status(:bad_request) + end + + it "returns 422 if invalid values" do + post "/api/v1/teams", { name: "" }, @header + + parsed = JSON.parse(response.body) + expect(response).to have_http_status(:unprocessable_entity) + expect(parsed["errors"]).to include("Name can't be blank") + end + + it "returns 403 if non-admins try to create a Team" do + APP_CONFIG["user_permission"]["create_team"]["enabled"] = false + + expect do + post "/api/v1/teams", valid_attributes, @user_header + end.to change(Team, :count).by(0) + + expect(response).to have_http_status(:forbidden) + end + + it "tracks the creations of new teams" do + expect do + post "/api/v1/teams", valid_attributes, @header + end.to change(PublicActivity::Activity, :count).by(1) + + team = Team.last + team_creation_activity = PublicActivity::Activity.find_by(key: "team.create") + expect(team_creation_activity.owner).to eq(admin) + expect(team_creation_activity.trackable).to eq(team) + end + end end diff --git a/spec/controllers/teams_controller_spec.rb b/spec/controllers/teams_controller_spec.rb index 307c894ec..4f50f55bd 100644 --- a/spec/controllers/teams_controller_spec.rb +++ b/spec/controllers/teams_controller_spec.rb @@ -83,61 +83,6 @@ expect(assigns(:teams)).to be_empty end end - - describe "POST #create" do - context "with valid params" do - it "creates a new Team" do - expect do - post :create, team: valid_attributes, format: :js - end.to change(Team, :count).by(1) - expect(assigns(:team).owners.exists?(owner.id)) - end - - it "assigns a newly created team as @team" do - post :create, team: valid_attributes, format: :js - expect(assigns(:team)).to be_a(Team) - expect(assigns(:team)).to be_persisted - end - - it "redirects to the created team" do - post :create, team: valid_attributes - expect(response).to redirect_to(Team.last) - end - end - - context "with invalid params" do - it "assigns a newly created but unsaved team as @team" do - post :create, team: invalid_attributes, format: :js - expect(assigns(:team)).to be_a_new(Team) - expect(response.status).to eq 422 - end - end - - context "non-admins are not allowed to create teams" do - it "prohibits user from creating a new Team" do - APP_CONFIG["user_permission"]["create_team"]["enabled"] = false - - expect do - post :create, team: valid_attributes, format: :js - end.to change(Team, :count).by(0) - end - end - end - end - - describe "as an admin" do - describe "POST #create" do - it "always creates a new Team" do - APP_CONFIG["user_permission"]["manage_team"]["enabled"] = false - admin = User.find_by(admin: true) - sign_in admin - - expect do - post :create, team: valid_attributes, format: :js - end.to change(Team, :count).by(1) - expect(assigns(:team).owners.exists?(admin.id)) - end - end end describe "PATCH #update" do @@ -252,17 +197,6 @@ sign_in owner end - it "creation of new teams" do - expect do - post :create, team: valid_attributes, format: :js - end.to change(PublicActivity::Activity, :count).by(1) - - team = Team.last - team_creation_activity = PublicActivity::Activity.find_by(key: "team.create") - expect(team_creation_activity.owner).to eq(owner) - expect(team_creation_activity.trackable).to eq(team) - end - it "editing of a team description" do old_description = team.description expect do diff --git a/spec/features/teams_spec.rb b/spec/features/teams_spec.rb index 91c2afd87..918c9fcab 100644 --- a/spec/features/teams_spec.rb +++ b/spec/features/teams_spec.rb @@ -9,43 +9,40 @@ login_as user, scope: :user end - describe "teams#index" do - scenario "A user cannot create an empty team", js: true do - teams_count = Team.count - + describe "teams#index", js: true do + scenario "A user cannot create an empty team" do visit teams_path - find("#add_team_btn").click - click_button "Add" - wait_for_ajax + find(".toggle-link-new-team").click + wait_for_effect_on("#new-team-form") - expect(Team.count).to eql teams_count - expect(page).to have_current_path(teams_path) - end + fill_in "Name", with: Team.first.name + fill_in "Name", with: "" - scenario "A team cannot be created if the name has already been picked", js: true do - teams_count = Team.count + expect(page).to have_content("Name can't be blank") + expect(page).to have_button("Add", disabled: true) + end + scenario "A team cannot be created if the name has already been picked" do visit teams_path - find("#add_team_btn").click - fill_in "Name", with: Team.first.name - click_button "Add" + find(".toggle-link-new-team").click + wait_for_effect_on("#new-team-form") + + fill_in "Name", with: Team.last.name wait_for_ajax - wait_for_effect_on("#float-alert") - expect(page).to have_css("#float-alert") expect(page).to have_content("Name has already been taken") - expect(page).to have_current_path(teams_path) - expect(Team.count).to eql teams_count + expect(page).to have_button("Add", disabled: true) end - scenario "A team can be created from the index page", js: true do + scenario "A team can be created from the index page" do teams_count = Team.count visit teams_path - find("#add_team_btn").click + find(".toggle-link-new-team").click fill_in "Name", with: "valid-team" + wait_for_ajax click_button "Add" wait_for_ajax @@ -58,28 +55,29 @@ expect(Team.count).to eql(teams_count + 1) end - scenario 'The "Create new team" link has a toggle effect', js: true do + scenario 'The "Create new team" link has a toggle effect' do visit teams_path - expect(page).to have_css("#add_team_btn i.fa-plus-circle") - expect(page).to_not have_css("#add_team_btn i.fa-minus-circle") + expect(page).to have_css(".toggle-link-new-team i.fa-plus-circle") + expect(page).to_not have_css(".toggle-link-new-team i.fa-minus-circle") - find("#add_team_btn").click - wait_for_effect_on("#add_team_form") + find(".toggle-link-new-team").click + wait_for_effect_on("#new-team-form") - expect(page).to_not have_css("#add_team_btn i.fa-plus-circle") - expect(page).to have_css("#add_team_btn i.fa-minus-circle") + expect(page).to_not have_css(".toggle-link-new-team i.fa-plus-circle") + expect(page).to have_css(".toggle-link-new-team i.fa-minus-circle") - find("#add_team_btn").click - wait_for_effect_on("#add_team_form") + find(".toggle-link-new-team").click + wait_for_effect_on("#new-team-form") - expect(page).to have_css("#add_team_btn i.fa-plus-circle") - expect(page).to_not have_css("#add_team_btn i.fa-minus-circle") + expect(page).to have_css(".toggle-link-new-team i.fa-plus-circle") + expect(page).to_not have_css(".toggle-link-new-team i.fa-minus-circle") end scenario "The name of each team is a link" do visit teams_path - expect(page).to have_content(team.name) - find("#teams a").click + + expect(page).to have_link(team.name) + find(".team_#{team.id} a").click expect(page).to have_current_path(team_path(team)) end diff --git a/spec/services/teams/create_service_spec.rb b/spec/services/teams/create_service_spec.rb new file mode 100644 index 000000000..c85dfc688 --- /dev/null +++ b/spec/services/teams/create_service_spec.rb @@ -0,0 +1,31 @@ +require "rails_helper" + +describe "Teams::CreateService" do + let!(:user) { create(:user) } + + describe "#execute" do + context "with params" do + subject(:service) { Teams::CreateService.new(user, name: "name") } + + it "creates a new team" do + expect { service.execute }.to change(Team, :count).by(1) + end + + it "creates a new activity" do + expect { service.execute }.to change(PublicActivity::Activity, :count).by(1) + end + end + + context "without params" do + subject(:service) { Teams::CreateService.new(user) } + + it "creates a new team" do + expect { service.execute }.to change(Team, :count).by(0) + end + + it "creates a new activity" do + expect { service.execute }.to change(PublicActivity::Activity, :count).by(0) + end + end + end +end