Commit 05f5d6e7 authored by Phil Hughes's avatar Phil Hughes

Merge branch '35428-web-ide-button-missing-from-project-pages' into 'master'

Step 2 - Fix Web IDE on projects without merge requests

See merge request gitlab-org/gitlab!24508
parents 48fd4071 a7840407
...@@ -70,7 +70,7 @@ export default { ...@@ -70,7 +70,7 @@ export default {
:title="$options.currentBranchPermissionsTooltip" :title="$options.currentBranchPermissionsTooltip"
> >
<span <span
class="ide-radio-label" class="ide-option-label"
data-qa-selector="commit_to_current_branch_radio" data-qa-selector="commit_to_current_branch_radio"
v-html="commitToCurrentBranchText" v-html="commitToCurrentBranchText"
></span> ></span>
......
<script> <script>
import { createNamespacedHelpers } from 'vuex'; import { createNamespacedHelpers } from 'vuex';
import { GlTooltipDirective } from '@gitlab/ui';
import { s__ } from '~/locale';
const { const { mapActions: mapCommitActions, mapGetters: mapCommitGetters } = createNamespacedHelpers(
mapState: mapCommitState, 'commit',
mapActions: mapCommitActions, );
mapGetters: mapCommitGetters,
} = createNamespacedHelpers('commit');
export default { export default {
directives: {
GlTooltip: GlTooltipDirective,
},
computed: { computed: {
...mapCommitState(['shouldCreateMR']), ...mapCommitGetters(['shouldHideNewMrOption', 'shouldDisableNewMrOption', 'shouldCreateMR']),
...mapCommitGetters(['shouldHideNewMrOption']), tooltipText() {
if (this.shouldDisableNewMrOption) {
return s__(
'IDE|This option is disabled because you are not allowed to create merge requests in this project.',
);
}
return '';
},
}, },
methods: { methods: {
...mapCommitActions(['toggleShouldCreateMR']), ...mapCommitActions(['toggleShouldCreateMR']),
...@@ -21,14 +32,19 @@ export default { ...@@ -21,14 +32,19 @@ export default {
<template> <template>
<fieldset v-if="!shouldHideNewMrOption"> <fieldset v-if="!shouldHideNewMrOption">
<hr class="my-2" /> <hr class="my-2" />
<label class="mb-0 js-ide-commit-new-mr"> <label
v-gl-tooltip="tooltipText"
class="mb-0 js-ide-commit-new-mr"
:class="{ 'is-disabled': shouldDisableNewMrOption }"
>
<input <input
:disabled="shouldDisableNewMrOption"
:checked="shouldCreateMR" :checked="shouldCreateMR"
type="checkbox" type="checkbox"
data-qa-selector="start_new_mr_checkbox" data-qa-selector="start_new_mr_checkbox"
@change="toggleShouldCreateMR" @change="toggleShouldCreateMR"
/> />
<span class="prepend-left-10"> <span class="prepend-left-10 ide-option-label">
{{ __('Start a new merge request') }} {{ __('Start a new merge request') }}
</span> </span>
</label> </label>
......
...@@ -67,7 +67,7 @@ export default { ...@@ -67,7 +67,7 @@ export default {
@change="updateCommitAction($event.target.value)" @change="updateCommitAction($event.target.value)"
/> />
<span class="prepend-left-10"> <span class="prepend-left-10">
<span v-if="label" class="ide-radio-label"> {{ label }} </span> <slot v-else></slot> <span v-if="label" class="ide-option-label"> {{ label }} </span> <slot v-else></slot>
</span> </span>
</label> </label>
<div v-if="commitAction === value && showInput" class="ide-commit-new-branch"> <div v-if="commitAction === value && showInput" class="ide-commit-new-branch">
......
<script> <script>
import $ from 'jquery'; import $ from 'jquery';
import { mapGetters } from 'vuex';
import NavForm from './nav_form.vue'; import NavForm from './nav_form.vue';
import NavDropdownButton from './nav_dropdown_button.vue'; import NavDropdownButton from './nav_dropdown_button.vue';
...@@ -13,6 +14,9 @@ export default { ...@@ -13,6 +14,9 @@ export default {
isVisibleDropdown: false, isVisibleDropdown: false,
}; };
}, },
computed: {
...mapGetters(['canReadMergeRequests']),
},
mounted() { mounted() {
this.addDropdownListeners(); this.addDropdownListeners();
}, },
...@@ -42,7 +46,9 @@ export default { ...@@ -42,7 +46,9 @@ export default {
<template> <template>
<div ref="dropdown" class="btn-group ide-nav-dropdown dropdown"> <div ref="dropdown" class="btn-group ide-nav-dropdown dropdown">
<nav-dropdown-button /> <nav-dropdown-button :show-merge-requests="canReadMergeRequests" />
<div class="dropdown-menu dropdown-menu-left p-0"><nav-form v-if="isVisibleDropdown" /></div> <div class="dropdown-menu dropdown-menu-left p-0">
<nav-form v-if="isVisibleDropdown" :show-merge-requests="canReadMergeRequests" />
</div>
</div> </div>
</template> </template>
...@@ -10,6 +10,13 @@ export default { ...@@ -10,6 +10,13 @@ export default {
Icon, Icon,
DropdownButton, DropdownButton,
}, },
props: {
showMergeRequests: {
type: Boolean,
required: false,
default: true,
},
},
computed: { computed: {
...mapState(['currentBranchId', 'currentMergeRequestId']), ...mapState(['currentBranchId', 'currentMergeRequestId']),
mergeRequestLabel() { mergeRequestLabel() {
...@@ -25,10 +32,10 @@ export default { ...@@ -25,10 +32,10 @@ export default {
<template> <template>
<dropdown-button> <dropdown-button>
<span class="row"> <span class="row">
<span class="col-7 text-truncate"> <span class="col-auto text-truncate" :class="{ 'col-7': showMergeRequests }">
<icon :size="16" :aria-label="__('Current Branch')" name="branch" /> {{ branchLabel }} <icon :size="16" :aria-label="__('Current Branch')" name="branch" /> {{ branchLabel }}
</span> </span>
<span class="col-5 pl-0 text-truncate"> <span v-if="showMergeRequests" class="col-5 pl-0 text-truncate">
<icon :size="16" :aria-label="__('Merge Request')" name="merge-request" /> <icon :size="16" :aria-label="__('Merge Request')" name="merge-request" />
{{ mergeRequestLabel }} {{ mergeRequestLabel }}
</span> </span>
......
...@@ -11,12 +11,19 @@ export default { ...@@ -11,12 +11,19 @@ export default {
BranchesSearchList, BranchesSearchList,
MergeRequestSearchList, MergeRequestSearchList,
}, },
props: {
showMergeRequests: {
type: Boolean,
required: false,
default: true,
},
},
}; };
</script> </script>
<template> <template>
<div class="ide-nav-form p-0"> <div class="ide-nav-form p-0">
<tabs stop-propagation> <tabs v-if="showMergeRequests" stop-propagation>
<tab active> <tab active>
<template slot="title"> <template slot="title">
{{ __('Branches') }} {{ __('Branches') }}
...@@ -30,5 +37,6 @@ export default { ...@@ -30,5 +37,6 @@ export default {
<merge-request-search-list /> <merge-request-search-list />
</tab> </tab>
</tabs> </tabs>
<branches-search-list v-else />
</div> </div>
</template> </template>
...@@ -8,6 +8,9 @@ export const MAX_BODY_LENGTH = 72; ...@@ -8,6 +8,9 @@ export const MAX_BODY_LENGTH = 72;
export const FILE_VIEW_MODE_EDITOR = 'editor'; export const FILE_VIEW_MODE_EDITOR = 'editor';
export const FILE_VIEW_MODE_PREVIEW = 'preview'; export const FILE_VIEW_MODE_PREVIEW = 'preview';
export const PERMISSION_CREATE_MR = 'createMergeRequestIn';
export const PERMISSION_READ_MR = 'readMergeRequest';
export const activityBarViews = { export const activityBarViews = {
edit: 'ide-tree', edit: 'ide-tree',
commit: 'commit-section', commit: 'commit-section',
......
query getUserPermissions($projectPath: ID!) {
project(fullPath: $projectPath) {
userPermissions {
createMergeRequestIn,
readMergeRequest
}
}
}
import createGqClient, { fetchPolicies } from '~/lib/graphql';
export default createGqClient(
{},
{
fetchPolicy: fetchPolicies.NO_CACHE,
},
);
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import { joinPaths, escapeFileUrl } from '~/lib/utils/url_utility'; import { joinPaths, escapeFileUrl } from '~/lib/utils/url_utility';
import Api from '~/api'; import Api from '~/api';
import getUserPermissions from '../queries/getUserPermissions.query.graphql';
import gqClient from './gql';
const fetchApiProjectData = projectPath => Api.project(projectPath).then(({ data }) => data);
const fetchGqlProjectData = projectPath =>
gqClient
.query({
query: getUserPermissions,
variables: { projectPath },
})
.then(({ data }) => data.project);
export default { export default {
getFileData(endpoint) { getFileData(endpoint) {
...@@ -47,7 +59,16 @@ export default { ...@@ -47,7 +59,16 @@ export default {
.then(({ data }) => data); .then(({ data }) => data);
}, },
getProjectData(namespace, project) { getProjectData(namespace, project) {
return Api.project(`${namespace}/${project}`); const projectPath = `${namespace}/${project}`;
return Promise.all([fetchApiProjectData(projectPath), fetchGqlProjectData(projectPath)]).then(
([apiProjectData, gqlProjectData]) => ({
data: {
...apiProjectData,
...gqlProjectData,
},
}),
);
}, },
getProjectMergeRequests(projectId, params = {}) { getProjectMergeRequests(projectId, params = {}) {
return Api.projectMergeRequests(projectId, params); return Api.projectMergeRequests(projectId, params);
......
...@@ -2,10 +2,17 @@ import flash from '~/flash'; ...@@ -2,10 +2,17 @@ import flash from '~/flash';
import { __ } from '~/locale'; import { __ } from '~/locale';
import service from '../../services'; import service from '../../services';
import * as types from '../mutation_types'; import * as types from '../mutation_types';
import { activityBarViews } from '../../constants'; import { activityBarViews, PERMISSION_READ_MR } from '../../constants';
export const getMergeRequestsForBranch = ({ commit, state }, { projectId, branchId } = {}) => export const getMergeRequestsForBranch = (
service { commit, state, getters },
{ projectId, branchId } = {},
) => {
if (!getters.findProjectPermissions(projectId)[PERMISSION_READ_MR]) {
return Promise.resolve();
}
return service
.getProjectMergeRequests(`${projectId}`, { .getProjectMergeRequests(`${projectId}`, {
source_branch: branchId, source_branch: branchId,
source_project_id: state.projects[projectId].id, source_project_id: state.projects[projectId].id,
...@@ -36,6 +43,7 @@ export const getMergeRequestsForBranch = ({ commit, state }, { projectId, branch ...@@ -36,6 +43,7 @@ export const getMergeRequestsForBranch = ({ commit, state }, { projectId, branch
); );
throw e; throw e;
}); });
};
export const getMergeRequestData = ( export const getMergeRequestData = (
{ commit, dispatch, state }, { commit, dispatch, state },
......
import { getChangesCountForFiles, filePathMatches } from './utils'; import { getChangesCountForFiles, filePathMatches } from './utils';
import { activityBarViews, packageJsonPath } from '../constants'; import {
activityBarViews,
packageJsonPath,
PERMISSION_READ_MR,
PERMISSION_CREATE_MR,
} from '../constants';
export const activeFile = state => state.openFiles.find(file => file.active) || null; export const activeFile = state => state.openFiles.find(file => file.active) || null;
...@@ -141,5 +146,14 @@ export const getDiffInfo = (state, getters) => path => { ...@@ -141,5 +146,14 @@ export const getDiffInfo = (state, getters) => path => {
}; };
}; };
export const findProjectPermissions = (state, getters) => projectId =>
getters.findProject(projectId)?.userPermissions || {};
export const canReadMergeRequests = (state, getters) =>
Boolean(getters.findProjectPermissions(state.currentProjectId)[PERMISSION_READ_MR]);
export const canCreateMergeRequests = (state, getters) =>
Boolean(getters.findProjectPermissions(state.currentProjectId)[PERMISSION_CREATE_MR]);
// prevent babel-plugin-rewire from generating an invalid default during karma tests // prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {}; export default () => {};
...@@ -158,7 +158,7 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState, roo ...@@ -158,7 +158,7 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState, roo
commit(rootTypes.SET_LAST_COMMIT_MSG, '', { root: true }); commit(rootTypes.SET_LAST_COMMIT_MSG, '', { root: true });
}, 5000); }, 5000);
if (state.shouldCreateMR) { if (getters.shouldCreateMR) {
const { currentProject } = rootGetters; const { currentProject } = rootGetters;
const targetBranch = getters.isCreatingNewBranch const targetBranch = getters.isCreatingNewBranch
? rootState.currentBranchId ? rootState.currentBranchId
......
...@@ -54,5 +54,11 @@ export const shouldHideNewMrOption = (_state, getters, _rootState, rootGetters) ...@@ -54,5 +54,11 @@ export const shouldHideNewMrOption = (_state, getters, _rootState, rootGetters)
(!rootGetters.hasMergeRequest && rootGetters.isOnDefaultBranch)) && (!rootGetters.hasMergeRequest && rootGetters.isOnDefaultBranch)) &&
rootGetters.canPushToBranch; rootGetters.canPushToBranch;
export const shouldDisableNewMrOption = (state, getters, rootState, rootGetters) =>
!rootGetters.canCreateMergeRequests;
export const shouldCreateMR = (state, getters) =>
state.shouldCreateMR && !getters.shouldDisableNewMrOption;
// prevent babel-plugin-rewire from generating an invalid default during karma tests // prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {}; export default () => {};
...@@ -688,7 +688,7 @@ $ide-commit-header-height: 48px; ...@@ -688,7 +688,7 @@ $ide-commit-header-height: 48px;
font-weight: normal; font-weight: normal;
&.is-disabled { &.is-disabled {
.ide-radio-label { .ide-option-label {
text-decoration: line-through; text-decoration: line-through;
} }
} }
......
...@@ -84,17 +84,16 @@ ...@@ -84,17 +84,16 @@
= render 'projects/find_file_link' = render 'projects/find_file_link'
- if can_create_mr_from_fork - if can_collaborate || current_user&.already_forked?(@project)
- if can_collaborate || current_user&.already_forked?(@project) - if vue_file_list_enabled?
- if vue_file_list_enabled? #js-tree-web-ide-link.d-inline-block
#js-tree-web-ide-link.d-inline-block
- else
= link_to ide_edit_path(@project, @ref, @path), class: 'btn btn-default qa-web-ide-button' do
= _('Web IDE')
- else - else
= link_to '#modal-confirm-fork', class: 'btn btn-default qa-web-ide-button', data: { target: '#modal-confirm-fork', toggle: 'modal'} do = link_to ide_edit_path(@project, @ref, @path), class: 'btn btn-default qa-web-ide-button' do
= _('Web IDE') = _('Web IDE')
= render 'shared/confirm_fork_modal', fork_path: ide_fork_and_edit_path(@project, @ref, @path) - elsif can_create_mr_from_fork
= link_to '#modal-confirm-fork', class: 'btn btn-default qa-web-ide-button', data: { target: '#modal-confirm-fork', toggle: 'modal'} do
= _('Web IDE')
= render 'shared/confirm_fork_modal', fork_path: ide_fork_and_edit_path(@project, @ref, @path)
- if show_xcode_link?(@project) - if show_xcode_link?(@project)
.project-action-button.project-xcode.inline< .project-action-button.project-xcode.inline<
......
---
title: Enable Web IDE on projects without Merge Requests
merge_request: 24508
author:
type: fixed
...@@ -10130,6 +10130,9 @@ msgstr "" ...@@ -10130,6 +10130,9 @@ msgstr ""
msgid "IDE|Successful commit" msgid "IDE|Successful commit"
msgstr "" msgstr ""
msgid "IDE|This option is disabled because you are not allowed to create merge requests in this project."
msgstr ""
msgid "IDE|This option is disabled because you don't have write permissions for the current branch." msgid "IDE|This option is disabled because you don't have write permissions for the current branch."
msgstr "" msgstr ""
......
...@@ -3,66 +3,96 @@ ...@@ -3,66 +3,96 @@
require 'spec_helper' require 'spec_helper'
describe 'Projects > Show > Collaboration links', :js do describe 'Projects > Show > Collaboration links', :js do
let(:project) { create(:project, :repository) } using RSpec::Parameterized::TableSyntax
let(:project) { create(:project, :repository, :public) }
let(:user) { create(:user) } let(:user) { create(:user) }
before do before do
project.add_developer(user)
sign_in(user) sign_in(user)
end end
it 'shows all the expected links' do context 'with developer user' do
visit project_path(project) before do
project.add_developer(user)
end
# The navigation bar it 'shows all the expected links' do
page.within('.header-new') do visit project_path(project)
find('.qa-new-menu-toggle').click
aggregate_failures 'dropdown links in the navigation bar' do # The navigation bar
expect(page).to have_link('New issue') page.within('.header-new') do
expect(page).to have_link('New merge request') find('.qa-new-menu-toggle').click
expect(page).to have_link('New snippet', href: new_project_snippet_path(project))
end
find('.qa-new-menu-toggle').click aggregate_failures 'dropdown links in the navigation bar' do
end expect(page).to have_link('New issue')
expect(page).to have_link('New merge request')
expect(page).to have_link('New snippet', href: new_project_snippet_path(project))
end
# The dropdown above the tree find('.qa-new-menu-toggle').click
page.within('.repo-breadcrumb') do end
find('.qa-add-to-tree').click
aggregate_failures 'dropdown links above the repo tree' do # The dropdown above the tree
expect(page).to have_link('New file') page.within('.repo-breadcrumb') do
expect(page).to have_link('Upload file') find('.qa-add-to-tree').click
expect(page).to have_link('New directory')
expect(page).to have_link('New branch') aggregate_failures 'dropdown links above the repo tree' do
expect(page).to have_link('New tag') expect(page).to have_link('New file')
expect(page).to have_link('Upload file')
expect(page).to have_link('New directory')
expect(page).to have_link('New branch')
expect(page).to have_link('New tag')
end
end end
# The Web IDE
expect(page).to have_link('Web IDE')
end end
# The Web IDE it 'hides the links when the project is archived' do
expect(page).to have_link('Web IDE') project.update!(archived: true)
end
it 'hides the links when the project is archived' do visit project_path(project)
project.update!(archived: true)
visit project_path(project) page.within('.header-new') do
find('.qa-new-menu-toggle').click
page.within('.header-new') do aggregate_failures 'dropdown links' do
find('.qa-new-menu-toggle').click expect(page).not_to have_link('New issue')
expect(page).not_to have_link('New merge request')
expect(page).not_to have_link('New snippet', href: new_project_snippet_path(project))
end
aggregate_failures 'dropdown links' do find('.qa-new-menu-toggle').click
expect(page).not_to have_link('New issue')
expect(page).not_to have_link('New merge request')
expect(page).not_to have_link('New snippet', href: new_project_snippet_path(project))
end end
find('.qa-new-menu-toggle').click expect(page).not_to have_selector('.qa-add-to-tree')
expect(page).not_to have_link('Web IDE')
end end
end
expect(page).not_to have_selector('.qa-add-to-tree') context "Web IDE link" do
where(:merge_requests_access_level, :user_level, :expect_ide_link) do
::ProjectFeature::DISABLED | :guest | false
::ProjectFeature::DISABLED | :developer | true
::ProjectFeature::PRIVATE | :guest | false
::ProjectFeature::PRIVATE | :developer | true
::ProjectFeature::ENABLED | :guest | true
::ProjectFeature::ENABLED | :developer | true
end
expect(page).not_to have_link('Web IDE') with_them do
before do
project.project_feature.update!({ merge_requests_access_level: merge_requests_access_level })
project.add_user(user, user_level)
visit project_path(project)
end
it "updates Web IDE link" do
expect(page.has_link?('Web IDE')).to be(expect_ide_link)
end
end
end end
end end
...@@ -18,6 +18,7 @@ export const projectData = { ...@@ -18,6 +18,7 @@ export const projectData = {
}, },
mergeRequests: {}, mergeRequests: {},
merge_requests_enabled: true, merge_requests_enabled: true,
userPermissions: {},
default_branch: 'master', default_branch: 'master',
}; };
......
...@@ -2,11 +2,17 @@ import axios from 'axios'; ...@@ -2,11 +2,17 @@ import axios from 'axios';
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
import services from '~/ide/services'; import services from '~/ide/services';
import Api from '~/api'; import Api from '~/api';
import gqClient from '~/ide/services/gql';
import { escapeFileUrl } from '~/lib/utils/url_utility'; import { escapeFileUrl } from '~/lib/utils/url_utility';
import getUserPermissions from '~/ide/queries/getUserPermissions.query.graphql';
import { projectData } from '../mock_data';
jest.mock('~/api'); jest.mock('~/api');
jest.mock('~/ide/services/gql');
const TEST_PROJECT_ID = 'alice/wonderland'; const TEST_NAMESPACE = 'alice';
const TEST_PROJECT = 'wonderland';
const TEST_PROJECT_ID = `${TEST_NAMESPACE}/${TEST_PROJECT}`;
const TEST_BRANCH = 'master-patch-123'; const TEST_BRANCH = 'master-patch-123';
const TEST_COMMIT_SHA = '123456789'; const TEST_COMMIT_SHA = '123456789';
const TEST_FILE_PATH = 'README2.md'; const TEST_FILE_PATH = 'README2.md';
...@@ -111,4 +117,27 @@ describe('IDE services', () => { ...@@ -111,4 +117,27 @@ describe('IDE services', () => {
}, },
); );
}); });
describe('getProjectData', () => {
it('combines gql and API requests', () => {
const gqlProjectData = {
userPermissions: {
bogus: true,
},
};
Api.project.mockReturnValue(Promise.resolve({ data: { ...projectData } }));
gqClient.query.mockReturnValue(Promise.resolve({ data: { project: gqlProjectData } }));
return services.getProjectData(TEST_NAMESPACE, TEST_PROJECT).then(response => {
expect(response).toEqual({ data: { ...projectData, ...gqlProjectData } });
expect(Api.project).toHaveBeenCalledWith(TEST_PROJECT_ID);
expect(gqClient.query).toHaveBeenCalledWith({
query: getUserPermissions,
variables: {
projectPath: TEST_PROJECT_ID,
},
});
});
});
});
}); });
...@@ -2,6 +2,8 @@ import * as getters from '~/ide/stores/getters'; ...@@ -2,6 +2,8 @@ import * as getters from '~/ide/stores/getters';
import { createStore } from '~/ide/stores'; import { createStore } from '~/ide/stores';
import { file } from '../helpers'; import { file } from '../helpers';
const TEST_PROJECT_ID = 'test_project';
describe('IDE store getters', () => { describe('IDE store getters', () => {
let localState; let localState;
let localStore; let localStore;
...@@ -398,4 +400,38 @@ describe('IDE store getters', () => { ...@@ -398,4 +400,38 @@ describe('IDE store getters', () => {
}, },
); );
}); });
describe('findProjectPermissions', () => {
it('returns false if project not found', () => {
expect(localStore.getters.findProjectPermissions(TEST_PROJECT_ID)).toEqual({});
});
it('finds permission in given project', () => {
const userPermissions = {
readMergeRequest: true,
createMergeRequestsIn: false,
};
localState.projects[TEST_PROJECT_ID] = { userPermissions };
expect(localStore.getters.findProjectPermissions(TEST_PROJECT_ID)).toBe(userPermissions);
});
});
describe.each`
getterName | permissionKey
${'canReadMergeRequests'} | ${'readMergeRequest'}
${'canCreateMergeRequests'} | ${'createMergeRequestIn'}
`('$getterName', ({ getterName, permissionKey }) => {
it.each([true, false])('finds permission for current project (%s)', val => {
localState.projects[TEST_PROJECT_ID] = {
userPermissions: {
[permissionKey]: val,
},
};
localState.currentProjectId = TEST_PROJECT_ID;
expect(localStore.getters[getterName]).toBe(val);
});
});
}); });
import Vue from 'vue'; import Vue from 'vue';
import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import { projectData, branches } from 'spec/ide/mock_data'; import { projectData, branches } from 'spec/ide/mock_data';
import { resetStore } from 'spec/ide/helpers';
import NewMergeRequestOption from '~/ide/components/commit_sidebar/new_merge_request_option.vue'; import NewMergeRequestOption from '~/ide/components/commit_sidebar/new_merge_request_option.vue';
import store from '~/ide/stores'; import { createStore } from '~/ide/stores';
import consts from '../../../../../app/assets/javascripts/ide/stores/modules/commit/constants'; import { PERMISSION_CREATE_MR } from '~/ide/constants';
import consts from '~/ide/stores/modules/commit/constants';
describe('create new MR checkbox', () => { describe('create new MR checkbox', () => {
let store;
let vm; let vm;
const setMR = () => { const setMR = () => {
vm.$store.state.currentMergeRequestId = '1'; vm.$store.state.currentMergeRequestId = '1';
vm.$store.state.projects[store.state.currentProjectId].mergeRequests[ vm.$store.state.projects[store.state.currentProjectId].mergeRequests[
...@@ -15,6 +17,10 @@ describe('create new MR checkbox', () => { ...@@ -15,6 +17,10 @@ describe('create new MR checkbox', () => {
] = { foo: 'bar' }; ] = { foo: 'bar' };
}; };
const setPermissions = permissions => {
store.state.projects[store.state.currentProjectId].userPermissions = permissions;
};
const createComponent = ({ currentBranchId = 'master', createNewBranch = false } = {}) => { const createComponent = ({ currentBranchId = 'master', createNewBranch = false } = {}) => {
const Component = Vue.extend(NewMergeRequestOption); const Component = Vue.extend(NewMergeRequestOption);
...@@ -25,20 +31,29 @@ describe('create new MR checkbox', () => { ...@@ -25,20 +31,29 @@ describe('create new MR checkbox', () => {
: consts.COMMIT_TO_CURRENT_BRANCH; : consts.COMMIT_TO_CURRENT_BRANCH;
vm.$store.state.currentBranchId = currentBranchId; vm.$store.state.currentBranchId = currentBranchId;
vm.$store.state.currentProjectId = 'abcproject';
const proj = JSON.parse(JSON.stringify(projectData)); store.state.projects.abcproject.branches[currentBranchId] = branches.find(
proj.branches[currentBranchId] = branches.find(branch => branch.name === currentBranchId); branch => branch.name === currentBranchId,
);
Vue.set(vm.$store.state.projects, 'abcproject', proj);
return vm.$mount(); return vm.$mount();
}; };
const findInput = () => vm.$el.querySelector('input[type="checkbox"]');
const findLabel = () => vm.$el.querySelector('.js-ide-commit-new-mr');
beforeEach(() => {
store = createStore();
store.state.currentProjectId = 'abcproject';
const proj = JSON.parse(JSON.stringify(projectData));
proj.userPermissions[PERMISSION_CREATE_MR] = true;
Vue.set(store.state.projects, 'abcproject', proj);
});
afterEach(() => { afterEach(() => {
vm.$destroy(); vm.$destroy();
resetStore(vm.$store);
}); });
describe('for default branch', () => { describe('for default branch', () => {
...@@ -160,6 +175,24 @@ describe('create new MR checkbox', () => { ...@@ -160,6 +175,24 @@ describe('create new MR checkbox', () => {
.then(done) .then(done)
.catch(done.fail); .catch(done.fail);
}); });
it('shows enablded checkbox', () => {
expect(findLabel().classList.contains('is-disabled')).toBe(false);
expect(findInput().disabled).toBe(false);
});
});
describe('when user cannot create MR', () => {
beforeEach(() => {
setPermissions({ [PERMISSION_CREATE_MR]: false });
createComponent({ currentBranchId: 'regular' });
});
it('disabled checkbox', () => {
expect(findLabel().classList.contains('is-disabled')).toBe(true);
expect(findInput().disabled).toBe(true);
});
}); });
it('dispatches toggleShouldCreateMR when clicking checkbox', () => { it('dispatches toggleShouldCreateMR when clicking checkbox', () => {
......
...@@ -2,62 +2,92 @@ import Vue from 'vue'; ...@@ -2,62 +2,92 @@ import Vue from 'vue';
import { trimText } from 'spec/helpers/text_helper'; import { trimText } from 'spec/helpers/text_helper';
import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import NavDropdownButton from '~/ide/components/nav_dropdown_button.vue'; import NavDropdownButton from '~/ide/components/nav_dropdown_button.vue';
import store from '~/ide/stores'; import { createStore } from '~/ide/stores';
import { resetStore } from '../helpers';
describe('NavDropdown', () => { describe('NavDropdown', () => {
const TEST_BRANCH_ID = 'lorem-ipsum-dolar'; const TEST_BRANCH_ID = 'lorem-ipsum-dolar';
const TEST_MR_ID = '12345'; const TEST_MR_ID = '12345';
const Component = Vue.extend(NavDropdownButton); let store;
let vm; let vm;
beforeEach(() => { beforeEach(() => {
vm = mountComponentWithStore(Component, { store }); store = createStore();
vm.$mount();
}); });
afterEach(() => { afterEach(() => {
vm.$destroy(); vm.$destroy();
resetStore(store);
}); });
it('renders empty placeholders, if state is falsey', () => { const createComponent = (props = {}) => {
expect(trimText(vm.$el.textContent)).toEqual('- -'); vm = mountComponentWithStore(Vue.extend(NavDropdownButton), { props, store });
}); vm.$mount();
};
it('renders branch name, if state has currentBranchId', done => { const findIcon = name => vm.$el.querySelector(`.ic-${name}`);
vm.$store.state.currentBranchId = TEST_BRANCH_ID; const findMRIcon = () => findIcon('merge-request');
const findBranchIcon = () => findIcon('branch');
vm.$nextTick() describe('normal', () => {
.then(() => { beforeEach(() => {
expect(trimText(vm.$el.textContent)).toEqual(`${TEST_BRANCH_ID} -`); createComponent();
}) });
.then(done)
.catch(done.fail); it('renders empty placeholders, if state is falsey', () => {
}); expect(trimText(vm.$el.textContent)).toEqual('- -');
});
it('renders mr id, if state has currentMergeRequestId', done => { it('renders branch name, if state has currentBranchId', done => {
vm.$store.state.currentMergeRequestId = TEST_MR_ID; vm.$store.state.currentBranchId = TEST_BRANCH_ID;
vm.$nextTick() vm.$nextTick()
.then(() => { .then(() => {
expect(trimText(vm.$el.textContent)).toEqual(`- !${TEST_MR_ID}`); expect(trimText(vm.$el.textContent)).toEqual(`${TEST_BRANCH_ID} -`);
}) })
.then(done) .then(done)
.catch(done.fail); .catch(done.fail);
});
it('renders mr id, if state has currentMergeRequestId', done => {
vm.$store.state.currentMergeRequestId = TEST_MR_ID;
vm.$nextTick()
.then(() => {
expect(trimText(vm.$el.textContent)).toEqual(`- !${TEST_MR_ID}`);
})
.then(done)
.catch(done.fail);
});
it('renders branch and mr, if state has both', done => {
vm.$store.state.currentBranchId = TEST_BRANCH_ID;
vm.$store.state.currentMergeRequestId = TEST_MR_ID;
vm.$nextTick()
.then(() => {
expect(trimText(vm.$el.textContent)).toEqual(`${TEST_BRANCH_ID} !${TEST_MR_ID}`);
})
.then(done)
.catch(done.fail);
});
it('shows icons', () => {
expect(findBranchIcon()).toBeTruthy();
expect(findMRIcon()).toBeTruthy();
});
}); });
it('renders branch and mr, if state has both', done => { describe('with showMergeRequests false', () => {
vm.$store.state.currentBranchId = TEST_BRANCH_ID; beforeEach(() => {
vm.$store.state.currentMergeRequestId = TEST_MR_ID; createComponent({ showMergeRequests: false });
});
it('shows single empty placeholder, if state is falsey', () => {
expect(trimText(vm.$el.textContent)).toEqual('-');
});
vm.$nextTick() it('shows only branch icon', () => {
.then(() => { expect(findBranchIcon()).toBeTruthy();
expect(trimText(vm.$el.textContent)).toEqual(`${TEST_BRANCH_ID} !${TEST_MR_ID}`); expect(findMRIcon()).toBe(null);
}) });
.then(done)
.catch(done.fail);
}); });
}); });
...@@ -3,6 +3,9 @@ import Vue from 'vue'; ...@@ -3,6 +3,9 @@ import Vue from 'vue';
import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import store from '~/ide/stores'; import store from '~/ide/stores';
import NavDropdown from '~/ide/components/nav_dropdown.vue'; import NavDropdown from '~/ide/components/nav_dropdown.vue';
import { PERMISSION_READ_MR } from '~/ide/constants';
const TEST_PROJECT_ID = 'lorem-ipsum';
describe('IDE NavDropdown', () => { describe('IDE NavDropdown', () => {
const Component = Vue.extend(NavDropdown); const Component = Vue.extend(NavDropdown);
...@@ -10,6 +13,12 @@ describe('IDE NavDropdown', () => { ...@@ -10,6 +13,12 @@ describe('IDE NavDropdown', () => {
let $dropdown; let $dropdown;
beforeEach(() => { beforeEach(() => {
store.state.currentProjectId = TEST_PROJECT_ID;
Vue.set(store.state.projects, TEST_PROJECT_ID, {
userPermissions: {
[PERMISSION_READ_MR]: true,
},
});
vm = mountComponentWithStore(Component, { store }); vm = mountComponentWithStore(Component, { store });
$dropdown = $(vm.$el); $dropdown = $(vm.$el);
...@@ -21,6 +30,9 @@ describe('IDE NavDropdown', () => { ...@@ -21,6 +30,9 @@ describe('IDE NavDropdown', () => {
vm.$destroy(); vm.$destroy();
}); });
const findIcon = name => vm.$el.querySelector(`.ic-${name}`);
const findMRIcon = () => findIcon('merge-request');
it('renders nothing initially', () => { it('renders nothing initially', () => {
expect(vm.$el).not.toContainElement('.ide-nav-form'); expect(vm.$el).not.toContainElement('.ide-nav-form');
}); });
...@@ -47,4 +59,22 @@ describe('IDE NavDropdown', () => { ...@@ -47,4 +59,22 @@ describe('IDE NavDropdown', () => {
.then(done) .then(done)
.catch(done.fail); .catch(done.fail);
}); });
it('renders merge request icon', () => {
expect(findMRIcon()).not.toBeNull();
});
describe('when user cannot read merge requests', () => {
beforeEach(done => {
store.state.projects[TEST_PROJECT_ID].userPermissions = {};
vm.$nextTick()
.then(done)
.catch(done.fail);
});
it('does not render merge requests', () => {
expect(findMRIcon()).toBeNull();
});
});
}); });
...@@ -8,7 +8,7 @@ import actions, { ...@@ -8,7 +8,7 @@ import actions, {
openMergeRequest, openMergeRequest,
} from '~/ide/stores/actions/merge_request'; } from '~/ide/stores/actions/merge_request';
import service from '~/ide/services'; import service from '~/ide/services';
import { activityBarViews } from '~/ide/constants'; import { activityBarViews, PERMISSION_READ_MR } from '~/ide/constants';
import { resetStore } from '../../helpers'; import { resetStore } from '../../helpers';
const TEST_PROJECT = 'abcproject'; const TEST_PROJECT = 'abcproject';
...@@ -23,6 +23,9 @@ describe('IDE store merge request actions', () => { ...@@ -23,6 +23,9 @@ describe('IDE store merge request actions', () => {
store.state.projects[TEST_PROJECT] = { store.state.projects[TEST_PROJECT] = {
id: TEST_PROJECT_ID, id: TEST_PROJECT_ID,
mergeRequests: {}, mergeRequests: {},
userPermissions: {
[PERMISSION_READ_MR]: true,
},
}; };
}); });
...@@ -79,6 +82,19 @@ describe('IDE store merge request actions', () => { ...@@ -79,6 +82,19 @@ describe('IDE store merge request actions', () => {
}) })
.catch(done.fail); .catch(done.fail);
}); });
it('does nothing if user cannot read MRs', done => {
store.state.projects[TEST_PROJECT].userPermissions[PERMISSION_READ_MR] = false;
store
.dispatch('getMergeRequestsForBranch', { projectId: TEST_PROJECT, branchId: 'bar' })
.then(() => {
expect(service.getProjectMergeRequests).not.toHaveBeenCalled();
expect(store.state.currentMergeRequestId).toBe('');
})
.then(done)
.catch(done.fail);
});
}); });
describe('no merge requests for branch available case', () => { describe('no merge requests for branch available case', () => {
......
...@@ -7,7 +7,7 @@ import eventHub from '~/ide/eventhub'; ...@@ -7,7 +7,7 @@ import eventHub from '~/ide/eventhub';
import consts from '~/ide/stores/modules/commit/constants'; import consts from '~/ide/stores/modules/commit/constants';
import * as mutationTypes from '~/ide/stores/modules/commit/mutation_types'; import * as mutationTypes from '~/ide/stores/modules/commit/mutation_types';
import * as actions from '~/ide/stores/modules/commit/actions'; import * as actions from '~/ide/stores/modules/commit/actions';
import { commitActionTypes } from '~/ide/constants'; import { commitActionTypes, PERMISSION_CREATE_MR } from '~/ide/constants';
import testAction from '../../../../helpers/vuex_action_helper'; import testAction from '../../../../helpers/vuex_action_helper';
const TEST_COMMIT_SHA = '123456789'; const TEST_COMMIT_SHA = '123456789';
...@@ -313,6 +313,9 @@ describe('IDE commit module actions', () => { ...@@ -313,6 +313,9 @@ describe('IDE commit module actions', () => {
}, },
}, },
}, },
userPermissions: {
[PERMISSION_CREATE_MR]: true,
},
}, },
}, },
}); });
......
...@@ -19,12 +19,12 @@ describe 'projects/tree/_tree_header' do ...@@ -19,12 +19,12 @@ describe 'projects/tree/_tree_header' do
allow(view).to receive(:can_collaborate_with_project?) { true } allow(view).to receive(:can_collaborate_with_project?) { true }
end end
it 'does not render the WebIDE button when user cannot create fork or cannot open MR' do it 'renders the WebIDE button when user can collaborate but not create fork or MR' do
allow(view).to receive(:can?) { false } allow(view).to receive(:can?) { false }
render render
expect(rendered).not_to have_link('Web IDE') expect(rendered).to have_link('Web IDE')
end end
it 'renders the WebIDE button when user can create fork and can open MR in project' do it 'renders the WebIDE button when user can create fork and can open MR in project' do
...@@ -43,4 +43,13 @@ describe 'projects/tree/_tree_header' do ...@@ -43,4 +43,13 @@ describe 'projects/tree/_tree_header' do
expect(rendered).to have_link('Web IDE', href: '#modal-confirm-fork') expect(rendered).to have_link('Web IDE', href: '#modal-confirm-fork')
end end
it 'does not render the WebIDE button when user cannot collaborate or create mr' do
allow(view).to receive(:can?) { false }
allow(view).to receive(:can_collaborate_with_project?) { false }
render
expect(rendered).not_to have_link('Web IDE')
end
end end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment