Commit 33116c22 authored by Sean McGivern's avatar Sean McGivern

Merge branch 'ide-merge-request-info' into 'master'

Added merge request info to Web IDE sidebar

Closes #45187

See merge request gitlab-org/gitlab-ce!19860
parents b0fa01fc ed85787f
...@@ -100,12 +100,12 @@ const Api = { ...@@ -100,12 +100,12 @@ const Api = {
}, },
// Return Merge Request for project // Return Merge Request for project
mergeRequest(projectPath, mergeRequestId) { mergeRequest(projectPath, mergeRequestId, params = {}) {
const url = Api.buildUrl(Api.mergeRequestPath) const url = Api.buildUrl(Api.mergeRequestPath)
.replace(':id', encodeURIComponent(projectPath)) .replace(':id', encodeURIComponent(projectPath))
.replace(':mrid', mergeRequestId); .replace(':mrid', mergeRequestId);
return axios.get(url); return axios.get(url, { params });
}, },
mergeRequests(params = {}) { mergeRequests(params = {}) {
......
<script>
import { mapGetters } from 'vuex';
import Icon from '../../../vue_shared/components/icon.vue';
import TitleComponent from '../../../issue_show/components/title.vue';
import DescriptionComponent from '../../../issue_show/components/description.vue';
export default {
components: {
Icon,
TitleComponent,
DescriptionComponent,
},
computed: {
...mapGetters(['currentMergeRequest']),
},
};
</script>
<template>
<div class="ide-merge-request-info h-100 d-flex flex-column">
<div class="detail-page-header">
<icon
name="git-merge"
class="align-self-center append-right-8"
/>
<strong>
!{{ currentMergeRequest.iid }}
</strong>
</div>
<div class="issuable-details">
<title-component
:issuable-ref="currentMergeRequest.iid"
:title-html="currentMergeRequest.title_html"
:title-text="currentMergeRequest.title"
/>
<description-component
:description-html="currentMergeRequest.description_html"
:description-text="currentMergeRequest.description"
:can-update="false"
/>
</div>
</div>
</template>
...@@ -5,6 +5,7 @@ import Icon from '../../../vue_shared/components/icon.vue'; ...@@ -5,6 +5,7 @@ import Icon from '../../../vue_shared/components/icon.vue';
import { rightSidebarViews } from '../../constants'; import { rightSidebarViews } from '../../constants';
import PipelinesList from '../pipelines/list.vue'; import PipelinesList from '../pipelines/list.vue';
import JobsDetail from '../jobs/detail.vue'; import JobsDetail from '../jobs/detail.vue';
import MergeRequestInfo from '../merge_requests/info.vue';
import ResizablePanel from '../resizable_panel.vue'; import ResizablePanel from '../resizable_panel.vue';
export default { export default {
...@@ -16,9 +17,10 @@ export default { ...@@ -16,9 +17,10 @@ export default {
PipelinesList, PipelinesList,
JobsDetail, JobsDetail,
ResizablePanel, ResizablePanel,
MergeRequestInfo,
}, },
computed: { computed: {
...mapState(['rightPane']), ...mapState(['rightPane', 'currentMergeRequestId']),
pipelinesActive() { pipelinesActive() {
return ( return (
this.rightPane === rightSidebarViews.pipelines || this.rightPane === rightSidebarViews.pipelines ||
...@@ -54,10 +56,33 @@ export default { ...@@ -54,10 +56,33 @@ export default {
</resizable-panel> </resizable-panel>
<nav class="ide-activity-bar"> <nav class="ide-activity-bar">
<ul class="list-unstyled"> <ul class="list-unstyled">
<li
v-if="currentMergeRequestId"
>
<button
v-tooltip
:title="__('Merge Request')"
:aria-label="__('Merge Request')"
:class="{
active: rightPane === $options.rightSidebarViews.mergeRequestInfo
}"
data-container="body"
data-placement="left"
class="ide-sidebar-link is-right"
type="button"
@click="clickTab($event, $options.rightSidebarViews.mergeRequestInfo)"
>
<icon
:size="16"
name="text-description"
/>
</button>
</li>
<li> <li>
<button <button
v-tooltip v-tooltip
:title="__('Pipelines')" :title="__('Pipelines')"
:aria-label="__('Pipelines')"
:class="{ :class="{
active: pipelinesActive active: pipelinesActive
}" }"
......
...@@ -31,6 +31,7 @@ export const diffModes = { ...@@ -31,6 +31,7 @@ export const diffModes = {
export const rightSidebarViews = { export const rightSidebarViews = {
pipelines: 'pipelines-list', pipelines: 'pipelines-list',
jobsDetail: 'jobs-detail', jobsDetail: 'jobs-detail',
mergeRequestInfo: 'merge-request-info',
}; };
export const stageKeys = { export const stageKeys = {
......
...@@ -40,8 +40,8 @@ export default { ...@@ -40,8 +40,8 @@ export default {
getProjectData(namespace, project) { getProjectData(namespace, project) {
return Api.project(`${namespace}/${project}`); return Api.project(`${namespace}/${project}`);
}, },
getProjectMergeRequestData(projectId, mergeRequestId) { getProjectMergeRequestData(projectId, mergeRequestId, params = {}) {
return Api.mergeRequest(projectId, mergeRequestId); return Api.mergeRequest(projectId, mergeRequestId, params);
}, },
getProjectMergeRequestChanges(projectId, mergeRequestId) { getProjectMergeRequestChanges(projectId, mergeRequestId) {
return Api.mergeRequestChanges(projectId, mergeRequestId); return Api.mergeRequestChanges(projectId, mergeRequestId);
......
...@@ -9,7 +9,7 @@ export const getMergeRequestData = ( ...@@ -9,7 +9,7 @@ export const getMergeRequestData = (
new Promise((resolve, reject) => { new Promise((resolve, reject) => {
if (!state.projects[projectId].mergeRequests[mergeRequestId] || force) { if (!state.projects[projectId].mergeRequests[mergeRequestId] || force) {
service service
.getProjectMergeRequestData(projectId, mergeRequestId) .getProjectMergeRequestData(projectId, mergeRequestId, { render_html: true })
.then(({ data }) => { .then(({ data }) => {
commit(types.SET_MERGE_REQUEST, { commit(types.SET_MERGE_REQUEST, {
projectPath: projectId, projectPath: projectId,
......
<script> <script>
import animateMixin from '../mixins/animate'; import animateMixin from '../mixins/animate';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
import tooltip from '../../vue_shared/directives/tooltip'; import tooltip from '../../vue_shared/directives/tooltip';
import { spriteIcon } from '../../lib/utils/common_utils'; import { spriteIcon } from '../../lib/utils/common_utils';
export default { export default {
directives: { directives: {
tooltip, tooltip,
},
mixins: [animateMixin],
props: {
issuableRef: {
type: [String, Number],
required: true,
}, },
mixins: [animateMixin], canUpdate: {
props: { required: false,
issuableRef: { type: Boolean,
type: String, default: false,
required: true,
},
canUpdate: {
required: false,
type: Boolean,
default: false,
},
titleHtml: {
type: String,
required: true,
},
titleText: {
type: String,
required: true,
},
showInlineEditButton: {
type: Boolean,
required: false,
default: false,
},
}, },
data() { titleHtml: {
return { type: String,
preAnimation: false, required: true,
pulseAnimation: false,
titleEl: document.querySelector('title'),
};
}, },
computed: { titleText: {
pencilIcon() { type: String,
return spriteIcon('pencil', 'link-highlight'); required: true,
},
}, },
watch: { showInlineEditButton: {
titleHtml() { type: Boolean,
this.setPageTitle(); required: false,
this.animateChange(); default: false,
},
}, },
methods: { },
setPageTitle() { data() {
const currentPageTitleScope = this.titleEl.innerText.split('·'); return {
currentPageTitleScope[0] = `${this.titleText} (${this.issuableRef}) `; preAnimation: false,
this.titleEl.textContent = currentPageTitleScope.join('·'); pulseAnimation: false,
}, titleEl: document.querySelector('title'),
edit() { };
eventHub.$emit('open.form'); },
}, computed: {
pencilIcon() {
return spriteIcon('pencil', 'link-highlight');
}, },
}; },
watch: {
titleHtml() {
this.setPageTitle();
this.animateChange();
},
},
methods: {
setPageTitle() {
const currentPageTitleScope = this.titleEl.innerText.split('·');
currentPageTitleScope[0] = `${this.titleText} (${this.issuableRef}) `;
this.titleEl.textContent = currentPageTitleScope.join('·');
},
edit() {
eventHub.$emit('open.form');
},
},
};
</script> </script>
<template> <template>
......
...@@ -1329,3 +1329,14 @@ ...@@ -1329,3 +1329,14 @@
line-height: 16px; line-height: 16px;
color: $gl-text-color-secondary; color: $gl-text-color-secondary;
} }
.ide-merge-request-info {
.detail-page-header {
line-height: initial;
min-height: 38px;
}
.issuable-details {
overflow: auto;
}
}
---
title: Display merge request title & description in Web IDE
merge_request:
author:
type: added
...@@ -358,6 +358,7 @@ Parameters: ...@@ -358,6 +358,7 @@ Parameters:
- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user - `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user
- `merge_request_iid` (required) - The internal ID of the merge request - `merge_request_iid` (required) - The internal ID of the merge request
- `render_html` (optional) - If `true` response includes rendered HTML for title and description
```json ```json
{ {
......
...@@ -532,6 +532,12 @@ module API ...@@ -532,6 +532,12 @@ module API
end end
class MergeRequestBasic < ProjectEntity class MergeRequestBasic < ProjectEntity
expose :title_html, if: -> (_, options) { options[:render_html] } do |entity|
MarkupHelper.markdown_field(entity, :title)
end
expose :description_html, if: -> (_, options) { options[:render_html] } do |entity|
MarkupHelper.markdown_field(entity, :description)
end
expose :target_branch, :source_branch expose :target_branch, :source_branch
expose :upvotes do |merge_request, options| expose :upvotes do |merge_request, options|
if options[:issuable_metadata] if options[:issuable_metadata]
......
...@@ -232,6 +232,7 @@ module API ...@@ -232,6 +232,7 @@ module API
params do params do
requires :merge_request_iid, type: Integer, desc: 'The IID of a merge request' requires :merge_request_iid, type: Integer, desc: 'The IID of a merge request'
optional :render_html, type: Boolean, desc: 'Returns the description and title rendered HTML'
end end
desc 'Get a single merge request' do desc 'Get a single merge request' do
success Entities::MergeRequest success Entities::MergeRequest
...@@ -239,7 +240,7 @@ module API ...@@ -239,7 +240,7 @@ module API
get ':id/merge_requests/:merge_request_iid' do get ':id/merge_requests/:merge_request_iid' do
merge_request = find_merge_request_with_access(params[:merge_request_iid]) merge_request = find_merge_request_with_access(params[:merge_request_iid])
present merge_request, with: Entities::MergeRequest, current_user: current_user, project: user_project present merge_request, with: Entities::MergeRequest, current_user: current_user, project: user_project, render_html: params[:render_html]
end end
desc 'Get the participants of a merge request' do desc 'Get the participants of a merge request' do
......
...@@ -18,7 +18,7 @@ ...@@ -18,7 +18,7 @@
"webpack-prod": "NODE_ENV=production webpack --config config/webpack.config.js" "webpack-prod": "NODE_ENV=production webpack --config config/webpack.config.js"
}, },
"dependencies": { "dependencies": {
"@gitlab-org/gitlab-svgs": "^1.24.0", "@gitlab-org/gitlab-svgs": "^1.25.0",
"autosize": "^4.0.0", "autosize": "^4.0.0",
"axios": "^0.17.1", "axios": "^0.17.1",
"babel-core": "^6.26.3", "babel-core": "^6.26.3",
......
import Vue from 'vue';
import '~/behaviors/markdown/render_gfm';
import { createStore } from '~/ide/stores';
import Info from '~/ide/components/merge_requests/info.vue';
import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper';
describe('IDE merge request details', () => {
let Component;
let vm;
beforeAll(() => {
Component = Vue.extend(Info);
});
beforeEach(() => {
const store = createStore();
store.state.currentProjectId = 'gitlab-ce';
store.state.currentMergeRequestId = 1;
store.state.projects['gitlab-ce'] = {
mergeRequests: {
1: {
iid: 1,
title: 'Testing',
title_html: '<span class="title-html">Testing</span>',
description: 'Description',
description_html: '<p class="description-html">Description HTML</p>',
},
},
};
vm = createComponentWithStore(Component, store).$mount();
});
afterEach(() => {
vm.$destroy();
});
it('renders merge request IID', () => {
expect(vm.$el.querySelector('.detail-page-header').textContent).toContain('!1');
});
it('renders title as HTML', () => {
expect(vm.$el.querySelector('.title-html')).not.toBe(null);
expect(vm.$el.querySelector('.title').textContent).toContain('Testing');
});
it('renders description as HTML', () => {
expect(vm.$el.querySelector('.description-html')).not.toBe(null);
expect(vm.$el.querySelector('.description').textContent).toContain('Description HTML');
});
});
import Vue from 'vue';
import '~/behaviors/markdown/render_gfm';
import { createStore } from '~/ide/stores';
import RightPane from '~/ide/components/panes/right.vue';
import { rightSidebarViews } from '~/ide/constants';
import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper';
describe('IDE right pane', () => {
let Component;
let vm;
beforeAll(() => {
Component = Vue.extend(RightPane);
});
beforeEach(() => {
const store = createStore();
vm = createComponentWithStore(Component, store).$mount();
});
afterEach(() => {
vm.$destroy();
});
describe('active', () => {
it('renders merge request button as active', done => {
vm.$store.state.rightPane = rightSidebarViews.mergeRequestInfo;
vm.$store.state.currentMergeRequestId = '123';
vm.$store.state.currentProjectId = 'gitlab-ce';
vm.$store.state.currentMergeRequestId = 1;
vm.$store.state.projects['gitlab-ce'] = {
mergeRequests: {
1: {
iid: 1,
title: 'Testing',
title_html: '<span class="title-html">Testing</span>',
description: 'Description',
description_html: '<p class="description-html">Description HTML</p>',
},
},
};
vm.$nextTick(() => {
expect(vm.$el.querySelector('.ide-sidebar-link.active')).not.toBe(null);
expect(
vm.$el.querySelector('.ide-sidebar-link.active').getAttribute('data-original-title'),
).toBe('Merge Request');
done();
});
});
});
describe('click', () => {
beforeEach(() => {
spyOn(vm, 'setRightPane');
});
it('sets view to merge request', done => {
vm.$store.state.currentMergeRequestId = '123';
vm.$nextTick(() => {
vm.$el.querySelector('.ide-sidebar-link').click();
expect(vm.setRightPane).toHaveBeenCalledWith(rightSidebarViews.mergeRequestInfo);
done();
});
});
});
});
...@@ -39,7 +39,9 @@ describe('IDE store merge request actions', () => { ...@@ -39,7 +39,9 @@ describe('IDE store merge request actions', () => {
store store
.dispatch('getMergeRequestData', { projectId: 'abcproject', mergeRequestId: 1 }) .dispatch('getMergeRequestData', { projectId: 'abcproject', mergeRequestId: 1 })
.then(() => { .then(() => {
expect(service.getProjectMergeRequestData).toHaveBeenCalledWith('abcproject', 1); expect(service.getProjectMergeRequestData).toHaveBeenCalledWith('abcproject', 1, {
render_html: true,
});
done(); done();
}) })
......
...@@ -306,6 +306,14 @@ describe API::MergeRequests do ...@@ -306,6 +306,14 @@ describe API::MergeRequests do
expect(json_response['changes_count']).to eq(merge_request.merge_request_diff.real_size) expect(json_response['changes_count']).to eq(merge_request.merge_request_diff.real_size)
end end
it 'exposes description and title html when render_html is true' do
get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user), render_html: true
expect(response).to have_gitlab_http_status(200)
expect(json_response).to include('title_html', 'description_html')
end
context 'merge_request_metrics' do context 'merge_request_metrics' do
before do before do
merge_request.metrics.update!(merged_by: user, merge_request.metrics.update!(merged_by: user,
......
...@@ -78,9 +78,9 @@ ...@@ -78,9 +78,9 @@
lodash "^4.2.0" lodash "^4.2.0"
to-fast-properties "^2.0.0" to-fast-properties "^2.0.0"
"@gitlab-org/gitlab-svgs@^1.24.0": "@gitlab-org/gitlab-svgs@^1.25.0":
version "1.24.0" version "1.25.0"
resolved "https://registry.yarnpkg.com/@gitlab-org/gitlab-svgs/-/gitlab-svgs-1.24.0.tgz#3b2b58c5a1d58ce784f486d648bd87cbbb06cedc" resolved "https://registry.yarnpkg.com/@gitlab-org/gitlab-svgs/-/gitlab-svgs-1.25.0.tgz#1a82b1be43e1a46e6b0767ef46f26f5fd6bbd101"
"@sindresorhus/is@^0.7.0": "@sindresorhus/is@^0.7.0":
version "0.7.0" version "0.7.0"
......
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