summaryrefslogtreecommitdiff
path: root/app/assets/javascripts
diff options
context:
space:
mode:
authorPhil Hughes <me@iamphill.com>2018-04-03 10:59:29 +0100
committerPhil Hughes <me@iamphill.com>2018-04-03 10:59:29 +0100
commitc0dddb511c3bedc9b07df97739a27e07354b2242 (patch)
tree3b1f6042c7fd274579aa82c81a3d5894b6e53d90 /app/assets/javascripts
parent6bec91bfc9ec15556e833f4d8f441328d135638e (diff)
parent8dca091ff7f04bb92a7835ebeff783b7f0ef76cd (diff)
downloadgitlab-ce-c0dddb511c3bedc9b07df97739a27e07354b2242.tar.gz
Merge branch 'master' into ide-pending-tab
Diffstat (limited to 'app/assets/javascripts')
-rw-r--r--app/assets/javascripts/api.js146
-rw-r--r--app/assets/javascripts/behaviors/markdown/render_math.js2
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js19
-rw-r--r--app/assets/javascripts/ide/components/changed_file_icon.vue34
-rw-r--r--app/assets/javascripts/ide/components/editor_mode_dropdown.vue87
-rw-r--r--app/assets/javascripts/ide/components/ide.vue3
-rw-r--r--app/assets/javascripts/ide/components/mr_file_icon.vue23
-rw-r--r--app/assets/javascripts/ide/components/repo_editor.vue30
-rw-r--r--app/assets/javascripts/ide/components/repo_file.vue21
-rw-r--r--app/assets/javascripts/ide/components/repo_file_status_icon.vue40
-rw-r--r--app/assets/javascripts/ide/components/repo_tabs.vue6
-rw-r--r--app/assets/javascripts/ide/ide_router.js56
-rw-r--r--app/assets/javascripts/ide/lib/common/model.js13
-rw-r--r--app/assets/javascripts/ide/lib/editor.js20
-rw-r--r--app/assets/javascripts/ide/services/index.js25
-rw-r--r--app/assets/javascripts/ide/stores/actions.js1
-rw-r--r--app/assets/javascripts/ide/stores/actions/file.js52
-rw-r--r--app/assets/javascripts/ide/stores/actions/merge_request.js84
-rw-r--r--app/assets/javascripts/ide/stores/actions/tree.js98
-rw-r--r--app/assets/javascripts/ide/stores/getters.js15
-rw-r--r--app/assets/javascripts/ide/stores/mutation_types.js8
-rw-r--r--app/assets/javascripts/ide/stores/mutations.js11
-rw-r--r--app/assets/javascripts/ide/stores/mutations/file.js12
-rw-r--r--app/assets/javascripts/ide/stores/mutations/merge_request.js33
-rw-r--r--app/assets/javascripts/ide/stores/mutations/project.js1
-rw-r--r--app/assets/javascripts/ide/stores/state.js1
-rw-r--r--app/assets/javascripts/ide/stores/utils.js22
-rw-r--r--app/assets/javascripts/jobs/components/sidebar_detail_row.vue24
-rw-r--r--app/assets/javascripts/jobs/components/sidebar_details_block.vue27
-rw-r--r--app/assets/javascripts/jobs/job_details_bundle.js1
-rw-r--r--app/assets/javascripts/lib/utils/url_utility.js10
-rw-r--r--app/assets/javascripts/notes/components/noteable_discussion.vue6
-rw-r--r--app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue8
-rw-r--r--app/assets/javascripts/pages/milestones/shared/promote_milestone_modal_init.js2
-rw-r--r--app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue17
-rw-r--r--app/assets/javascripts/pages/projects/labels/index/index.js2
-rw-r--r--app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue101
-rw-r--r--app/assets/javascripts/sidebar/components/confidential/edit_form.vue53
-rw-r--r--app/assets/javascripts/sidebar/components/confidential/edit_form_buttons.vue21
-rw-r--r--app/assets/javascripts/sidebar/components/lock/edit_form.vue66
-rw-r--r--app/assets/javascripts/sidebar/components/lock/edit_form_buttons.vue23
-rw-r--r--app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue114
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue97
43 files changed, 977 insertions, 458 deletions
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js
index cbcefb2c18f..8ad3d18b302 100644
--- a/app/assets/javascripts/api.js
+++ b/app/assets/javascripts/api.js
@@ -10,6 +10,9 @@ const Api = {
projectsPath: '/api/:version/projects.json',
projectPath: '/api/:version/projects/:id',
projectLabelsPath: '/:namespace_path/:project_path/labels',
+ mergeRequestPath: '/api/:version/projects/:id/merge_requests/:mrid',
+ mergeRequestChangesPath: '/api/:version/projects/:id/merge_requests/:mrid/changes',
+ mergeRequestVersionsPath: '/api/:version/projects/:id/merge_requests/:mrid/versions',
groupLabelsPath: '/groups/:namespace_path/-/labels',
licensePath: '/api/:version/templates/licenses/:key',
gitignorePath: '/api/:version/templates/gitignores/:key',
@@ -22,25 +25,27 @@ const Api = {
createBranchPath: '/api/:version/projects/:id/repository/branches',
group(groupId, callback) {
- const url = Api.buildUrl(Api.groupPath)
- .replace(':id', groupId);
- return axios.get(url)
- .then(({ data }) => {
- callback(data);
+ const url = Api.buildUrl(Api.groupPath).replace(':id', groupId);
+ return axios.get(url).then(({ data }) => {
+ callback(data);
- return data;
- });
+ return data;
+ });
},
// Return groups list. Filtered by query
groups(query, options, callback = $.noop) {
const url = Api.buildUrl(Api.groupsPath);
- return axios.get(url, {
- params: Object.assign({
- search: query,
- per_page: 20,
- }, options),
- })
+ return axios
+ .get(url, {
+ params: Object.assign(
+ {
+ search: query,
+ per_page: 20,
+ },
+ options,
+ ),
+ })
.then(({ data }) => {
callback(data);
@@ -51,12 +56,13 @@ const Api = {
// Return namespaces list. Filtered by query
namespaces(query, callback) {
const url = Api.buildUrl(Api.namespacesPath);
- return axios.get(url, {
- params: {
- search: query,
- per_page: 20,
- },
- })
+ return axios
+ .get(url, {
+ params: {
+ search: query,
+ per_page: 20,
+ },
+ })
.then(({ data }) => callback(data));
},
@@ -73,9 +79,10 @@ const Api = {
defaults.membership = true;
}
- return axios.get(url, {
- params: Object.assign(defaults, options),
- })
+ return axios
+ .get(url, {
+ params: Object.assign(defaults, options),
+ })
.then(({ data }) => {
callback(data);
@@ -85,8 +92,32 @@ const Api = {
// Return single project
project(projectPath) {
- const url = Api.buildUrl(Api.projectPath)
- .replace(':id', encodeURIComponent(projectPath));
+ const url = Api.buildUrl(Api.projectPath).replace(':id', encodeURIComponent(projectPath));
+
+ return axios.get(url);
+ },
+
+ // Return Merge Request for project
+ mergeRequest(projectPath, mergeRequestId) {
+ const url = Api.buildUrl(Api.mergeRequestPath)
+ .replace(':id', encodeURIComponent(projectPath))
+ .replace(':mrid', mergeRequestId);
+
+ return axios.get(url);
+ },
+
+ mergeRequestChanges(projectPath, mergeRequestId) {
+ const url = Api.buildUrl(Api.mergeRequestChangesPath)
+ .replace(':id', encodeURIComponent(projectPath))
+ .replace(':mrid', mergeRequestId);
+
+ return axios.get(url);
+ },
+
+ mergeRequestVersions(projectPath, mergeRequestId) {
+ const url = Api.buildUrl(Api.mergeRequestVersionsPath)
+ .replace(':id', encodeURIComponent(projectPath))
+ .replace(':mrid', mergeRequestId);
return axios.get(url);
},
@@ -102,30 +133,30 @@ const Api = {
url = Api.buildUrl(Api.groupLabelsPath).replace(':namespace_path', namespacePath);
}
- return axios.post(url, {
- label: data,
- })
+ return axios
+ .post(url, {
+ label: data,
+ })
.then(res => callback(res.data))
.catch(e => callback(e.response.data));
},
// Return group projects list. Filtered by query
groupProjects(groupId, query, callback) {
- const url = Api.buildUrl(Api.groupProjectsPath)
- .replace(':id', groupId);
- return axios.get(url, {
- params: {
- search: query,
- per_page: 20,
- },
- })
+ const url = Api.buildUrl(Api.groupProjectsPath).replace(':id', groupId);
+ return axios
+ .get(url, {
+ params: {
+ search: query,
+ per_page: 20,
+ },
+ })
.then(({ data }) => callback(data));
},
commitMultiple(id, data) {
// see https://docs.gitlab.com/ce/api/commits.html#create-a-commit-with-multiple-files-and-actions
- const url = Api.buildUrl(Api.commitPath)
- .replace(':id', encodeURIComponent(id));
+ const url = Api.buildUrl(Api.commitPath).replace(':id', encodeURIComponent(id));
return axios.post(url, JSON.stringify(data), {
headers: {
'Content-Type': 'application/json; charset=utf-8',
@@ -136,39 +167,34 @@ const Api = {
branchSingle(id, branch) {
const url = Api.buildUrl(Api.branchSinglePath)
.replace(':id', encodeURIComponent(id))
- .replace(':branch', branch);
+ .replace(':branch', encodeURIComponent(branch));
return axios.get(url);
},
// Return text for a specific license
licenseText(key, data, callback) {
- const url = Api.buildUrl(Api.licensePath)
- .replace(':key', key);
- return axios.get(url, {
- params: data,
- })
+ const url = Api.buildUrl(Api.licensePath).replace(':key', key);
+ return axios
+ .get(url, {
+ params: data,
+ })
.then(res => callback(res.data));
},
gitignoreText(key, callback) {
- const url = Api.buildUrl(Api.gitignorePath)
- .replace(':key', key);
- return axios.get(url)
- .then(({ data }) => callback(data));
+ const url = Api.buildUrl(Api.gitignorePath).replace(':key', key);
+ return axios.get(url).then(({ data }) => callback(data));
},
gitlabCiYml(key, callback) {
- const url = Api.buildUrl(Api.gitlabCiYmlPath)
- .replace(':key', key);
- return axios.get(url)
- .then(({ data }) => callback(data));
+ const url = Api.buildUrl(Api.gitlabCiYmlPath).replace(':key', key);
+ return axios.get(url).then(({ data }) => callback(data));
},
dockerfileYml(key, callback) {
const url = Api.buildUrl(Api.dockerfilePath).replace(':key', key);
- return axios.get(url)
- .then(({ data }) => callback(data));
+ return axios.get(url).then(({ data }) => callback(data));
},
issueTemplate(namespacePath, projectPath, key, type, callback) {
@@ -177,7 +203,8 @@ const Api = {
.replace(':type', type)
.replace(':project_path', projectPath)
.replace(':namespace_path', namespacePath);
- return axios.get(url)
+ return axios
+ .get(url)
.then(({ data }) => callback(null, data))
.catch(callback);
},
@@ -185,10 +212,13 @@ const Api = {
users(query, options) {
const url = Api.buildUrl(this.usersPath);
return axios.get(url, {
- params: Object.assign({
- search: query,
- per_page: 20,
- }, options),
+ params: Object.assign(
+ {
+ search: query,
+ per_page: 20,
+ },
+ options,
+ ),
});
},
diff --git a/app/assets/javascripts/behaviors/markdown/render_math.js b/app/assets/javascripts/behaviors/markdown/render_math.js
index 7dcf1aeed17..eb4e59d12b1 100644
--- a/app/assets/javascripts/behaviors/markdown/render_math.js
+++ b/app/assets/javascripts/behaviors/markdown/render_math.js
@@ -31,7 +31,7 @@ export default function renderMath($els) {
if (!$els.length) return;
Promise.all([
import(/* webpackChunkName: 'katex' */ 'katex'),
- import(/* webpackChunkName: 'katex' */ 'katex/dist/katex.css'),
+ import(/* webpackChunkName: 'katex' */ 'katex/dist/katex.min.css'),
]).then(([katex]) => {
renderWithKaTeX($els, katex);
}).catch(() => flash(__('An error occurred while rendering KaTeX')));
diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js
index 8259133c95b..7e9770a9ea2 100644
--- a/app/assets/javascripts/gfm_auto_complete.js
+++ b/app/assets/javascripts/gfm_auto_complete.js
@@ -54,6 +54,7 @@ class GfmAutoComplete {
alias: 'commands',
searchKey: 'search',
skipSpecialCharacterTest: true,
+ skipMarkdownCharacterTest: true,
data: GfmAutoComplete.defaultLoadingData,
displayTpl(value) {
if (GfmAutoComplete.isLoading(value)) return GfmAutoComplete.Loading.template;
@@ -376,15 +377,23 @@ class GfmAutoComplete {
return $.fn.atwho.default.callbacks.filter(query, data, searchKey);
},
beforeInsert(value) {
- let resultantValue = value;
+ let withoutAt = value.substring(1);
+ const at = value.charAt();
+
if (value && !this.setting.skipSpecialCharacterTest) {
- const withoutAt = value.substring(1);
- const regex = value.charAt() === '~' ? /\W|^\d+$/ : /\W/;
+ const regex = at === '~' ? /\W|^\d+$/ : /\W/;
if (withoutAt && regex.test(withoutAt)) {
- resultantValue = `${value.charAt()}"${withoutAt}"`;
+ withoutAt = `"${withoutAt}"`;
}
}
- return resultantValue;
+
+ // We can ignore this for quick actions because they are processed
+ // before Markdown.
+ if (!this.setting.skipMarkdownCharacterTest) {
+ withoutAt = withoutAt.replace(/([~\-_*`])/g, '\\$&');
+ }
+
+ return `${at}${withoutAt}`;
},
matcher(flag, subtext) {
const match = GfmAutoComplete.defaultMatcher(flag, subtext, this.app.controllers);
diff --git a/app/assets/javascripts/ide/components/changed_file_icon.vue b/app/assets/javascripts/ide/components/changed_file_icon.vue
index 0c54c992e51..037e3efb4ce 100644
--- a/app/assets/javascripts/ide/components/changed_file_icon.vue
+++ b/app/assets/javascripts/ide/components/changed_file_icon.vue
@@ -1,25 +1,25 @@
<script>
- import icon from '~/vue_shared/components/icon.vue';
+import icon from '~/vue_shared/components/icon.vue';
- export default {
- components: {
- icon,
+export default {
+ components: {
+ icon,
+ },
+ props: {
+ file: {
+ type: Object,
+ required: true,
},
- props: {
- file: {
- type: Object,
- required: true,
- },
+ },
+ computed: {
+ changedIcon() {
+ return this.file.tempFile ? 'file-addition' : 'file-modified';
},
- computed: {
- changedIcon() {
- return this.file.tempFile ? 'file-addition' : 'file-modified';
- },
- changedIconClass() {
- return `multi-${this.changedIcon}`;
- },
+ changedIconClass() {
+ return `multi-${this.changedIcon}`;
},
- };
+ },
+};
</script>
<template>
diff --git a/app/assets/javascripts/ide/components/editor_mode_dropdown.vue b/app/assets/javascripts/ide/components/editor_mode_dropdown.vue
index 170347881e0..0c44a755f56 100644
--- a/app/assets/javascripts/ide/components/editor_mode_dropdown.vue
+++ b/app/assets/javascripts/ide/components/editor_mode_dropdown.vue
@@ -1,31 +1,44 @@
<script>
- import Icon from '~/vue_shared/components/icon.vue';
+import Icon from '~/vue_shared/components/icon.vue';
+import { __, sprintf } from '~/locale';
- export default {
- components: {
- Icon,
+export default {
+ components: {
+ Icon,
+ },
+ props: {
+ hasChanges: {
+ type: Boolean,
+ required: false,
+ default: false,
},
- props: {
- hasChanges: {
- type: Boolean,
- required: false,
- default: false,
- },
- viewer: {
- type: String,
- required: true,
- },
- showShadow: {
- type: Boolean,
- required: true,
- },
+ mergeRequestId: {
+ type: String,
+ required: false,
+ default: '',
},
- methods: {
- changeMode(mode) {
- this.$emit('click', mode);
- },
+ viewer: {
+ type: String,
+ required: true,
},
- };
+ showShadow: {
+ type: Boolean,
+ required: true,
+ },
+ },
+ computed: {
+ mergeReviewLine() {
+ return sprintf(__('Reviewing (merge request !%{mergeRequestId})'), {
+ mergeRequestId: this.mergeRequestId,
+ });
+ },
+ },
+ methods: {
+ changeMode(mode) {
+ this.$emit('click', mode);
+ },
+ },
+};
</script>
<template>
@@ -43,7 +56,10 @@
}"
data-toggle="dropdown"
>
- <template v-if="viewer === 'editor'">
+ <template v-if="viewer === 'mrdiff' && mergeRequestId">
+ {{ mergeReviewLine }}
+ </template>
+ <template v-else-if="viewer === 'editor'">
{{ __('Editing') }}
</template>
<template v-else>
@@ -57,6 +73,29 @@
</button>
<div class="dropdown-menu dropdown-menu-selectable dropdown-open-left">
<ul>
+ <template v-if="mergeRequestId">
+ <li>
+ <a
+ href="#"
+ @click.prevent="changeMode('mrdiff')"
+ :class="{
+ 'is-active': viewer === 'mrdiff',
+ }"
+ >
+ <strong class="dropdown-menu-inner-title">
+ {{ mergeReviewLine }}
+ </strong>
+ <span class="dropdown-menu-inner-content">
+ {{ __('Compare changes with the merge request target branch') }}
+ </span>
+ </a>
+ </li>
+ <li
+ role="separator"
+ class="divider"
+ >
+ </li>
+ </template>
<li>
<a
href="#"
diff --git a/app/assets/javascripts/ide/components/ide.vue b/app/assets/javascripts/ide/components/ide.vue
index 1581e015c39..d22869466c9 100644
--- a/app/assets/javascripts/ide/components/ide.vue
+++ b/app/assets/javascripts/ide/components/ide.vue
@@ -31,7 +31,7 @@ export default {
},
},
computed: {
- ...mapState(['changedFiles', 'openFiles', 'viewer']),
+ ...mapState(['changedFiles', 'openFiles', 'viewer', 'currentMergeRequestId']),
...mapGetters(['activeFile', 'hasChanges']),
},
mounted() {
@@ -64,6 +64,7 @@ export default {
:files="openFiles"
:viewer="viewer"
:has-changes="hasChanges"
+ :merge-request-id="currentMergeRequestId"
/>
<repo-editor
class="multi-file-edit-pane-content"
diff --git a/app/assets/javascripts/ide/components/mr_file_icon.vue b/app/assets/javascripts/ide/components/mr_file_icon.vue
new file mode 100644
index 00000000000..8a440902dfc
--- /dev/null
+++ b/app/assets/javascripts/ide/components/mr_file_icon.vue
@@ -0,0 +1,23 @@
+<script>
+import icon from '~/vue_shared/components/icon.vue';
+import tooltip from '~/vue_shared/directives/tooltip';
+
+export default {
+ components: {
+ icon,
+ },
+ directives: {
+ tooltip,
+ },
+};
+</script>
+
+<template>
+ <icon
+ name="git-merge"
+ v-tooltip
+ title="__('Part of merge request changes')"
+ css-classes="ide-file-changed-icon"
+ :size="12"
+ />
+</template>
diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue
index 5cf1d9f09c6..0a61b49c950 100644
--- a/app/assets/javascripts/ide/components/repo_editor.vue
+++ b/app/assets/javascripts/ide/components/repo_editor.vue
@@ -1,6 +1,6 @@
<script>
/* global monaco */
-import { mapState, mapActions } from 'vuex';
+import { mapState, mapGetters, mapActions } from 'vuex';
import flash from '~/flash';
import monacoLoader from '../monaco_loader';
import Editor from '../lib/editor';
@@ -13,12 +13,8 @@ export default {
},
},
computed: {
- ...mapState([
- 'leftPanelCollapsed',
- 'rightPanelCollapsed',
- 'viewer',
- 'delayViewerUpdated',
- ]),
+ ...mapState(['leftPanelCollapsed', 'rightPanelCollapsed', 'viewer', 'delayViewerUpdated']),
+ ...mapGetters(['currentMergeRequest']),
shouldHideEditor() {
return this.file && this.file.binary && !this.file.raw;
},
@@ -68,7 +64,10 @@ export default {
this.editor.clearEditor();
- this.getRawFileData(this.file)
+ this.getRawFileData({
+ path: this.file.path,
+ baseSha: this.currentMergeRequest ? this.currentMergeRequest.baseCommitSha : '',
+ })
.then(() => {
const viewerPromise = this.delayViewerUpdated
? this.updateViewer(this.file.pending ? 'diff' : 'editor')
@@ -81,14 +80,7 @@ export default {
this.createEditorInstance();
})
.catch(err => {
- flash(
- 'Error setting up monaco. Please try again.',
- 'alert',
- document,
- null,
- false,
- true,
- );
+ flash('Error setting up monaco. Please try again.', 'alert', document, null, false, true);
throw err;
});
},
@@ -110,7 +102,11 @@ export default {
this.model = this.editor.createModel(this.file);
- this.editor.attachModel(this.model);
+ if (this.viewer === 'mrdiff') {
+ this.editor.attachMergeRequestModel(this.model);
+ } else {
+ this.editor.attachModel(this.model);
+ }
this.model.onChange(model => {
const { file } = model;
diff --git a/app/assets/javascripts/ide/components/repo_file.vue b/app/assets/javascripts/ide/components/repo_file.vue
index fedb13339b8..3b5068d4910 100644
--- a/app/assets/javascripts/ide/components/repo_file.vue
+++ b/app/assets/javascripts/ide/components/repo_file.vue
@@ -6,6 +6,7 @@ import router from '../ide_router';
import newDropdown from './new_dropdown/index.vue';
import fileStatusIcon from './repo_file_status_icon.vue';
import changedFileIcon from './changed_file_icon.vue';
+import mrFileIcon from './mr_file_icon.vue';
export default {
name: 'RepoFile',
@@ -15,6 +16,7 @@ export default {
fileStatusIcon,
fileIcon,
changedFileIcon,
+ mrFileIcon,
},
props: {
file: {
@@ -56,10 +58,7 @@ export default {
...mapActions(['toggleTreeOpen', 'updateDelayViewerUpdated']),
clickFile() {
// Manual Action if a tree is selected/opened
- if (
- this.isTree &&
- this.$router.currentRoute.path === `/project${this.file.url}`
- ) {
+ if (this.isTree && this.$router.currentRoute.path === `/project${this.file.url}`) {
this.toggleTreeOpen(this.file.path);
}
@@ -98,11 +97,15 @@ export default {
:file="file"
/>
</span>
- <changed-file-icon
- :file="file"
- v-if="file.changed || file.tempFile"
- class="prepend-top-5 pull-right"
- />
+ <span class="pull-right">
+ <mr-file-icon
+ v-if="file.mrChange"
+ />
+ <changed-file-icon
+ :file="file"
+ v-if="file.changed || file.tempFile"
+ />
+ </span>
<new-dropdown
v-if="isTree"
:project-id="file.projectId"
diff --git a/app/assets/javascripts/ide/components/repo_file_status_icon.vue b/app/assets/javascripts/ide/components/repo_file_status_icon.vue
index 25d311142d5..97589e116c5 100644
--- a/app/assets/javascripts/ide/components/repo_file_status_icon.vue
+++ b/app/assets/javascripts/ide/components/repo_file_status_icon.vue
@@ -1,27 +1,27 @@
<script>
- import icon from '~/vue_shared/components/icon.vue';
- import tooltip from '~/vue_shared/directives/tooltip';
- import '~/lib/utils/datetime_utility';
+import icon from '~/vue_shared/components/icon.vue';
+import tooltip from '~/vue_shared/directives/tooltip';
+import '~/lib/utils/datetime_utility';
- export default {
- components: {
- icon,
+export default {
+ components: {
+ icon,
+ },
+ directives: {
+ tooltip,
+ },
+ props: {
+ file: {
+ type: Object,
+ required: true,
},
- directives: {
- tooltip,
+ },
+ computed: {
+ lockTooltip() {
+ return `Locked by ${this.file.file_lock.user.name}`;
},
- props: {
- file: {
- type: Object,
- required: true,
- },
- },
- computed: {
- lockTooltip() {
- return `Locked by ${this.file.file_lock.user.name}`;
- },
- },
- };
+ },
+};
</script>
<template>
diff --git a/app/assets/javascripts/ide/components/repo_tabs.vue b/app/assets/javascripts/ide/components/repo_tabs.vue
index acbb43e867d..7bd646ba9b0 100644
--- a/app/assets/javascripts/ide/components/repo_tabs.vue
+++ b/app/assets/javascripts/ide/components/repo_tabs.vue
@@ -26,6 +26,11 @@ export default {
type: Boolean,
required: true,
},
+ mergeRequestId: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
data() {
return {
@@ -70,6 +75,7 @@ export default {
:viewer="viewer"
:show-shadow="showShadow"
:has-changes="hasChanges"
+ :merge-request-id="mergeRequestId"
@click="openFileViewer"
/>
</div>
diff --git a/app/assets/javascripts/ide/ide_router.js b/app/assets/javascripts/ide/ide_router.js
index a6013784677..20983666b4a 100644
--- a/app/assets/javascripts/ide/ide_router.js
+++ b/app/assets/javascripts/ide/ide_router.js
@@ -44,7 +44,7 @@ const router = new VueRouter({
component: EmptyRouterComponent,
},
{
- path: 'mr/:mrid',
+ path: 'merge_requests/:mrid',
component: EmptyRouterComponent,
},
],
@@ -98,6 +98,60 @@ router.beforeEach((to, from, next) => {
);
throw e;
});
+ } else if (to.params.mrid) {
+ store.dispatch('updateViewer', 'mrdiff');
+
+ store
+ .dispatch('getMergeRequestData', {
+ projectId: fullProjectId,
+ mergeRequestId: to.params.mrid,
+ })
+ .then(mr => {
+ store.dispatch('getBranchData', {
+ projectId: fullProjectId,
+ branchId: mr.source_branch,
+ });
+
+ return store.dispatch('getFiles', {
+ projectId: fullProjectId,
+ branchId: mr.source_branch,
+ });
+ })
+ .then(() =>
+ store.dispatch('getMergeRequestVersions', {
+ projectId: fullProjectId,
+ mergeRequestId: to.params.mrid,
+ }),
+ )
+ .then(() =>
+ store.dispatch('getMergeRequestChanges', {
+ projectId: fullProjectId,
+ mergeRequestId: to.params.mrid,
+ }),
+ )
+ .then(mrChanges => {
+ mrChanges.changes.forEach((change, ind) => {
+ const changeTreeEntry = store.state.entries[change.new_path];
+
+ if (changeTreeEntry) {
+ store.dispatch('setFileMrChange', {
+ file: changeTreeEntry,
+ mrChange: change,
+ });
+
+ if (ind < 10) {
+ store.dispatch('getFileData', {
+ path: change.new_path,
+ makeFileActive: ind === 0,
+ });
+ }
+ }
+ });
+ })
+ .catch(e => {
+ flash('Error while loading the merge request. Please try again.');
+ throw e;
+ });
}
})
.catch(e => {
diff --git a/app/assets/javascripts/ide/lib/common/model.js b/app/assets/javascripts/ide/lib/common/model.js
index e659a6868ba..e47adae99ed 100644
--- a/app/assets/javascripts/ide/lib/common/model.js
+++ b/app/assets/javascripts/ide/lib/common/model.js
@@ -21,6 +21,15 @@ export default class Model {
new this.monaco.Uri(null, null, this.file.key),
)),
);
+ if (this.file.mrChange) {
+ this.disposable.add(
+ (this.baseModel = this.monaco.editor.createModel(
+ this.file.baseRaw,
+ undefined,
+ new this.monaco.Uri(null, null, `target/${this.file.path}`),
+ )),
+ );
+ }
this.events = new Map();
@@ -55,6 +64,10 @@ export default class Model {
return this.originalModel;
}
+ getBaseModel() {
+ return this.baseModel;
+ }
+
setValue(value) {
this.getModel().setValue(value);
}
diff --git a/app/assets/javascripts/ide/lib/editor.js b/app/assets/javascripts/ide/lib/editor.js
index 887dd7e39b1..6b4ba30e086 100644
--- a/app/assets/javascripts/ide/lib/editor.js
+++ b/app/assets/javascripts/ide/lib/editor.js
@@ -109,11 +109,19 @@ export default class Editor {
if (this.dirtyDiffController) this.dirtyDiffController.reDecorate(model);
}
+ attachMergeRequestModel(model) {
+ this.instance.setModel({
+ original: model.getBaseModel(),
+ modified: model.getModel(),
+ });
+
+ this.monaco.editor.createDiffNavigator(this.instance, {
+ alwaysRevealFirst: true,
+ });
+ }
+
setupMonacoTheme() {
- this.monaco.editor.defineTheme(
- gitlabTheme.themeName,
- gitlabTheme.monacoTheme,
- );
+ this.monaco.editor.defineTheme(gitlabTheme.themeName, gitlabTheme.monacoTheme);
this.monaco.editor.setTheme('gitlab');
}
@@ -161,8 +169,6 @@ export default class Editor {
onPositionChange(cb) {
if (!this.instance.onDidChangeCursorPosition) return;
- this.disposable.add(
- this.instance.onDidChangeCursorPosition(e => cb(this.instance, e)),
- );
+ this.disposable.add(this.instance.onDidChangeCursorPosition(e => cb(this.instance, e)));
}
}
diff --git a/app/assets/javascripts/ide/services/index.js b/app/assets/javascripts/ide/services/index.js
index 5f1fb6cf843..a12e637616a 100644
--- a/app/assets/javascripts/ide/services/index.js
+++ b/app/assets/javascripts/ide/services/index.js
@@ -20,12 +20,35 @@ export default {
return Promise.resolve(file.raw);
}
- return Vue.http.get(file.rawPath, { params: { format: 'json' } })
+ return Vue.http.get(file.rawPath, { params: { format: 'json' } }).then(res => res.text());
+ },
+ getBaseRawFileData(file, sha) {
+ if (file.tempFile) {
+ return Promise.resolve(file.baseRaw);
+ }
+
+ if (file.baseRaw) {
+ return Promise.resolve(file.baseRaw);
+ }
+
+ return Vue.http
+ .get(file.rawPath.replace(`/raw/${file.branchId}/${file.path}`, `/raw/${sha}/${file.path}`), {
+ params: { format: 'json' },
+ })
.then(res => res.text());
},
getProjectData(namespace, project) {
return Api.project(`${namespace}/${project}`);
},
+ getProjectMergeRequestData(projectId, mergeRequestId) {
+ return Api.mergeRequest(projectId, mergeRequestId);
+ },
+ getProjectMergeRequestChanges(projectId, mergeRequestId) {
+ return Api.mergeRequestChanges(projectId, mergeRequestId);
+ },
+ getProjectMergeRequestVersions(projectId, mergeRequestId) {
+ return Api.mergeRequestVersions(projectId, mergeRequestId);
+ },
getBranchData(projectId, currentBranchId) {
return Api.branchSingle(projectId, currentBranchId);
},
diff --git a/app/assets/javascripts/ide/stores/actions.js b/app/assets/javascripts/ide/stores/actions.js
index 373ea3f5c08..c6ba679d99c 100644
--- a/app/assets/javascripts/ide/stores/actions.js
+++ b/app/assets/javascripts/ide/stores/actions.js
@@ -115,3 +115,4 @@ export const updateDelayViewerUpdated = ({ commit }, delay) => {
export * from './actions/tree';
export * from './actions/file';
export * from './actions/project';
+export * from './actions/merge_request';
diff --git a/app/assets/javascripts/ide/stores/actions/file.js b/app/assets/javascripts/ide/stores/actions/file.js
index 7aacec89116..6b034ea1e82 100644
--- a/app/assets/javascripts/ide/stores/actions/file.js
+++ b/app/assets/javascripts/ide/stores/actions/file.js
@@ -56,22 +56,21 @@ export const setFileActive = ({ commit, state, getters, dispatch }, path) => {
commit(types.SET_CURRENT_BRANCH, file.branchId);
};
-export const getFileData = ({ state, commit, dispatch }, file) => {
+export const getFileData = ({ state, commit, dispatch }, { path, makeFileActive = true }) => {
+ const file = state.entries[path];
commit(types.TOGGLE_LOADING, { entry: file });
-
return service
.getFileData(file.url)
.then(res => {
const pageTitle = decodeURI(normalizeHeaders(res.headers)['PAGE-TITLE']);
-
setPageTitle(pageTitle);
return res.json();
})
.then(data => {
commit(types.SET_FILE_DATA, { data, file });
- commit(types.TOGGLE_FILE_OPEN, file.path);
- dispatch('setFileActive', file.path);
+ commit(types.TOGGLE_FILE_OPEN, path);
+ if (makeFileActive) dispatch('setFileActive', path);
commit(types.TOGGLE_LOADING, { entry: file });
})
.catch(() => {
@@ -80,15 +79,40 @@ export const getFileData = ({ state, commit, dispatch }, file) => {
});
};
-export const getRawFileData = ({ commit, dispatch }, file) =>
- service
- .getRawFileData(file)
- .then(raw => {
- commit(types.SET_FILE_RAW_DATA, { file, raw });
- })
- .catch(() =>
- flash('Error loading file content. Please try again.', 'alert', document, null, false, true),
- );
+export const setFileMrChange = ({ state, commit }, { file, mrChange }) => {
+ commit(types.SET_FILE_MERGE_REQUEST_CHANGE, { file, mrChange });
+};
+
+export const getRawFileData = ({ state, commit, dispatch }, { path, baseSha }) => {
+ const file = state.entries[path];
+ return new Promise((resolve, reject) => {
+ service
+ .getRawFileData(file)
+ .then(raw => {
+ commit(types.SET_FILE_RAW_DATA, { file, raw });
+ if (file.mrChange && file.mrChange.new_file === false) {
+ service
+ .getBaseRawFileData(file, baseSha)
+ .then(baseRaw => {
+ commit(types.SET_FILE_BASE_RAW_DATA, {
+ file,
+ baseRaw,
+ });
+ resolve(raw);
+ })
+ .catch(e => {
+ reject(e);
+ });
+ } else {
+ resolve(raw);
+ }
+ })
+ .catch(() => {
+ flash('Error loading file content. Please try again.');
+ reject();
+ });
+ });
+};
export const changeFileContent = ({ state, commit }, { path, content }) => {
const file = state.entries[path];
diff --git a/app/assets/javascripts/ide/stores/actions/merge_request.js b/app/assets/javascripts/ide/stores/actions/merge_request.js
new file mode 100644
index 00000000000..da73034fd7d
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/actions/merge_request.js
@@ -0,0 +1,84 @@
+import flash from '~/flash';
+import service from '../../services';
+import * as types from '../mutation_types';
+
+export const getMergeRequestData = (
+ { commit, state, dispatch },
+ { projectId, mergeRequestId, force = false } = {},
+) =>
+ new Promise((resolve, reject) => {
+ if (!state.projects[projectId].mergeRequests[mergeRequestId] || force) {
+ service
+ .getProjectMergeRequestData(projectId, mergeRequestId)
+ .then(res => res.data)
+ .then(data => {
+ commit(types.SET_MERGE_REQUEST, {
+ projectPath: projectId,
+ mergeRequestId,
+ mergeRequest: data,
+ });
+ if (!state.currentMergeRequestId) {
+ commit(types.SET_CURRENT_MERGE_REQUEST, mergeRequestId);
+ }
+ resolve(data);
+ })
+ .catch(() => {
+ flash('Error loading merge request data. Please try again.');
+ reject(new Error(`Merge Request not loaded ${projectId}`));
+ });
+ } else {
+ resolve(state.projects[projectId].mergeRequests[mergeRequestId]);
+ }
+ });
+
+export const getMergeRequestChanges = (
+ { commit, state, dispatch },
+ { projectId, mergeRequestId, force = false } = {},
+) =>
+ new Promise((resolve, reject) => {
+ if (!state.projects[projectId].mergeRequests[mergeRequestId].changes.length || force) {
+ service
+ .getProjectMergeRequestChanges(projectId, mergeRequestId)
+ .then(res => res.data)
+ .then(data => {
+ commit(types.SET_MERGE_REQUEST_CHANGES, {
+ projectPath: projectId,
+ mergeRequestId,
+ changes: data,
+ });
+ resolve(data);
+ })
+ .catch(() => {
+ flash('Error loading merge request changes. Please try again.');
+ reject(new Error(`Merge Request Changes not loaded ${projectId}`));
+ });
+ } else {
+ resolve(state.projects[projectId].mergeRequests[mergeRequestId].changes);
+ }
+ });
+
+export const getMergeRequestVersions = (
+ { commit, state, dispatch },
+ { projectId, mergeRequestId, force = false } = {},
+) =>
+ new Promise((resolve, reject) => {
+ if (!state.projects[projectId].mergeRequests[mergeRequestId].versions.length || force) {
+ service
+ .getProjectMergeRequestVersions(projectId, mergeRequestId)
+ .then(res => res.data)
+ .then(data => {
+ commit(types.SET_MERGE_REQUEST_VERSIONS, {
+ projectPath: projectId,
+ mergeRequestId,
+ versions: data,
+ });
+ resolve(data);
+ })
+ .catch(() => {
+ flash('Error loading merge request versions. Please try again.');
+ reject(new Error(`Merge Request Versions not loaded ${projectId}`));
+ });
+ } else {
+ resolve(state.projects[projectId].mergeRequests[mergeRequestId].versions);
+ }
+ });
diff --git a/app/assets/javascripts/ide/stores/actions/tree.js b/app/assets/javascripts/ide/stores/actions/tree.js
index 70a969a0325..6536be04f0a 100644
--- a/app/assets/javascripts/ide/stores/actions/tree.js
+++ b/app/assets/javascripts/ide/stores/actions/tree.js
@@ -2,9 +2,7 @@ import { normalizeHeaders } from '~/lib/utils/common_utils';
import flash from '~/flash';
import service from '../../services';
import * as types from '../mutation_types';
-import {
- findEntry,
-} from '../utils';
+import { findEntry } from '../utils';
import FilesDecoratorWorker from '../workers/files_decorator_worker';
export const toggleTreeOpen = ({ commit, dispatch }, path) => {
@@ -21,23 +19,24 @@ export const handleTreeEntryAction = ({ commit, dispatch }, row) => {
dispatch('setFileActive', row.path);
} else {
- dispatch('getFileData', row);
+ dispatch('getFileData', { path: row.path });
}
};
export const getLastCommitData = ({ state, commit, dispatch, getters }, tree = state) => {
if (!tree || tree.lastCommitPath === null || !tree.lastCommitPath) return;
- service.getTreeLastCommit(tree.lastCommitPath)
- .then((res) => {
+ service
+ .getTreeLastCommit(tree.lastCommitPath)
+ .then(res => {
const lastCommitPath = normalizeHeaders(res.headers)['MORE-LOGS-URL'] || null;
commit(types.SET_LAST_COMMIT_URL, { tree, url: lastCommitPath });
return res.json();
})
- .then((data) => {
- data.forEach((lastCommit) => {
+ .then(data => {
+ data.forEach(lastCommit => {
const entry = findEntry(tree.tree, lastCommit.type, lastCommit.file_name);
if (entry) {
@@ -50,44 +49,47 @@ export const getLastCommitData = ({ state, commit, dispatch, getters }, tree = s
.catch(() => flash('Error fetching log data.', 'alert', document, null, false, true));
};
-export const getFiles = (
- { state, commit, dispatch },
- { projectId, branchId } = {},
-) => new Promise((resolve, reject) => {
- if (!state.trees[`${projectId}/${branchId}`]) {
- const selectedProject = state.projects[projectId];
- commit(types.CREATE_TREE, { treePath: `${projectId}/${branchId}` });
-
- service
- .getFiles(selectedProject.web_url, branchId)
- .then(res => res.json())
- .then((data) => {
- const worker = new FilesDecoratorWorker();
- worker.addEventListener('message', (e) => {
- const { entries, treeList } = e.data;
- const selectedTree = state.trees[`${projectId}/${branchId}`];
-
- commit(types.SET_ENTRIES, entries);
- commit(types.SET_DIRECTORY_DATA, { treePath: `${projectId}/${branchId}`, data: treeList });
- commit(types.TOGGLE_LOADING, { entry: selectedTree, forceValue: false });
-
- worker.terminate();
-
- resolve();
- });
-
- worker.postMessage({
- data,
- projectId,
- branchId,
+export const getFiles = ({ state, commit, dispatch }, { projectId, branchId } = {}) =>
+ new Promise((resolve, reject) => {
+ if (!state.trees[`${projectId}/${branchId}`]) {
+ const selectedProject = state.projects[projectId];
+ commit(types.CREATE_TREE, { treePath: `${projectId}/${branchId}` });
+
+ service
+ .getFiles(selectedProject.web_url, branchId)
+ .then(res => res.json())
+ .then(data => {
+ const worker = new FilesDecoratorWorker();
+ worker.addEventListener('message', e => {
+ const { entries, treeList } = e.data;
+ const selectedTree = state.trees[`${projectId}/${branchId}`];
+
+ commit(types.SET_ENTRIES, entries);
+ commit(types.SET_DIRECTORY_DATA, {
+ treePath: `${projectId}/${branchId}`,
+ data: treeList,
+ });
+ commit(types.TOGGLE_LOADING, {
+ entry: selectedTree,
+ forceValue: false,
+ });
+
+ worker.terminate();
+
+ resolve();
+ });
+
+ worker.postMessage({
+ data,
+ projectId,
+ branchId,
+ });
+ })
+ .catch(e => {
+ flash('Error loading tree data. Please try again.', 'alert', document, null, false, true);
+ reject(e);
});
- })
- .catch((e) => {
- flash('Error loading tree data. Please try again.', 'alert', document, null, false, true);
- reject(e);
- });
- } else {
- resolve();
- }
-});
-
+ } else {
+ resolve();
+ }
+ });
diff --git a/app/assets/javascripts/ide/stores/getters.js b/app/assets/javascripts/ide/stores/getters.js
index eba325a31df..a77cdbc13c8 100644
--- a/app/assets/javascripts/ide/stores/getters.js
+++ b/app/assets/javascripts/ide/stores/getters.js
@@ -1,10 +1,8 @@
-export const activeFile = state =>
- state.openFiles.find(file => file.active) || null;
+export const activeFile = state => state.openFiles.find(file => file.active) || null;
export const addedFiles = state => state.changedFiles.filter(f => f.tempFile);
-export const modifiedFiles = state =>
- state.changedFiles.filter(f => !f.tempFile);
+export const modifiedFiles = state => state.changedFiles.filter(f => !f.tempFile);
export const projectsWithTrees = state =>
Object.keys(state.projects).map(projectId => {
@@ -23,8 +21,17 @@ export const projectsWithTrees = state =>
};
});
+export const currentMergeRequest = state => {
+ if (state.projects[state.currentProjectId]) {
+ return state.projects[state.currentProjectId].mergeRequests[state.currentMergeRequestId];
+ }
+ return null;
+};
+
// eslint-disable-next-line no-confusing-arrow
export const currentIcon = state =>
state.rightPanelCollapsed ? 'angle-double-left' : 'angle-double-right';
export const hasChanges = state => !!state.changedFiles.length;
+
+export const hasMergeRequest = state => !!state.currentMergeRequestId;
diff --git a/app/assets/javascripts/ide/stores/mutation_types.js b/app/assets/javascripts/ide/stores/mutation_types.js
index fa2fbaf8683..ee759bff516 100644
--- a/app/assets/javascripts/ide/stores/mutation_types.js
+++ b/app/assets/javascripts/ide/stores/mutation_types.js
@@ -11,6 +11,12 @@ export const SET_PROJECT = 'SET_PROJECT';
export const SET_CURRENT_PROJECT = 'SET_CURRENT_PROJECT';
export const TOGGLE_PROJECT_OPEN = 'TOGGLE_PROJECT_OPEN';
+// Merge Request Mutation Types
+export const SET_MERGE_REQUEST = 'SET_MERGE_REQUEST';
+export const SET_CURRENT_MERGE_REQUEST = 'SET_CURRENT_MERGE_REQUEST';
+export const SET_MERGE_REQUEST_CHANGES = 'SET_MERGE_REQUEST_CHANGES';
+export const SET_MERGE_REQUEST_VERSIONS = 'SET_MERGE_REQUEST_VERSIONS';
+
// Branch Mutation Types
export const SET_BRANCH = 'SET_BRANCH';
export const SET_BRANCH_WORKING_REFERENCE = 'SET_BRANCH_WORKING_REFERENCE';
@@ -28,6 +34,7 @@ export const SET_FILE_DATA = 'SET_FILE_DATA';
export const TOGGLE_FILE_OPEN = 'TOGGLE_FILE_OPEN';
export const SET_FILE_ACTIVE = 'SET_FILE_ACTIVE';
export const SET_FILE_RAW_DATA = 'SET_FILE_RAW_DATA';
+export const SET_FILE_BASE_RAW_DATA = 'SET_FILE_BASE_RAW_DATA';
export const UPDATE_FILE_CONTENT = 'UPDATE_FILE_CONTENT';
export const SET_FILE_LANGUAGE = 'SET_FILE_LANGUAGE';
export const SET_FILE_POSITION = 'SET_FILE_POSITION';
@@ -39,6 +46,7 @@ export const TOGGLE_FILE_CHANGED = 'TOGGLE_FILE_CHANGED';
export const SET_CURRENT_BRANCH = 'SET_CURRENT_BRANCH';
export const SET_ENTRIES = 'SET_ENTRIES';
export const CREATE_TMP_ENTRY = 'CREATE_TMP_ENTRY';
+export const SET_FILE_MERGE_REQUEST_CHANGE = 'SET_FILE_MERGE_REQUEST_CHANGE';
export const UPDATE_VIEWER = 'UPDATE_VIEWER';
export const UPDATE_DELAY_VIEWER_CHANGE = 'UPDATE_DELAY_VIEWER_CHANGE';
diff --git a/app/assets/javascripts/ide/stores/mutations.js b/app/assets/javascripts/ide/stores/mutations.js
index da41fc9285c..5e5eb831662 100644
--- a/app/assets/javascripts/ide/stores/mutations.js
+++ b/app/assets/javascripts/ide/stores/mutations.js
@@ -1,5 +1,6 @@
import * as types from './mutation_types';
import projectMutations from './mutations/project';
+import mergeRequestMutation from './mutations/merge_request';
import fileMutations from './mutations/file';
import treeMutations from './mutations/tree';
import branchMutations from './mutations/branch';
@@ -11,10 +12,7 @@ export default {
[types.TOGGLE_LOADING](state, { entry, forceValue = undefined }) {
if (entry.path) {
Object.assign(state.entries[entry.path], {
- loading:
- forceValue !== undefined
- ? forceValue
- : !state.entries[entry.path].loading,
+ loading: forceValue !== undefined ? forceValue : !state.entries[entry.path].loading,
});
} else {
Object.assign(entry, {
@@ -83,9 +81,7 @@ export default {
if (!foundEntry) {
Object.assign(state.trees[`${projectId}/${branchId}`], {
- tree: state.trees[`${projectId}/${branchId}`].tree.concat(
- data.treeList,
- ),
+ tree: state.trees[`${projectId}/${branchId}`].tree.concat(data.treeList),
});
}
},
@@ -100,6 +96,7 @@ export default {
});
},
...projectMutations,
+ ...mergeRequestMutation,
...fileMutations,
...treeMutations,
...branchMutations,
diff --git a/app/assets/javascripts/ide/stores/mutations/file.js b/app/assets/javascripts/ide/stores/mutations/file.js
index 4fafcfd0ea1..926b6f66d78 100644
--- a/app/assets/javascripts/ide/stores/mutations/file.js
+++ b/app/assets/javascripts/ide/stores/mutations/file.js
@@ -40,6 +40,8 @@ export default {
rawPath: data.raw_path,
binary: data.binary,
renderError: data.render_error,
+ raw: null,
+ baseRaw: null,
});
},
[types.SET_FILE_RAW_DATA](state, { file, raw }) {
@@ -47,6 +49,11 @@ export default {
raw,
});
},
+ [types.SET_FILE_BASE_RAW_DATA](state, { file, baseRaw }) {
+ Object.assign(state.entries[file.path], {
+ baseRaw,
+ });
+ },
[types.UPDATE_FILE_CONTENT](state, { path, content }) {
const changed = content !== state.entries[path].raw;
@@ -71,6 +78,11 @@ export default {
editorColumn,
});
},
+ [types.SET_FILE_MERGE_REQUEST_CHANGE](state, { file, mrChange }) {
+ Object.assign(state.entries[file.path], {
+ mrChange,
+ });
+ },
[types.DISCARD_FILE_CHANGES](state, path) {
Object.assign(state.entries[path], {
content: state.entries[path].raw,
diff --git a/app/assets/javascripts/ide/stores/mutations/merge_request.js b/app/assets/javascripts/ide/stores/mutations/merge_request.js
new file mode 100644
index 00000000000..334819fe702
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/mutations/merge_request.js
@@ -0,0 +1,33 @@
+import * as types from '../mutation_types';
+
+export default {
+ [types.SET_CURRENT_MERGE_REQUEST](state, currentMergeRequestId) {
+ Object.assign(state, {
+ currentMergeRequestId,
+ });
+ },
+ [types.SET_MERGE_REQUEST](state, { projectPath, mergeRequestId, mergeRequest }) {
+ Object.assign(state.projects[projectPath], {
+ mergeRequests: {
+ [mergeRequestId]: {
+ ...mergeRequest,
+ active: true,
+ changes: [],
+ versions: [],
+ baseCommitSha: null,
+ },
+ },
+ });
+ },
+ [types.SET_MERGE_REQUEST_CHANGES](state, { projectPath, mergeRequestId, changes }) {
+ Object.assign(state.projects[projectPath].mergeRequests[mergeRequestId], {
+ changes,
+ });
+ },
+ [types.SET_MERGE_REQUEST_VERSIONS](state, { projectPath, mergeRequestId, versions }) {
+ Object.assign(state.projects[projectPath].mergeRequests[mergeRequestId], {
+ versions,
+ baseCommitSha: versions.length ? versions[0].base_commit_sha : null,
+ });
+ },
+};
diff --git a/app/assets/javascripts/ide/stores/mutations/project.js b/app/assets/javascripts/ide/stores/mutations/project.js
index 2816562a919..284b39a2c72 100644
--- a/app/assets/javascripts/ide/stores/mutations/project.js
+++ b/app/assets/javascripts/ide/stores/mutations/project.js
@@ -11,6 +11,7 @@ export default {
Object.assign(project, {
tree: [],
branches: {},
+ mergeRequests: {},
active: true,
});
diff --git a/app/assets/javascripts/ide/stores/state.js b/app/assets/javascripts/ide/stores/state.js
index 6110f54951c..e5cc8814000 100644
--- a/app/assets/javascripts/ide/stores/state.js
+++ b/app/assets/javascripts/ide/stores/state.js
@@ -1,6 +1,7 @@
export default () => ({
currentProjectId: '',
currentBranchId: '',
+ currentMergeRequestId: '',
changedFiles: [],
endpoints: {},
lastCommitMsg: '',
diff --git a/app/assets/javascripts/ide/stores/utils.js b/app/assets/javascripts/ide/stores/utils.js
index 487ea1ead8e..3389eeeaa2e 100644
--- a/app/assets/javascripts/ide/stores/utils.js
+++ b/app/assets/javascripts/ide/stores/utils.js
@@ -38,7 +38,7 @@ export const dataStructure = () => ({
eol: '',
});
-export const decorateData = (entity) => {
+export const decorateData = entity => {
const {
id,
projectId,
@@ -57,7 +57,6 @@ export const decorateData = (entity) => {
base64 = false,
file_lock,
-
} = entity;
return {
@@ -80,17 +79,15 @@ export const decorateData = (entity) => {
base64,
file_lock,
-
};
};
-export const findEntry = (tree, type, name, prop = 'name') => tree.find(
- f => f.type === type && f[prop] === name,
-);
+export const findEntry = (tree, type, name, prop = 'name') =>
+ tree.find(f => f.type === type && f[prop] === name);
export const findIndexOfFile = (state, file) => state.findIndex(f => f.path === file.path);
-export const setPageTitle = (title) => {
+export const setPageTitle = title => {
document.title = title;
};
@@ -120,6 +117,11 @@ const sortTreesByTypeAndName = (a, b) => {
return 0;
};
-export const sortTree = sortedTree => sortedTree.map(entity => Object.assign(entity, {
- tree: entity.tree.length ? sortTree(entity.tree) : [],
-})).sort(sortTreesByTypeAndName);
+export const sortTree = sortedTree =>
+ sortedTree
+ .map(entity =>
+ Object.assign(entity, {
+ tree: entity.tree.length ? sortTree(entity.tree) : [],
+ }),
+ )
+ .sort(sortTreesByTypeAndName);
diff --git a/app/assets/javascripts/jobs/components/sidebar_detail_row.vue b/app/assets/javascripts/jobs/components/sidebar_detail_row.vue
index a6819aaeb12..dfe87d89a39 100644
--- a/app/assets/javascripts/jobs/components/sidebar_detail_row.vue
+++ b/app/assets/javascripts/jobs/components/sidebar_detail_row.vue
@@ -11,11 +11,19 @@
type: String,
required: true,
},
+ helpUrl: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
computed: {
hasTitle() {
return this.title.length > 0;
},
+ hasHelpURL() {
+ return this.helpUrl.length > 0;
+ },
},
};
</script>
@@ -28,5 +36,21 @@
{{ title }}:
</span>
{{ value }}
+
+ <span
+ v-if="hasHelpURL"
+ class="help-button pull-right"
+ >
+ <a
+ :href="helpUrl"
+ target="_blank"
+ rel="noopener noreferrer nofollow"
+ >
+ <i
+ class="fa fa-question-circle"
+ aria-hidden="true"
+ ></i>
+ </a>
+ </span>
</p>
</template>
diff --git a/app/assets/javascripts/jobs/components/sidebar_details_block.vue b/app/assets/javascripts/jobs/components/sidebar_details_block.vue
index 56814a52525..172de6b3679 100644
--- a/app/assets/javascripts/jobs/components/sidebar_details_block.vue
+++ b/app/assets/javascripts/jobs/components/sidebar_details_block.vue
@@ -22,6 +22,11 @@
type: Boolean,
required: true,
},
+ runnerHelpUrl: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
computed: {
shouldRenderContent() {
@@ -39,6 +44,21 @@
runnerId() {
return `#${this.job.runner.id}`;
},
+ hasTimeout() {
+ return this.job.metadata != null && this.job.metadata.timeout_human_readable !== '';
+ },
+ timeout() {
+ if (this.job.metadata == null) {
+ return '';
+ }
+
+ let t = this.job.metadata.timeout_human_readable;
+ if (this.job.metadata.timeout_source !== '') {
+ t += ` (from ${this.job.metadata.timeout_source})`;
+ }
+
+ return t;
+ },
renderBlock() {
return this.job.merge_request ||
this.job.duration ||
@@ -115,6 +135,13 @@
:value="queued"
/>
<detail-row
+ class="js-job-timeout"
+ v-if="hasTimeout"
+ title="Timeout"
+ :help-url="runnerHelpUrl"
+ :value="timeout"
+ />
+ <detail-row
class="js-job-runner"
v-if="job.runner"
title="Runner"
diff --git a/app/assets/javascripts/jobs/job_details_bundle.js b/app/assets/javascripts/jobs/job_details_bundle.js
index 85a88ae409b..656676ead91 100644
--- a/app/assets/javascripts/jobs/job_details_bundle.js
+++ b/app/assets/javascripts/jobs/job_details_bundle.js
@@ -51,6 +51,7 @@ export default () => {
props: {
isLoading: this.mediator.state.isLoading,
job: this.mediator.store.state.job,
+ runnerHelpUrl: dataset.runnerHelpUrl,
},
});
},
diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js
index a266bb6771f..dd17544b656 100644
--- a/app/assets/javascripts/lib/utils/url_utility.js
+++ b/app/assets/javascripts/lib/utils/url_utility.js
@@ -51,7 +51,7 @@ export function removeParams(params) {
const url = document.createElement('a');
url.href = window.location.href;
- params.forEach((param) => {
+ params.forEach(param => {
url.search = removeParamQueryString(url.search, param);
});
@@ -83,3 +83,11 @@ export function refreshCurrentPage() {
export function redirectTo(url) {
return window.location.assign(url);
}
+
+export function webIDEUrl(route = undefined) {
+ let returnUrl = `${gon.relative_url_root}/-/ide/`;
+ if (route) {
+ returnUrl += `project${route}`;
+ }
+ return returnUrl;
+}
diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue
index cf579c5d4dc..e0f883a8e08 100644
--- a/app/assets/javascripts/notes/components/noteable_discussion.vue
+++ b/app/assets/javascripts/notes/components/noteable_discussion.vue
@@ -292,10 +292,12 @@ Please check your network connection and try again.`;
</button>
</div>
<div
+ v-if="note.resolvable"
class="btn-group discussion-actions"
- role="group">
+ role="group"
+ >
<div
- v-if="note.resolvable && !discussionResolved"
+ v-if="!discussionResolved"
class="btn-group"
role="group">
<a
diff --git a/app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue b/app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue
index 22248418c41..2bda2aeb3a1 100644
--- a/app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue
+++ b/app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue
@@ -19,15 +19,19 @@
type: String,
required: true,
},
+ groupName: {
+ type: String,
+ required: true,
+ },
},
computed: {
title() {
return sprintf(s__('Milestones|Promote %{milestoneTitle} to group milestone?'), { milestoneTitle: this.milestoneTitle });
},
text() {
- return s__(`Milestones|Promoting this milestone will make it available for all projects inside the group.
+ return sprintf(s__(`Milestones|Promoting %{milestoneTitle} will make it available for all projects inside %{groupName}.
Existing project milestones with the same title will be merged.
- This action cannot be reversed.`);
+ This action cannot be reversed.`), { milestoneTitle: this.milestoneTitle, groupName: this.groupName });
},
},
methods: {
diff --git a/app/assets/javascripts/pages/milestones/shared/promote_milestone_modal_init.js b/app/assets/javascripts/pages/milestones/shared/promote_milestone_modal_init.js
index d00f81c9094..8e79341e96a 100644
--- a/app/assets/javascripts/pages/milestones/shared/promote_milestone_modal_init.js
+++ b/app/assets/javascripts/pages/milestones/shared/promote_milestone_modal_init.js
@@ -25,6 +25,7 @@ export default () => {
const modalProps = {
milestoneTitle: button.dataset.milestoneTitle,
url: button.dataset.url,
+ groupName: button.dataset.groupName,
};
eventHub.$once('promoteMilestoneModal.requestStarted', onRequestStarted);
eventHub.$emit('promoteMilestoneModal.props', modalProps);
@@ -54,6 +55,7 @@ export default () => {
return {
modalProps: {
milestoneTitle: '',
+ groupName: '',
url: '',
},
};
diff --git a/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue b/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue
index 54695dfeb99..ad6df51bb7a 100644
--- a/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue
+++ b/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue
@@ -1,4 +1,5 @@
<script>
+ import _ from 'underscore';
import axios from '~/lib/utils/axios_utils';
import createFlash from '~/flash';
import GlModal from '~/vue_shared/components/gl_modal.vue';
@@ -27,19 +28,26 @@
type: String,
required: true,
},
+ groupName: {
+ type: String,
+ required: true,
+ },
},
computed: {
text() {
- return s__(`Milestones|Promoting this label will make it available for all projects inside the group.
- Existing project labels with the same title will be merged. This action cannot be reversed.`);
+ return sprintf(s__(`Labels|Promoting %{labelTitle} will make it available for all projects inside %{groupName}.
+ Existing project labels with the same title will be merged. This action cannot be reversed.`), {
+ labelTitle: this.labelTitle,
+ groupName: this.groupName,
+ });
},
title() {
const label = `<span
class="label color-label"
style="background-color: ${this.labelColor}; color: ${this.labelTextColor};"
- >${this.labelTitle}</span>`;
+ >${_.escape(this.labelTitle)}</span>`;
- return sprintf(s__('Labels|Promote label %{labelTitle} to Group Label?'), {
+ return sprintf(s__('Labels|<span>Promote label</span> %{labelTitle} <span>to Group Label?</span>'), {
labelTitle: label,
}, false);
},
@@ -69,6 +77,7 @@
>
<div
slot="title"
+ class="modal-title-with-label"
v-html="title"
>
{{ title }}
diff --git a/app/assets/javascripts/pages/projects/labels/index/index.js b/app/assets/javascripts/pages/projects/labels/index/index.js
index 2abcbfab1ed..03cfef61311 100644
--- a/app/assets/javascripts/pages/projects/labels/index/index.js
+++ b/app/assets/javascripts/pages/projects/labels/index/index.js
@@ -30,6 +30,7 @@ const initLabelIndex = () => {
labelColor: button.dataset.labelColor,
labelTextColor: button.dataset.labelTextColor,
url: button.dataset.url,
+ groupName: button.dataset.groupName,
};
eventHub.$once('promoteLabelModal.requestStarted', onRequestStarted);
eventHub.$emit('promoteLabelModal.props', modalProps);
@@ -62,6 +63,7 @@ const initLabelIndex = () => {
labelColor: '',
labelTextColor: '',
url: '',
+ groupName: '',
},
};
},
diff --git a/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue b/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue
index 8a86c409b62..ceb02309959 100644
--- a/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue
+++ b/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue
@@ -1,59 +1,73 @@
<script>
- import Flash from '../../../flash';
- import editForm from './edit_form.vue';
- import Icon from '../../../vue_shared/components/icon.vue';
- import { __ } from '../../../locale';
+import Flash from '../../../flash';
+import editForm from './edit_form.vue';
+import Icon from '../../../vue_shared/components/icon.vue';
+import { __ } from '../../../locale';
+import eventHub from '../../event_hub';
- export default {
- components: {
- editForm,
- Icon,
+export default {
+ components: {
+ editForm,
+ Icon,
+ },
+ props: {
+ isConfidential: {
+ required: true,
+ type: Boolean,
},
- props: {
- isConfidential: {
- required: true,
- type: Boolean,
- },
- isEditable: {
- required: true,
- type: Boolean,
- },
- service: {
- required: true,
- type: Object,
- },
+ isEditable: {
+ required: true,
+ type: Boolean,
},
- data() {
- return {
- edit: false,
- };
+ service: {
+ required: true,
+ type: Object,
},
- computed: {
- confidentialityIcon() {
- return this.isConfidential ? 'eye-slash' : 'eye';
- },
+ },
+ data() {
+ return {
+ edit: false,
+ };
+ },
+ computed: {
+ confidentialityIcon() {
+ return this.isConfidential ? 'eye-slash' : 'eye';
},
- methods: {
- toggleForm() {
- this.edit = !this.edit;
- },
- updateConfidentialAttribute(confidential) {
- this.service.update('issue', { confidential })
- .then(() => location.reload())
- .catch(() => {
- Flash(__('Something went wrong trying to change the confidentiality of this issue'));
- });
- },
+ },
+ created() {
+ eventHub.$on('closeConfidentialityForm', this.toggleForm);
+ },
+ beforeDestroy() {
+ eventHub.$off('closeConfidentialityForm', this.toggleForm);
+ },
+ methods: {
+ toggleForm() {
+ this.edit = !this.edit;
},
- };
+ updateConfidentialAttribute(confidential) {
+ this.service
+ .update('issue', { confidential })
+ .then(() => location.reload())
+ .catch(() => {
+ Flash(
+ __(
+ 'Something went wrong trying to change the confidentiality of this issue',
+ ),
+ );
+ });
+ },
+ },
+};
</script>
<template>
<div class="block issuable-sidebar-item confidentiality">
- <div class="sidebar-collapsed-icon">
+ <div
+ class="sidebar-collapsed-icon"
+ @click="toggleForm"
+ >
<icon
:name="confidentialityIcon"
- :size="16"
aria-hidden="true"
/>
</div>
@@ -71,7 +85,6 @@
<div class="value sidebar-item-value hide-collapsed">
<editForm
v-if="edit"
- :toggle-form="toggleForm"
:is-confidential="isConfidential"
:update-confidential-attribute="updateConfidentialAttribute"
/>
diff --git a/app/assets/javascripts/sidebar/components/confidential/edit_form.vue b/app/assets/javascripts/sidebar/components/confidential/edit_form.vue
index c569843b05f..3783f71a848 100644
--- a/app/assets/javascripts/sidebar/components/confidential/edit_form.vue
+++ b/app/assets/javascripts/sidebar/components/confidential/edit_form.vue
@@ -1,34 +1,34 @@
<script>
- import editFormButtons from './edit_form_buttons.vue';
- import { s__ } from '../../../locale';
+import editFormButtons from './edit_form_buttons.vue';
+import { s__ } from '../../../locale';
- export default {
- components: {
- editFormButtons,
+export default {
+ components: {
+ editFormButtons,
+ },
+ props: {
+ isConfidential: {
+ required: true,
+ type: Boolean,
},
- props: {
- isConfidential: {
- required: true,
- type: Boolean,
- },
- toggleForm: {
- required: true,
- type: Function,
- },
- updateConfidentialAttribute: {
- required: true,
- type: Function,
- },
+ updateConfidentialAttribute: {
+ required: true,
+ type: Function,
},
- computed: {
- confidentialityOnWarning() {
- return s__('confidentiality|You are going to turn on the confidentiality. This means that only team members with <strong>at least Reporter access</strong> are able to see and leave comments on the issue.');
- },
- confidentialityOffWarning() {
- return s__('confidentiality|You are going to turn off the confidentiality. This means <strong>everyone</strong> will be able to see and leave a comment on this issue.');
- },
+ },
+ computed: {
+ confidentialityOnWarning() {
+ return s__(
+ 'confidentiality|You are going to turn on the confidentiality. This means that only team members with <strong>at least Reporter access</strong> are able to see and leave comments on the issue.',
+ );
},
- };
+ confidentialityOffWarning() {
+ return s__(
+ 'confidentiality|You are going to turn off the confidentiality. This means <strong>everyone</strong> will be able to see and leave a comment on this issue.',
+ );
+ },
+ },
+};
</script>
<template>
@@ -45,7 +45,6 @@
</p>
<edit-form-buttons
:is-confidential="isConfidential"
- :toggle-form="toggleForm"
:update-confidential-attribute="updateConfidentialAttribute"
/>
</div>
diff --git a/app/assets/javascripts/sidebar/components/confidential/edit_form_buttons.vue b/app/assets/javascripts/sidebar/components/confidential/edit_form_buttons.vue
index 49d5dfeea1a..38b1ddbfd5b 100644
--- a/app/assets/javascripts/sidebar/components/confidential/edit_form_buttons.vue
+++ b/app/assets/javascripts/sidebar/components/confidential/edit_form_buttons.vue
@@ -1,14 +1,13 @@
<script>
+import $ from 'jquery';
+import eventHub from '../../event_hub';
+
export default {
props: {
isConfidential: {
required: true,
type: Boolean,
},
- toggleForm: {
- required: true,
- type: Function,
- },
updateConfidentialAttribute: {
required: true,
type: Function,
@@ -22,6 +21,16 @@ export default {
return !this.isConfidential;
},
},
+ methods: {
+ closeForm() {
+ eventHub.$emit('closeConfidentialityForm');
+ $(this.$el).trigger('hidden.gl.dropdown');
+ },
+ submitForm() {
+ this.closeForm();
+ this.updateConfidentialAttribute(this.updateConfidentialBool);
+ },
+ },
};
</script>
@@ -30,14 +39,14 @@ export default {
<button
type="button"
class="btn btn-default append-right-10"
- @click="toggleForm"
+ @click="closeForm"
>
{{ __('Cancel') }}
</button>
<button
type="button"
class="btn btn-close"
- @click.prevent="updateConfidentialAttribute(updateConfidentialBool)"
+ @click.prevent="submitForm"
>
{{ toggleButtonText }}
</button>
diff --git a/app/assets/javascripts/sidebar/components/lock/edit_form.vue b/app/assets/javascripts/sidebar/components/lock/edit_form.vue
index bc32e974bc3..e1e4715826a 100644
--- a/app/assets/javascripts/sidebar/components/lock/edit_form.vue
+++ b/app/assets/javascripts/sidebar/components/lock/edit_form.vue
@@ -1,40 +1,43 @@
<script>
- import editFormButtons from './edit_form_buttons.vue';
- import issuableMixin from '../../../vue_shared/mixins/issuable';
- import { __, sprintf } from '../../../locale';
+import editFormButtons from './edit_form_buttons.vue';
+import issuableMixin from '../../../vue_shared/mixins/issuable';
+import { __, sprintf } from '../../../locale';
- export default {
- components: {
- editFormButtons,
+export default {
+ components: {
+ editFormButtons,
+ },
+ mixins: [issuableMixin],
+ props: {
+ isLocked: {
+ required: true,
+ type: Boolean,
},
- mixins: [
- issuableMixin,
- ],
- props: {
- isLocked: {
- required: true,
- type: Boolean,
- },
- toggleForm: {
- required: true,
- type: Function,
- },
-
- updateLockedAttribute: {
- required: true,
- type: Function,
- },
+ updateLockedAttribute: {
+ required: true,
+ type: Function,
+ },
+ },
+ computed: {
+ lockWarning() {
+ return sprintf(
+ __(
+ 'Lock this %{issuableDisplayName}? Only <strong>project members</strong> will be able to comment.',
+ ),
+ { issuableDisplayName: this.issuableDisplayName },
+ );
},
- computed: {
- lockWarning() {
- return sprintf(__('Lock this %{issuableDisplayName}? Only <strong>project members</strong> will be able to comment.'), { issuableDisplayName: this.issuableDisplayName });
- },
- unlockWarning() {
- return sprintf(__('Unlock this %{issuableDisplayName}? <strong>Everyone</strong> will be able to comment.'), { issuableDisplayName: this.issuableDisplayName });
- },
+ unlockWarning() {
+ return sprintf(
+ __(
+ 'Unlock this %{issuableDisplayName}? <strong>Everyone</strong> will be able to comment.',
+ ),
+ { issuableDisplayName: this.issuableDisplayName },
+ );
},
- };
+ },
+};
</script>
<template>
@@ -54,7 +57,6 @@
<edit-form-buttons
:is-locked="isLocked"
- :toggle-form="toggleForm"
:update-locked-attribute="updateLockedAttribute"
/>
</div>
diff --git a/app/assets/javascripts/sidebar/components/lock/edit_form_buttons.vue b/app/assets/javascripts/sidebar/components/lock/edit_form_buttons.vue
index c3a553a7605..5e7b8f9698f 100644
--- a/app/assets/javascripts/sidebar/components/lock/edit_form_buttons.vue
+++ b/app/assets/javascripts/sidebar/components/lock/edit_form_buttons.vue
@@ -1,4 +1,7 @@
<script>
+import $ from 'jquery';
+import eventHub from '../../event_hub';
+
export default {
props: {
isLocked: {
@@ -6,11 +9,6 @@ export default {
type: Boolean,
},
- toggleForm: {
- required: true,
- type: Function,
- },
-
updateLockedAttribute: {
required: true,
type: Function,
@@ -26,6 +24,17 @@ export default {
return !this.isLocked;
},
},
+
+ methods: {
+ closeForm() {
+ eventHub.$emit('closeLockForm');
+ $(this.$el).trigger('hidden.gl.dropdown');
+ },
+ submitForm() {
+ this.closeForm();
+ this.updateLockedAttribute(this.toggleLock);
+ },
+ },
};
</script>
@@ -34,7 +43,7 @@ export default {
<button
type="button"
class="btn btn-default append-right-10"
- @click="toggleForm"
+ @click="closeForm"
>
{{ __('Cancel') }}
</button>
@@ -42,7 +51,7 @@ export default {
<button
type="button"
class="btn btn-close"
- @click.prevent="updateLockedAttribute(toggleLock)"
+ @click.prevent="submitForm"
>
{{ buttonText }}
</button>
diff --git a/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue b/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue
index 0686910fc7e..e4893451af3 100644
--- a/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue
+++ b/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue
@@ -1,70 +1,93 @@
<script>
- import Flash from '~/flash';
- import editForm from './edit_form.vue';
- import issuableMixin from '../../../vue_shared/mixins/issuable';
- import Icon from '../../../vue_shared/components/icon.vue';
+import Flash from '~/flash';
+import editForm from './edit_form.vue';
+import issuableMixin from '../../../vue_shared/mixins/issuable';
+import Icon from '../../../vue_shared/components/icon.vue';
+import eventHub from '../../event_hub';
- export default {
- components: {
- editForm,
- Icon,
- },
- mixins: [
- issuableMixin,
- ],
+export default {
+ components: {
+ editForm,
+ Icon,
+ },
+ mixins: [issuableMixin],
- props: {
- isLocked: {
- required: true,
- type: Boolean,
- },
+ props: {
+ isLocked: {
+ required: true,
+ type: Boolean,
+ },
- isEditable: {
- required: true,
- type: Boolean,
- },
+ isEditable: {
+ required: true,
+ type: Boolean,
+ },
- mediator: {
- required: true,
- type: Object,
- validator(mediatorObject) {
- return mediatorObject.service && mediatorObject.service.update && mediatorObject.store;
- },
+ mediator: {
+ required: true,
+ type: Object,
+ validator(mediatorObject) {
+ return (
+ mediatorObject.service &&
+ mediatorObject.service.update &&
+ mediatorObject.store
+ );
},
},
+ },
- computed: {
- lockIcon() {
- return this.isLocked ? 'lock' : 'lock-open';
- },
+ computed: {
+ lockIcon() {
+ return this.isLocked ? 'lock' : 'lock-open';
+ },
- isLockDialogOpen() {
- return this.mediator.store.isLockDialogOpen;
- },
+ isLockDialogOpen() {
+ return this.mediator.store.isLockDialogOpen;
},
+ },
- methods: {
- toggleForm() {
- this.mediator.store.isLockDialogOpen = !this.mediator.store.isLockDialogOpen;
- },
+ created() {
+ eventHub.$on('closeLockForm', this.toggleForm);
+ },
+
+ beforeDestroy() {
+ eventHub.$off('closeLockForm', this.toggleForm);
+ },
- updateLockedAttribute(locked) {
- this.mediator.service.update(this.issuableType, {
+ methods: {
+ toggleForm() {
+ this.mediator.store.isLockDialogOpen = !this.mediator.store
+ .isLockDialogOpen;
+ },
+
+ updateLockedAttribute(locked) {
+ this.mediator.service
+ .update(this.issuableType, {
discussion_locked: locked,
})
.then(() => location.reload())
- .catch(() => Flash(this.__(`Something went wrong trying to change the locked state of this ${this.issuableDisplayName}`)));
- },
+ .catch(() =>
+ Flash(
+ this.__(
+ `Something went wrong trying to change the locked state of this ${
+ this.issuableDisplayName
+ }`,
+ ),
+ ),
+ );
},
- };
+ },
+};
</script>
<template>
<div class="block issuable-sidebar-item lock">
- <div class="sidebar-collapsed-icon">
+ <div
+ class="sidebar-collapsed-icon"
+ @click="toggleForm"
+ >
<icon
:name="lockIcon"
- :size="16"
aria-hidden="true"
class="sidebar-item-icon is-active"
/>
@@ -85,7 +108,6 @@
<div class="value sidebar-item-value hide-collapsed">
<edit-form
v-if="isLockDialogOpen"
- :toggle-form="toggleForm"
:is-locked="isLocked"
:update-locked-attribute="updateLockedAttribute"
:issuable-type="issuableType"
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue
index 3d886e7d628..18ee4c62bf1 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue
@@ -1,53 +1,57 @@
<script>
- import tooltip from '~/vue_shared/directives/tooltip';
- import { n__ } from '~/locale';
- import icon from '~/vue_shared/components/icon.vue';
- import clipboardButton from '~/vue_shared/components/clipboard_button.vue';
+import tooltip from '~/vue_shared/directives/tooltip';
+import { n__ } from '~/locale';
+import { webIDEUrl } from '~/lib/utils/url_utility';
+import icon from '~/vue_shared/components/icon.vue';
+import clipboardButton from '~/vue_shared/components/clipboard_button.vue';
- export default {
- name: 'MRWidgetHeader',
- directives: {
- tooltip,
+export default {
+ name: 'MRWidgetHeader',
+ directives: {
+ tooltip,
+ },
+ components: {
+ icon,
+ clipboardButton,
+ },
+ props: {
+ mr: {
+ type: Object,
+ required: true,
},
- components: {
- icon,
- clipboardButton,
+ },
+ computed: {
+ shouldShowCommitsBehindText() {
+ return this.mr.divergedCommitsCount > 0;
},
- props: {
- mr: {
- type: Object,
- required: true,
- },
+ commitsText() {
+ return n__('%d commit behind', '%d commits behind', this.mr.divergedCommitsCount);
},
- computed: {
- shouldShowCommitsBehindText() {
- return this.mr.divergedCommitsCount > 0;
- },
- commitsText() {
- return n__('%d commit behind', '%d commits behind', this.mr.divergedCommitsCount);
- },
- branchNameClipboardData() {
- // This supports code in app/assets/javascripts/copy_to_clipboard.js that
- // works around ClipboardJS limitations to allow the context-specific
- // copy/pasting of plain text or GFM.
- return JSON.stringify({
- text: this.mr.sourceBranch,
- gfm: `\`${this.mr.sourceBranch}\``,
- });
- },
- isSourceBranchLong() {
- return this.isBranchTitleLong(this.mr.sourceBranch);
- },
- isTargetBranchLong() {
- return this.isBranchTitleLong(this.mr.targetBranch);
- },
+ branchNameClipboardData() {
+ // This supports code in app/assets/javascripts/copy_to_clipboard.js that
+ // works around ClipboardJS limitations to allow the context-specific
+ // copy/pasting of plain text or GFM.
+ return JSON.stringify({
+ text: this.mr.sourceBranch,
+ gfm: `\`${this.mr.sourceBranch}\``,
+ });
},
- methods: {
- isBranchTitleLong(branchTitle) {
- return branchTitle.length > 32;
- },
+ isSourceBranchLong() {
+ return this.isBranchTitleLong(this.mr.sourceBranch);
},
- };
+ isTargetBranchLong() {
+ return this.isBranchTitleLong(this.mr.targetBranch);
+ },
+ webIdePath() {
+ return webIDEUrl(this.mr.statusPath.replace('.json', ''));
+ },
+ },
+ methods: {
+ isBranchTitleLong(branchTitle) {
+ return branchTitle.length > 32;
+ },
+ },
+};
</script>
<template>
<div class="mr-source-target">
@@ -96,6 +100,13 @@
</div>
<div v-if="mr.isOpen">
+ <a
+ v-if="!mr.sourceBranchRemoved"
+ :href="webIdePath"
+ class="btn btn-sm btn-default inline js-web-ide"
+ >
+ {{ s__("mrWidget|Web IDE") }}
+ </a>
<button
data-target="#modal_merge_info"
data-toggle="modal"