summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/api.js4
-rw-r--r--app/assets/javascripts/diffs/components/app.vue5
-rw-r--r--app/assets/javascripts/diffs/components/changed_files.vue4
-rw-r--r--app/assets/javascripts/diffs/components/diff_content.vue6
-rw-r--r--app/assets/javascripts/diffs/components/diff_discussions.vue4
-rw-r--r--app/assets/javascripts/diffs/components/diff_file.vue9
-rw-r--r--app/assets/javascripts/diffs/components/diff_file_header.vue1
-rw-r--r--app/assets/javascripts/diffs/components/diff_line_gutter_content.vue3
-rw-r--r--app/assets/javascripts/diffs/components/diff_line_note_form.vue7
-rw-r--r--app/assets/javascripts/diffs/components/diff_table_cell.vue8
-rw-r--r--app/assets/javascripts/diffs/components/inline_diff_comment_row.vue15
-rw-r--r--app/assets/javascripts/diffs/components/inline_diff_table_row.vue13
-rw-r--r--app/assets/javascripts/diffs/components/inline_diff_view.vue24
-rw-r--r--app/assets/javascripts/diffs/components/parallel_diff_comment_row.vue10
-rw-r--r--app/assets/javascripts/diffs/components/parallel_diff_table_row.vue30
-rw-r--r--app/assets/javascripts/diffs/components/parallel_diff_view.vue38
-rw-r--r--app/assets/javascripts/diffs/store/getters.js24
-rw-r--r--app/assets/javascripts/diffs/store/modules/diff_state.js18
-rw-r--r--app/assets/javascripts/diffs/store/modules/index.js22
-rw-r--r--app/assets/javascripts/diffs/store/mutations.js15
-rw-r--r--app/assets/javascripts/due_date_select.js4
-rw-r--r--app/assets/javascripts/frequent_items/components/app.vue122
-rw-r--r--app/assets/javascripts/frequent_items/components/frequent_items_list.vue78
-rw-r--r--app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue117
-rw-r--r--app/assets/javascripts/frequent_items/components/frequent_items_mixin.js23
-rw-r--r--app/assets/javascripts/frequent_items/components/frequent_items_search_input.vue55
-rw-r--r--app/assets/javascripts/frequent_items/constants.js38
-rw-r--r--app/assets/javascripts/frequent_items/event_hub.js (renamed from app/assets/javascripts/projects_dropdown/event_hub.js)0
-rw-r--r--app/assets/javascripts/frequent_items/index.js69
-rw-r--r--app/assets/javascripts/frequent_items/store/actions.js81
-rw-r--r--app/assets/javascripts/frequent_items/store/getters.js4
-rw-r--r--app/assets/javascripts/frequent_items/store/index.js16
-rw-r--r--app/assets/javascripts/frequent_items/store/mutation_types.js9
-rw-r--r--app/assets/javascripts/frequent_items/store/mutations.js71
-rw-r--r--app/assets/javascripts/frequent_items/store/state.js8
-rw-r--r--app/assets/javascripts/frequent_items/utils.js49
-rw-r--r--app/assets/javascripts/ide/components/merge_requests/info.vue43
-rw-r--r--app/assets/javascripts/ide/components/panes/right.vue27
-rw-r--r--app/assets/javascripts/ide/constants.js1
-rw-r--r--app/assets/javascripts/ide/lib/themes/gl_theme.js1
-rw-r--r--app/assets/javascripts/ide/services/index.js4
-rw-r--r--app/assets/javascripts/ide/stores/actions/merge_request.js2
-rw-r--r--app/assets/javascripts/issue_show/components/app.vue6
-rw-r--r--app/assets/javascripts/issue_show/components/fields/description.vue6
-rw-r--r--app/assets/javascripts/issue_show/components/form.vue6
-rw-r--r--app/assets/javascripts/issue_show/components/title.vue112
-rw-r--r--app/assets/javascripts/main.js2
-rw-r--r--app/assets/javascripts/notes.js4
-rw-r--r--app/assets/javascripts/notes/components/comment_form.vue6
-rw-r--r--app/assets/javascripts/notes/components/note_body.vue1
-rw-r--r--app/assets/javascripts/notes/components/note_form.vue6
-rw-r--r--app/assets/javascripts/notes/components/notes_app.vue6
-rw-r--r--app/assets/javascripts/notes/index.js3
-rw-r--r--app/assets/javascripts/notes/stores/actions.js9
-rw-r--r--app/assets/javascripts/notes/stores/mutations.js1
-rw-r--r--app/assets/javascripts/pages/profiles/keys/index.js16
-rw-r--r--app/assets/javascripts/preview_markdown.js14
-rw-r--r--app/assets/javascripts/profile/add_ssh_key_validation.js43
-rw-r--r--app/assets/javascripts/projects_dropdown/components/app.vue158
-rw-r--r--app/assets/javascripts/projects_dropdown/components/projects_list_frequent.vue57
-rw-r--r--app/assets/javascripts/projects_dropdown/components/projects_list_item.vue116
-rw-r--r--app/assets/javascripts/projects_dropdown/components/projects_list_search.vue63
-rw-r--r--app/assets/javascripts/projects_dropdown/components/search.vue65
-rw-r--r--app/assets/javascripts/projects_dropdown/constants.js10
-rw-r--r--app/assets/javascripts/projects_dropdown/index.js66
-rw-r--r--app/assets/javascripts/projects_dropdown/service/projects_service.js137
-rw-r--r--app/assets/javascripts/projects_dropdown/store/projects_store.js33
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/deployment.vue92
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue190
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue104
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue1
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue55
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/field.vue22
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss69
-rw-r--r--app/assets/stylesheets/framework/filters.scss5
-rw-r--r--app/assets/stylesheets/framework/forms.scss5
-rw-r--r--app/assets/stylesheets/framework/gitlab_theme.scss21
-rw-r--r--app/assets/stylesheets/framework/header.scss24
-rw-r--r--app/assets/stylesheets/framework/icons.scss24
-rw-r--r--app/assets/stylesheets/framework/mixins.scss7
-rw-r--r--app/assets/stylesheets/framework/typography.scss3
-rw-r--r--app/assets/stylesheets/framework/variables.scss1
-rw-r--r--app/assets/stylesheets/pages/boards.scss11
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss182
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss15
-rw-r--r--app/assets/stylesheets/pages/repo.scss11
-rw-r--r--app/controllers/admin/deploy_keys_controller.rb4
-rw-r--r--app/controllers/admin/groups_controller.rb2
-rw-r--r--app/controllers/admin/hooks_controller.rb4
-rw-r--r--app/controllers/admin/identities_controller.rb2
-rw-r--r--app/controllers/admin/impersonations_controller.rb2
-rw-r--r--app/controllers/admin/jobs_controller.rb2
-rw-r--r--app/controllers/admin/runner_projects_controller.rb2
-rw-r--r--app/controllers/admin/runners_controller.rb2
-rw-r--r--app/controllers/admin/services_controller.rb2
-rw-r--r--app/controllers/admin/users_controller.rb2
-rw-r--r--app/controllers/concerns/group_tree.rb40
-rw-r--r--app/controllers/concerns/issuable_actions.rb2
-rw-r--r--app/controllers/concerns/lfs_request.rb2
-rw-r--r--app/controllers/concerns/preview_markdown.rb3
-rw-r--r--app/controllers/dashboard/projects_controller.rb5
-rw-r--r--app/controllers/groups/avatars_controller.rb2
-rw-r--r--app/controllers/groups/runners_controller.rb2
-rw-r--r--app/controllers/jwt_controller.rb4
-rw-r--r--app/controllers/notification_settings_controller.rb4
-rw-r--r--app/controllers/profiles/active_sessions_controller.rb2
-rw-r--r--app/controllers/profiles/avatars_controller.rb2
-rw-r--r--app/controllers/profiles/chat_names_controller.rb2
-rw-r--r--app/controllers/profiles/emails_controller.rb2
-rw-r--r--app/controllers/profiles/gpg_keys_controller.rb4
-rw-r--r--app/controllers/profiles/keys_controller.rb2
-rw-r--r--app/controllers/profiles/two_factor_auths_controller.rb2
-rw-r--r--app/controllers/projects/application_controller.rb2
-rw-r--r--app/controllers/projects/autocomplete_sources_controller.rb2
-rw-r--r--app/controllers/projects/avatars_controller.rb2
-rw-r--r--app/controllers/projects/branches_controller.rb2
-rw-r--r--app/controllers/projects/clusters_controller.rb2
-rw-r--r--app/controllers/projects/deploy_keys_controller.rb2
-rw-r--r--app/controllers/projects/environments_controller.rb2
-rw-r--r--app/controllers/projects/git_http_client_controller.rb4
-rw-r--r--app/controllers/projects/group_links_controller.rb4
-rw-r--r--app/controllers/projects/hooks_controller.rb4
-rw-r--r--app/controllers/projects/labels_controller.rb4
-rw-r--r--app/controllers/projects/lfs_api_controller.rb2
-rw-r--r--app/controllers/projects/lfs_storage_controller.rb2
-rw-r--r--app/controllers/projects/merge_requests_controller.rb2
-rw-r--r--app/controllers/projects/milestones_controller.rb2
-rw-r--r--app/controllers/projects/mirrors_controller.rb2
-rw-r--r--app/controllers/projects/pipeline_schedules_controller.rb2
-rw-r--r--app/controllers/projects/releases_controller.rb2
-rw-r--r--app/controllers/projects/repositories_controller.rb2
-rw-r--r--app/controllers/projects/runner_projects_controller.rb2
-rw-r--r--app/controllers/projects/runners_controller.rb2
-rw-r--r--app/controllers/projects/services_controller.rb2
-rw-r--r--app/controllers/projects/snippets_controller.rb2
-rw-r--r--app/controllers/projects/tags_controller.rb2
-rw-r--r--app/controllers/projects/templates_controller.rb2
-rw-r--r--app/controllers/projects/triggers_controller.rb2
-rw-r--r--app/controllers/projects/wikis_controller.rb4
-rw-r--r--app/controllers/projects_controller.rb19
-rw-r--r--app/controllers/sessions_controller.rb4
-rw-r--r--app/controllers/sherlock/transactions_controller.rb2
-rw-r--r--app/controllers/snippets_controller.rb2
-rw-r--r--app/helpers/groups_helper.rb6
-rw-r--r--app/helpers/issuables_helper.rb1
-rw-r--r--app/helpers/markup_helper.rb4
-rw-r--r--app/helpers/notes_helper.rb1
-rw-r--r--app/helpers/projects_helper.rb1
-rw-r--r--app/helpers/users_helper.rb8
-rw-r--r--app/mailers/previews/devise_mailer_preview.rb30
-rw-r--r--app/mailers/previews/email_rejection_mailer_preview.rb5
-rw-r--r--app/mailers/previews/notify_preview.rb170
-rw-r--r--app/mailers/previews/repository_check_mailer_preview.rb5
-rw-r--r--app/models/ci/build.rb8
-rw-r--r--app/models/ci/build_trace_chunk.rb149
-rw-r--r--app/models/ci/build_trace_chunks/database.rb29
-rw-r--r--app/models/ci/build_trace_chunks/fog.rb59
-rw-r--r--app/models/ci/build_trace_chunks/redis.rb51
-rw-r--r--app/models/concerns/cache_markdown_field.rb22
-rw-r--r--app/models/concerns/cacheable_attributes.rb4
-rw-r--r--app/models/concerns/group_descendant.rb4
-rw-r--r--app/models/concerns/protected_ref.rb2
-rw-r--r--app/models/import_export_upload.rb13
-rw-r--r--app/models/milestone.rb3
-rw-r--r--app/models/network/commit.rb4
-rw-r--r--app/models/project.rb20
-rw-r--r--app/models/project_wiki.rb2
-rw-r--r--app/models/remote_mirror.rb2
-rw-r--r--app/models/repository.rb16
-rw-r--r--app/models/user.rb8
-rw-r--r--app/models/wiki_page.rb1
-rw-r--r--app/policies/group_policy.rb13
-rw-r--r--app/serializers/note_entity.rb2
-rw-r--r--app/services/badges/update_service.rb2
-rw-r--r--app/services/commits/change_service.rb2
-rw-r--r--app/services/import_export_clean_up_service.rb11
-rw-r--r--app/services/issuable_base_service.rb2
-rw-r--r--app/services/members/update_service.rb2
-rw-r--r--app/services/merge_requests/rebase_service.rb2
-rw-r--r--app/services/milestones/update_service.rb2
-rw-r--r--app/services/notes/update_service.rb2
-rw-r--r--app/services/notification_recipient_service.rb13
-rw-r--r--app/services/notification_service.rb6
-rw-r--r--app/services/preview_markdown_service.rb7
-rw-r--r--app/services/projects/autocomplete_service.rb34
-rw-r--r--app/services/projects/destroy_service.rb2
-rw-r--r--app/services/projects/fork_service.rb2
-rw-r--r--app/services/projects/lfs_pointers/lfs_download_service.rb2
-rw-r--r--app/services/projects/update_service.rb2
-rw-r--r--app/services/update_release_service.rb2
-rw-r--r--app/uploaders/import_export_uploader.rb15
-rw-r--r--app/views/import/_githubish_status.html.haml4
-rw-r--r--app/views/layouts/header/_current_user_dropdown.html.haml6
-rw-r--r--app/views/layouts/header/_default.html.haml4
-rw-r--r--app/views/layouts/header/_new_dropdown.haml2
-rw-r--r--app/views/layouts/nav/_dashboard.html.haml16
-rw-r--r--app/views/layouts/nav/groups_dropdown/_show.html.haml12
-rw-r--r--app/views/layouts/nav/projects_dropdown/_show.html.haml6
-rw-r--r--app/views/profiles/keys/_form.html.haml13
-rw-r--r--app/views/projects/_export.html.haml2
-rw-r--r--app/views/projects/issues/_form.html.haml4
-rw-r--r--app/views/projects/merge_requests/_form.html.haml4
-rw-r--r--app/views/projects/milestones/_form.html.haml4
-rw-r--r--app/views/projects/releases/edit.html.haml4
-rw-r--r--app/views/projects/wikis/_form.html.haml4
-rw-r--r--app/views/shared/_user_dropdown_contributing_link.html.haml5
-rw-r--r--app/views/shared/hook_logs/_content.html.haml4
-rw-r--r--app/views/shared/notes/_note.html.haml2
-rw-r--r--app/views/shared/projects/_project.html.haml2
-rw-r--r--app/views/shared/snippets/_form.html.haml4
-rw-r--r--app/workers/all_queues.yml1
-rw-r--r--app/workers/archive_trace_worker.rb2
-rw-r--r--app/workers/ci/archive_traces_cron_worker.rb1
-rw-r--r--app/workers/ci/build_trace_chunk_flush_worker.rb2
-rw-r--r--app/workers/email_receiver_worker.rb8
-rw-r--r--app/workers/object_storage/migrate_uploads_worker.rb2
-rw-r--r--app/workers/object_storage_upload_worker.rb23
-rw-r--r--app/workers/repository_check/batch_worker.rb20
-rw-r--r--app/workers/repository_check/dispatch_worker.rb13
219 files changed, 2489 insertions, 1503 deletions
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js
index 0ca0e8f35dd..422becb7db8 100644
--- a/app/assets/javascripts/api.js
+++ b/app/assets/javascripts/api.js
@@ -100,12 +100,12 @@ const Api = {
},
// Return Merge Request for project
- mergeRequest(projectPath, mergeRequestId) {
+ mergeRequest(projectPath, mergeRequestId, params = {}) {
const url = Api.buildUrl(Api.mergeRequestPath)
.replace(':id', encodeURIComponent(projectPath))
.replace(':mrid', mergeRequestId);
- return axios.get(url);
+ return axios.get(url, { params });
},
mergeRequests(params = {}) {
diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue
index eb0985e5603..0327fceb38d 100644
--- a/app/assets/javascripts/diffs/components/app.vue
+++ b/app/assets/javascripts/diffs/components/app.vue
@@ -63,7 +63,8 @@ export default {
plainDiffPath: state => state.diffs.plainDiffPath,
emailPatchPath: state => state.diffs.emailPatchPath,
}),
- ...mapGetters(['isParallelView', 'isNotesFetched']),
+ ...mapGetters('diffs', ['isParallelView']),
+ ...mapGetters(['isNotesFetched']),
targetBranch() {
return {
branchName: this.targetBranchName,
@@ -115,7 +116,7 @@ export default {
this.adjustView();
},
methods: {
- ...mapActions(['setBaseConfig', 'fetchDiffFiles']),
+ ...mapActions('diffs', ['setBaseConfig', 'fetchDiffFiles']),
fetchData() {
this.fetchDiffFiles().catch(() => {
createFlash(__('Something went wrong on our end. Please try again!'));
diff --git a/app/assets/javascripts/diffs/components/changed_files.vue b/app/assets/javascripts/diffs/components/changed_files.vue
index c5ef9fefc2f..9d29357d800 100644
--- a/app/assets/javascripts/diffs/components/changed_files.vue
+++ b/app/assets/javascripts/diffs/components/changed_files.vue
@@ -31,7 +31,7 @@ export default {
};
},
computed: {
- ...mapGetters(['isInlineView', 'isParallelView', 'areAllFilesCollapsed']),
+ ...mapGetters('diffs', ['isInlineView', 'isParallelView', 'areAllFilesCollapsed']),
sumAddedLines() {
return this.sumValues('addedLines');
},
@@ -66,7 +66,7 @@ export default {
document.removeEventListener('scroll', this.handleScroll);
},
methods: {
- ...mapActions(['setInlineDiffViewType', 'setParallelDiffViewType', 'expandAllFiles']),
+ ...mapActions('diffs', ['setInlineDiffViewType', 'setParallelDiffViewType', 'expandAllFiles']),
pluralize,
handleScroll() {
if (!this.updating) {
diff --git a/app/assets/javascripts/diffs/components/diff_content.vue b/app/assets/javascripts/diffs/components/diff_content.vue
index b6af49c7e2e..02d5be1821b 100644
--- a/app/assets/javascripts/diffs/components/diff_content.vue
+++ b/app/assets/javascripts/diffs/components/diff_content.vue
@@ -22,7 +22,7 @@ export default {
projectPath: state => state.diffs.projectPath,
endpoint: state => state.diffs.endpoint,
}),
- ...mapGetters(['isInlineView', 'isParallelView']),
+ ...mapGetters('diffs', ['isInlineView', 'isParallelView']),
diffMode() {
const diffModeKey = Object.keys(diffModes).find(key => this.diffFile[`${key}File`]);
return diffModes[diffModeKey] || diffModes.replaced;
@@ -39,12 +39,12 @@ export default {
<div class="diff-viewer">
<template v-if="isTextFile">
<inline-diff-view
- v-show="isInlineView"
+ v-if="isInlineView"
:diff-file="diffFile"
:diff-lines="diffFile.highlightedDiffLines || []"
/>
<parallel-diff-view
- v-show="isParallelView"
+ v-if="isParallelView"
:diff-file="diffFile"
:diff-lines="diffFile.parallelDiffLines || []"
/>
diff --git a/app/assets/javascripts/diffs/components/diff_discussions.vue b/app/assets/javascripts/diffs/components/diff_discussions.vue
index 39d535036f6..20483161033 100644
--- a/app/assets/javascripts/diffs/components/diff_discussions.vue
+++ b/app/assets/javascripts/diffs/components/diff_discussions.vue
@@ -15,9 +15,7 @@ export default {
</script>
<template>
- <div
- v-if="discussions.length"
- >
+ <div>
<div
v-for="discussion in discussions"
:key="discussion.id"
diff --git a/app/assets/javascripts/diffs/components/diff_file.vue b/app/assets/javascripts/diffs/components/diff_file.vue
index 108eefdac5f..060386c3ecb 100644
--- a/app/assets/javascripts/diffs/components/diff_file.vue
+++ b/app/assets/javascripts/diffs/components/diff_file.vue
@@ -47,6 +47,9 @@ export default {
false,
);
},
+ showExpandMessage() {
+ return this.isCollapsed && !this.isLoadingCollapsedDiff && !this.file.tooLarge;
+ },
},
mounted() {
document.addEventListener('scroll', this.handleScroll);
@@ -55,7 +58,7 @@ export default {
document.removeEventListener('scroll', this.handleScroll);
},
methods: {
- ...mapActions(['loadCollapsedDiff']),
+ ...mapActions('diffs', ['loadCollapsedDiff']),
handleToggle() {
const { collapsed, highlightedDiffLines, parallelDiffLines } = this.file;
@@ -159,7 +162,7 @@ export default {
</div>
<diff-content
- v-show="!isCollapsed"
+ v-if="!isCollapsed"
:class="{ hidden: isCollapsed || file.tooLarge }"
:diff-file="file"
/>
@@ -168,7 +171,7 @@ export default {
class="diff-content loading"
/>
<div
- v-show="isCollapsed && !isLoadingCollapsedDiff && !file.tooLarge"
+ v-if="showExpandMessage"
class="nothing-here-block diff-collapsed"
>
{{ __('This diff is collapsed.') }}
diff --git a/app/assets/javascripts/diffs/components/diff_file_header.vue b/app/assets/javascripts/diffs/components/diff_file_header.vue
index a8e8732053b..1957698c6c1 100644
--- a/app/assets/javascripts/diffs/components/diff_file_header.vue
+++ b/app/assets/javascripts/diffs/components/diff_file_header.vue
@@ -145,6 +145,7 @@ export default {
@click.stop="handleToggle"
/>
<a
+ v-once
ref="titleWrapper"
:href="titleLink"
class="append-right-4"
diff --git a/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue b/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue
index a74ea4bfaaf..ad838a32518 100644
--- a/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue
+++ b/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue
@@ -108,7 +108,7 @@ export default {
},
},
methods: {
- ...mapActions(['loadMoreLines', 'showCommentForm']),
+ ...mapActions('diffs', ['loadMoreLines', 'showCommentForm']),
handleCommentButton() {
this.showCommentForm({ lineCode: this.lineCode });
},
@@ -189,6 +189,7 @@ export default {
</button>
<a
v-if="lineNumber"
+ v-once
:data-linenumber="lineNumber"
:href="lineHref"
>
diff --git a/app/assets/javascripts/diffs/components/diff_line_note_form.vue b/app/assets/javascripts/diffs/components/diff_line_note_form.vue
index 6943b462e86..db380e68bd1 100644
--- a/app/assets/javascripts/diffs/components/diff_line_note_form.vue
+++ b/app/assets/javascripts/diffs/components/diff_line_note_form.vue
@@ -59,7 +59,8 @@ export default {
}
},
methods: {
- ...mapActions(['cancelCommentForm', 'saveNote', 'fetchDiscussions']),
+ ...mapActions('diffs', ['cancelCommentForm']),
+ ...mapActions(['saveNote', 'refetchDiscussionById']),
handleCancelCommentForm() {
this.autosave.reset();
this.cancelCommentForm({
@@ -78,10 +79,10 @@ export default {
});
this.saveNote(postData)
- .then(() => {
+ .then(result => {
const endpoint = this.getNotesDataByProp('discussionsPath');
- this.fetchDiscussions(endpoint)
+ this.refetchDiscussionById({ path: endpoint, discussionId: result.discussion_id })
.then(() => {
this.handleCancelCommentForm();
})
diff --git a/app/assets/javascripts/diffs/components/diff_table_cell.vue b/app/assets/javascripts/diffs/components/diff_table_cell.vue
index 5b08b161114..bd02b45a63c 100644
--- a/app/assets/javascripts/diffs/components/diff_table_cell.vue
+++ b/app/assets/javascripts/diffs/components/diff_table_cell.vue
@@ -117,14 +117,6 @@ export default {
<template>
<td
- v-if="isContentLine"
- :class="lineType"
- class="line_content"
- v-html="normalizedLine.richText"
- >
- </td>
- <td
- v-else
:class="classNameMap"
>
<diff-line-gutter-content
diff --git a/app/assets/javascripts/diffs/components/inline_diff_comment_row.vue b/app/assets/javascripts/diffs/components/inline_diff_comment_row.vue
index 0e935f1d68e..1e8f2eecd76 100644
--- a/app/assets/javascripts/diffs/components/inline_diff_comment_row.vue
+++ b/app/assets/javascripts/diffs/components/inline_diff_comment_row.vue
@@ -31,22 +31,9 @@ export default {
diffLineCommentForms: state => state.diffs.diffLineCommentForms,
}),
...mapGetters(['discussionsByLineCode']),
- isDiscussionExpanded() {
- if (!this.discussions.length) {
- return false;
- }
-
- return this.discussions.every(discussion => discussion.expanded);
- },
- hasCommentForm() {
- return this.diffLineCommentForms[this.line.lineCode];
- },
discussions() {
return this.discussionsByLineCode[this.line.lineCode] || [];
},
- shouldRender() {
- return this.isDiscussionExpanded || this.hasCommentForm;
- },
className() {
return this.discussions.length ? '' : 'js-temp-notes-holder';
},
@@ -56,7 +43,6 @@ export default {
<template>
<tr
- v-if="shouldRender"
:class="className"
class="notes_holder"
>
@@ -67,6 +53,7 @@ export default {
<td class="notes_content">
<div class="content">
<diff-discussions
+ v-if="discussions.length"
:discussions="discussions"
/>
<diff-line-note-form
diff --git a/app/assets/javascripts/diffs/components/inline_diff_table_row.vue b/app/assets/javascripts/diffs/components/inline_diff_table_row.vue
index a2470843ca6..8e4715c9862 100644
--- a/app/assets/javascripts/diffs/components/inline_diff_table_row.vue
+++ b/app/assets/javascripts/diffs/components/inline_diff_table_row.vue
@@ -36,7 +36,7 @@ export default {
};
},
computed: {
- ...mapGetters(['isInlineView']),
+ ...mapGetters('diffs', ['isInlineView']),
isContextLine() {
return this.line.type === CONTEXT_LINE_TYPE;
},
@@ -94,11 +94,12 @@ export default {
:is-hover="isHover"
class="diff-line-num new_line"
/>
- <diff-table-cell
+ <td
+ v-once
:class="line.type"
- :diff-file="diffFile"
- :line="line"
- :is-content-line="true"
- />
+ class="line_content"
+ v-html="line.richText"
+ >
+ </td>
</tr>
</template>
diff --git a/app/assets/javascripts/diffs/components/inline_diff_view.vue b/app/assets/javascripts/diffs/components/inline_diff_view.vue
index b884230fb63..9c1359f7c89 100644
--- a/app/assets/javascripts/diffs/components/inline_diff_view.vue
+++ b/app/assets/javascripts/diffs/components/inline_diff_view.vue
@@ -1,5 +1,5 @@
<script>
-import { mapGetters } from 'vuex';
+import { mapGetters, mapState } from 'vuex';
import inlineDiffTableRow from './inline_diff_table_row.vue';
import inlineDiffCommentRow from './inline_diff_comment_row.vue';
import { trimFirstCharOfLineContent } from '../store/utils';
@@ -20,20 +20,33 @@ export default {
},
},
computed: {
- ...mapGetters(['commit']),
+ ...mapGetters('diffs', ['commitId']),
+ ...mapGetters(['discussionsByLineCode']),
+ ...mapState({
+ diffLineCommentForms: state => state.diffs.diffLineCommentForms,
+ }),
normalizedDiffLines() {
return this.diffLines.map(line => (line.richText ? trimFirstCharOfLineContent(line) : line));
},
diffLinesLength() {
return this.normalizedDiffLines.length;
},
- commitId() {
- return this.commit && this.commit.id;
- },
userColorScheme() {
return window.gon.user_color_scheme;
},
},
+ methods: {
+ shouldRenderCommentRow(line) {
+ if (this.diffLineCommentForms[line.lineCode]) return true;
+
+ const lineDiscussions = this.discussionsByLineCode[line.lineCode];
+ if (lineDiscussions === undefined) {
+ return false;
+ }
+
+ return lineDiscussions.every(discussion => discussion.expanded);
+ },
+ },
};
</script>
@@ -53,6 +66,7 @@ export default {
:key="line.lineCode"
/>
<inline-diff-comment-row
+ v-if="shouldRenderCommentRow(line)"
:diff-file="diffFile"
:diff-lines="normalizedDiffLines"
:line="line"
diff --git a/app/assets/javascripts/diffs/components/parallel_diff_comment_row.vue b/app/assets/javascripts/diffs/components/parallel_diff_comment_row.vue
index 5f33ec7a3c2..1e20792b647 100644
--- a/app/assets/javascripts/diffs/components/parallel_diff_comment_row.vue
+++ b/app/assets/javascripts/diffs/components/parallel_diff_comment_row.vue
@@ -55,13 +55,6 @@ export default {
hasAnyExpandedDiscussion() {
return this.hasExpandedDiscussionOnLeft || this.hasExpandedDiscussionOnRight;
},
- shouldRenderDiscussionsRow() {
- const hasDiscussion = this.hasDiscussion && this.hasAnyExpandedDiscussion;
- const hasCommentFormOnLeft = this.diffLineCommentForms[this.leftLineCode];
- const hasCommentFormOnRight = this.diffLineCommentForms[this.rightLineCode];
-
- return hasDiscussion || hasCommentFormOnLeft || hasCommentFormOnRight;
- },
shouldRenderDiscussionsOnLeft() {
return this.discussionsByLineCode[this.leftLineCode] && this.hasExpandedDiscussionOnLeft;
},
@@ -81,7 +74,6 @@ export default {
<template>
<tr
- v-if="shouldRenderDiscussionsRow"
:class="className"
class="notes_holder"
>
@@ -92,6 +84,7 @@ export default {
class="content"
>
<diff-discussions
+ v-if="discussionsByLineCode[leftLineCode].length"
:discussions="discussionsByLineCode[leftLineCode]"
/>
</div>
@@ -112,6 +105,7 @@ export default {
class="content"
>
<diff-discussions
+ v-if="discussionsByLineCode[rightLineCode].length"
:discussions="discussionsByLineCode[rightLineCode]"
/>
</div>
diff --git a/app/assets/javascripts/diffs/components/parallel_diff_table_row.vue b/app/assets/javascripts/diffs/components/parallel_diff_table_row.vue
index eb769584d74..b76fc63205b 100644
--- a/app/assets/javascripts/diffs/components/parallel_diff_table_row.vue
+++ b/app/assets/javascripts/diffs/components/parallel_diff_table_row.vue
@@ -40,7 +40,7 @@ export default {
};
},
computed: {
- ...mapGetters(['isParallelView']),
+ ...mapGetters('diffs', ['isParallelView']),
isContextLine() {
return this.line.left.type === CONTEXT_LINE_TYPE;
},
@@ -113,17 +113,15 @@ export default {
:diff-view-type="parallelDiffViewType"
class="diff-line-num old_line"
/>
- <diff-table-cell
+ <td
+ v-once
:id="line.left.lineCode"
- :diff-file="diffFile"
- :line="line"
- :is-content-line="true"
- :line-position="linePositionLeft"
- :line-type="parallelViewLeftLineType"
- :diff-view-type="parallelDiffViewType"
+ :class="parallelViewLeftLineType"
class="line_content parallel left-side"
@mousedown.native="handleParallelLineMouseDown"
- />
+ v-html="line.left.richText"
+ >
+ </td>
<diff-table-cell
:diff-file="diffFile"
:line="line"
@@ -135,16 +133,14 @@ export default {
:diff-view-type="parallelDiffViewType"
class="diff-line-num new_line"
/>
- <diff-table-cell
+ <td
+ v-once
:id="line.right.lineCode"
- :diff-file="diffFile"
- :line="line"
- :is-content-line="true"
- :line-position="linePositionRight"
- :line-type="line.right.type"
- :diff-view-type="parallelDiffViewType"
+ :class="line.right.type"
class="line_content parallel right-side"
@mousedown.native="handleParallelLineMouseDown"
- />
+ v-html="line.right.richText"
+ >
+ </td>
</tr>
</template>
diff --git a/app/assets/javascripts/diffs/components/parallel_diff_view.vue b/app/assets/javascripts/diffs/components/parallel_diff_view.vue
index 52561e197e6..216865474a6 100644
--- a/app/assets/javascripts/diffs/components/parallel_diff_view.vue
+++ b/app/assets/javascripts/diffs/components/parallel_diff_view.vue
@@ -1,5 +1,5 @@
<script>
-import { mapGetters } from 'vuex';
+import { mapState, mapGetters } from 'vuex';
import parallelDiffTableRow from './parallel_diff_table_row.vue';
import parallelDiffCommentRow from './parallel_diff_comment_row.vue';
import { EMPTY_CELL_TYPE } from '../constants';
@@ -21,7 +21,11 @@ export default {
},
},
computed: {
- ...mapGetters(['commit']),
+ ...mapGetters('diffs', ['commitId']),
+ ...mapGetters(['discussionsByLineCode']),
+ ...mapState({
+ diffLineCommentForms: state => state.diffs.diffLineCommentForms,
+ }),
parallelDiffLines() {
return this.diffLines.map(line => {
const parallelLine = Object.assign({}, line);
@@ -44,13 +48,36 @@ export default {
diffLinesLength() {
return this.parallelDiffLines.length;
},
- commitId() {
- return this.commit && this.commit.id;
- },
userColorScheme() {
return window.gon.user_color_scheme;
},
},
+ methods: {
+ shouldRenderCommentRow(line) {
+ const leftLineCode = line.left.lineCode;
+ const rightLineCode = line.right.lineCode;
+ const discussions = this.discussionsByLineCode;
+ const leftDiscussions = discussions[leftLineCode];
+ const rightDiscussions = discussions[rightLineCode];
+ const hasDiscussion = leftDiscussions || rightDiscussions;
+
+ const hasExpandedDiscussionOnLeft = leftDiscussions
+ ? leftDiscussions.every(discussion => discussion.expanded)
+ : false;
+ const hasExpandedDiscussionOnRight = rightDiscussions
+ ? rightDiscussions.every(discussion => discussion.expanded)
+ : false;
+
+ if (hasDiscussion && (hasExpandedDiscussionOnLeft || hasExpandedDiscussionOnRight)) {
+ return true;
+ }
+
+ const hasCommentFormOnLeft = this.diffLineCommentForms[leftLineCode];
+ const hasCommentFormOnRight = this.diffLineCommentForms[rightLineCode];
+
+ return hasCommentFormOnLeft || hasCommentFormOnRight;
+ },
+ },
};
</script>
@@ -72,6 +99,7 @@ export default {
:key="index"
/>
<parallel-diff-comment-row
+ v-if="shouldRenderCommentRow(line)"
:key="line.left.lineCode || line.right.lineCode"
:line="line"
:diff-file="diffFile"
diff --git a/app/assets/javascripts/diffs/store/getters.js b/app/assets/javascripts/diffs/store/getters.js
index 66d0f47d102..f3c2d7427e7 100644
--- a/app/assets/javascripts/diffs/store/getters.js
+++ b/app/assets/javascripts/diffs/store/getters.js
@@ -1,16 +1,12 @@
import { PARALLEL_DIFF_VIEW_TYPE, INLINE_DIFF_VIEW_TYPE } from '../constants';
-export default {
- isParallelView(state) {
- return state.diffViewType === PARALLEL_DIFF_VIEW_TYPE;
- },
- isInlineView(state) {
- return state.diffViewType === INLINE_DIFF_VIEW_TYPE;
- },
- areAllFilesCollapsed(state) {
- return state.diffFiles.every(file => file.collapsed);
- },
- commit(state) {
- return state.commit;
- },
-};
+export const isParallelView = state => state.diffViewType === PARALLEL_DIFF_VIEW_TYPE;
+
+export const isInlineView = state => state.diffViewType === INLINE_DIFF_VIEW_TYPE;
+
+export const areAllFilesCollapsed = state => state.diffFiles.every(file => file.collapsed);
+
+export const commitId = state => (state.commit && state.commit.id ? state.commit.id : null);
+
+// prevent babel-plugin-rewire from generating an invalid default during karma tests
+export default () => {};
diff --git a/app/assets/javascripts/diffs/store/modules/diff_state.js b/app/assets/javascripts/diffs/store/modules/diff_state.js
new file mode 100644
index 00000000000..39d90a64aab
--- /dev/null
+++ b/app/assets/javascripts/diffs/store/modules/diff_state.js
@@ -0,0 +1,18 @@
+import Cookies from 'js-cookie';
+import { getParameterValues } from '~/lib/utils/url_utility';
+import { INLINE_DIFF_VIEW_TYPE, DIFF_VIEW_COOKIE_NAME } from '../../constants';
+
+const viewTypeFromQueryString = getParameterValues('view')[0];
+const viewTypeFromCookie = Cookies.get(DIFF_VIEW_COOKIE_NAME);
+const defaultViewType = INLINE_DIFF_VIEW_TYPE;
+
+export default () => ({
+ isLoading: true,
+ endpoint: '',
+ basePath: '',
+ commit: null,
+ diffFiles: [],
+ mergeRequestDiffs: [],
+ diffLineCommentForms: {},
+ diffViewType: viewTypeFromQueryString || viewTypeFromCookie || defaultViewType,
+});
diff --git a/app/assets/javascripts/diffs/store/modules/index.js b/app/assets/javascripts/diffs/store/modules/index.js
index 94caa131506..90505f83b60 100644
--- a/app/assets/javascripts/diffs/store/modules/index.js
+++ b/app/assets/javascripts/diffs/store/modules/index.js
@@ -1,25 +1,11 @@
-import Cookies from 'js-cookie';
-import { getParameterValues } from '~/lib/utils/url_utility';
import actions from '../actions';
-import getters from '../getters';
+import * as getters from '../getters';
import mutations from '../mutations';
-import { INLINE_DIFF_VIEW_TYPE, DIFF_VIEW_COOKIE_NAME } from '../../constants';
-
-const viewTypeFromQueryString = getParameterValues('view')[0];
-const viewTypeFromCookie = Cookies.get(DIFF_VIEW_COOKIE_NAME);
-const defaultViewType = INLINE_DIFF_VIEW_TYPE;
+import createState from './diff_state';
export default {
- state: {
- isLoading: true,
- endpoint: '',
- basePath: '',
- commit: null,
- diffFiles: [],
- mergeRequestDiffs: [],
- diffLineCommentForms: {},
- diffViewType: viewTypeFromQueryString || viewTypeFromCookie || defaultViewType,
- },
+ namespaced: true,
+ state: createState(),
getters,
actions,
mutations,
diff --git a/app/assets/javascripts/diffs/store/mutations.js b/app/assets/javascripts/diffs/store/mutations.js
index 8aa8a114c6f..a98b2be89a3 100644
--- a/app/assets/javascripts/diffs/store/mutations.js
+++ b/app/assets/javascripts/diffs/store/mutations.js
@@ -66,15 +66,10 @@ export default {
},
[types.EXPAND_ALL_FILES](state) {
- const diffFiles = [];
-
- state.diffFiles.forEach(file => {
- diffFiles.push({
- ...file,
- collapsed: false,
- });
- });
-
- Object.assign(state, { diffFiles });
+ // eslint-disable-next-line no-param-reassign
+ state.diffFiles = state.diffFiles.map(file => ({
+ ...file,
+ collapsed: false,
+ }));
},
};
diff --git a/app/assets/javascripts/due_date_select.js b/app/assets/javascripts/due_date_select.js
index 17ea3bdb179..8abd8bc581a 100644
--- a/app/assets/javascripts/due_date_select.js
+++ b/app/assets/javascripts/due_date_select.js
@@ -171,6 +171,8 @@ export default class DueDateSelectors {
initMilestoneDatePicker() {
$('.datepicker').each(function initPikadayMilestone() {
const $datePicker = $(this);
+ const datePickerVal = $datePicker.val();
+
const calendar = new Pikaday({
field: $datePicker.get(0),
theme: 'gitlab-theme animate-picker',
@@ -183,7 +185,7 @@ export default class DueDateSelectors {
},
});
- calendar.setDate(parsePikadayDate($datePicker.val()));
+ calendar.setDate(parsePikadayDate(datePickerVal));
$datePicker.data('pikaday', calendar);
});
diff --git a/app/assets/javascripts/frequent_items/components/app.vue b/app/assets/javascripts/frequent_items/components/app.vue
new file mode 100644
index 00000000000..2f030de8967
--- /dev/null
+++ b/app/assets/javascripts/frequent_items/components/app.vue
@@ -0,0 +1,122 @@
+<script>
+import { mapState, mapActions, mapGetters } from 'vuex';
+import LoadingIcon from '~/vue_shared/components/loading_icon.vue';
+import AccessorUtilities from '~/lib/utils/accessor';
+import eventHub from '../event_hub';
+import store from '../store/';
+import { FREQUENT_ITEMS, STORAGE_KEY } from '../constants';
+import { isMobile, updateExistingFrequentItem } from '../utils';
+import FrequentItemsSearchInput from './frequent_items_search_input.vue';
+import FrequentItemsList from './frequent_items_list.vue';
+import frequentItemsMixin from './frequent_items_mixin';
+
+export default {
+ store,
+ components: {
+ LoadingIcon,
+ FrequentItemsSearchInput,
+ FrequentItemsList,
+ },
+ mixins: [frequentItemsMixin],
+ props: {
+ currentUserName: {
+ type: String,
+ required: true,
+ },
+ currentItem: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ ...mapState(['searchQuery', 'isLoadingItems', 'isFetchFailed', 'items']),
+ ...mapGetters(['hasSearchQuery']),
+ translations() {
+ return this.getTranslations(['loadingMessage', 'header']);
+ },
+ },
+ created() {
+ const { namespace, currentUserName, currentItem } = this;
+ const storageKey = `${currentUserName}/${STORAGE_KEY[namespace]}`;
+
+ this.setNamespace(namespace);
+ this.setStorageKey(storageKey);
+
+ if (currentItem.id) {
+ this.logItemAccess(storageKey, currentItem);
+ }
+
+ eventHub.$on(`${this.namespace}-dropdownOpen`, this.dropdownOpenHandler);
+ },
+ beforeDestroy() {
+ eventHub.$off(`${this.namespace}-dropdownOpen`, this.dropdownOpenHandler);
+ },
+ methods: {
+ ...mapActions(['setNamespace', 'setStorageKey', 'fetchFrequentItems']),
+ dropdownOpenHandler() {
+ if (this.searchQuery === '' || isMobile()) {
+ this.fetchFrequentItems();
+ }
+ },
+ logItemAccess(storageKey, item) {
+ if (!AccessorUtilities.isLocalStorageAccessSafe()) {
+ return false;
+ }
+
+ // Check if there's any frequent items list set
+ const storedRawItems = localStorage.getItem(storageKey);
+ const storedFrequentItems = storedRawItems
+ ? JSON.parse(storedRawItems)
+ : [{ ...item, frequency: 1 }]; // No frequent items list set, set one up.
+
+ // Check if item already exists in list
+ const itemMatchIndex = storedFrequentItems.findIndex(
+ frequentItem => frequentItem.id === item.id,
+ );
+
+ if (itemMatchIndex > -1) {
+ storedFrequentItems[itemMatchIndex] = updateExistingFrequentItem(
+ storedFrequentItems[itemMatchIndex],
+ item,
+ );
+ } else {
+ if (storedFrequentItems.length === FREQUENT_ITEMS.MAX_COUNT) {
+ storedFrequentItems.shift();
+ }
+
+ storedFrequentItems.push({ ...item, frequency: 1 });
+ }
+
+ return localStorage.setItem(storageKey, JSON.stringify(storedFrequentItems));
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <frequent-items-search-input
+ :namespace="namespace"
+ />
+ <loading-icon
+ v-if="isLoadingItems"
+ :label="translations.loadingMessage"
+ class="loading-animation prepend-top-20"
+ size="2"
+ />
+ <div
+ v-if="!isLoadingItems && !hasSearchQuery"
+ class="section-header"
+ >
+ {{ translations.header }}
+ </div>
+ <frequent-items-list
+ v-if="!isLoadingItems"
+ :items="items"
+ :namespace="namespace"
+ :has-search-query="hasSearchQuery"
+ :is-fetch-failed="isFetchFailed"
+ :matcher="searchQuery"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/frequent_items/components/frequent_items_list.vue b/app/assets/javascripts/frequent_items/components/frequent_items_list.vue
new file mode 100644
index 00000000000..8e511aa2a36
--- /dev/null
+++ b/app/assets/javascripts/frequent_items/components/frequent_items_list.vue
@@ -0,0 +1,78 @@
+<script>
+import FrequentItemsListItem from './frequent_items_list_item.vue';
+import frequentItemsMixin from './frequent_items_mixin';
+
+export default {
+ components: {
+ FrequentItemsListItem,
+ },
+ mixins: [frequentItemsMixin],
+ props: {
+ items: {
+ type: Array,
+ required: true,
+ },
+ hasSearchQuery: {
+ type: Boolean,
+ required: true,
+ },
+ isFetchFailed: {
+ type: Boolean,
+ required: true,
+ },
+ matcher: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ translations() {
+ return this.getTranslations([
+ 'itemListEmptyMessage',
+ 'itemListErrorMessage',
+ 'searchListEmptyMessage',
+ 'searchListErrorMessage',
+ ]);
+ },
+ isListEmpty() {
+ return this.items.length === 0;
+ },
+ listEmptyMessage() {
+ if (this.hasSearchQuery) {
+ return this.isFetchFailed
+ ? this.translations.searchListErrorMessage
+ : this.translations.searchListEmptyMessage;
+ }
+
+ return this.isFetchFailed
+ ? this.translations.itemListErrorMessage
+ : this.translations.itemListEmptyMessage;
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="frequent-items-list-container">
+ <ul class="list-unstyled">
+ <li
+ v-if="isListEmpty"
+ :class="{ 'section-failure': isFetchFailed }"
+ class="section-empty"
+ >
+ {{ listEmptyMessage }}
+ </li>
+ <frequent-items-list-item
+ v-for="item in items"
+ v-else
+ :key="item.id"
+ :item-id="item.id"
+ :item-name="item.name"
+ :namespace="item.namespace"
+ :web-url="item.webUrl"
+ :avatar-url="item.avatarUrl"
+ :matcher="matcher"
+ />
+ </ul>
+ </div>
+</template>
diff --git a/app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue b/app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue
new file mode 100644
index 00000000000..1f1665ff7fe
--- /dev/null
+++ b/app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue
@@ -0,0 +1,117 @@
+<script>
+/* eslint-disable vue/require-default-prop, vue/require-prop-types */
+import Identicon from '../../vue_shared/components/identicon.vue';
+
+export default {
+ components: {
+ Identicon,
+ },
+ props: {
+ matcher: {
+ type: String,
+ required: false,
+ },
+ itemId: {
+ type: Number,
+ required: true,
+ },
+ itemName: {
+ type: String,
+ required: true,
+ },
+ namespace: {
+ type: String,
+ required: false,
+ },
+ webUrl: {
+ type: String,
+ required: true,
+ },
+ avatarUrl: {
+ required: true,
+ validator(value) {
+ return value === null || typeof value === 'string';
+ },
+ },
+ },
+ computed: {
+ hasAvatar() {
+ return this.avatarUrl !== null;
+ },
+ highlightedItemName() {
+ if (this.matcher) {
+ const matcherRegEx = new RegExp(this.matcher, 'gi');
+ const matches = this.itemName.match(matcherRegEx);
+
+ if (matches && matches.length > 0) {
+ return this.itemName.replace(matches[0], `<b>${matches[0]}</b>`);
+ }
+ }
+ return this.itemName;
+ },
+ /**
+ * Smartly truncates item namespace by doing two things;
+ * 1. Only include Group names in path by removing item name
+ * 2. Only include first and last group names in the path
+ * when namespace has more than 2 groups present
+ *
+ * First part (removal of item name from namespace) can be
+ * done from backend but doing so involves migration of
+ * existing item namespaces which is not wise thing to do.
+ */
+ truncatedNamespace() {
+ if (!this.namespace) {
+ return null;
+ }
+ const namespaceArr = this.namespace.split(' / ');
+
+ namespaceArr.splice(-1, 1);
+ let namespace = namespaceArr.join(' / ');
+
+ if (namespaceArr.length > 2) {
+ namespace = `${namespaceArr[0]} / ... / ${namespaceArr.pop()}`;
+ }
+
+ return namespace;
+ },
+ },
+};
+</script>
+
+<template>
+ <li class="frequent-items-list-item-container">
+ <a
+ :href="webUrl"
+ class="clearfix"
+ >
+ <div class="frequent-items-item-avatar-container">
+ <img
+ v-if="hasAvatar"
+ :src="avatarUrl"
+ class="avatar s32"
+ />
+ <identicon
+ v-else
+ :entity-id="itemId"
+ :entity-name="itemName"
+ size-class="s32"
+ />
+ </div>
+ <div class="frequent-items-item-metadata-container">
+ <div
+ :title="itemName"
+ class="frequent-items-item-title"
+ v-html="highlightedItemName"
+ >
+ </div>
+ <div
+ v-if="truncatedNamespace"
+ :title="namespace"
+ class="frequent-items-item-namespace"
+ >
+ {{ truncatedNamespace }}
+ </div>
+ </div>
+ </a>
+ </li>
+</template>
diff --git a/app/assets/javascripts/frequent_items/components/frequent_items_mixin.js b/app/assets/javascripts/frequent_items/components/frequent_items_mixin.js
new file mode 100644
index 00000000000..704dc83ca8e
--- /dev/null
+++ b/app/assets/javascripts/frequent_items/components/frequent_items_mixin.js
@@ -0,0 +1,23 @@
+import { TRANSLATION_KEYS } from '../constants';
+
+export default {
+ props: {
+ namespace: {
+ type: String,
+ required: true,
+ },
+ },
+ methods: {
+ getTranslations(keys) {
+ const translationStrings = keys.reduce(
+ (acc, key) => ({
+ ...acc,
+ [key]: TRANSLATION_KEYS[this.namespace][key],
+ }),
+ {},
+ );
+
+ return translationStrings;
+ },
+ },
+};
diff --git a/app/assets/javascripts/frequent_items/components/frequent_items_search_input.vue b/app/assets/javascripts/frequent_items/components/frequent_items_search_input.vue
new file mode 100644
index 00000000000..a6a265eb3fd
--- /dev/null
+++ b/app/assets/javascripts/frequent_items/components/frequent_items_search_input.vue
@@ -0,0 +1,55 @@
+<script>
+import _ from 'underscore';
+import { mapActions } from 'vuex';
+import eventHub from '../event_hub';
+import frequentItemsMixin from './frequent_items_mixin';
+
+export default {
+ mixins: [frequentItemsMixin],
+ data() {
+ return {
+ searchQuery: '',
+ };
+ },
+ computed: {
+ translations() {
+ return this.getTranslations(['searchInputPlaceholder']);
+ },
+ },
+ watch: {
+ searchQuery: _.debounce(function debounceSearchQuery() {
+ this.setSearchQuery(this.searchQuery);
+ }, 500),
+ },
+ mounted() {
+ eventHub.$on(`${this.namespace}-dropdownOpen`, this.setFocus);
+ },
+ beforeDestroy() {
+ eventHub.$off(`${this.namespace}-dropdownOpen`, this.setFocus);
+ },
+ methods: {
+ ...mapActions(['setSearchQuery']),
+ setFocus() {
+ this.$refs.search.focus();
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="search-input-container d-none d-sm-block">
+ <input
+ ref="search"
+ v-model="searchQuery"
+ :placeholder="translations.searchInputPlaceholder"
+ type="search"
+ class="form-control"
+ />
+ <i
+ v-if="!searchQuery"
+ class="search-icon fa fa-fw fa-search"
+ aria-hidden="true"
+ >
+ </i>
+ </div>
+</template>
diff --git a/app/assets/javascripts/frequent_items/constants.js b/app/assets/javascripts/frequent_items/constants.js
new file mode 100644
index 00000000000..9bc17f5ef4f
--- /dev/null
+++ b/app/assets/javascripts/frequent_items/constants.js
@@ -0,0 +1,38 @@
+import { s__ } from '~/locale';
+
+export const FREQUENT_ITEMS = {
+ MAX_COUNT: 20,
+ LIST_COUNT_DESKTOP: 5,
+ LIST_COUNT_MOBILE: 3,
+ ELIGIBLE_FREQUENCY: 3,
+};
+
+export const HOUR_IN_MS = 3600000;
+
+export const STORAGE_KEY = {
+ projects: 'frequent-projects',
+ groups: 'frequent-groups',
+};
+
+export const TRANSLATION_KEYS = {
+ projects: {
+ loadingMessage: s__('ProjectsDropdown|Loading projects'),
+ header: s__('ProjectsDropdown|Frequently visited'),
+ itemListErrorMessage: s__(
+ 'ProjectsDropdown|This feature requires browser localStorage support',
+ ),
+ itemListEmptyMessage: s__('ProjectsDropdown|Projects you visit often will appear here'),
+ searchListErrorMessage: s__('ProjectsDropdown|Something went wrong on our end.'),
+ searchListEmptyMessage: s__('ProjectsDropdown|Sorry, no projects matched your search'),
+ searchInputPlaceholder: s__('ProjectsDropdown|Search your projects'),
+ },
+ groups: {
+ loadingMessage: s__('GroupsDropdown|Loading groups'),
+ header: s__('GroupsDropdown|Frequently visited'),
+ itemListErrorMessage: s__('GroupsDropdown|This feature requires browser localStorage support'),
+ itemListEmptyMessage: s__('GroupsDropdown|Groups you visit often will appear here'),
+ searchListErrorMessage: s__('GroupsDropdown|Something went wrong on our end.'),
+ searchListEmptyMessage: s__('GroupsDropdown|Sorry, no groups matched your search'),
+ searchInputPlaceholder: s__('GroupsDropdown|Search your groups'),
+ },
+};
diff --git a/app/assets/javascripts/projects_dropdown/event_hub.js b/app/assets/javascripts/frequent_items/event_hub.js
index 0948c2e5352..0948c2e5352 100644
--- a/app/assets/javascripts/projects_dropdown/event_hub.js
+++ b/app/assets/javascripts/frequent_items/event_hub.js
diff --git a/app/assets/javascripts/frequent_items/index.js b/app/assets/javascripts/frequent_items/index.js
new file mode 100644
index 00000000000..5157ff211dc
--- /dev/null
+++ b/app/assets/javascripts/frequent_items/index.js
@@ -0,0 +1,69 @@
+import $ from 'jquery';
+import Vue from 'vue';
+import Translate from '~/vue_shared/translate';
+import eventHub from '~/frequent_items/event_hub';
+import frequentItems from './components/app.vue';
+
+Vue.use(Translate);
+
+const frequentItemDropdowns = [
+ {
+ namespace: 'projects',
+ key: 'project',
+ },
+ {
+ namespace: 'groups',
+ key: 'group',
+ },
+];
+
+document.addEventListener('DOMContentLoaded', () => {
+ frequentItemDropdowns.forEach(dropdown => {
+ const { namespace, key } = dropdown;
+ const el = document.getElementById(`js-${namespace}-dropdown`);
+ const navEl = document.getElementById(`nav-${namespace}-dropdown`);
+
+ // Don't do anything if element doesn't exist (No groups dropdown)
+ // This is for when the user accesses GitLab without logging in
+ if (!el || !navEl) {
+ return;
+ }
+
+ $(navEl).on('shown.bs.dropdown', () => {
+ eventHub.$emit(`${namespace}-dropdownOpen`);
+ });
+
+ // eslint-disable-next-line no-new
+ new Vue({
+ el,
+ components: {
+ frequentItems,
+ },
+ data() {
+ const { dataset } = this.$options.el;
+ const item = {
+ id: Number(dataset[`${key}Id`]),
+ name: dataset[`${key}Name`],
+ namespace: dataset[`${key}Namespace`],
+ webUrl: dataset[`${key}WebUrl`],
+ avatarUrl: dataset[`${key}AvatarUrl`] || null,
+ lastAccessedOn: Date.now(),
+ };
+
+ return {
+ currentUserName: dataset.userName,
+ currentItem: item,
+ };
+ },
+ render(createElement) {
+ return createElement('frequent-items', {
+ props: {
+ namespace,
+ currentUserName: this.currentUserName,
+ currentItem: this.currentItem,
+ },
+ });
+ },
+ });
+ });
+});
diff --git a/app/assets/javascripts/frequent_items/store/actions.js b/app/assets/javascripts/frequent_items/store/actions.js
new file mode 100644
index 00000000000..3dd89a82a42
--- /dev/null
+++ b/app/assets/javascripts/frequent_items/store/actions.js
@@ -0,0 +1,81 @@
+import Api from '~/api';
+import AccessorUtilities from '~/lib/utils/accessor';
+import * as types from './mutation_types';
+import { getTopFrequentItems } from '../utils';
+
+export const setNamespace = ({ commit }, namespace) => {
+ commit(types.SET_NAMESPACE, namespace);
+};
+
+export const setStorageKey = ({ commit }, key) => {
+ commit(types.SET_STORAGE_KEY, key);
+};
+
+export const requestFrequentItems = ({ commit }) => {
+ commit(types.REQUEST_FREQUENT_ITEMS);
+};
+export const receiveFrequentItemsSuccess = ({ commit }, data) => {
+ commit(types.RECEIVE_FREQUENT_ITEMS_SUCCESS, data);
+};
+export const receiveFrequentItemsError = ({ commit }) => {
+ commit(types.RECEIVE_FREQUENT_ITEMS_ERROR);
+};
+
+export const fetchFrequentItems = ({ state, dispatch }) => {
+ dispatch('requestFrequentItems');
+
+ if (AccessorUtilities.isLocalStorageAccessSafe()) {
+ const storedFrequentItems = JSON.parse(localStorage.getItem(state.storageKey));
+
+ dispatch(
+ 'receiveFrequentItemsSuccess',
+ !storedFrequentItems ? [] : getTopFrequentItems(storedFrequentItems),
+ );
+ } else {
+ dispatch('receiveFrequentItemsError');
+ }
+};
+
+export const requestSearchedItems = ({ commit }) => {
+ commit(types.REQUEST_SEARCHED_ITEMS);
+};
+export const receiveSearchedItemsSuccess = ({ commit }, data) => {
+ commit(types.RECEIVE_SEARCHED_ITEMS_SUCCESS, data);
+};
+export const receiveSearchedItemsError = ({ commit }) => {
+ commit(types.RECEIVE_SEARCHED_ITEMS_ERROR);
+};
+export const fetchSearchedItems = ({ state, dispatch }, searchQuery) => {
+ dispatch('requestSearchedItems');
+
+ const params = {
+ simple: true,
+ per_page: 20,
+ membership: !!gon.current_user_id,
+ };
+
+ if (state.namespace === 'projects') {
+ params.order_by = 'last_activity_at';
+ }
+
+ return Api[state.namespace](searchQuery, params)
+ .then(results => {
+ dispatch('receiveSearchedItemsSuccess', results);
+ })
+ .catch(() => {
+ dispatch('receiveSearchedItemsError');
+ });
+};
+
+export const setSearchQuery = ({ commit, dispatch }, query) => {
+ commit(types.SET_SEARCH_QUERY, query);
+
+ if (query) {
+ dispatch('fetchSearchedItems', query);
+ } else {
+ dispatch('fetchFrequentItems');
+ }
+};
+
+// prevent babel-plugin-rewire from generating an invalid default during karma tests
+export default () => {};
diff --git a/app/assets/javascripts/frequent_items/store/getters.js b/app/assets/javascripts/frequent_items/store/getters.js
new file mode 100644
index 00000000000..00165db6684
--- /dev/null
+++ b/app/assets/javascripts/frequent_items/store/getters.js
@@ -0,0 +1,4 @@
+export const hasSearchQuery = state => state.searchQuery !== '';
+
+// prevent babel-plugin-rewire from generating an invalid default during karma tests
+export default () => {};
diff --git a/app/assets/javascripts/frequent_items/store/index.js b/app/assets/javascripts/frequent_items/store/index.js
new file mode 100644
index 00000000000..ece9e6419dd
--- /dev/null
+++ b/app/assets/javascripts/frequent_items/store/index.js
@@ -0,0 +1,16 @@
+import Vue from 'vue';
+import Vuex from 'vuex';
+import * as actions from './actions';
+import * as getters from './getters';
+import mutations from './mutations';
+import state from './state';
+
+Vue.use(Vuex);
+
+export default () =>
+ new Vuex.Store({
+ actions,
+ getters,
+ mutations,
+ state: state(),
+ });
diff --git a/app/assets/javascripts/frequent_items/store/mutation_types.js b/app/assets/javascripts/frequent_items/store/mutation_types.js
new file mode 100644
index 00000000000..cbe2c9401ad
--- /dev/null
+++ b/app/assets/javascripts/frequent_items/store/mutation_types.js
@@ -0,0 +1,9 @@
+export const SET_NAMESPACE = 'SET_NAMESPACE';
+export const SET_STORAGE_KEY = 'SET_STORAGE_KEY';
+export const SET_SEARCH_QUERY = 'SET_SEARCH_QUERY';
+export const REQUEST_FREQUENT_ITEMS = 'REQUEST_FREQUENT_ITEMS';
+export const RECEIVE_FREQUENT_ITEMS_SUCCESS = 'RECEIVE_FREQUENT_ITEMS_SUCCESS';
+export const RECEIVE_FREQUENT_ITEMS_ERROR = 'RECEIVE_FREQUENT_ITEMS_ERROR';
+export const REQUEST_SEARCHED_ITEMS = 'REQUEST_SEARCHED_ITEMS';
+export const RECEIVE_SEARCHED_ITEMS_SUCCESS = 'RECEIVE_SEARCHED_ITEMS_SUCCESS';
+export const RECEIVE_SEARCHED_ITEMS_ERROR = 'RECEIVE_SEARCHED_ITEMS_ERROR';
diff --git a/app/assets/javascripts/frequent_items/store/mutations.js b/app/assets/javascripts/frequent_items/store/mutations.js
new file mode 100644
index 00000000000..41b660a243f
--- /dev/null
+++ b/app/assets/javascripts/frequent_items/store/mutations.js
@@ -0,0 +1,71 @@
+import * as types from './mutation_types';
+
+export default {
+ [types.SET_NAMESPACE](state, namespace) {
+ Object.assign(state, {
+ namespace,
+ });
+ },
+ [types.SET_STORAGE_KEY](state, storageKey) {
+ Object.assign(state, {
+ storageKey,
+ });
+ },
+ [types.SET_SEARCH_QUERY](state, searchQuery) {
+ const hasSearchQuery = searchQuery !== '';
+
+ Object.assign(state, {
+ searchQuery,
+ isLoadingItems: true,
+ hasSearchQuery,
+ });
+ },
+ [types.REQUEST_FREQUENT_ITEMS](state) {
+ Object.assign(state, {
+ isLoadingItems: true,
+ hasSearchQuery: false,
+ });
+ },
+ [types.RECEIVE_FREQUENT_ITEMS_SUCCESS](state, rawItems) {
+ Object.assign(state, {
+ items: rawItems,
+ isLoadingItems: false,
+ hasSearchQuery: false,
+ isFetchFailed: false,
+ });
+ },
+ [types.RECEIVE_FREQUENT_ITEMS_ERROR](state) {
+ Object.assign(state, {
+ isLoadingItems: false,
+ hasSearchQuery: false,
+ isFetchFailed: true,
+ });
+ },
+ [types.REQUEST_SEARCHED_ITEMS](state) {
+ Object.assign(state, {
+ isLoadingItems: true,
+ hasSearchQuery: true,
+ });
+ },
+ [types.RECEIVE_SEARCHED_ITEMS_SUCCESS](state, rawItems) {
+ Object.assign(state, {
+ items: rawItems.map(rawItem => ({
+ id: rawItem.id,
+ name: rawItem.name,
+ namespace: rawItem.name_with_namespace || rawItem.full_name,
+ webUrl: rawItem.web_url,
+ avatarUrl: rawItem.avatar_url,
+ })),
+ isLoadingItems: false,
+ hasSearchQuery: true,
+ isFetchFailed: false,
+ });
+ },
+ [types.RECEIVE_SEARCHED_ITEMS_ERROR](state) {
+ Object.assign(state, {
+ isLoadingItems: false,
+ hasSearchQuery: true,
+ isFetchFailed: true,
+ });
+ },
+};
diff --git a/app/assets/javascripts/frequent_items/store/state.js b/app/assets/javascripts/frequent_items/store/state.js
new file mode 100644
index 00000000000..75b04febee4
--- /dev/null
+++ b/app/assets/javascripts/frequent_items/store/state.js
@@ -0,0 +1,8 @@
+export default () => ({
+ namespace: '',
+ storageKey: '',
+ searchQuery: '',
+ isLoadingItems: false,
+ isFetchFailed: false,
+ items: [],
+});
diff --git a/app/assets/javascripts/frequent_items/utils.js b/app/assets/javascripts/frequent_items/utils.js
new file mode 100644
index 00000000000..aba692e4b99
--- /dev/null
+++ b/app/assets/javascripts/frequent_items/utils.js
@@ -0,0 +1,49 @@
+import _ from 'underscore';
+import bp from '~/breakpoints';
+import { FREQUENT_ITEMS, HOUR_IN_MS } from './constants';
+
+export const isMobile = () => {
+ const screenSize = bp.getBreakpointSize();
+
+ return screenSize === 'sm' || screenSize === 'xs';
+};
+
+export const getTopFrequentItems = items => {
+ if (!items) {
+ return [];
+ }
+ const frequentItemsCount = isMobile()
+ ? FREQUENT_ITEMS.LIST_COUNT_MOBILE
+ : FREQUENT_ITEMS.LIST_COUNT_DESKTOP;
+
+ const frequentItems = items.filter(item => item.frequency >= FREQUENT_ITEMS.ELIGIBLE_FREQUENCY);
+
+ if (!frequentItems || frequentItems.length === 0) {
+ return [];
+ }
+
+ frequentItems.sort((itemA, itemB) => {
+ // Sort all frequent items in decending order of frequency
+ // and then by lastAccessedOn with recent most first
+ if (itemA.frequency !== itemB.frequency) {
+ return itemB.frequency - itemA.frequency;
+ } else if (itemA.lastAccessedOn !== itemB.lastAccessedOn) {
+ return itemB.lastAccessedOn - itemA.lastAccessedOn;
+ }
+
+ return 0;
+ });
+
+ return _.first(frequentItems, frequentItemsCount);
+};
+
+export const updateExistingFrequentItem = (frequentItem, item) => {
+ const accessedOverHourAgo =
+ Math.abs(item.lastAccessedOn - frequentItem.lastAccessedOn) / HOUR_IN_MS > 1;
+
+ return {
+ ...item,
+ frequency: accessedOverHourAgo ? frequentItem.frequency + 1 : frequentItem.frequency,
+ lastAccessedOn: accessedOverHourAgo ? Date.now() : frequentItem.lastAccessedOn,
+ };
+};
diff --git a/app/assets/javascripts/ide/components/merge_requests/info.vue b/app/assets/javascripts/ide/components/merge_requests/info.vue
new file mode 100644
index 00000000000..199d2e74971
--- /dev/null
+++ b/app/assets/javascripts/ide/components/merge_requests/info.vue
@@ -0,0 +1,43 @@
+<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>
diff --git a/app/assets/javascripts/ide/components/panes/right.vue b/app/assets/javascripts/ide/components/panes/right.vue
index 5cd2c9ce188..e4a5fcc67c4 100644
--- a/app/assets/javascripts/ide/components/panes/right.vue
+++ b/app/assets/javascripts/ide/components/panes/right.vue
@@ -5,6 +5,7 @@ import Icon from '../../../vue_shared/components/icon.vue';
import { rightSidebarViews } from '../../constants';
import PipelinesList from '../pipelines/list.vue';
import JobsDetail from '../jobs/detail.vue';
+import MergeRequestInfo from '../merge_requests/info.vue';
import ResizablePanel from '../resizable_panel.vue';
export default {
@@ -16,9 +17,10 @@ export default {
PipelinesList,
JobsDetail,
ResizablePanel,
+ MergeRequestInfo,
},
computed: {
- ...mapState(['rightPane']),
+ ...mapState(['rightPane', 'currentMergeRequestId']),
pipelinesActive() {
return (
this.rightPane === rightSidebarViews.pipelines ||
@@ -54,10 +56,33 @@ export default {
</resizable-panel>
<nav class="ide-activity-bar">
<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>
<button
v-tooltip
:title="__('Pipelines')"
+ :aria-label="__('Pipelines')"
:class="{
active: pipelinesActive
}"
diff --git a/app/assets/javascripts/ide/constants.js b/app/assets/javascripts/ide/constants.js
index 12e0c3aeef0..45d36f6f42c 100644
--- a/app/assets/javascripts/ide/constants.js
+++ b/app/assets/javascripts/ide/constants.js
@@ -31,6 +31,7 @@ export const diffModes = {
export const rightSidebarViews = {
pipelines: 'pipelines-list',
jobsDetail: 'jobs-detail',
+ mergeRequestInfo: 'merge-request-info',
};
export const stageKeys = {
diff --git a/app/assets/javascripts/ide/lib/themes/gl_theme.js b/app/assets/javascripts/ide/lib/themes/gl_theme.js
index 2fc96250c7d..439ae50448a 100644
--- a/app/assets/javascripts/ide/lib/themes/gl_theme.js
+++ b/app/assets/javascripts/ide/lib/themes/gl_theme.js
@@ -9,6 +9,7 @@ export default {
'diffEditor.insertedTextBackground': '#ddfbe6',
'diffEditor.removedTextBackground': '#f9d7dc',
'editor.selectionBackground': '#aad6f8',
+ 'editorIndentGuide.activeBackground': '#cccccc',
},
},
};
diff --git a/app/assets/javascripts/ide/services/index.js b/app/assets/javascripts/ide/services/index.js
index 3e939f0c1a3..49a481f25d5 100644
--- a/app/assets/javascripts/ide/services/index.js
+++ b/app/assets/javascripts/ide/services/index.js
@@ -40,8 +40,8 @@ export default {
getProjectData(namespace, project) {
return Api.project(`${namespace}/${project}`);
},
- getProjectMergeRequestData(projectId, mergeRequestId) {
- return Api.mergeRequest(projectId, mergeRequestId);
+ getProjectMergeRequestData(projectId, mergeRequestId, params = {}) {
+ return Api.mergeRequest(projectId, mergeRequestId, params);
},
getProjectMergeRequestChanges(projectId, mergeRequestId) {
return Api.mergeRequestChanges(projectId, mergeRequestId);
diff --git a/app/assets/javascripts/ide/stores/actions/merge_request.js b/app/assets/javascripts/ide/stores/actions/merge_request.js
index 4aa151abcb7..6bdf9dc3028 100644
--- a/app/assets/javascripts/ide/stores/actions/merge_request.js
+++ b/app/assets/javascripts/ide/stores/actions/merge_request.js
@@ -9,7 +9,7 @@ export const getMergeRequestData = (
new Promise((resolve, reject) => {
if (!state.projects[projectId].mergeRequests[mergeRequestId] || force) {
service
- .getProjectMergeRequestData(projectId, mergeRequestId)
+ .getProjectMergeRequestData(projectId, mergeRequestId, { render_html: true })
.then(({ data }) => {
commit(types.SET_MERGE_REQUEST, {
projectPath: projectId,
diff --git a/app/assets/javascripts/issue_show/components/app.vue b/app/assets/javascripts/issue_show/components/app.vue
index b6364318537..ad928484952 100644
--- a/app/assets/javascripts/issue_show/components/app.vue
+++ b/app/assets/javascripts/issue_show/components/app.vue
@@ -108,6 +108,11 @@
type: String,
required: true,
},
+ markdownVersion: {
+ type: Number,
+ required: false,
+ default: 0,
+ },
projectPath: {
type: String,
required: true,
@@ -282,6 +287,7 @@
:issuable-templates="issuableTemplates"
:markdown-docs-path="markdownDocsPath"
:markdown-preview-path="markdownPreviewPath"
+ :markdown-version="markdownVersion"
:project-path="projectPath"
:project-namespace="projectNamespace"
:show-delete-button="showDeleteButton"
diff --git a/app/assets/javascripts/issue_show/components/fields/description.vue b/app/assets/javascripts/issue_show/components/fields/description.vue
index 5f58f671c73..97acc5ba385 100644
--- a/app/assets/javascripts/issue_show/components/fields/description.vue
+++ b/app/assets/javascripts/issue_show/components/fields/description.vue
@@ -20,6 +20,11 @@
type: String,
required: true,
},
+ markdownVersion: {
+ type: Number,
+ required: false,
+ default: 0,
+ },
canAttachFile: {
type: Boolean,
required: false,
@@ -47,6 +52,7 @@
<markdown-field
:markdown-preview-path="markdownPreviewPath"
:markdown-docs-path="markdownDocsPath"
+ :markdown-version="markdownVersion"
:can-attach-file="canAttachFile"
:enable-autocomplete="enableAutocomplete"
>
diff --git a/app/assets/javascripts/issue_show/components/form.vue b/app/assets/javascripts/issue_show/components/form.vue
index 5bfc072e3da..e509bb52f7d 100644
--- a/app/assets/javascripts/issue_show/components/form.vue
+++ b/app/assets/javascripts/issue_show/components/form.vue
@@ -35,6 +35,11 @@
type: String,
required: true,
},
+ markdownVersion: {
+ type: Number,
+ required: false,
+ default: 0,
+ },
projectPath: {
type: String,
required: true,
@@ -97,6 +102,7 @@
:form-state="formState"
:markdown-preview-path="markdownPreviewPath"
:markdown-docs-path="markdownDocsPath"
+ :markdown-version="markdownVersion"
:can-attach-file="canAttachFile"
:enable-autocomplete="enableAutocomplete"
/>
diff --git a/app/assets/javascripts/issue_show/components/title.vue b/app/assets/javascripts/issue_show/components/title.vue
index 12101c0daa5..b5e8e0ea44b 100644
--- a/app/assets/javascripts/issue_show/components/title.vue
+++ b/app/assets/javascripts/issue_show/components/title.vue
@@ -1,67 +1,67 @@
<script>
- import animateMixin from '../mixins/animate';
- import eventHub from '../event_hub';
- import tooltip from '../../vue_shared/directives/tooltip';
- import { spriteIcon } from '../../lib/utils/common_utils';
+import animateMixin from '../mixins/animate';
+import eventHub from '../event_hub';
+import tooltip from '../../vue_shared/directives/tooltip';
+import { spriteIcon } from '../../lib/utils/common_utils';
- export default {
- directives: {
- tooltip,
+export default {
+ directives: {
+ tooltip,
+ },
+ mixins: [animateMixin],
+ props: {
+ issuableRef: {
+ type: [String, Number],
+ required: true,
},
- mixins: [animateMixin],
- props: {
- issuableRef: {
- type: String,
- 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,
- },
+ canUpdate: {
+ required: false,
+ type: Boolean,
+ default: false,
},
- data() {
- return {
- preAnimation: false,
- pulseAnimation: false,
- titleEl: document.querySelector('title'),
- };
+ titleHtml: {
+ type: String,
+ required: true,
},
- computed: {
- pencilIcon() {
- return spriteIcon('pencil', 'link-highlight');
- },
+ titleText: {
+ type: String,
+ required: true,
},
- watch: {
- titleHtml() {
- this.setPageTitle();
- this.animateChange();
- },
+ showInlineEditButton: {
+ type: Boolean,
+ required: false,
+ default: false,
},
- methods: {
- setPageTitle() {
- const currentPageTitleScope = this.titleEl.innerText.split('·');
- currentPageTitleScope[0] = `${this.titleText} (${this.issuableRef}) `;
- this.titleEl.textContent = currentPageTitleScope.join('·');
- },
- edit() {
- eventHub.$emit('open.form');
- },
+ },
+ data() {
+ return {
+ preAnimation: false,
+ pulseAnimation: false,
+ titleEl: document.querySelector('title'),
+ };
+ },
+ 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>
<template>
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index c9ce838cd48..2718f73a830 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -26,7 +26,7 @@ import './feature_highlight/feature_highlight_options';
import LazyLoader from './lazy_loader';
import initLogoAnimation from './logo';
import './milestone_select';
-import './projects_dropdown';
+import './frequent_items';
import initBreadcrumbs from './breadcrumb';
import initDispatcher from './dispatcher';
diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js
index 48cda28a1ae..8124ae6201f 100644
--- a/app/assets/javascripts/notes.js
+++ b/app/assets/javascripts/notes.js
@@ -1251,13 +1251,15 @@ export default class Notes {
var postUrl = $originalContentEl.data('postUrl');
var targetId = $originalContentEl.data('targetId');
var targetType = $originalContentEl.data('targetType');
+ var markdownVersion = $originalContentEl.data('markdownVersion');
this.glForm = new GLForm($editForm.find('form'), this.enableGFM);
$editForm
.find('form')
.attr('action', `${postUrl}?html=true`)
- .attr('data-remote', 'true');
+ .attr('data-remote', 'true')
+ .attr('data-markdown-version', markdownVersion);
$editForm.find('.js-form-target-id').val(targetId);
$editForm.find('.js-form-target-type').val(targetType);
$editForm
diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue
index c6a524f68cb..6612bc44e0b 100644
--- a/app/assets/javascripts/notes/components/comment_form.vue
+++ b/app/assets/javascripts/notes/components/comment_form.vue
@@ -34,6 +34,11 @@ export default {
type: String,
required: true,
},
+ markdownVersion: {
+ type: Number,
+ required: false,
+ default: 0,
+ },
},
data() {
return {
@@ -344,6 +349,7 @@ Please check your network connection and try again.`;
:markdown-preview-path="markdownPreviewPath"
:markdown-docs-path="markdownDocsPath"
:quick-actions-docs-path="quickActionsDocsPath"
+ :markdown-version="markdownVersion"
:add-spacing-classes="false">
<textarea
id="note-body"
diff --git a/app/assets/javascripts/notes/components/note_body.vue b/app/assets/javascripts/notes/components/note_body.vue
index d2db68df98e..6f4a0709825 100644
--- a/app/assets/javascripts/notes/components/note_body.vue
+++ b/app/assets/javascripts/notes/components/note_body.vue
@@ -92,6 +92,7 @@ export default {
:is-editing="isEditing"
:note-body="noteBody"
:note-id="note.id"
+ :markdown-version="note.cached_markdown_version"
@handleFormUpdate="handleFormUpdate"
@cancelForm="formCancelHandler"
/>
diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue
index a4e3faa5d75..963e3a37b39 100644
--- a/app/assets/javascripts/notes/components/note_form.vue
+++ b/app/assets/javascripts/notes/components/note_form.vue
@@ -24,6 +24,11 @@ export default {
required: false,
default: 0,
},
+ markdownVersion: {
+ type: Number,
+ required: false,
+ default: 0,
+ },
saveButtonTitle: {
type: String,
required: false,
@@ -156,6 +161,7 @@ export default {
<markdown-field
:markdown-preview-path="markdownPreviewPath"
:markdown-docs-path="markdownDocsPath"
+ :markdown-version="markdownVersion"
:quick-actions-docs-path="quickActionsDocsPath"
:add-spacing-classes="false">
<textarea
diff --git a/app/assets/javascripts/notes/components/notes_app.vue b/app/assets/javascripts/notes/components/notes_app.vue
index a8995021699..9b8713b40fb 100644
--- a/app/assets/javascripts/notes/components/notes_app.vue
+++ b/app/assets/javascripts/notes/components/notes_app.vue
@@ -43,6 +43,11 @@ export default {
required: false,
default: true,
},
+ markdownVersion: {
+ type: Number,
+ required: false,
+ default: 0,
+ },
},
data() {
return {
@@ -192,6 +197,7 @@ export default {
<comment-form
:noteable-type="noteableType"
+ :markdown-version="markdownVersion"
/>
</div>
</template>
diff --git a/app/assets/javascripts/notes/index.js b/app/assets/javascripts/notes/index.js
index eed3a82854d..6dd4c9d66ac 100644
--- a/app/assets/javascripts/notes/index.js
+++ b/app/assets/javascripts/notes/index.js
@@ -15,6 +15,7 @@ document.addEventListener('DOMContentLoaded', () => {
const notesDataset = document.getElementById('js-vue-notes').dataset;
const parsedUserData = JSON.parse(notesDataset.currentUserData);
const noteableData = JSON.parse(notesDataset.noteableData);
+ const { markdownVersion } = notesDataset;
let currentUserData = {};
noteableData.noteableType = notesDataset.noteableType;
@@ -33,6 +34,7 @@ document.addEventListener('DOMContentLoaded', () => {
return {
noteableData,
currentUserData,
+ markdownVersion,
notesData: JSON.parse(notesDataset.notesData),
};
},
@@ -42,6 +44,7 @@ document.addEventListener('DOMContentLoaded', () => {
noteableData: this.noteableData,
notesData: this.notesData,
userData: this.currentUserData,
+ markdownVersion: this.markdownVersion,
},
});
},
diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js
index 671fa4d7d22..b2bf86eea56 100644
--- a/app/assets/javascripts/notes/stores/actions.js
+++ b/app/assets/javascripts/notes/stores/actions.js
@@ -41,6 +41,15 @@ export const fetchDiscussions = ({ commit }, path) =>
commit(types.SET_INITIAL_DISCUSSIONS, discussions);
});
+export const refetchDiscussionById = ({ commit }, { path, discussionId }) =>
+ service
+ .fetchDiscussions(path)
+ .then(res => res.json())
+ .then(discussions => {
+ const selectedDiscussion = discussions.find(discussion => discussion.id === discussionId);
+ if (selectedDiscussion) commit(types.UPDATE_DISCUSSION, selectedDiscussion);
+ });
+
export const deleteNote = ({ commit }, note) =>
service.deleteNote(note.path).then(() => {
commit(types.DELETE_NOTE, note);
diff --git a/app/assets/javascripts/notes/stores/mutations.js b/app/assets/javascripts/notes/stores/mutations.js
index e5e40ce07fa..a1849269010 100644
--- a/app/assets/javascripts/notes/stores/mutations.js
+++ b/app/assets/javascripts/notes/stores/mutations.js
@@ -114,7 +114,6 @@ export default {
Object.assign(state, { discussions });
},
-
[types.SET_LAST_FETCHED_AT](state, fetchedAt) {
Object.assign(state, { lastFetchedAt: fetchedAt });
},
diff --git a/app/assets/javascripts/pages/profiles/keys/index.js b/app/assets/javascripts/pages/profiles/keys/index.js
new file mode 100644
index 00000000000..1cd3ee1dfdb
--- /dev/null
+++ b/app/assets/javascripts/pages/profiles/keys/index.js
@@ -0,0 +1,16 @@
+import AddSshKeyValidation from '~/profile/add_ssh_key_validation';
+
+document.addEventListener('DOMContentLoaded', () => {
+ const input = document.querySelector('.js-add-ssh-key-validation-input');
+ const warning = document.querySelector('.js-add-ssh-key-validation-warning');
+ const originalSubmit = input.form.querySelector('.js-add-ssh-key-validation-original-submit');
+ const confirmSubmit = warning.querySelector('.js-add-ssh-key-validation-confirm-submit');
+
+ const addSshKeyValidation = new AddSshKeyValidation(
+ input,
+ warning,
+ originalSubmit,
+ confirmSubmit,
+ );
+ addSshKeyValidation.register();
+});
diff --git a/app/assets/javascripts/preview_markdown.js b/app/assets/javascripts/preview_markdown.js
index 0e973cab4d2..0964baf8954 100644
--- a/app/assets/javascripts/preview_markdown.js
+++ b/app/assets/javascripts/preview_markdown.js
@@ -28,12 +28,16 @@ MarkdownPreview.prototype.ajaxCache = {};
MarkdownPreview.prototype.showPreview = function ($form) {
var mdText;
+ var markdownVersion;
+ var url;
var preview = $form.find('.js-md-preview');
- var url = preview.data('url');
if (preview.hasClass('md-preview-loading')) {
return;
}
+
mdText = $form.find('textarea.markdown-area').val();
+ markdownVersion = $form.attr('data-markdown-version');
+ url = this.versionedPreviewPath(preview.data('url'), markdownVersion);
if (mdText.trim().length === 0) {
preview.text(this.emptyMessage);
@@ -59,6 +63,14 @@ MarkdownPreview.prototype.showPreview = function ($form) {
}
};
+MarkdownPreview.prototype.versionedPreviewPath = function (markdownPreviewPath, markdownVersion) {
+ if (typeof markdownVersion === 'undefined') {
+ return markdownPreviewPath;
+ }
+
+ return `${markdownPreviewPath}${markdownPreviewPath.indexOf('?') === -1 ? '?' : '&'}markdown_version=${markdownVersion}`;
+};
+
MarkdownPreview.prototype.fetchMarkdownPreview = function (text, url, success) {
if (!url) {
return;
diff --git a/app/assets/javascripts/profile/add_ssh_key_validation.js b/app/assets/javascripts/profile/add_ssh_key_validation.js
new file mode 100644
index 00000000000..ab6a6c1896c
--- /dev/null
+++ b/app/assets/javascripts/profile/add_ssh_key_validation.js
@@ -0,0 +1,43 @@
+export default class AddSshKeyValidation {
+ constructor(inputElement, warningElement, originalSubmitElement, confirmSubmitElement) {
+ this.inputElement = inputElement;
+ this.form = inputElement.form;
+
+ this.warningElement = warningElement;
+
+ this.originalSubmitElement = originalSubmitElement;
+ this.confirmSubmitElement = confirmSubmitElement;
+
+ this.isValid = false;
+ }
+
+ register() {
+ this.form.addEventListener('submit', event => this.submit(event));
+
+ this.confirmSubmitElement.addEventListener('click', () => {
+ this.isValid = true;
+ this.form.submit();
+ });
+
+ this.inputElement.addEventListener('input', () => this.toggleWarning(false));
+ }
+
+ submit(event) {
+ this.isValid = AddSshKeyValidation.isPublicKey(this.inputElement.value);
+
+ if (this.isValid) return true;
+
+ event.preventDefault();
+ this.toggleWarning(true);
+ return false;
+ }
+
+ toggleWarning(isVisible) {
+ this.warningElement.classList.toggle('hide', !isVisible);
+ this.originalSubmitElement.classList.toggle('hide', isVisible);
+ }
+
+ static isPublicKey(value) {
+ return /^(ssh|ecdsa-sha2)-/.test(value);
+ }
+}
diff --git a/app/assets/javascripts/projects_dropdown/components/app.vue b/app/assets/javascripts/projects_dropdown/components/app.vue
deleted file mode 100644
index 73d49488299..00000000000
--- a/app/assets/javascripts/projects_dropdown/components/app.vue
+++ /dev/null
@@ -1,158 +0,0 @@
-<script>
-import bs from '../../breakpoints';
-import eventHub from '../event_hub';
-import loadingIcon from '../../vue_shared/components/loading_icon.vue';
-
-import projectsListFrequent from './projects_list_frequent.vue';
-import projectsListSearch from './projects_list_search.vue';
-
-import search from './search.vue';
-
-export default {
- components: {
- search,
- loadingIcon,
- projectsListFrequent,
- projectsListSearch,
- },
- props: {
- currentProject: {
- type: Object,
- required: true,
- },
- store: {
- type: Object,
- required: true,
- },
- service: {
- type: Object,
- required: true,
- },
- },
- data() {
- return {
- isLoadingProjects: false,
- isFrequentsListVisible: false,
- isSearchListVisible: false,
- isLocalStorageFailed: false,
- isSearchFailed: false,
- searchQuery: '',
- };
- },
- computed: {
- frequentProjects() {
- return this.store.getFrequentProjects();
- },
- searchProjects() {
- return this.store.getSearchedProjects();
- },
- },
- created() {
- if (this.currentProject.id) {
- this.logCurrentProjectAccess();
- }
-
- eventHub.$on('dropdownOpen', this.fetchFrequentProjects);
- eventHub.$on('searchProjects', this.fetchSearchedProjects);
- eventHub.$on('searchCleared', this.handleSearchClear);
- eventHub.$on('searchFailed', this.handleSearchFailure);
- },
- beforeDestroy() {
- eventHub.$off('dropdownOpen', this.fetchFrequentProjects);
- eventHub.$off('searchProjects', this.fetchSearchedProjects);
- eventHub.$off('searchCleared', this.handleSearchClear);
- eventHub.$off('searchFailed', this.handleSearchFailure);
- },
- methods: {
- toggleFrequentProjectsList(state) {
- this.isLoadingProjects = !state;
- this.isSearchListVisible = !state;
- this.isFrequentsListVisible = state;
- },
- toggleSearchProjectsList(state) {
- this.isLoadingProjects = !state;
- this.isFrequentsListVisible = !state;
- this.isSearchListVisible = state;
- },
- toggleLoader(state) {
- this.isFrequentsListVisible = !state;
- this.isSearchListVisible = !state;
- this.isLoadingProjects = state;
- },
- fetchFrequentProjects() {
- const screenSize = bs.getBreakpointSize();
- if (this.searchQuery && (screenSize !== 'sm' && screenSize !== 'xs')) {
- this.toggleSearchProjectsList(true);
- } else {
- this.toggleLoader(true);
- this.isLocalStorageFailed = false;
- const projects = this.service.getFrequentProjects();
- if (projects) {
- this.toggleFrequentProjectsList(true);
- this.store.setFrequentProjects(projects);
- } else {
- this.isLocalStorageFailed = true;
- this.toggleFrequentProjectsList(true);
- this.store.setFrequentProjects([]);
- }
- }
- },
- fetchSearchedProjects(searchQuery) {
- this.searchQuery = searchQuery;
- this.toggleLoader(true);
- this.service
- .getSearchedProjects(this.searchQuery)
- .then(res => res.json())
- .then(results => {
- this.toggleSearchProjectsList(true);
- this.store.setSearchedProjects(results);
- })
- .catch(() => {
- this.isSearchFailed = true;
- this.toggleSearchProjectsList(true);
- });
- },
- logCurrentProjectAccess() {
- this.service.logProjectAccess(this.currentProject);
- },
- handleSearchClear() {
- this.searchQuery = '';
- this.toggleFrequentProjectsList(true);
- this.store.clearSearchedProjects();
- },
- handleSearchFailure() {
- this.isSearchFailed = true;
- this.toggleSearchProjectsList(true);
- },
- },
-};
-</script>
-
-<template>
- <div>
- <search/>
- <loading-icon
- v-if="isLoadingProjects"
- :label="s__('ProjectsDropdown|Loading projects')"
- class="loading-animation prepend-top-20"
- size="2"
- />
- <div
- v-if="isFrequentsListVisible"
- class="section-header"
- >
- {{ s__('ProjectsDropdown|Frequently visited') }}
- </div>
- <projects-list-frequent
- v-if="isFrequentsListVisible"
- :local-storage-failed="isLocalStorageFailed"
- :projects="frequentProjects"
- />
- <projects-list-search
- v-if="isSearchListVisible"
- :search-failed="isSearchFailed"
- :matcher="searchQuery"
- :projects="searchProjects"
- />
- </div>
-</template>
diff --git a/app/assets/javascripts/projects_dropdown/components/projects_list_frequent.vue b/app/assets/javascripts/projects_dropdown/components/projects_list_frequent.vue
deleted file mode 100644
index 625e0aa548c..00000000000
--- a/app/assets/javascripts/projects_dropdown/components/projects_list_frequent.vue
+++ /dev/null
@@ -1,57 +0,0 @@
-<script>
- import { s__ } from '../../locale';
- import projectsListItem from './projects_list_item.vue';
-
- export default {
- components: {
- projectsListItem,
- },
- props: {
- projects: {
- type: Array,
- required: true,
- },
- localStorageFailed: {
- type: Boolean,
- required: true,
- },
- },
- computed: {
- isListEmpty() {
- return this.projects.length === 0;
- },
- listEmptyMessage() {
- return this.localStorageFailed ?
- s__('ProjectsDropdown|This feature requires browser localStorage support') :
- s__('ProjectsDropdown|Projects you visit often will appear here');
- },
- },
- };
-</script>
-
-<template>
- <div
- class="projects-list-frequent-container"
- >
- <ul
- class="list-unstyled"
- >
- <li
- v-if="isListEmpty"
- class="section-empty"
- >
- {{ listEmptyMessage }}
- </li>
- <projects-list-item
- v-for="(project, index) in projects"
- v-else
- :key="index"
- :project-id="project.id"
- :project-name="project.name"
- :namespace="project.namespace"
- :web-url="project.webUrl"
- :avatar-url="project.avatarUrl"
- />
- </ul>
- </div>
-</template>
diff --git a/app/assets/javascripts/projects_dropdown/components/projects_list_item.vue b/app/assets/javascripts/projects_dropdown/components/projects_list_item.vue
deleted file mode 100644
index eafbf6c99e2..00000000000
--- a/app/assets/javascripts/projects_dropdown/components/projects_list_item.vue
+++ /dev/null
@@ -1,116 +0,0 @@
-<script>
- /* eslint-disable vue/require-default-prop, vue/require-prop-types */
- import identicon from '../../vue_shared/components/identicon.vue';
-
- export default {
- components: {
- identicon,
- },
- props: {
- matcher: {
- type: String,
- required: false,
- },
- projectId: {
- type: Number,
- required: true,
- },
- projectName: {
- type: String,
- required: true,
- },
- namespace: {
- type: String,
- required: true,
- },
- webUrl: {
- type: String,
- required: true,
- },
- avatarUrl: {
- required: true,
- validator(value) {
- return value === null || typeof value === 'string';
- },
- },
- },
- computed: {
- hasAvatar() {
- return this.avatarUrl !== null;
- },
- highlightedProjectName() {
- if (this.matcher) {
- const matcherRegEx = new RegExp(this.matcher, 'gi');
- const matches = this.projectName.match(matcherRegEx);
-
- if (matches && matches.length > 0) {
- return this.projectName.replace(matches[0], `<b>${matches[0]}</b>`);
- }
- }
- return this.projectName;
- },
- /**
- * Smartly truncates project namespace by doing two things;
- * 1. Only include Group names in path by removing project name
- * 2. Only include first and last group names in the path
- * when namespace has more than 2 groups present
- *
- * First part (removal of project name from namespace) can be
- * done from backend but doing so involves migration of
- * existing project namespaces which is not wise thing to do.
- */
- truncatedNamespace() {
- const namespaceArr = this.namespace.split(' / ');
- namespaceArr.splice(-1, 1);
- let namespace = namespaceArr.join(' / ');
-
- if (namespaceArr.length > 2) {
- namespace = `${namespaceArr[0]} / ... / ${namespaceArr.pop()}`;
- }
-
- return namespace;
- },
- },
- };
-</script>
-
-<template>
- <li
- class="projects-list-item-container"
- >
- <a
- :href="webUrl"
- class="clearfix"
- >
- <div
- class="project-item-avatar-container"
- >
- <img
- v-if="hasAvatar"
- :src="avatarUrl"
- class="avatar s32"
- />
- <identicon
- v-else
- :entity-id="projectId"
- :entity-name="projectName"
- size-class="s32"
- />
- </div>
- <div
- class="project-item-metadata-container"
- >
- <div
- :title="projectName"
- class="project-title"
- v-html="highlightedProjectName"
- >
- </div>
- <div
- :title="namespace"
- class="project-namespace"
- >{{ truncatedNamespace }}</div>
- </div>
- </a>
- </li>
-</template>
diff --git a/app/assets/javascripts/projects_dropdown/components/projects_list_search.vue b/app/assets/javascripts/projects_dropdown/components/projects_list_search.vue
deleted file mode 100644
index 76e9cb9e53f..00000000000
--- a/app/assets/javascripts/projects_dropdown/components/projects_list_search.vue
+++ /dev/null
@@ -1,63 +0,0 @@
-<script>
-import { s__ } from '../../locale';
-import projectsListItem from './projects_list_item.vue';
-
-export default {
- components: {
- projectsListItem,
- },
- props: {
- matcher: {
- type: String,
- required: true,
- },
- projects: {
- type: Array,
- required: true,
- },
- searchFailed: {
- type: Boolean,
- required: true,
- },
- },
- computed: {
- isListEmpty() {
- return this.projects.length === 0;
- },
- listEmptyMessage() {
- return this.searchFailed ?
- s__('ProjectsDropdown|Something went wrong on our end.') :
- s__('ProjectsDropdown|Sorry, no projects matched your search');
- },
- },
-};
-</script>
-
-<template>
- <div
- class="projects-list-search-container"
- >
- <ul
- class="list-unstyled"
- >
- <li
- v-if="isListEmpty"
- :class="{ 'section-failure': searchFailed }"
- class="section-empty"
- >
- {{ listEmptyMessage }}
- </li>
- <projects-list-item
- v-for="(project, index) in projects"
- v-else
- :key="index"
- :project-id="project.id"
- :project-name="project.name"
- :namespace="project.namespace"
- :web-url="project.webUrl"
- :avatar-url="project.avatarUrl"
- :matcher="matcher"
- />
- </ul>
- </div>
-</template>
diff --git a/app/assets/javascripts/projects_dropdown/components/search.vue b/app/assets/javascripts/projects_dropdown/components/search.vue
deleted file mode 100644
index 28f2a18f2a6..00000000000
--- a/app/assets/javascripts/projects_dropdown/components/search.vue
+++ /dev/null
@@ -1,65 +0,0 @@
-<script>
- import _ from 'underscore';
- import eventHub from '../event_hub';
-
- export default {
- data() {
- return {
- searchQuery: '',
- };
- },
- watch: {
- searchQuery() {
- this.handleInput();
- },
- },
- mounted() {
- eventHub.$on('dropdownOpen', this.setFocus);
- },
- beforeDestroy() {
- eventHub.$off('dropdownOpen', this.setFocus);
- },
- methods: {
- setFocus() {
- this.$refs.search.focus();
- },
- emitSearchEvents() {
- if (this.searchQuery) {
- eventHub.$emit('searchProjects', this.searchQuery);
- } else {
- eventHub.$emit('searchCleared');
- }
- },
- /**
- * Callback function within _.debounce is intentionally
- * kept as ES5 `function() {}` instead of ES6 `() => {}`
- * as it otherwise messes up function context
- * and component reference is no longer accessible via `this`
- */
- // eslint-disable-next-line func-names
- handleInput: _.debounce(function () {
- this.emitSearchEvents();
- }, 500),
- },
- };
-</script>
-
-<template>
- <div
- class="search-input-container d-none d-sm-block"
- >
- <input
- ref="search"
- v-model="searchQuery"
- :placeholder="s__('ProjectsDropdown|Search your projects')"
- type="search"
- class="form-control"
- />
- <i
- v-if="!searchQuery"
- class="search-icon fa fa-fw fa-search"
- aria-hidden="true"
- >
- </i>
- </div>
-</template>
diff --git a/app/assets/javascripts/projects_dropdown/constants.js b/app/assets/javascripts/projects_dropdown/constants.js
deleted file mode 100644
index 8937097184c..00000000000
--- a/app/assets/javascripts/projects_dropdown/constants.js
+++ /dev/null
@@ -1,10 +0,0 @@
-export const FREQUENT_PROJECTS = {
- MAX_COUNT: 20,
- LIST_COUNT_DESKTOP: 5,
- LIST_COUNT_MOBILE: 3,
- ELIGIBLE_FREQUENCY: 3,
-};
-
-export const HOUR_IN_MS = 3600000;
-
-export const STORAGE_KEY = 'frequent-projects';
diff --git a/app/assets/javascripts/projects_dropdown/index.js b/app/assets/javascripts/projects_dropdown/index.js
deleted file mode 100644
index 6056f12aa4f..00000000000
--- a/app/assets/javascripts/projects_dropdown/index.js
+++ /dev/null
@@ -1,66 +0,0 @@
-import $ from 'jquery';
-import Vue from 'vue';
-
-import Translate from '../vue_shared/translate';
-import eventHub from './event_hub';
-import ProjectsService from './service/projects_service';
-import ProjectsStore from './store/projects_store';
-
-import projectsDropdownApp from './components/app.vue';
-
-Vue.use(Translate);
-
-document.addEventListener('DOMContentLoaded', () => {
- const el = document.getElementById('js-projects-dropdown');
- const navEl = document.getElementById('nav-projects-dropdown');
-
- // Don't do anything if element doesn't exist (No projects dropdown)
- // This is for when the user accesses GitLab without logging in
- if (!el || !navEl) {
- return;
- }
-
- $(navEl).on('shown.bs.dropdown', () => {
- eventHub.$emit('dropdownOpen');
- });
-
- // eslint-disable-next-line no-new
- new Vue({
- el,
- components: {
- projectsDropdownApp,
- },
- data() {
- const { dataset } = this.$options.el;
- const store = new ProjectsStore();
- const service = new ProjectsService(dataset.userName);
-
- const project = {
- id: Number(dataset.projectId),
- name: dataset.projectName,
- namespace: dataset.projectNamespace,
- webUrl: dataset.projectWebUrl,
- avatarUrl: dataset.projectAvatarUrl || null,
- lastAccessedOn: Date.now(),
- };
-
- return {
- store,
- service,
- state: store.state,
- currentUserName: dataset.userName,
- currentProject: project,
- };
- },
- render(createElement) {
- return createElement('projects-dropdown-app', {
- props: {
- currentUserName: this.currentUserName,
- currentProject: this.currentProject,
- store: this.store,
- service: this.service,
- },
- });
- },
- });
-});
diff --git a/app/assets/javascripts/projects_dropdown/service/projects_service.js b/app/assets/javascripts/projects_dropdown/service/projects_service.js
deleted file mode 100644
index ed1c3deead2..00000000000
--- a/app/assets/javascripts/projects_dropdown/service/projects_service.js
+++ /dev/null
@@ -1,137 +0,0 @@
-import _ from 'underscore';
-import Vue from 'vue';
-import VueResource from 'vue-resource';
-
-import bp from '../../breakpoints';
-import Api from '../../api';
-import AccessorUtilities from '../../lib/utils/accessor';
-
-import { FREQUENT_PROJECTS, HOUR_IN_MS, STORAGE_KEY } from '../constants';
-
-Vue.use(VueResource);
-
-export default class ProjectsService {
- constructor(currentUserName) {
- this.isLocalStorageAvailable = AccessorUtilities.isLocalStorageAccessSafe();
- this.currentUserName = currentUserName;
- this.storageKey = `${this.currentUserName}/${STORAGE_KEY}`;
- this.projectsPath = Vue.resource(Api.buildUrl(Api.projectsPath));
- }
-
- getSearchedProjects(searchQuery) {
- return this.projectsPath.get({
- simple: true,
- per_page: 20,
- membership: !!gon.current_user_id,
- order_by: 'last_activity_at',
- search: searchQuery,
- });
- }
-
- getFrequentProjects() {
- if (this.isLocalStorageAvailable) {
- return this.getTopFrequentProjects();
- }
- return null;
- }
-
- logProjectAccess(project) {
- let matchFound = false;
- let storedFrequentProjects;
-
- if (this.isLocalStorageAvailable) {
- const storedRawProjects = localStorage.getItem(this.storageKey);
-
- // Check if there's any frequent projects list set
- if (!storedRawProjects) {
- // No frequent projects list set, set one up.
- storedFrequentProjects = [];
- storedFrequentProjects.push({ ...project, frequency: 1 });
- } else {
- // Check if project is already present in frequents list
- // When found, update metadata of it.
- storedFrequentProjects = JSON.parse(storedRawProjects).map(projectItem => {
- if (projectItem.id === project.id) {
- matchFound = true;
- const diff = Math.abs(project.lastAccessedOn - projectItem.lastAccessedOn) / HOUR_IN_MS;
- const updatedProject = {
- ...project,
- frequency: projectItem.frequency,
- lastAccessedOn: projectItem.lastAccessedOn,
- };
-
- // Check if duration since last access of this project
- // is over an hour
- if (diff > 1) {
- return {
- ...updatedProject,
- frequency: updatedProject.frequency + 1,
- lastAccessedOn: Date.now(),
- };
- }
-
- return {
- ...updatedProject,
- };
- }
-
- return projectItem;
- });
-
- // Check whether currently logged project is present in frequents list
- if (!matchFound) {
- // We always keep size of frequents collection to 20 projects
- // out of which only 5 projects with
- // highest value of `frequency` and most recent `lastAccessedOn`
- // are shown in projects dropdown
- if (storedFrequentProjects.length === FREQUENT_PROJECTS.MAX_COUNT) {
- storedFrequentProjects.shift(); // Remove an item from head of array
- }
-
- storedFrequentProjects.push({ ...project, frequency: 1 });
- }
- }
-
- localStorage.setItem(this.storageKey, JSON.stringify(storedFrequentProjects));
- }
- }
-
- getTopFrequentProjects() {
- const storedFrequentProjects = JSON.parse(localStorage.getItem(this.storageKey));
- let frequentProjectsCount = FREQUENT_PROJECTS.LIST_COUNT_DESKTOP;
-
- if (!storedFrequentProjects) {
- return [];
- }
-
- if (bp.getBreakpointSize() === 'sm' || bp.getBreakpointSize() === 'xs') {
- frequentProjectsCount = FREQUENT_PROJECTS.LIST_COUNT_MOBILE;
- }
-
- const frequentProjects = storedFrequentProjects.filter(
- project => project.frequency >= FREQUENT_PROJECTS.ELIGIBLE_FREQUENCY,
- );
-
- if (!frequentProjects || frequentProjects.length === 0) {
- return [];
- }
-
- // Sort all frequent projects in decending order of frequency
- // and then by lastAccessedOn with recent most first
- frequentProjects.sort((projectA, projectB) => {
- if (projectA.frequency < projectB.frequency) {
- return 1;
- } else if (projectA.frequency > projectB.frequency) {
- return -1;
- } else if (projectA.lastAccessedOn < projectB.lastAccessedOn) {
- return 1;
- } else if (projectA.lastAccessedOn > projectB.lastAccessedOn) {
- return -1;
- }
-
- return 0;
- });
-
- return _.first(frequentProjects, frequentProjectsCount);
- }
-}
diff --git a/app/assets/javascripts/projects_dropdown/store/projects_store.js b/app/assets/javascripts/projects_dropdown/store/projects_store.js
deleted file mode 100644
index ffefbe693f4..00000000000
--- a/app/assets/javascripts/projects_dropdown/store/projects_store.js
+++ /dev/null
@@ -1,33 +0,0 @@
-export default class ProjectsStore {
- constructor() {
- this.state = {};
- this.state.frequentProjects = [];
- this.state.searchedProjects = [];
- }
-
- setFrequentProjects(rawProjects) {
- this.state.frequentProjects = rawProjects;
- }
-
- getFrequentProjects() {
- return this.state.frequentProjects;
- }
-
- setSearchedProjects(rawProjects) {
- this.state.searchedProjects = rawProjects.map(rawProject => ({
- id: rawProject.id,
- name: rawProject.name,
- namespace: rawProject.name_with_namespace,
- webUrl: rawProject.web_url,
- avatarUrl: rawProject.avatar_url,
- }));
- }
-
- getSearchedProjects() {
- return this.state.searchedProjects;
- }
-
- clearSearchedProjects() {
- this.state.searchedProjects = [];
- }
-}
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue
index 5e464f8a0e2..21f21232596 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue
@@ -79,66 +79,62 @@ export default {
</script>
<template>
- <div class="mr-widget-heading deploy-heading">
+ <div class="mr-widget-heading deploy-heading append-bottom-default">
<div class="ci-widget media">
- <div class="ci-status-icon ci-status-icon-success">
- <span class="js-icon-link icon-link">
- <status-icon status="success" />
- </span>
- </div>
<div class="media-body">
<div class="deploy-body">
- <template v-if="hasDeploymentMeta">
- <span>
- Deployed to
- </span>
- <a
- :href="deployment.url"
- target="_blank"
- rel="noopener noreferrer nofollow"
- class="deploy-link js-deploy-meta"
+ <div class="deployment-info">
+ <template v-if="hasDeploymentMeta">
+ <span>
+ Deployed to
+ </span>
+ <a
+ :href="deployment.url"
+ target="_blank"
+ rel="noopener noreferrer nofollow"
+ class="deploy-link js-deploy-meta"
+ >
+ {{ deployment.name }}
+ </a>
+ </template>
+ <span
+ v-tooltip
+ v-if="hasDeploymentTime"
+ :title="deployment.deployed_at_formatted"
+ class="js-deploy-time"
>
- {{ deployment.name }}
- </a>
- </template>
- <template v-if="hasExternalUrls">
- <span>
- on
+ {{ deployTimeago }}
</span>
+ <memory-usage
+ v-if="hasMetrics"
+ :metrics-url="deployment.metrics_url"
+ :metrics-monitoring-url="deployment.metrics_monitoring_url"
+ />
+ </div>
+ <div>
<a
+ v-if="hasExternalUrls"
:href="deployment.external_url"
target="_blank"
rel="noopener noreferrer nofollow"
- class="deploy-link js-deploy-url"
+ class="deploy-link js-deploy-url btn btn-default btn-sm inline"
>
- {{ deployment.external_url_formatted }}
- <icon
- :size="16"
- name="external-link"
- />
+ <span>
+ View app
+ <icon name="external-link" />
+ </span>
</a>
- </template>
- <span
- v-tooltip
- v-if="hasDeploymentTime"
- :title="deployment.deployed_at_formatted"
- class="js-deploy-time"
- >
- {{ deployTimeago }}
- </span>
- <loading-button
- v-if="deployment.stop_url"
- :loading="isStopping"
- container-class="btn btn-default btn-sm prepend-left-default"
- label="Stop environment"
- @click="stopEnvironment"
- />
+ <loading-button
+ v-if="deployment.stop_url"
+ :loading="isStopping"
+ container-class="btn btn-default btn-sm inline prepend-left-4"
+ title="Stop environment"
+ @click="stopEnvironment"
+ >
+ <icon name="stop" />
+ </loading-button>
+ </div>
</div>
- <memory-usage
- v-if="hasMetrics"
- :metrics-url="deployment.metrics_url"
- :metrics-monitoring-url="deployment.metrics_monitoring_url"
- />
</div>
</div>
</div>
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 3ce9d8dc26a..c18b74743e4 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
@@ -2,7 +2,7 @@
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 Icon from '~/vue_shared/components/icon.vue';
import clipboardButton from '~/vue_shared/components/clipboard_button.vue';
export default {
@@ -11,7 +11,7 @@ export default {
tooltip,
},
components: {
- icon,
+ Icon,
clipboardButton,
},
props: {
@@ -54,104 +54,114 @@ export default {
};
</script>
<template>
- <div class="mr-source-target">
- <div class="normal">
- <strong>
- {{ s__("mrWidget|Request to merge") }}
- <span
- :class="{ 'label-truncated': isSourceBranchLong }"
- :title="isSourceBranchLong ? mr.sourceBranch : ''"
- :v-tooltip="isSourceBranchLong"
- class="label-branch js-source-branch"
- data-placement="bottom"
- v-html="mr.sourceBranchLink"
- >
- </span>
+ <div class="mr-source-target append-bottom-default">
+ <div class="git-merge-icon-container append-right-default">
+ <icon name="git-merge" />
+ </div>
+ <div class="git-merge-container d-flex">
+ <div class="normal">
+ <strong>
+ {{ s__("mrWidget|Request to merge") }}
+ <span
+ :class="{ 'label-truncated': isSourceBranchLong }"
+ :title="isSourceBranchLong ? mr.sourceBranch : ''"
+ :v-tooltip="isSourceBranchLong"
+ class="label-branch js-source-branch"
+ data-placement="bottom"
+ v-html="mr.sourceBranchLink"
+ >
+ </span>
- <clipboard-button
- :text="branchNameClipboardData"
- :title="__('Copy branch name to clipboard')"
- css-class="btn-default btn-transparent btn-clipboard"
- />
+ <clipboard-button
+ :text="branchNameClipboardData"
+ :title="__('Copy branch name to clipboard')"
+ css-class="btn-default btn-transparent btn-clipboard"
+ />
- {{ s__("mrWidget|into") }}
+ {{ s__("mrWidget|into") }}
- <span
- :v-tooltip="isTargetBranchLong"
- :class="{ 'label-truncatedtooltip': isTargetBranchLong }"
- :title="isTargetBranchLong ? mr.targetBranch : ''"
- class="label-branch"
- data-placement="bottom"
- >
- <a
- :href="mr.targetBranchTreePath"
- class="js-target-branch"
+ <span
+ :v-tooltip="isTargetBranchLong"
+ :class="{ 'label-truncatedtooltip': isTargetBranchLong }"
+ :title="isTargetBranchLong ? mr.targetBranch : ''"
+ class="label-branch"
+ data-placement="bottom"
>
- {{ mr.targetBranch }}
- </a>
- </span>
- </strong>
- <span
- v-if="shouldShowCommitsBehindText"
- class="diverged-commits-count"
- >
- (<a :href="mr.targetBranchPath">{{ commitsText }}</a>)
- </span>
- </div>
+ <a
+ :href="mr.targetBranchTreePath"
+ class="js-target-branch"
+ >
+ {{ mr.targetBranch }}
+ </a>
+ </span>
+ </strong>
+ <div
+ v-if="shouldShowCommitsBehindText"
+ class="diverged-commits-count"
+ >
+ <span class="monospace">{{ mr.sourceBranch }}</span>
+ is {{ commitsText }}
+ <span class="monospace">{{ mr.targetBranch }}</span>
+ </div>
+ </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
- :disabled="mr.sourceBranchRemoved"
- data-target="#modal_merge_info"
- data-toggle="modal"
- class="btn btn-sm btn-default inline js-check-out-branch"
- type="button"
+ <div
+ v-if="mr.isOpen"
+ class="branch-actions"
>
- {{ s__("mrWidget|Check out branch") }}
- </button>
- <span class="dropdown prepend-left-10">
+ <a
+ v-if="!mr.sourceBranchRemoved"
+ :href="webIdePath"
+ class="btn btn-default inline js-web-ide d-none d-md-inline-block"
+ >
+ {{ s__("mrWidget|Open in Web IDE") }}
+ </a>
<button
+ :disabled="mr.sourceBranchRemoved"
+ data-target="#modal_merge_info"
+ data-toggle="modal"
+ class="btn btn-default inline js-check-out-branch"
type="button"
- class="btn btn-sm inline dropdown-toggle"
- data-toggle="dropdown"
- aria-label="Download as"
- aria-haspopup="true"
- aria-expanded="false"
>
- <icon name="download" />
- <i
- class="fa fa-caret-down"
- aria-hidden="true">
- </i>
+ {{ s__("mrWidget|Check out branch") }}
</button>
- <ul class="dropdown-menu dropdown-menu-right">
- <li>
- <a
- :href="mr.emailPatchesPath"
- class="js-download-email-patches"
- download
- >
- {{ s__("mrWidget|Email patches") }}
- </a>
- </li>
- <li>
- <a
- :href="mr.plainDiffPath"
- class="js-download-plain-diff"
- download
- >
- {{ s__("mrWidget|Plain diff") }}
- </a>
- </li>
- </ul>
- </span>
+ <span class="dropdown prepend-left-10">
+ <button
+ type="button"
+ class="btn inline dropdown-toggle"
+ data-toggle="dropdown"
+ aria-label="Download as"
+ aria-haspopup="true"
+ aria-expanded="false"
+ >
+ <icon name="download" />
+ <i
+ class="fa fa-caret-down"
+ aria-hidden="true">
+ </i>
+ </button>
+ <ul class="dropdown-menu dropdown-menu-right">
+ <li>
+ <a
+ :href="mr.emailPatchesPath"
+ class="js-download-email-patches"
+ download
+ >
+ {{ s__("mrWidget|Email patches") }}
+ </a>
+ </li>
+ <li>
+ <a
+ :href="mr.plainDiffPath"
+ class="js-download-plain-diff"
+ download
+ >
+ {{ s__("mrWidget|Plain diff") }}
+ </a>
+ </li>
+ </ul>
+ </span>
+ </div>
</div>
</div>
</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue
index 2f0b5e12c12..4a3fd01fa39 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue
@@ -26,6 +26,10 @@ export default {
type: String,
required: false,
},
+ sourceBranchLink: {
+ type: String,
+ required: false,
+ },
},
computed: {
hasPipeline() {
@@ -54,12 +58,18 @@ export default {
<template>
<div
v-if="hasPipeline || hasCIError"
- class="mr-widget-heading"
+ class="mr-widget-heading append-bottom-default"
>
<div class="ci-widget media">
<template v-if="hasCIError">
- <div class="ci-status-icon ci-status-icon-failed ci-error js-ci-error append-right-10">
- <icon name="status_failed" />
+ <div
+ class="add-border ci-status-icon ci-status-icon-failed ci-error
+ js-ci-error append-right-default"
+ >
+ <icon
+ :size="32"
+ name="status_failed_borderless"
+ />
</div>
<div class="media-body">
Could not connect to the CI server. Please check your settings and try again
@@ -68,50 +78,66 @@ export default {
<template v-else-if="hasPipeline">
<a
:href="status.details_path"
- class="append-right-10"
+ class="align-self-start append-right-default"
>
- <ci-icon :status="status" />
+ <ci-icon
+ :status="status"
+ :size="32"
+ :borderless="true"
+ class="add-border"
+ />
</a>
+ <div class="ci-widget-container d-flex">
+ <div class="ci-widget-content">
+ <div class="media-body">
+ <div class="font-weight-bold">
+ Pipeline
+ <a
+ :href="pipeline.path"
+ class="pipeline-id font-weight-normal pipeline-number"
+ >#{{ pipeline.id }}</a>
- <div class="media-body">
- Pipeline
- <a
- :href="pipeline.path"
- class="pipeline-id"
- >
- #{{ pipeline.id }}
- </a>
-
- {{ pipeline.details.status.label }}
+ {{ pipeline.details.status.label }}
- <template v-if="hasCommitInfo">
- for
-
- <a
- :href="pipeline.commit.commit_path"
- class="commit-sha js-commit-link"
- >
- {{ pipeline.commit.short_id }}</a>.
- </template>
-
- <span class="mr-widget-pipeline-graph">
- <span
- v-if="hasStages"
- class="stage-cell"
- >
+ <template v-if="hasCommitInfo">
+ for
+ <a
+ :href="pipeline.commit.commit_path"
+ class="commit-sha js-commit-link font-weight-normal"
+ >
+ {{ pipeline.commit.short_id }}</a>
+ on
+ <span
+ class="label-branch"
+ v-html="sourceBranchLink"
+ >
+ </span>
+ </template>
+ </div>
<div
- v-for="(stage, i) in pipeline.details.stages"
- :key="i"
- class="stage-container dropdown js-mini-pipeline-graph"
+ v-if="pipeline.coverage"
+ class="coverage"
>
- <pipeline-stage :stage="stage" />
+ Coverage {{ pipeline.coverage }}%
</div>
+ </div>
+ </div>
+ <div>
+ <span class="mr-widget-pipeline-graph">
+ <span
+ v-if="hasStages"
+ class="stage-cell"
+ >
+ <div
+ v-for="(stage, i) in pipeline.details.stages"
+ :key="i"
+ class="stage-container dropdown js-mini-pipeline-graph mr-widget-pipeline-stages"
+ >
+ <pipeline-stage :stage="stage" />
+ </div>
+ </span>
</span>
- </span>
-
- <template v-if="pipeline.coverage">
- Coverage {{ pipeline.coverage }}%
- </template>
+ </div>
</div>
</template>
</div>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue
index 53c4dc8c8f4..55b87f3a8ec 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue
@@ -43,6 +43,7 @@
<ci-icon
v-else
:status="statusObj"
+ :size="24"
/>
<button
diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
index 09477da40b5..b5de3dd6d73 100644
--- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
@@ -252,41 +252,44 @@ export default {
:pipeline="mr.pipeline"
:ci-status="mr.ciStatus"
:has-ci="mr.hasCI"
+ :source-branch-link="mr.sourceBranchLink"
/>
<deployment
v-for="deployment in mr.deployments"
:key="deployment.id"
:deployment="deployment"
/>
- <div class="mr-widget-section">
- <component
- :is="componentName"
- :mr="mr"
- :service="service"
- />
+ <div class="mr-section-container">
+ <div class="mr-widget-section">
+ <component
+ :is="componentName"
+ :mr="mr"
+ :service="service"
+ />
- <section
- v-if="mr.allowCollaboration"
- class="mr-info-list mr-links"
- >
- {{ s__("mrWidget|Allows commits from members who can merge to the target branch") }}
- </section>
+ <section
+ v-if="mr.allowCollaboration"
+ class="mr-info-list mr-links"
+ >
+ {{ s__("mrWidget|Allows commits from members who can merge to the target branch") }}
+ </section>
- <mr-widget-related-links
- v-if="shouldRenderRelatedLinks"
- :state="mr.state"
- :related-links="mr.relatedLinks"
- />
+ <mr-widget-related-links
+ v-if="shouldRenderRelatedLinks"
+ :state="mr.state"
+ :related-links="mr.relatedLinks"
+ />
- <source-branch-removal-status
- v-if="shouldRenderSourceBranchRemovalStatus"
- />
- </div>
- <div
- v-if="shouldRenderMergeHelp"
- class="mr-widget-footer"
- >
- <mr-widget-merge-help />
+ <source-branch-removal-status
+ v-if="shouldRenderSourceBranchRemovalStatus"
+ />
+ </div>
+ <div
+ v-if="shouldRenderMergeHelp"
+ class="mr-widget-footer"
+ >
+ <mr-widget-merge-help />
+ </div>
</div>
</div>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue
index 298971a36b2..d62537021ca 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/field.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue
@@ -1,5 +1,6 @@
<script>
import $ from 'jquery';
+ import { s__ } from '~/locale';
import Flash from '../../../flash';
import GLForm from '../../../gl_form';
import markdownHeader from './header.vue';
@@ -22,6 +23,11 @@
type: String,
required: true,
},
+ markdownVersion: {
+ type: Number,
+ required: false,
+ default: 0,
+ },
addSpacingClasses: {
type: Boolean,
required: false,
@@ -92,10 +98,11 @@
if (text) {
this.markdownPreviewLoading = true;
- this.$http.post(this.markdownPreviewPath, { text })
- .then(resp => resp.json())
- .then(data => this.renderMarkdown(data))
- .catch(() => new Flash('Error loading markdown preview'));
+ this.$http
+ .post(this.versionedPreviewPath(), { text })
+ .then(resp => resp.json())
+ .then(data => this.renderMarkdown(data))
+ .catch(() => new Flash(s__('Error loading markdown preview')));
} else {
this.renderMarkdown();
}
@@ -119,6 +126,13 @@
$(this.$refs['markdown-preview']).renderGFM();
});
},
+
+ versionedPreviewPath() {
+ const { markdownPreviewPath, markdownVersion } = this;
+ return `${markdownPreviewPath}${
+ markdownPreviewPath.indexOf('?') === -1 ? '?' : '&'
+ }markdown_version=${markdownVersion}`;
+ },
},
};
</script>
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index 74475daae14..c7b5e22c33d 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -36,7 +36,7 @@
width: 100%;
}
- &.projects-dropdown-menu {
+ &.frequent-items-dropdown-menu {
padding: 0;
overflow-y: initial;
max-height: initial;
@@ -790,6 +790,7 @@
@include media-breakpoint-down(xs) {
.navbar-gitlab {
li.header-projects,
+ li.header-groups,
li.header-more,
li.header-new,
li.header-user {
@@ -813,18 +814,18 @@
}
}
-header.header-content .dropdown-menu.projects-dropdown-menu {
+header.header-content .dropdown-menu.frequent-items-dropdown-menu {
padding: 0;
}
-.projects-dropdown-container {
+.frequent-items-dropdown-container {
display: flex;
flex-direction: row;
width: 500px;
height: 334px;
- .project-dropdown-sidebar,
- .project-dropdown-content {
+ .frequent-items-dropdown-sidebar,
+ .frequent-items-dropdown-content {
padding: 8px 0;
}
@@ -832,12 +833,12 @@ header.header-content .dropdown-menu.projects-dropdown-menu {
color: $almost-black;
}
- .project-dropdown-sidebar {
+ .frequent-items-dropdown-sidebar {
width: 30%;
border-right: 1px solid $border-color;
}
- .project-dropdown-content {
+ .frequent-items-dropdown-content {
position: relative;
width: 70%;
}
@@ -848,33 +849,35 @@ header.header-content .dropdown-menu.projects-dropdown-menu {
height: auto;
flex: 1;
- .project-dropdown-sidebar,
- .project-dropdown-content {
+ .frequent-items-dropdown-sidebar,
+ .frequent-items-dropdown-content {
width: 100%;
}
- .project-dropdown-sidebar {
+ .frequent-items-dropdown-sidebar {
border-bottom: 1px solid $border-color;
border-right: 0;
}
}
- .projects-list-frequent-container,
- .projects-list-search-container {
+ .section-header,
+ .frequent-items-list-container li.section-empty {
+ padding: 0 $gl-padding;
+ color: $gl-text-color-secondary;
+ font-size: $gl-font-size;
+ }
+
+ .frequent-items-list-container {
padding: 8px 0;
overflow-y: auto;
li.section-empty.section-failure {
color: $callout-danger-color;
}
- }
- .section-header,
- .projects-list-frequent-container li.section-empty,
- .projects-list-search-container li.section-empty {
- padding: 0 15px;
- color: $gl-text-color-secondary;
- font-size: $gl-font-size;
+ .frequent-items-list-item-container a {
+ display: flex;
+ }
}
.search-input-container {
@@ -894,12 +897,12 @@ header.header-content .dropdown-menu.projects-dropdown-menu {
margin-top: 8px;
}
- .projects-list-search-container {
+ .frequent-items-search-container {
height: 284px;
}
@include media-breakpoint-down(xs) {
- .projects-list-frequent-container {
+ .frequent-items-list-container {
width: auto;
height: auto;
padding-bottom: 0;
@@ -907,32 +910,38 @@ header.header-content .dropdown-menu.projects-dropdown-menu {
}
}
-.projects-list-item-container {
- .project-item-avatar-container .project-item-metadata-container {
+.frequent-items-list-item-container {
+ .frequent-items-item-avatar-container,
+ .frequent-items-item-metadata-container {
float: left;
}
- .project-title,
- .project-namespace {
+ .frequent-items-item-metadata-container {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ }
+
+ .frequent-items-item-title,
+ .frequent-items-item-namespace {
max-width: 250px;
- overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
&:hover {
- .project-item-avatar-container .avatar {
+ .frequent-items-item-avatar-container .avatar {
border-color: $md-area-border;
}
}
- .project-title {
+ .frequent-items-item-title {
font-size: $gl-font-size;
font-weight: 400;
line-height: 16px;
}
- .project-namespace {
+ .frequent-items-item-namespace {
margin-top: 4px;
font-size: 12px;
line-height: 12px;
@@ -940,7 +949,7 @@ header.header-content .dropdown-menu.projects-dropdown-menu {
}
@include media-breakpoint-down(xs) {
- .project-item-metadata-container {
+ .frequent-items-item-metadata-container {
float: none;
}
}
diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss
index 551a7e852ae..5d79610b21e 100644
--- a/app/assets/stylesheets/framework/filters.scss
+++ b/app/assets/stylesheets/framework/filters.scss
@@ -224,7 +224,10 @@
.form-control {
position: relative;
min-width: 200px;
- padding: 5px 25px 6px 0;
+ padding-right: 25px;
+ padding-left: 0;
+ height: $input-height;
+ line-height: inherit;
border-color: transparent;
&:focus,
diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss
index 282e424fc38..a22454c24e2 100644
--- a/app/assets/stylesheets/framework/forms.scss
+++ b/app/assets/stylesheets/framework/forms.scss
@@ -255,3 +255,8 @@ label {
color: $theme-gray-600;
}
}
+
+.input-lg {
+ max-width: 320px;
+ width: 100%;
+}
diff --git a/app/assets/stylesheets/framework/gitlab_theme.scss b/app/assets/stylesheets/framework/gitlab_theme.scss
index aaa8bed3df0..dff6bce370f 100644
--- a/app/assets/stylesheets/framework/gitlab_theme.scss
+++ b/app/assets/stylesheets/framework/gitlab_theme.scss
@@ -29,15 +29,21 @@
.navbar-sub-nav,
.navbar-nav {
> li {
- > a:hover,
- > a:focus {
- background-color: rgba($search-and-nav-links, 0.2);
+ > a,
+ > button {
+ &:hover,
+ &:focus {
+ background-color: rgba($search-and-nav-links, 0.2);
+ }
}
- &.active > a,
- &.dropdown.show > a {
- color: $nav-svg-color;
- background-color: $color-alternate;
+ &.active,
+ &.dropdown.show {
+ > a,
+ > button {
+ color: $nav-svg-color;
+ background-color: $color-alternate;
+ }
}
&.line-separator {
@@ -147,7 +153,6 @@
}
}
-
// Sidebar
.nav-sidebar li.active {
box-shadow: inset 4px 0 0 $border-and-box-shadow;
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index 8bcaf5eb6ac..2097bcebf69 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -269,14 +269,8 @@
.navbar-sub-nav,
.navbar-nav {
> li {
- > a:hover,
- > a:focus {
- text-decoration: none;
- outline: 0;
- color: $white-light;
- }
-
- > a {
+ > a,
+ > button {
display: -webkit-flex;
display: flex;
align-items: center;
@@ -288,6 +282,18 @@
border-radius: $border-radius-default;
height: 32px;
font-weight: $gl-font-weight-bold;
+
+ &:hover,
+ &:focus {
+ text-decoration: none;
+ outline: 0;
+ color: $white-light;
+ }
+ }
+
+ > button {
+ background: transparent;
+ border: 0;
}
&.line-separator {
@@ -311,7 +317,7 @@
font-size: 10px;
}
- .project-item-select-holder {
+ .frequent-items-item-select-holder {
display: inline;
}
diff --git a/app/assets/stylesheets/framework/icons.scss b/app/assets/stylesheets/framework/icons.scss
index 30314f3d6cb..d1f7ff4438b 100644
--- a/app/assets/stylesheets/framework/icons.scss
+++ b/app/assets/stylesheets/framework/icons.scss
@@ -3,12 +3,20 @@
svg {
fill: $green-500;
}
+
+ &.add-border {
+ @include borderless-status-icon($green-500);
+ }
}
.ci-status-icon-failed {
svg {
fill: $gl-danger;
}
+
+ &.add-border {
+ @include borderless-status-icon($red-500);
+ }
}
.ci-status-icon-pending,
@@ -17,12 +25,20 @@
svg {
fill: $orange-500;
}
+
+ &.add-border {
+ @include borderless-status-icon($orange-500);
+ }
}
.ci-status-icon-running {
svg {
fill: $blue-400;
}
+
+ &.add-border {
+ @include borderless-status-icon($blue-400);
+ }
}
.ci-status-icon-canceled,
@@ -30,6 +46,10 @@
svg {
fill: $gl-text-color;
}
+
+ &.add-border {
+ @include borderless-status-icon($gl-text-color);
+ }
}
.ci-status-icon-created,
@@ -38,6 +58,10 @@
svg {
fill: $gray-darkest;
}
+
+ &.add-border {
+ @include borderless-status-icon($gray-darkest);
+ }
}
.ci-status-icon-manual {
diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss
index 0b645eb811b..76ebfc22ef7 100644
--- a/app/assets/stylesheets/framework/mixins.scss
+++ b/app/assets/stylesheets/framework/mixins.scss
@@ -232,3 +232,10 @@
word-break: break-word;
max-width: 100%;
}
+
+@mixin borderless-status-icon($color) {
+ svg {
+ border: 1px solid $color;
+ border-radius: 50%;
+ }
+}
diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss
index 54799593727..c1836228a49 100644
--- a/app/assets/stylesheets/framework/typography.scss
+++ b/app/assets/stylesheets/framework/typography.scss
@@ -350,7 +350,8 @@ code {
}
.commit-sha,
-.ref-name {
+.ref-name,
+.pipeline-number {
@extend .monospace;
font-size: 95%;
}
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 7808f6d3a25..6cfa09b56a7 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -743,6 +743,7 @@ Pipeline Graph
*/
$stage-hover-bg: $gray-darker;
$ci-action-icon-size: 22px;
+$ci-action-icon-size-lg: 24px;
$pipeline-dropdown-line-height: 20px;
$pipeline-dropdown-status-icon-size: 18px;
$ci-action-dropdown-button-size: 24px;
diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss
index ca63a8ae916..202377d6d9e 100644
--- a/app/assets/stylesheets/pages/boards.scss
+++ b/app/assets/stylesheets/pages/boards.scss
@@ -80,7 +80,6 @@
overflow-x: scroll;
white-space: nowrap;
min-height: 200px;
- display: flex;
@include media-breakpoint-only(sm) {
height: calc(100vh - #{$issue-board-list-difference-sm});
@@ -111,15 +110,17 @@
.board {
display: inline-block;
- flex: 1;
- min-width: 300px;
- max-width: 400px;
+ width: calc(85vw - 15px);
height: 100%;
padding-right: ($gl-padding / 2);
padding-left: ($gl-padding / 2);
white-space: normal;
vertical-align: top;
+ @include media-breakpoint-up(sm) {
+ width: 400px;
+ }
+
&.is-expandable {
.board-header {
cursor: pointer;
@@ -127,8 +128,6 @@
}
&.is-collapsed {
- flex: none;
- min-width: 0;
width: 50px;
.board-header {
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index efd730af558..c32049e1b33 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -15,16 +15,38 @@
}
}
+.mr-widget-heading {
+ position: relative;
+ border: 1px solid $border-color;
+ border-radius: 4px;
+
+ &:not(.deploy-heading)::before {
+ content: '';
+ border-left: 1px solid $theme-gray-200;
+ position: absolute;
+ left: 32px;
+ top: -17px;
+ height: 16px;
+ }
+}
+
+.mr-section-container {
+ border: 1px solid $border-color;
+ border-radius: $border-radius-default;
+ border-top: 0;
+}
+
+.mr-widget-heading,
+.mr-widget-section,
+.mr-widget-footer {
+ padding: $gl-padding;
+}
+
.mr-state-widget {
color: $gl-text-color;
- border: 1px solid $border-color;
- border-radius: 2px;
- line-height: 28px;
- .mr-widget-heading,
.mr-widget-section,
.mr-widget-footer {
- padding: $gl-padding;
border-top: solid 1px $border-color;
}
@@ -124,10 +146,17 @@
.ci-widget {
color: $gl-text-color;
display: flex;
+ align-items: center;
+ justify-content: space-between;
@include media-breakpoint-down(xs) {
flex-wrap: wrap;
}
+
+ .ci-widget-content {
+ display: flex;
+ align-items: center;
+ }
}
.mr-widget-icon {
@@ -136,8 +165,6 @@
}
.ci-status-icon svg {
- width: $status-icon-size;
- height: $status-icon-size;
margin: 3px 0;
position: relative;
overflow: visible;
@@ -145,8 +172,6 @@
}
.mr-widget-pipeline-graph {
- padding: 0 4px;
-
.dropdown-menu {
z-index: 300;
}
@@ -157,7 +182,7 @@
}
.normal {
- line-height: 28px;
+ flex: 1;
}
.capitalize {
@@ -168,7 +193,7 @@
@extend .ref-name;
color: $gl-text-color;
- font-weight: $gl-font-weight-bold;
+ font-weight: normal;
overflow: hidden;
word-break: break-all;
@@ -192,6 +217,8 @@
}
.mr-widget-body {
+ line-height: 28px;
+
@include clearfix;
&.media > *:first-child {
@@ -474,18 +501,66 @@
}
}
+.merge-request-details .content-block {
+ border-bottom: 0;
+}
+
.mr-source-target {
display: flex;
flex-wrap: wrap;
- justify-content: space-between;
- align-items: center;
- background-color: $gray-light;
- border-radius: $border-radius-default $border-radius-default 0 0;
- padding: $gl-padding / 2 $gl-padding;
+ border-radius: $border-radius-default;
+ padding: $gl-padding;
+ border: 1px solid $border-color;
+ min-height: 69px;
+
+ @include media-breakpoint-up(md) {
+ align-items: center;
+ }
.dropdown-toggle .fa {
color: $gl-text-color;
}
+
+ .git-merge-icon-container {
+ border: 1px solid $theme-gray-400;
+ border-radius: 50%;
+ height: 32px;
+ width: 32px;
+ color: $theme-gray-700;
+ line-height: 28px;
+
+ .ic-git-merge {
+ vertical-align: middle;
+ width: 31px;
+ }
+ }
+
+ .git-merge-container {
+ justify-content: space-between;
+ flex: 1;
+ flex-direction: row;
+ align-items: center;
+
+ @include media-breakpoint-down(md) {
+ flex-direction: column;
+ align-items: flex-start;
+
+ .branch-actions {
+ margin-top: 16px;
+ }
+ }
+
+ @include media-breakpoint-up(lg) {
+ .branch-actions {
+ align-self: center;
+ }
+ }
+ }
+
+ .diverged-commits-count {
+ color: $gl-text-color-secondary;
+ font-size: 12px;
+ }
}
.card-new-merge-request {
@@ -720,13 +795,25 @@
}
.deploy-heading {
+ margin-top: -19px;
+ border-top-left-radius: 0;
+ border-top-right-radius: 0;
+ background-color: $gray-light;
+
+ @include media-breakpoint-up(md) {
+ padding: $gl-padding-8 $gl-padding;
+ }
+
.media-body {
min-width: 0;
+ font-size: 12px;
+ margin-left: 48px;
}
}
.deploy-body {
display: flex;
+ align-items: center;
flex-wrap: wrap;
@include media-breakpoint-up(xs) {
@@ -734,6 +821,15 @@
white-space: nowrap;
}
+ @include media-breakpoint-down(md) {
+ flex-direction: column;
+ align-items: flex-start;
+
+ .deployment-info {
+ margin-bottom: $gl-padding;
+ }
+ }
+
> *:not(:last-child) {
margin-right: .3em;
}
@@ -741,18 +837,22 @@
svg {
vertical-align: text-top;
}
-}
-.deploy-link {
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- min-width: 100px;
- max-width: 150px;
+ .deployment-info {
+ flex: 1;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ min-width: 100px;
- @include media-breakpoint-up(xs) {
- min-width: 0;
- max-width: 100%;
+ @include media-breakpoint-up(xs) {
+ min-width: 0;
+ max-width: 100%;
+ }
+ }
+
+ .btn svg {
+ fill: $theme-gray-700;
}
}
@@ -772,3 +872,33 @@
}
}
}
+
+.ci-widget-container {
+ justify-content: space-between;
+ flex: 1;
+ flex-direction: row;
+
+ @include media-breakpoint-down(md) {
+ flex-direction: column;
+
+ .stage-cell .stage-container {
+ margin-top: 16px;
+ }
+
+ .dropdown .mini-pipeline-graph-dropdown-menu.dropdown-menu {
+ transform: initial;
+ }
+ }
+
+ .coverage {
+ font-size: 12px;
+ color: $theme-gray-700;
+ line-height: initial;
+ }
+
+ .mini-pipeline-graph-dropdown-toggle,
+ .stage-cell .mini-pipeline-graph-dropdown-toggle svg {
+ height: $ci-action-icon-size-lg;
+ width: $ci-action-icon-size-lg;
+ }
+}
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index 52332ac97dd..b68c89c25d8 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -301,6 +301,21 @@
border-bottom: 2px solid $border-color;
}
}
+
+ //delete when all pipelines are updated to new size
+ &.mr-widget-pipeline-stages {
+ + .stage-container {
+ margin-left: 4px;
+ }
+
+ &:not(:last-child) {
+ &::after {
+ width: 4px;
+ right: -4px;
+ top: 11px;
+ }
+ }
+ }
}
}
diff --git a/app/assets/stylesheets/pages/repo.scss b/app/assets/stylesheets/pages/repo.scss
index 3c24aaa65e8..6e2b285285a 100644
--- a/app/assets/stylesheets/pages/repo.scss
+++ b/app/assets/stylesheets/pages/repo.scss
@@ -1329,3 +1329,14 @@
line-height: 16px;
color: $gl-text-color-secondary;
}
+
+.ide-merge-request-info {
+ .detail-page-header {
+ line-height: initial;
+ min-height: 38px;
+ }
+
+ .issuable-details {
+ overflow: auto;
+ }
+}
diff --git a/app/controllers/admin/deploy_keys_controller.rb b/app/controllers/admin/deploy_keys_controller.rb
index b0c4c31cffc..5c2025c1988 100644
--- a/app/controllers/admin/deploy_keys_controller.rb
+++ b/app/controllers/admin/deploy_keys_controller.rb
@@ -22,7 +22,7 @@ class Admin::DeployKeysController < Admin::ApplicationController
end
def update
- if deploy_key.update_attributes(update_params)
+ if deploy_key.update(update_params)
flash[:notice] = 'Deploy key was successfully updated.'
redirect_to admin_deploy_keys_path
else
@@ -34,7 +34,7 @@ class Admin::DeployKeysController < Admin::ApplicationController
deploy_key.destroy
respond_to do |format|
- format.html { redirect_to admin_deploy_keys_path, status: 302 }
+ format.html { redirect_to admin_deploy_keys_path, status: :found }
format.json { head :ok }
end
end
diff --git a/app/controllers/admin/groups_controller.rb b/app/controllers/admin/groups_controller.rb
index 96b7bc65ac9..d7a5b745d3f 100644
--- a/app/controllers/admin/groups_controller.rb
+++ b/app/controllers/admin/groups_controller.rb
@@ -39,7 +39,7 @@ class Admin::GroupsController < Admin::ApplicationController
end
def update
- if @group.update_attributes(group_params)
+ if @group.update(group_params)
redirect_to [:admin, @group], notice: 'Group was successfully updated.'
else
render "edit"
diff --git a/app/controllers/admin/hooks_controller.rb b/app/controllers/admin/hooks_controller.rb
index 6944857bd33..a98c355c7ba 100644
--- a/app/controllers/admin/hooks_controller.rb
+++ b/app/controllers/admin/hooks_controller.rb
@@ -23,7 +23,7 @@ class Admin::HooksController < Admin::ApplicationController
end
def update
- if hook.update_attributes(hook_params)
+ if hook.update(hook_params)
flash[:notice] = 'System hook was successfully updated.'
redirect_to admin_hooks_path
else
@@ -34,7 +34,7 @@ class Admin::HooksController < Admin::ApplicationController
def destroy
hook.destroy
- redirect_to admin_hooks_path, status: 302
+ redirect_to admin_hooks_path, status: :found
end
def test
diff --git a/app/controllers/admin/identities_controller.rb b/app/controllers/admin/identities_controller.rb
index 43b4e3a2cc3..ceb45865804 100644
--- a/app/controllers/admin/identities_controller.rb
+++ b/app/controllers/admin/identities_controller.rb
@@ -25,7 +25,7 @@ class Admin::IdentitiesController < Admin::ApplicationController
end
def update
- if @identity.update_attributes(identity_params)
+ if @identity.update(identity_params)
RepairLdapBlockedUserService.new(@user).execute
redirect_to admin_user_identities_path(@user), notice: 'User identity was successfully updated.'
else
diff --git a/app/controllers/admin/impersonations_controller.rb b/app/controllers/admin/impersonations_controller.rb
index 39dbf85f6c0..d2f947d2c66 100644
--- a/app/controllers/admin/impersonations_controller.rb
+++ b/app/controllers/admin/impersonations_controller.rb
@@ -11,7 +11,7 @@ class Admin::ImpersonationsController < Admin::ApplicationController
session[:impersonator_id] = nil
- redirect_to admin_user_path(original_user), status: 302
+ redirect_to admin_user_path(original_user), status: :found
end
private
diff --git a/app/controllers/admin/jobs_controller.rb b/app/controllers/admin/jobs_controller.rb
index ae7a7f6279c..ac1ae0f16b3 100644
--- a/app/controllers/admin/jobs_controller.rb
+++ b/app/controllers/admin/jobs_controller.rb
@@ -20,6 +20,6 @@ class Admin::JobsController < Admin::ApplicationController
def cancel_all
Ci::Build.running_or_pending.each(&:cancel)
- redirect_to admin_jobs_path, status: 303
+ redirect_to admin_jobs_path, status: :see_other
end
end
diff --git a/app/controllers/admin/runner_projects_controller.rb b/app/controllers/admin/runner_projects_controller.rb
index 7aba77d8129..51d5799cd89 100644
--- a/app/controllers/admin/runner_projects_controller.rb
+++ b/app/controllers/admin/runner_projects_controller.rb
@@ -16,7 +16,7 @@ class Admin::RunnerProjectsController < Admin::ApplicationController
runner = rp.runner
rp.destroy
- redirect_to admin_runner_path(runner), status: 302
+ redirect_to admin_runner_path(runner), status: :found
end
private
diff --git a/app/controllers/admin/runners_controller.rb b/app/controllers/admin/runners_controller.rb
index 4b01904f2a1..6c76c55a9d4 100644
--- a/app/controllers/admin/runners_controller.rb
+++ b/app/controllers/admin/runners_controller.rb
@@ -28,7 +28,7 @@ class Admin::RunnersController < Admin::ApplicationController
def destroy
@runner.destroy
- redirect_to admin_runners_path, status: 302
+ redirect_to admin_runners_path, status: :found
end
def resume
diff --git a/app/controllers/admin/services_controller.rb b/app/controllers/admin/services_controller.rb
index a7025b62ad7..e70aa549140 100644
--- a/app/controllers/admin/services_controller.rb
+++ b/app/controllers/admin/services_controller.rb
@@ -16,7 +16,7 @@ class Admin::ServicesController < Admin::ApplicationController
end
def update
- if service.update_attributes(service_params[:service])
+ if service.update(service_params[:service])
PropagateServiceTemplateWorker.perform_async(service.id) if service.active?
redirect_to admin_application_settings_services_path,
diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb
index 653f3dfffc4..a51a8c3ed4a 100644
--- a/app/controllers/admin/users_controller.rb
+++ b/app/controllers/admin/users_controller.rb
@@ -163,7 +163,7 @@ class Admin::UsersController < Admin::ApplicationController
format.json { head :ok }
else
format.html { redirect_back_or_admin_user(alert: 'There was an error removing the e-mail.') }
- format.json { render json: 'There was an error removing the e-mail.', status: 400 }
+ format.json { render json: 'There was an error removing the e-mail.', status: :bad_request }
end
end
end
diff --git a/app/controllers/concerns/group_tree.rb b/app/controllers/concerns/group_tree.rb
index 56770a17406..6ec6897e707 100644
--- a/app/controllers/concerns/group_tree.rb
+++ b/app/controllers/concerns/group_tree.rb
@@ -1,21 +1,16 @@
module GroupTree
# rubocop:disable Gitlab/ModuleWithInstanceVariables
def render_group_tree(groups)
- @groups = if params[:filter].present?
- # We find the ancestors by ID of the search results here.
- # Otherwise the ancestors would also have filters applied,
- # which would cause them not to be preloaded.
- group_ids = groups.search(params[:filter]).select(:id)
- Gitlab::GroupHierarchy.new(Group.where(id: group_ids))
- .base_and_ancestors
- else
- # Only show root groups if no parent-id is given
- groups.where(parent_id: params[:parent_id])
- end
+ groups = groups.sort_by_attribute(@sort = params[:sort])
- @groups = @groups.with_selects_for_list(archived: params[:archived])
- .sort_by_attribute(@sort = params[:sort])
- .page(params[:page])
+ groups = if params[:filter].present?
+ filtered_groups_with_ancestors(groups)
+ else
+ # If `params[:parent_id]` is `nil`, we will only show root-groups
+ groups.where(parent_id: params[:parent_id]).page(params[:page])
+ end
+
+ @groups = groups.with_selects_for_list(archived: params[:archived])
respond_to do |format|
format.html
@@ -28,4 +23,21 @@ module GroupTree
end
# rubocop:enable Gitlab/ModuleWithInstanceVariables
end
+
+ def filtered_groups_with_ancestors(groups)
+ filtered_groups = groups.search(params[:filter]).page(params[:page])
+
+ if Group.supports_nested_groups?
+ # We find the ancestors by ID of the search results here.
+ # Otherwise the ancestors would also have filters applied,
+ # which would cause them not to be preloaded.
+ #
+ # Pagination needs to be applied before loading the ancestors to
+ # make sure ancestors are not cut off by pagination.
+ Gitlab::GroupHierarchy.new(Group.where(id: filtered_groups.select(:id)))
+ .base_and_ancestors
+ else
+ filtered_groups
+ end
+ end
end
diff --git a/app/controllers/concerns/issuable_actions.rb b/app/controllers/concerns/issuable_actions.rb
index ba510968684..37e03d70b6f 100644
--- a/app/controllers/concerns/issuable_actions.rb
+++ b/app/controllers/concerns/issuable_actions.rb
@@ -127,7 +127,7 @@ module IssuableActions
errors: [
"Someone edited this #{issuable.human_class_name} at the same time you did. Please refresh your browser and make sure your changes will not unintentionally remove theirs."
]
- }, status: 409
+ }, status: :conflict
end
end
end
diff --git a/app/controllers/concerns/lfs_request.rb b/app/controllers/concerns/lfs_request.rb
index 5e4e8a87153..79ee5b2f91e 100644
--- a/app/controllers/concerns/lfs_request.rb
+++ b/app/controllers/concerns/lfs_request.rb
@@ -27,7 +27,7 @@ module LfsRequest
message: 'Git LFS is not enabled on this GitLab server, contact your admin.',
documentation_url: help_url
},
- status: 501
+ status: :not_implemented
)
end
diff --git a/app/controllers/concerns/preview_markdown.rb b/app/controllers/concerns/preview_markdown.rb
index 90bb7a87b45..99123fcb3b0 100644
--- a/app/controllers/concerns/preview_markdown.rb
+++ b/app/controllers/concerns/preview_markdown.rb
@@ -10,9 +10,12 @@ module PreviewMarkdown
when 'wikis' then { pipeline: :wiki, project_wiki: @project_wiki, page_slug: params[:id] }
when 'snippets' then { skip_project_check: true }
when 'groups' then { group: group }
+ when 'projects' then { issuable_state_filter_enabled: true }
else {}
end
+ markdown_params[:markdown_engine] = result[:markdown_engine]
+
render json: {
body: view_context.markdown(result[:text], markdown_params),
references: {
diff --git a/app/controllers/dashboard/projects_controller.rb b/app/controllers/dashboard/projects_controller.rb
index 4d4ac025f8c..ccfcbbdc776 100644
--- a/app/controllers/dashboard/projects_controller.rb
+++ b/app/controllers/dashboard/projects_controller.rb
@@ -7,7 +7,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
skip_cross_project_access_check :index, :starred
def index
- @projects = load_projects(params.merge(non_public: true)).page(params[:page])
+ @projects = load_projects(params.merge(non_public: true))
respond_to do |format|
format.html
@@ -25,7 +25,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
def starred
@projects = load_projects(params.merge(starred: true))
- .includes(:forked_from_project, :tags).page(params[:page])
+ .includes(:forked_from_project, :tags)
@groups = []
@@ -51,6 +51,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
.new(params: finder_params, current_user: current_user)
.execute
.includes(:route, :creator, namespace: [:route, :owner])
+ .page(finder_params[:page])
prepare_projects_for_rendering(projects)
end
diff --git a/app/controllers/groups/avatars_controller.rb b/app/controllers/groups/avatars_controller.rb
index cc5ba5878f8..35a61b359c8 100644
--- a/app/controllers/groups/avatars_controller.rb
+++ b/app/controllers/groups/avatars_controller.rb
@@ -7,6 +7,6 @@ class Groups::AvatarsController < Groups::ApplicationController
@group.remove_avatar!
@group.save
- redirect_to edit_group_path(@group), status: 302
+ redirect_to edit_group_path(@group), status: :found
end
end
diff --git a/app/controllers/groups/runners_controller.rb b/app/controllers/groups/runners_controller.rb
index 78992ec7f46..1036b4e6ed3 100644
--- a/app/controllers/groups/runners_controller.rb
+++ b/app/controllers/groups/runners_controller.rb
@@ -23,7 +23,7 @@ class Groups::RunnersController < Groups::ApplicationController
def destroy
@runner.destroy
- redirect_to group_settings_ci_cd_path(@group, anchor: 'runners-settings'), status: 302
+ redirect_to group_settings_ci_cd_path(@group, anchor: 'runners-settings'), status: :found
end
def resume
diff --git a/app/controllers/jwt_controller.rb b/app/controllers/jwt_controller.rb
index 67057b5b126..3cb9e46b548 100644
--- a/app/controllers/jwt_controller.rb
+++ b/app/controllers/jwt_controller.rb
@@ -41,7 +41,7 @@ class JwtController < ApplicationController
"You must use a personal access token with 'api' scope for Git over HTTP.\n" \
"You can generate one at #{profile_personal_access_tokens_url}" }
]
- }, status: 401
+ }, status: :unauthorized
end
def render_unauthorized
@@ -50,7 +50,7 @@ class JwtController < ApplicationController
{ code: 'UNAUTHORIZED',
message: 'HTTP Basic: Access denied' }
]
- }, status: 401
+ }, status: :unauthorized
end
def auth_params
diff --git a/app/controllers/notification_settings_controller.rb b/app/controllers/notification_settings_controller.rb
index 8ec4bb1233f..ed20302487c 100644
--- a/app/controllers/notification_settings_controller.rb
+++ b/app/controllers/notification_settings_controller.rb
@@ -5,14 +5,14 @@ class NotificationSettingsController < ApplicationController
return render_404 unless can_read?(resource)
@notification_setting = current_user.notification_settings_for(resource)
- @saved = @notification_setting.update_attributes(notification_setting_params)
+ @saved = @notification_setting.update(notification_setting_params)
render_response
end
def update
@notification_setting = current_user.notification_settings.find(params[:id])
- @saved = @notification_setting.update_attributes(notification_setting_params)
+ @saved = @notification_setting.update(notification_setting_params)
render_response
end
diff --git a/app/controllers/profiles/active_sessions_controller.rb b/app/controllers/profiles/active_sessions_controller.rb
index f0cdc228366..f1e77d68acd 100644
--- a/app/controllers/profiles/active_sessions_controller.rb
+++ b/app/controllers/profiles/active_sessions_controller.rb
@@ -7,7 +7,7 @@ class Profiles::ActiveSessionsController < Profiles::ApplicationController
ActiveSession.destroy(current_user, params[:id])
respond_to do |format|
- format.html { redirect_to profile_active_sessions_url, status: 302 }
+ format.html { redirect_to profile_active_sessions_url, status: :found }
format.js { head :ok }
end
end
diff --git a/app/controllers/profiles/avatars_controller.rb b/app/controllers/profiles/avatars_controller.rb
index 39b9f8a84d1..4f030ded80f 100644
--- a/app/controllers/profiles/avatars_controller.rb
+++ b/app/controllers/profiles/avatars_controller.rb
@@ -4,6 +4,6 @@ class Profiles::AvatarsController < Profiles::ApplicationController
Users::UpdateService.new(current_user, user: @user).execute { |user| user.remove_avatar! }
- redirect_to profile_path, status: 302
+ redirect_to profile_path, status: :found
end
end
diff --git a/app/controllers/profiles/chat_names_controller.rb b/app/controllers/profiles/chat_names_controller.rb
index 2353f0840d6..a186c5f36a8 100644
--- a/app/controllers/profiles/chat_names_controller.rb
+++ b/app/controllers/profiles/chat_names_controller.rb
@@ -39,7 +39,7 @@ class Profiles::ChatNamesController < Profiles::ApplicationController
flash[:alert] = "Could not delete chat nickname #{@chat_name.chat_name}."
end
- redirect_to profile_chat_names_path, status: 302
+ redirect_to profile_chat_names_path, status: :found
end
private
diff --git a/app/controllers/profiles/emails_controller.rb b/app/controllers/profiles/emails_controller.rb
index bbd7ba49d77..a39824ec9c8 100644
--- a/app/controllers/profiles/emails_controller.rb
+++ b/app/controllers/profiles/emails_controller.rb
@@ -19,7 +19,7 @@ class Profiles::EmailsController < Profiles::ApplicationController
Emails::DestroyService.new(current_user, user: current_user).execute(@email)
respond_to do |format|
- format.html { redirect_to profile_emails_url, status: 302 }
+ format.html { redirect_to profile_emails_url, status: :found }
format.js { head :ok }
end
end
diff --git a/app/controllers/profiles/gpg_keys_controller.rb b/app/controllers/profiles/gpg_keys_controller.rb
index 38e3eacd229..c32507756e8 100644
--- a/app/controllers/profiles/gpg_keys_controller.rb
+++ b/app/controllers/profiles/gpg_keys_controller.rb
@@ -21,7 +21,7 @@ class Profiles::GpgKeysController < Profiles::ApplicationController
@gpg_key.destroy
respond_to do |format|
- format.html { redirect_to profile_gpg_keys_url, status: 302 }
+ format.html { redirect_to profile_gpg_keys_url, status: :found }
format.js { head :ok }
end
end
@@ -30,7 +30,7 @@ class Profiles::GpgKeysController < Profiles::ApplicationController
@gpg_key.revoke
respond_to do |format|
- format.html { redirect_to profile_gpg_keys_url, status: 302 }
+ format.html { redirect_to profile_gpg_keys_url, status: :found }
format.js { head :ok }
end
end
diff --git a/app/controllers/profiles/keys_controller.rb b/app/controllers/profiles/keys_controller.rb
index 12a6cd11f80..6035258667e 100644
--- a/app/controllers/profiles/keys_controller.rb
+++ b/app/controllers/profiles/keys_controller.rb
@@ -26,7 +26,7 @@ class Profiles::KeysController < Profiles::ApplicationController
Keys::DestroyService.new(current_user).execute(@key)
respond_to do |format|
- format.html { redirect_to profile_keys_url, status: 302 }
+ format.html { redirect_to profile_keys_url, status: :found }
format.js { head :ok }
end
end
diff --git a/app/controllers/profiles/two_factor_auths_controller.rb b/app/controllers/profiles/two_factor_auths_controller.rb
index aa9789f8a0f..29ff18a1219 100644
--- a/app/controllers/profiles/two_factor_auths_controller.rb
+++ b/app/controllers/profiles/two_factor_auths_controller.rb
@@ -78,7 +78,7 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
def destroy
current_user.disable_two_factor!
- redirect_to profile_account_path, status: 302
+ redirect_to profile_account_path, status: :found
end
def skip
diff --git a/app/controllers/projects/application_controller.rb b/app/controllers/projects/application_controller.rb
index 5ab6d103c89..b4f814fd3a4 100644
--- a/app/controllers/projects/application_controller.rb
+++ b/app/controllers/projects/application_controller.rb
@@ -61,7 +61,7 @@ class Projects::ApplicationController < ApplicationController
def require_non_empty_project
# Be sure to return status code 303 to avoid a double DELETE:
# http://api.rubyonrails.org/classes/ActionController/Redirecting.html
- redirect_to project_path(@project), status: 303 if @project.empty_repo?
+ redirect_to project_path(@project), status: :see_other if @project.empty_repo?
end
def require_branch_head
diff --git a/app/controllers/projects/autocomplete_sources_controller.rb b/app/controllers/projects/autocomplete_sources_controller.rb
index 992c8ea6992..07627ffb69f 100644
--- a/app/controllers/projects/autocomplete_sources_controller.rb
+++ b/app/controllers/projects/autocomplete_sources_controller.rb
@@ -14,7 +14,7 @@ class Projects::AutocompleteSourcesController < Projects::ApplicationController
end
def labels
- render json: @autocomplete_service.labels(target)
+ render json: @autocomplete_service.labels_as_hash(target)
end
def milestones
diff --git a/app/controllers/projects/avatars_controller.rb b/app/controllers/projects/avatars_controller.rb
index 21a403f3765..a13d552dbd8 100644
--- a/app/controllers/projects/avatars_controller.rb
+++ b/app/controllers/projects/avatars_controller.rb
@@ -21,6 +21,6 @@ class Projects::AvatarsController < Projects::ApplicationController
@project.save
- redirect_to edit_project_path(@project), status: 302
+ redirect_to edit_project_path(@project), status: :found
end
end
diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb
index cd7250b10fc..d1dc9fe9600 100644
--- a/app/controllers/projects/branches_controller.rb
+++ b/app/controllers/projects/branches_controller.rb
@@ -98,7 +98,7 @@ class Projects::BranchesController < Projects::ApplicationController
flash_type = result[:status] == :error ? :alert : :notice
flash[flash_type] = result[:message]
- redirect_to project_branches_path(@project), status: 303
+ redirect_to project_branches_path(@project), status: :see_other
end
format.js { render nothing: true, status: result[:return_code] }
diff --git a/app/controllers/projects/clusters_controller.rb b/app/controllers/projects/clusters_controller.rb
index 62193257940..358fe59618b 100644
--- a/app/controllers/projects/clusters_controller.rb
+++ b/app/controllers/projects/clusters_controller.rb
@@ -62,7 +62,7 @@ class Projects::ClustersController < Projects::ApplicationController
def destroy
if cluster.destroy
flash[:notice] = _('Kubernetes cluster integration was successfully removed.')
- redirect_to project_clusters_path(project), status: 302
+ redirect_to project_clusters_path(project), status: :found
else
flash[:notice] = _('Kubernetes cluster integration was not removed.')
render :show
diff --git a/app/controllers/projects/deploy_keys_controller.rb b/app/controllers/projects/deploy_keys_controller.rb
index f43ef2e5f2f..06739d8fd4a 100644
--- a/app/controllers/projects/deploy_keys_controller.rb
+++ b/app/controllers/projects/deploy_keys_controller.rb
@@ -35,7 +35,7 @@ class Projects::DeployKeysController < Projects::ApplicationController
end
def update
- if deploy_key.update_attributes(update_params)
+ if deploy_key.update(update_params)
flash[:notice] = 'Deploy key was successfully updated.'
redirect_to_repository_settings(@project)
else
diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb
index 27b7425b965..395c5336ad5 100644
--- a/app/controllers/projects/environments_controller.rb
+++ b/app/controllers/projects/environments_controller.rb
@@ -116,7 +116,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController
set_workhorse_internal_api_content_type
render json: Gitlab::Workhorse.terminal_websocket(terminal)
else
- render text: 'Not found', status: 404
+ render text: 'Not found', status: :not_found
end
end
diff --git a/app/controllers/projects/git_http_client_controller.rb b/app/controllers/projects/git_http_client_controller.rb
index 07249fe3182..a52814e6e52 100644
--- a/app/controllers/projects/git_http_client_controller.rb
+++ b/app/controllers/projects/git_http_client_controller.rb
@@ -53,7 +53,7 @@ class Projects::GitHttpClientController < Projects::ApplicationController
end
send_challenges
- render plain: "HTTP Basic: Access denied\n", status: 401
+ render plain: "HTTP Basic: Access denied\n", status: :unauthorized
rescue Gitlab::Auth::MissingPersonalAccessTokenError
render_missing_personal_access_token
end
@@ -83,7 +83,7 @@ class Projects::GitHttpClientController < Projects::ApplicationController
render plain: "HTTP Basic: Access denied\n" \
"You must use a personal access token with 'api' scope for Git over HTTP.\n" \
"You can generate one at #{profile_personal_access_tokens_url}",
- status: 401
+ status: :unauthorized
end
def repository
diff --git a/app/controllers/projects/group_links_controller.rb b/app/controllers/projects/group_links_controller.rb
index f58ee3e9109..bc5f38f3c2b 100644
--- a/app/controllers/projects/group_links_controller.rb
+++ b/app/controllers/projects/group_links_controller.rb
@@ -24,7 +24,7 @@ class Projects::GroupLinksController < Projects::ApplicationController
def update
@group_link = @project.project_group_links.find(params[:id])
- @group_link.update_attributes(group_link_params)
+ @group_link.update(group_link_params)
end
def destroy
@@ -34,7 +34,7 @@ class Projects::GroupLinksController < Projects::ApplicationController
respond_to do |format|
format.html do
- redirect_to project_project_members_path(project), status: 302
+ redirect_to project_project_members_path(project), status: :found
end
format.js { head :ok }
end
diff --git a/app/controllers/projects/hooks_controller.rb b/app/controllers/projects/hooks_controller.rb
index 6800d742b0a..2da2aad9b33 100644
--- a/app/controllers/projects/hooks_controller.rb
+++ b/app/controllers/projects/hooks_controller.rb
@@ -29,7 +29,7 @@ class Projects::HooksController < Projects::ApplicationController
end
def update
- if hook.update_attributes(hook_params)
+ if hook.update(hook_params)
flash[:notice] = 'Hook was successfully updated.'
redirect_to project_settings_integrations_path(@project)
else
@@ -48,7 +48,7 @@ class Projects::HooksController < Projects::ApplicationController
def destroy
hook.destroy
- redirect_to project_settings_integrations_path(@project), status: 302
+ redirect_to project_settings_integrations_path(@project), status: :found
end
private
diff --git a/app/controllers/projects/labels_controller.rb b/app/controllers/projects/labels_controller.rb
index 91016f6494e..21d3c918581 100644
--- a/app/controllers/projects/labels_controller.rb
+++ b/app/controllers/projects/labels_controller.rb
@@ -39,7 +39,7 @@ class Projects::LabelsController < Projects::ApplicationController
else
respond_to do |format|
format.html { render :new }
- format.json { render json: { message: @label.errors.messages }, status: 400 }
+ format.json { render json: { message: @label.errors.messages }, status: :bad_request }
end
end
end
@@ -115,7 +115,7 @@ class Projects::LabelsController < Projects::ApplicationController
flash[:notice] = "#{@label.title} promoted to <a href=\"#{group_labels_path(@project.group)}\">group label</a>.".html_safe
respond_to do |format|
format.html do
- redirect_to(project_labels_path(@project), status: 303)
+ redirect_to(project_labels_path(@project), status: :see_other)
end
format.json do
render json: { url: project_labels_path(@project) }
diff --git a/app/controllers/projects/lfs_api_controller.rb b/app/controllers/projects/lfs_api_controller.rb
index 3f4962b543d..c64ccc3d473 100644
--- a/app/controllers/projects/lfs_api_controller.rb
+++ b/app/controllers/projects/lfs_api_controller.rb
@@ -25,7 +25,7 @@ class Projects::LfsApiController < Projects::GitHttpClientController
message: 'Server supports batch API only, please update your Git LFS client to version 1.0.1 and up.',
documentation_url: "#{Gitlab.config.gitlab.url}/help"
},
- status: 501
+ status: :not_implemented
)
end
diff --git a/app/controllers/projects/lfs_storage_controller.rb b/app/controllers/projects/lfs_storage_controller.rb
index 45c98d60822..dd7e673ec75 100644
--- a/app/controllers/projects/lfs_storage_controller.rb
+++ b/app/controllers/projects/lfs_storage_controller.rb
@@ -28,7 +28,7 @@ class Projects::LfsStorageController < Projects::GitHttpClientController
if store_file!(oid, size)
head 200
else
- render plain: 'Unprocessable entity', status: 422
+ render plain: 'Unprocessable entity', status: :unprocessable_entity
end
rescue ActiveRecord::RecordInvalid
render_lfs_forbidden
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index a7c5f858c42..1ad2e93c85f 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -227,7 +227,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
def rebase
RebaseWorker.perform_async(@merge_request.id, current_user.id)
- render nothing: true, status: 200
+ render nothing: true, status: :ok
end
protected
diff --git a/app/controllers/projects/milestones_controller.rb b/app/controllers/projects/milestones_controller.rb
index 594563d1f6f..5e86ec93f34 100644
--- a/app/controllers/projects/milestones_controller.rb
+++ b/app/controllers/projects/milestones_controller.rb
@@ -96,7 +96,7 @@ class Projects::MilestonesController < Projects::ApplicationController
Milestones::DestroyService.new(project, current_user).execute(milestone)
respond_to do |format|
- format.html { redirect_to namespace_project_milestones_path, status: 303 }
+ format.html { redirect_to namespace_project_milestones_path, status: :see_other }
format.js { head :ok }
end
end
diff --git a/app/controllers/projects/mirrors_controller.rb b/app/controllers/projects/mirrors_controller.rb
index 5698ff4e706..3b24d231f3d 100644
--- a/app/controllers/projects/mirrors_controller.rb
+++ b/app/controllers/projects/mirrors_controller.rb
@@ -13,7 +13,7 @@ class Projects::MirrorsController < Projects::ApplicationController
end
def update
- if project.update_attributes(mirror_params)
+ if project.update(mirror_params)
flash[:notice] = 'Mirroring settings were successfully updated.'
else
flash[:alert] = project.errors.full_messages.join(', ').html_safe
diff --git a/app/controllers/projects/pipeline_schedules_controller.rb b/app/controllers/projects/pipeline_schedules_controller.rb
index fa258f3d9af..aeda7b3edf5 100644
--- a/app/controllers/projects/pipeline_schedules_controller.rb
+++ b/app/controllers/projects/pipeline_schedules_controller.rb
@@ -64,7 +64,7 @@ class Projects::PipelineSchedulesController < Projects::ApplicationController
def destroy
if schedule.destroy
- redirect_to pipeline_schedules_path(@project), status: 302
+ redirect_to pipeline_schedules_path(@project), status: :found
else
redirect_to pipeline_schedules_path(@project),
status: :forbidden,
diff --git a/app/controllers/projects/releases_controller.rb b/app/controllers/projects/releases_controller.rb
index 3e0a530fdb9..19e09b3af6f 100644
--- a/app/controllers/projects/releases_controller.rb
+++ b/app/controllers/projects/releases_controller.rb
@@ -14,7 +14,7 @@ class Projects::ReleasesController < Projects::ApplicationController
# it exists only to save a description to each Tag.
# If description is empty we should destroy the existing record.
if release_params[:description].present?
- release.update_attributes(release_params)
+ release.update(release_params)
else
release.destroy
end
diff --git a/app/controllers/projects/repositories_controller.rb b/app/controllers/projects/repositories_controller.rb
index d01f324e6fd..ecb2ece7532 100644
--- a/app/controllers/projects/repositories_controller.rb
+++ b/app/controllers/projects/repositories_controller.rb
@@ -24,7 +24,7 @@ class Projects::RepositoriesController < Projects::ApplicationController
send_git_archive @repository, ref: @ref, format: params[:format], append_sha: append_sha
rescue => ex
logger.error("#{self.class.name}: #{ex}")
- return git_not_found!
+ git_not_found!
end
def assign_archive_vars
diff --git a/app/controllers/projects/runner_projects_controller.rb b/app/controllers/projects/runner_projects_controller.rb
index a080724634b..c098c82081e 100644
--- a/app/controllers/projects/runner_projects_controller.rb
+++ b/app/controllers/projects/runner_projects_controller.rb
@@ -21,6 +21,6 @@ class Projects::RunnerProjectsController < Projects::ApplicationController
runner_project = project.runner_projects.find(params[:id])
runner_project.destroy
- redirect_to project_runners_path(project), status: 302
+ redirect_to project_runners_path(project), status: :found
end
end
diff --git a/app/controllers/projects/runners_controller.rb b/app/controllers/projects/runners_controller.rb
index bef94cea989..cc7cce887bf 100644
--- a/app/controllers/projects/runners_controller.rb
+++ b/app/controllers/projects/runners_controller.rb
@@ -24,7 +24,7 @@ class Projects::RunnersController < Projects::ApplicationController
@runner.destroy
end
- redirect_to project_runners_path(@project), status: 302
+ redirect_to project_runners_path(@project), status: :found
end
def resume
diff --git a/app/controllers/projects/services_controller.rb b/app/controllers/projects/services_controller.rb
index 690596b12db..d55046047ae 100644
--- a/app/controllers/projects/services_controller.rb
+++ b/app/controllers/projects/services_controller.rb
@@ -34,7 +34,7 @@ class Projects::ServicesController < Projects::ApplicationController
private
def service_test_response
- if @service.update_attributes(service_params[:service])
+ if @service.update(service_params[:service])
data = @service.test_data(project, current_user)
outcome = @service.test(data)
diff --git a/app/controllers/projects/snippets_controller.rb b/app/controllers/projects/snippets_controller.rb
index 208a1d19862..f742d7edf83 100644
--- a/app/controllers/projects/snippets_controller.rb
+++ b/app/controllers/projects/snippets_controller.rb
@@ -82,7 +82,7 @@ class Projects::SnippetsController < Projects::ApplicationController
@snippet.destroy
- redirect_to project_snippets_path(@project), status: 302
+ redirect_to project_snippets_path(@project), status: :found
end
protected
diff --git a/app/controllers/projects/tags_controller.rb b/app/controllers/projects/tags_controller.rb
index b62d7d9b7c5..b17753222a0 100644
--- a/app/controllers/projects/tags_controller.rb
+++ b/app/controllers/projects/tags_controller.rb
@@ -50,7 +50,7 @@ class Projects::TagsController < Projects::ApplicationController
respond_to do |format|
if result[:status] == :success
format.html do
- redirect_to project_tags_path(@project), status: 303
+ redirect_to project_tags_path(@project), status: :see_other
end
format.js
diff --git a/app/controllers/projects/templates_controller.rb b/app/controllers/projects/templates_controller.rb
index 694b468c8d3..52d6fb82093 100644
--- a/app/controllers/projects/templates_controller.rb
+++ b/app/controllers/projects/templates_controller.rb
@@ -14,6 +14,6 @@ class Projects::TemplatesController < Projects::ApplicationController
def get_template_class
template_types = { issue: Gitlab::Template::IssueTemplate, merge_request: Gitlab::Template::MergeRequestTemplate }.with_indifferent_access
@template_type = template_types[params[:template_type]]
- render json: [], status: 404 unless @template_type
+ render json: [], status: :not_found unless @template_type
end
end
diff --git a/app/controllers/projects/triggers_controller.rb b/app/controllers/projects/triggers_controller.rb
index e04145dd0b3..6f3de43f85a 100644
--- a/app/controllers/projects/triggers_controller.rb
+++ b/app/controllers/projects/triggers_controller.rb
@@ -50,7 +50,7 @@ class Projects::TriggersController < Projects::ApplicationController
flash[:alert] = "Could not remove the trigger."
end
- redirect_to project_settings_ci_cd_path(@project), status: 302
+ redirect_to project_settings_ci_cd_path(@project), status: :found
end
private
diff --git a/app/controllers/projects/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb
index aa844e94d89..c01066c688a 100644
--- a/app/controllers/projects/wikis_controller.rb
+++ b/app/controllers/projects/wikis_controller.rb
@@ -120,7 +120,7 @@ class Projects::WikisController < Projects::ApplicationController
rescue ProjectWiki::CouldNotCreateWikiError
flash[:notice] = "Could not create Wiki Repository at this time. Please try again later."
redirect_to project_path(@project)
- return false
+ false
end
def wiki_params
@@ -129,7 +129,7 @@ class Projects::WikisController < Projects::ApplicationController
def build_page(args)
WikiPage.new(@project_wiki).tap do |page|
- page.update_attributes(args)
+ page.update_attributes(args) # rubocop:disable Rails/ActiveRecordAliases
end
end
end
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index ec3a5788ba1..9d1c44db137 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -2,6 +2,7 @@ class ProjectsController < Projects::ApplicationController
include IssuableCollections
include ExtractsPath
include PreviewMarkdown
+ include SendFileUpload
before_action :whitelist_query_limiting, only: [:create]
before_action :authenticate_user!, except: [:index, :show, :activity, :refs]
@@ -132,7 +133,7 @@ class ProjectsController < Projects::ApplicationController
::Projects::DestroyService.new(@project, current_user, {}).async_execute
flash[:notice] = _("Project '%{project_name}' is in the process of being deleted.") % { project_name: @project.full_name }
- redirect_to dashboard_projects_path, status: 302
+ redirect_to dashboard_projects_path, status: :found
rescue Projects::DestroyService::DestroyError => ex
redirect_to edit_project_path(@project), status: 302, alert: ex.message
end
@@ -188,9 +189,9 @@ class ProjectsController < Projects::ApplicationController
end
def download_export
- export_project_path = @project.export_project_path
-
- if export_project_path
+ if export_project_object_storage?
+ send_upload(@project.import_export_upload.export_file)
+ elsif export_project_path
send_file export_project_path, disposition: 'attachment'
else
redirect_to(
@@ -265,8 +266,6 @@ class ProjectsController < Projects::ApplicationController
render json: options.to_json
end
- private
-
# Render project landing depending of which features are available
# So if page is not availble in the list it renders the next page
#
@@ -424,4 +423,12 @@ class ProjectsController < Projects::ApplicationController
def whitelist_query_limiting
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42440')
end
+
+ def export_project_path
+ @export_project_path ||= @project.export_project_path
+ end
+
+ def export_project_object_storage?
+ @project.export_project_object_exists?
+ end
end
diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb
index 1de6ae24622..9dd652206fe 100644
--- a/app/controllers/sessions_controller.rb
+++ b/app/controllers/sessions_controller.rb
@@ -32,8 +32,8 @@ class SessionsController < Devise::SessionsController
super do |resource|
# User has successfully signed in, so clear any unused reset token
if resource.reset_password_token.present?
- resource.update_attributes(reset_password_token: nil,
- reset_password_sent_at: nil)
+ resource.update(reset_password_token: nil,
+ reset_password_sent_at: nil)
end
# hide the signed-in notification
diff --git a/app/controllers/sherlock/transactions_controller.rb b/app/controllers/sherlock/transactions_controller.rb
index cb6c3a7cd98..ae4953c3259 100644
--- a/app/controllers/sherlock/transactions_controller.rb
+++ b/app/controllers/sherlock/transactions_controller.rb
@@ -13,7 +13,7 @@ module Sherlock
def destroy_all
Gitlab::Sherlock.collection.clear
- redirect_to :back, status: 302
+ redirect_to :back, status: :found
end
end
end
diff --git a/app/controllers/snippets_controller.rb b/app/controllers/snippets_controller.rb
index 3d51520ddf4..1d6d0943674 100644
--- a/app/controllers/snippets_controller.rb
+++ b/app/controllers/snippets_controller.rb
@@ -89,7 +89,7 @@ class SnippetsController < ApplicationController
@snippet.destroy
- redirect_to snippets_path, status: 302
+ redirect_to snippets_path, status: :found
end
protected
diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb
index 95fea2f18d1..3c5c8bbd71b 100644
--- a/app/helpers/groups_helper.rb
+++ b/app/helpers/groups_helper.rb
@@ -128,8 +128,10 @@ module GroupsHelper
def get_group_sidebar_links
links = [:overview, :group_members]
- if can?(current_user, :read_cross_project)
- links += [:activity, :issues, :boards, :labels, :milestones, :merge_requests]
+ resources = [:activity, :issues, :boards, :labels, :milestones,
+ :merge_requests]
+ links += resources.select do |resource|
+ can?(current_user, "read_group_#{resource}".to_sym, @group)
end
if can?(current_user, :admin_group, @group)
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index 353479776b8..7bbdc798ddd 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -249,6 +249,7 @@ module IssuablesHelper
issuableRef: issuable.to_reference,
markdownPreviewPath: preview_markdown_path(parent),
markdownDocsPath: help_page_path('user/markdown'),
+ markdownVersion: issuable.cached_markdown_version,
issuableTemplates: issuable_templates(issuable),
initialTitleHtml: markdown_field(issuable, :title),
initialTitleText: issuable.title,
diff --git a/app/helpers/markup_helper.rb b/app/helpers/markup_helper.rb
index 39e7a7fd396..cbb971cf8b7 100644
--- a/app/helpers/markup_helper.rb
+++ b/app/helpers/markup_helper.rb
@@ -107,6 +107,7 @@ module MarkupHelper
def markup(file_name, text, context = {})
context[:project] ||= @project
+ context[:markdown_engine] ||= :redcarpet
html = context.delete(:rendered) || markup_unsafe(file_name, text, context)
prepare_for_rendering(html, context)
end
@@ -120,7 +121,8 @@ module MarkupHelper
project: @project,
project_wiki: @project_wiki,
page_slug: wiki_page.slug,
- issuable_state_filter_enabled: true
+ issuable_state_filter_enabled: true,
+ markdown_engine: :redcarpet
}
html =
diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb
index 3fa2e5452c8..5404ead44f3 100644
--- a/app/helpers/notes_helper.rb
+++ b/app/helpers/notes_helper.rb
@@ -169,6 +169,7 @@ module NotesHelper
registerPath: new_session_path(:user, redirect_to_referer: 'yes', anchor: 'register-pane'),
newSessionPath: new_session_path(:user, redirect_to_referer: 'yes'),
markdownDocsPath: help_page_path('user/markdown'),
+ markdownVersion: issuable.cached_markdown_version,
quickActionsDocsPath: help_page_path('user/project/quick_actions'),
closePath: close_issuable_path(issuable),
reopenPath: reopen_issuable_path(issuable),
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index c7a434ea092..b0f381db5ab 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -177,6 +177,7 @@ module ProjectsHelper
controller.action_name,
Gitlab::CurrentSettings.cache_key,
"cross-project:#{can?(current_user, :read_cross_project)}",
+ max_project_member_access_cache_key(project),
'v2.6'
]
diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb
index ce9373f5883..4d17b22a4a1 100644
--- a/app/helpers/users_helper.rb
+++ b/app/helpers/users_helper.rb
@@ -31,6 +31,14 @@ module UsersHelper
current_user_menu_items.include?(item)
end
+ def max_project_member_access(project)
+ current_user&.max_member_access_for_project(project.id) || Gitlab::Access::NO_ACCESS
+ end
+
+ def max_project_member_access_cache_key(project)
+ "access:#{max_project_member_access(project)}"
+ end
+
private
def get_profile_tabs
diff --git a/app/mailers/previews/devise_mailer_preview.rb b/app/mailers/previews/devise_mailer_preview.rb
new file mode 100644
index 00000000000..d6588efc486
--- /dev/null
+++ b/app/mailers/previews/devise_mailer_preview.rb
@@ -0,0 +1,30 @@
+class DeviseMailerPreview < ActionMailer::Preview
+ def confirmation_instructions_for_signup
+ DeviseMailer.confirmation_instructions(unsaved_user, 'faketoken', {})
+ end
+
+ def confirmation_instructions_for_new_email
+ user = User.last
+ user.unconfirmed_email = 'unconfirmed@example.com'
+
+ DeviseMailer.confirmation_instructions(user, 'faketoken', {})
+ end
+
+ def reset_password_instructions
+ DeviseMailer.reset_password_instructions(unsaved_user, 'faketoken', {})
+ end
+
+ def unlock_instructions
+ DeviseMailer.unlock_instructions(unsaved_user, 'faketoken', {})
+ end
+
+ def password_change
+ DeviseMailer.password_change(unsaved_user, {})
+ end
+
+ private
+
+ def unsaved_user
+ User.new(name: 'Jane Doe', email: 'jdoe@example.com')
+ end
+end
diff --git a/app/mailers/previews/email_rejection_mailer_preview.rb b/app/mailers/previews/email_rejection_mailer_preview.rb
new file mode 100644
index 00000000000..639e8471232
--- /dev/null
+++ b/app/mailers/previews/email_rejection_mailer_preview.rb
@@ -0,0 +1,5 @@
+class EmailRejectionMailerPreview < ActionMailer::Preview
+ def rejection
+ EmailRejectionMailer.rejection("some rejection reason", "From: someone@example.com\nraw email here").message
+ end
+end
diff --git a/app/mailers/previews/notify_preview.rb b/app/mailers/previews/notify_preview.rb
new file mode 100644
index 00000000000..3615cde8026
--- /dev/null
+++ b/app/mailers/previews/notify_preview.rb
@@ -0,0 +1,170 @@
+class NotifyPreview < ActionMailer::Preview
+ def note_merge_request_email_for_individual_note
+ note_email(:note_merge_request_email) do
+ note = <<-MD.strip_heredoc
+ This is an individual note on a merge request :smiley:
+
+ In this notification email, we expect to see:
+
+ - The note contents (that's what you're looking at)
+ - A link to view this note on Gitlab
+ - An explanation for why the user is receiving this notification
+ MD
+
+ create_note(noteable_type: 'merge_request', noteable_id: merge_request.id, note: note)
+ end
+ end
+
+ def note_merge_request_email_for_discussion
+ note_email(:note_merge_request_email) do
+ note = <<-MD.strip_heredoc
+ This is a new discussion on a merge request :smiley:
+
+ In this notification email, we expect to see:
+
+ - A line saying who started this discussion
+ - The note contents (that's what you're looking at)
+ - A link to view this discussion on Gitlab
+ - An explanation for why the user is receiving this notification
+ MD
+
+ create_note(noteable_type: 'merge_request', noteable_id: merge_request.id, type: 'DiscussionNote', note: note)
+ end
+ end
+
+ def note_merge_request_email_for_diff_discussion
+ note_email(:note_merge_request_email) do
+ note = <<-MD.strip_heredoc
+ This is a new discussion on a merge request :smiley:
+
+ In this notification email, we expect to see:
+
+ - A line saying who started this discussion and on what file
+ - The diff
+ - The note contents (that's what you're looking at)
+ - A link to view this discussion on Gitlab
+ - An explanation for why the user is receiving this notification
+ MD
+
+ position = Gitlab::Diff::Position.new(
+ old_path: "files/ruby/popen.rb",
+ new_path: "files/ruby/popen.rb",
+ old_line: nil,
+ new_line: 14,
+ diff_refs: merge_request.diff_refs
+ )
+
+ create_note(noteable_type: 'merge_request', noteable_id: merge_request.id, type: 'DiffNote', position: position, note: note)
+ end
+ end
+
+ def closed_issue_email
+ Notify.closed_issue_email(user.id, issue.id, user.id).message
+ end
+
+ def issue_status_changed_email
+ Notify.issue_status_changed_email(user.id, issue.id, 'closed', user.id).message
+ end
+
+ def closed_merge_request_email
+ Notify.closed_merge_request_email(user.id, issue.id, user.id).message
+ end
+
+ def merge_request_status_email
+ Notify.merge_request_status_email(user.id, merge_request.id, 'closed', user.id).message
+ end
+
+ def merged_merge_request_email
+ Notify.merged_merge_request_email(user.id, merge_request.id, user.id).message
+ end
+
+ def member_access_denied_email
+ Notify.member_access_denied_email('project', project.id, user.id).message
+ end
+
+ def member_access_granted_email
+ Notify.member_access_granted_email('project', user.id).message
+ end
+
+ def member_access_requested_email
+ Notify.member_access_requested_email('group', user.id, 'some@example.com').message
+ end
+
+ def member_invite_accepted_email
+ Notify.member_invite_accepted_email('project', user.id).message
+ end
+
+ def member_invite_declined_email
+ Notify.member_invite_declined_email(
+ 'project',
+ project.id,
+ 'invite@example.com',
+ user.id
+ ).message
+ end
+
+ def member_invited_email
+ Notify.member_invited_email('project', user.id, '1234').message
+ end
+
+ def pages_domain_enabled_email
+ cleanup do
+ pages_domain = PagesDomain.new(domain: 'my.example.com', project: project, verified_at: Time.now, enabled_until: 1.week.from_now)
+
+ Notify.pages_domain_enabled_email(pages_domain, user).message
+ end
+ end
+
+ def pipeline_success_email
+ Notify.pipeline_success_email(pipeline, pipeline.user.try(:email))
+ end
+
+ def pipeline_failed_email
+ Notify.pipeline_failed_email(pipeline, pipeline.user.try(:email))
+ end
+
+ private
+
+ def project
+ @project ||= Project.find_by_full_path('gitlab-org/gitlab-test')
+ end
+
+ def issue
+ @merge_request ||= project.issues.first
+ end
+
+ def merge_request
+ @merge_request ||= project.merge_requests.first
+ end
+
+ def pipeline
+ @pipeline = Ci::Pipeline.last
+ end
+
+ def user
+ @user ||= User.last
+ end
+
+ def create_note(params)
+ Notes::CreateService.new(project, user, params).execute
+ end
+
+ def note_email(method)
+ cleanup do
+ note = yield
+
+ Notify.public_send(method, user.id, note) # rubocop:disable GitlabSecurity/PublicSend
+ end
+ end
+
+ def cleanup
+ email = nil
+
+ ActiveRecord::Base.transaction do
+ email = yield
+ raise ActiveRecord::Rollback
+ end
+
+ email
+ end
+end
diff --git a/app/mailers/previews/repository_check_mailer_preview.rb b/app/mailers/previews/repository_check_mailer_preview.rb
new file mode 100644
index 00000000000..19d4eab1805
--- /dev/null
+++ b/app/mailers/previews/repository_check_mailer_preview.rb
@@ -0,0 +1,5 @@
+class RepositoryCheckMailerPreview < ActionMailer::Preview
+ def notify
+ RepositoryCheckMailer.notify(3).message
+ end
+end
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index bf93a2caf72..44103e3bc4f 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -371,7 +371,7 @@ module Ci
def update_coverage
coverage = trace.extract_coverage(coverage_regex)
- update_attributes(coverage: coverage) if coverage.present?
+ update(coverage: coverage) if coverage.present?
end
def parse_trace_sections!
@@ -386,6 +386,10 @@ module Ci
trace.exist?
end
+ def has_old_trace?
+ old_trace.present?
+ end
+
def trace=(data)
raise NotImplementedError
end
@@ -395,6 +399,8 @@ module Ci
end
def erase_old_trace!
+ return unless has_old_trace?
+
update_column(:trace, nil)
end
diff --git a/app/models/ci/build_trace_chunk.rb b/app/models/ci/build_trace_chunk.rb
index 4856f10846c..b442de34061 100644
--- a/app/models/ci/build_trace_chunk.rb
+++ b/app/models/ci/build_trace_chunk.rb
@@ -1,54 +1,58 @@
module Ci
class BuildTraceChunk < ActiveRecord::Base
include FastDestroyAll
+ include ::Gitlab::ExclusiveLeaseHelpers
extend Gitlab::Ci::Model
belongs_to :build, class_name: "Ci::Build", foreign_key: :build_id
default_value_for :data_store, :redis
- WriteError = Class.new(StandardError)
-
CHUNK_SIZE = 128.kilobytes
- CHUNK_REDIS_TTL = 1.week
WRITE_LOCK_RETRY = 10
WRITE_LOCK_SLEEP = 0.01.seconds
WRITE_LOCK_TTL = 1.minute
+ # Note: The ordering of this enum is related to the precedence of persist store.
+ # The bottom item takes the higest precedence, and the top item takes the lowest precedence.
enum data_store: {
redis: 1,
- db: 2
+ database: 2,
+ fog: 3
}
class << self
- def redis_data_key(build_id, chunk_index)
- "gitlab:ci:trace:#{build_id}:chunks:#{chunk_index}"
+ def all_stores
+ @all_stores ||= self.data_stores.keys
end
- def redis_data_keys
- redis.pluck(:build_id, :chunk_index).map do |data|
- redis_data_key(data.first, data.second)
- end
+ def persistable_store
+ # get first available store from the back of the list
+ all_stores.reverse.find { |store| get_store_class(store).available? }
end
- def redis_delete_data(keys)
- return if keys.empty?
-
- Gitlab::Redis::SharedState.with do |redis|
- redis.del(keys)
- end
+ def get_store_class(store)
+ @stores ||= {}
+ @stores[store] ||= "Ci::BuildTraceChunks::#{store.capitalize}".constantize.new
end
##
# FastDestroyAll concerns
def begin_fast_destroy
- redis_data_keys
+ all_stores.each_with_object({}) do |store, result|
+ relation = public_send(store) # rubocop:disable GitlabSecurity/PublicSend
+ keys = get_store_class(store).keys(relation)
+
+ result[store] = keys if keys.present?
+ end
end
##
# FastDestroyAll concerns
def finalize_fast_destroy(keys)
- redis_delete_data(keys)
+ keys.each do |store, value|
+ get_store_class(store).delete_keys(value)
+ end
end
end
@@ -66,10 +70,15 @@ module Ci
end
def append(new_data, offset)
+ raise ArgumentError, 'New data is missing' unless new_data
raise ArgumentError, 'Offset is out of range' if offset > size || offset < 0
raise ArgumentError, 'Chunk size overflow' if CHUNK_SIZE < (offset + new_data.bytesize)
- set_data(data.byteslice(0, offset) + new_data)
+ in_lock(*lock_params) do # Write opetation is atomic
+ unsafe_set_data!(data.byteslice(0, offset) + new_data)
+ end
+
+ schedule_to_persist if full?
end
def size
@@ -88,93 +97,63 @@ module Ci
(start_offset...end_offset)
end
- def use_database!
- in_lock do
- break if db?
- break unless size > 0
-
- self.update!(raw_data: data, data_store: :db)
- self.class.redis_delete_data([redis_data_key])
+ def persist_data!
+ in_lock(*lock_params) do # Write opetation is atomic
+ unsafe_persist_to!(self.class.persistable_store)
end
end
private
- def get_data
- if redis?
- redis_data
- elsif db?
- raw_data
- else
- raise 'Unsupported data store'
- end&.force_encoding(Encoding::BINARY) # Redis/Database return UTF-8 string as default
- end
-
- def set_data(value)
- raise ArgumentError, 'too much data' if value.bytesize > CHUNK_SIZE
-
- in_lock do
- if redis?
- redis_set_data(value)
- elsif db?
- self.raw_data = value
- else
- raise 'Unsupported data store'
- end
+ def unsafe_persist_to!(new_store)
+ return if data_store == new_store.to_s
+ raise ArgumentError, 'Can not persist empty data' unless size > 0
- @data = value
+ old_store_class = self.class.get_store_class(data_store)
- save! if changed?
+ get_data.tap do |the_data|
+ self.raw_data = nil
+ self.data_store = new_store
+ unsafe_set_data!(the_data)
end
- schedule_to_db if full?
- end
-
- def schedule_to_db
- return if db?
-
- Ci::BuildTraceChunkFlushWorker.perform_async(id)
+ old_store_class.delete_data(self)
end
- def full?
- size == CHUNK_SIZE
+ def get_data
+ self.class.get_store_class(data_store).data(self)&.force_encoding(Encoding::BINARY) # Redis/Database return UTF-8 string as default
+ rescue Excon::Error::NotFound
+ # If the data store is :fog and the file does not exist in the object storage, this method returns nil.
end
- def redis_data
- Gitlab::Redis::SharedState.with do |redis|
- redis.get(redis_data_key)
- end
- end
+ def unsafe_set_data!(value)
+ raise ArgumentError, 'New data size exceeds chunk size' if value.bytesize > CHUNK_SIZE
- def redis_set_data(data)
- Gitlab::Redis::SharedState.with do |redis|
- redis.set(redis_data_key, data, ex: CHUNK_REDIS_TTL)
- end
- end
+ self.class.get_store_class(data_store).set_data(self, value)
+ @data = value
- def redis_data_key
- self.class.redis_data_key(build_id, chunk_index)
+ save! if changed?
end
- def in_lock
- write_lock_key = "trace_write:#{build_id}:chunks:#{chunk_index}"
+ def schedule_to_persist
+ return if data_persisted?
- lease = Gitlab::ExclusiveLease.new(write_lock_key, timeout: WRITE_LOCK_TTL)
- retry_count = 0
+ Ci::BuildTraceChunkFlushWorker.perform_async(id)
+ end
- until uuid = lease.try_obtain
- # Keep trying until we obtain the lease. To prevent hammering Redis too
- # much we'll wait for a bit between retries.
- sleep(WRITE_LOCK_SLEEP)
- break if WRITE_LOCK_RETRY < (retry_count += 1)
- end
+ def data_persisted?
+ !redis?
+ end
- raise WriteError, 'Failed to obtain write lock' unless uuid
+ def full?
+ size == CHUNK_SIZE
+ end
- self.reload if self.persisted?
- return yield
- ensure
- Gitlab::ExclusiveLease.cancel(write_lock_key, uuid)
+ def lock_params
+ ["trace_write:#{build_id}:chunks:#{chunk_index}",
+ { ttl: WRITE_LOCK_TTL,
+ retries: WRITE_LOCK_RETRY,
+ sleep_sec: WRITE_LOCK_SLEEP }]
end
end
end
diff --git a/app/models/ci/build_trace_chunks/database.rb b/app/models/ci/build_trace_chunks/database.rb
new file mode 100644
index 00000000000..3666d77c790
--- /dev/null
+++ b/app/models/ci/build_trace_chunks/database.rb
@@ -0,0 +1,29 @@
+module Ci
+ module BuildTraceChunks
+ class Database
+ def available?
+ true
+ end
+
+ def keys(relation)
+ []
+ end
+
+ def delete_keys(keys)
+ # no-op
+ end
+
+ def data(model)
+ model.raw_data
+ end
+
+ def set_data(model, data)
+ model.raw_data = data
+ end
+
+ def delete_data(model)
+ model.update_columns(raw_data: nil) unless model.raw_data.nil?
+ end
+ end
+ end
+end
diff --git a/app/models/ci/build_trace_chunks/fog.rb b/app/models/ci/build_trace_chunks/fog.rb
new file mode 100644
index 00000000000..7506c40a39d
--- /dev/null
+++ b/app/models/ci/build_trace_chunks/fog.rb
@@ -0,0 +1,59 @@
+module Ci
+ module BuildTraceChunks
+ class Fog
+ def available?
+ object_store.enabled
+ end
+
+ def data(model)
+ connection.get_object(bucket_name, key(model))[:body]
+ end
+
+ def set_data(model, data)
+ connection.put_object(bucket_name, key(model), data)
+ end
+
+ def delete_data(model)
+ delete_keys([[model.build_id, model.chunk_index]])
+ end
+
+ def keys(relation)
+ return [] unless available?
+
+ relation.pluck(:build_id, :chunk_index)
+ end
+
+ def delete_keys(keys)
+ keys.each do |key|
+ connection.delete_object(bucket_name, key_raw(*key))
+ end
+ end
+
+ private
+
+ def key(model)
+ key_raw(model.build_id, model.chunk_index)
+ end
+
+ def key_raw(build_id, chunk_index)
+ "tmp/builds/#{build_id.to_i}/chunks/#{chunk_index.to_i}.log"
+ end
+
+ def bucket_name
+ return unless available?
+
+ object_store.remote_directory
+ end
+
+ def connection
+ return unless available?
+
+ @connection ||= ::Fog::Storage.new(object_store.connection.to_hash.deep_symbolize_keys)
+ end
+
+ def object_store
+ Gitlab.config.artifacts.object_store
+ end
+ end
+ end
+end
diff --git a/app/models/ci/build_trace_chunks/redis.rb b/app/models/ci/build_trace_chunks/redis.rb
new file mode 100644
index 00000000000..fdb6065e2a0
--- /dev/null
+++ b/app/models/ci/build_trace_chunks/redis.rb
@@ -0,0 +1,51 @@
+module Ci
+ module BuildTraceChunks
+ class Redis
+ CHUNK_REDIS_TTL = 1.week
+
+ def available?
+ true
+ end
+
+ def data(model)
+ Gitlab::Redis::SharedState.with do |redis|
+ redis.get(key(model))
+ end
+ end
+
+ def set_data(model, data)
+ Gitlab::Redis::SharedState.with do |redis|
+ redis.set(key(model), data, ex: CHUNK_REDIS_TTL)
+ end
+ end
+
+ def delete_data(model)
+ delete_keys([[model.build_id, model.chunk_index]])
+ end
+
+ def keys(relation)
+ relation.pluck(:build_id, :chunk_index)
+ end
+
+ def delete_keys(keys)
+ return if keys.empty?
+
+ keys = keys.map { |key| key_raw(*key) }
+
+ Gitlab::Redis::SharedState.with do |redis|
+ redis.del(keys)
+ end
+ end
+
+ private
+
+ def key(model)
+ key_raw(model.build_id, model.chunk_index)
+ end
+
+ def key_raw(build_id, chunk_index)
+ "gitlab:ci:trace:#{build_id.to_i}:chunks:#{chunk_index.to_i}"
+ end
+ end
+ end
+end
diff --git a/app/models/concerns/cache_markdown_field.rb b/app/models/concerns/cache_markdown_field.rb
index 9f6358cecbe..b05bf909058 100644
--- a/app/models/concerns/cache_markdown_field.rb
+++ b/app/models/concerns/cache_markdown_field.rb
@@ -40,6 +40,18 @@ module CacheMarkdownField
end
end
+ class MarkdownEngine
+ def self.from_version(version = nil)
+ return :common_mark if version.nil? || version == 0
+
+ if version < CacheMarkdownField::CACHE_COMMONMARK_VERSION_START
+ :redcarpet
+ else
+ :common_mark
+ end
+ end
+ end
+
def skip_project_check?
false
end
@@ -57,7 +69,7 @@ module CacheMarkdownField
# Banzai is less strict about authors, so don't always have an author key
context[:author] = self.author if self.respond_to?(:author)
- context[:markdown_engine] = markdown_engine
+ context[:markdown_engine] = MarkdownEngine.from_version(latest_cached_markdown_version)
context
end
@@ -123,14 +135,6 @@ module CacheMarkdownField
end
end
- def markdown_engine
- if latest_cached_markdown_version < CacheMarkdownField::CACHE_COMMONMARK_VERSION_START
- :redcarpet
- else
- :common_mark
- end
- end
-
included do
cattr_reader :cached_markdown_fields do
FieldData.new
diff --git a/app/models/concerns/cacheable_attributes.rb b/app/models/concerns/cacheable_attributes.rb
index d58d7165969..606549b947f 100644
--- a/app/models/concerns/cacheable_attributes.rb
+++ b/app/models/concerns/cacheable_attributes.rb
@@ -7,7 +7,7 @@ module CacheableAttributes
class_methods do
def cache_key
- "#{name}:#{Gitlab::VERSION}:#{Gitlab.migrations_hash}:#{Rails.version}".freeze
+ "#{name}:#{Gitlab::VERSION}:#{Rails.version}".freeze
end
# Can be overriden
@@ -69,6 +69,6 @@ module CacheableAttributes
end
def cache!
- Rails.cache.write(self.class.cache_key, self)
+ Rails.cache.write(self.class.cache_key, self, expires_in: 1.minute)
end
end
diff --git a/app/models/concerns/group_descendant.rb b/app/models/concerns/group_descendant.rb
index 261ace57a17..5e9a95c3282 100644
--- a/app/models/concerns/group_descendant.rb
+++ b/app/models/concerns/group_descendant.rb
@@ -44,8 +44,8 @@ module GroupDescendant
This error is not user facing, but causes a +1 query.
MSG
extras = {
- parent: parent,
- child: child,
+ parent: parent.inspect,
+ child: child.inspect,
preloaded: preloaded.map(&:full_path)
}
issue_url = 'https://gitlab.com/gitlab-org/gitlab-ce/issues/40785'
diff --git a/app/models/concerns/protected_ref.rb b/app/models/concerns/protected_ref.rb
index 94eef4ff7cd..dbe8d31de37 100644
--- a/app/models/concerns/protected_ref.rb
+++ b/app/models/concerns/protected_ref.rb
@@ -23,7 +23,7 @@ module ProtectedRef
# If we don't `protected_branch` or `protected_tag` would be empty and
# `project` cannot be delegated to it, which in turn would cause validations
# to fail.
- has_many :"#{type}_access_levels", inverse_of: self.model_name.singular # rubocop:disable Cop/ActiveRecordDependent
+ has_many :"#{type}_access_levels", inverse_of: self.model_name.singular
validates :"#{type}_access_levels", length: { is: 1, message: "are restricted to a single instance per #{self.model_name.human}." }
diff --git a/app/models/import_export_upload.rb b/app/models/import_export_upload.rb
new file mode 100644
index 00000000000..60d53d6c2c8
--- /dev/null
+++ b/app/models/import_export_upload.rb
@@ -0,0 +1,13 @@
+class ImportExportUpload < ActiveRecord::Base
+ include WithUploads
+ include ObjectStorage::BackgroundMove
+
+ belongs_to :project
+
+ mount_uploader :import_file, ImportExportUploader
+ mount_uploader :export_file, ImportExportUploader
+
+ def retrieve_upload(_identifier, paths)
+ Upload.find_by(model: self, path: paths)
+ end
+end
diff --git a/app/models/milestone.rb b/app/models/milestone.rb
index d05dcfd083a..14cc12b38a5 100644
--- a/app/models/milestone.rb
+++ b/app/models/milestone.rb
@@ -131,9 +131,10 @@ class Milestone < ActiveRecord::Base
rel.order(:project_id, :due_date).select('DISTINCT ON (project_id) id')
else
rel
- .group(:project_id)
+ .group(:project_id, :due_date, :id)
.having('due_date = MIN(due_date)')
.pluck(:id, :project_id, :due_date)
+ .uniq(&:second)
.map(&:first)
end
end
diff --git a/app/models/network/commit.rb b/app/models/network/commit.rb
index 22d48c9e661..d667948deae 100644
--- a/app/models/network/commit.rb
+++ b/app/models/network/commit.rb
@@ -11,8 +11,8 @@ module Network
@parent_spaces = []
end
- def method_missing(m, *args, &block)
- @commit.__send__(m, *args, &block) # rubocop:disable GitlabSecurity/PublicSend
+ def method_missing(msg, *args, &block)
+ @commit.__send__(msg, *args, &block) # rubocop:disable GitlabSecurity/PublicSend
end
def space
diff --git a/app/models/project.rb b/app/models/project.rb
index 8f40470de82..770262f6193 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -171,6 +171,7 @@ class Project < ActiveRecord::Base
has_one :fork_network, through: :fork_network_member
has_one :import_state, autosave: true, class_name: 'ProjectImportState', inverse_of: :project
+ has_one :import_export_upload, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
# Merge Requests for target project should be removed with it
has_many :merge_requests, foreign_key: 'target_project_id'
@@ -1712,7 +1713,7 @@ class Project < ActiveRecord::Base
:started
elsif after_export_in_progress?
:after_export_action
- elsif export_project_path
+ elsif export_project_path || export_project_object_exists?
:finished
else
:none
@@ -1727,16 +1728,21 @@ class Project < ActiveRecord::Base
import_export_shared.after_export_in_progress?
end
- def remove_exports
- return nil unless export_path.present?
-
- FileUtils.rm_rf(export_path)
+ def remove_exports(path = export_path)
+ if path.present?
+ FileUtils.rm_rf(path)
+ elsif export_project_object_exists?
+ import_export_upload.remove_export_file!
+ import_export_upload.save
+ end
end
def remove_exported_project_file
- return unless export_project_path.present?
+ remove_exports(export_project_path)
+ end
- FileUtils.rm_f(export_project_path)
+ def export_project_object_exists?
+ Gitlab::ImportExport.object_storage? && import_export_upload&.export_file&.file
end
def full_path_slug
diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb
index a6f94b3e3b0..fc868c3ebb7 100644
--- a/app/models/project_wiki.rb
+++ b/app/models/project_wiki.rb
@@ -107,7 +107,7 @@ class ProjectWiki
update_project_activity
rescue Gitlab::Git::Wiki::DuplicatePageError => e
@error_message = "Duplicate page: #{e.message}"
- return false
+ false
end
def update_page(page, content:, title: nil, format: :markdown, message: nil)
diff --git a/app/models/remote_mirror.rb b/app/models/remote_mirror.rb
index c4b5dd2dc96..976b501e297 100644
--- a/app/models/remote_mirror.rb
+++ b/app/models/remote_mirror.rb
@@ -57,7 +57,7 @@ class RemoteMirror < ActiveRecord::Base
Gitlab::Metrics.add_event(:remote_mirrors_finished, path: remote_mirror.project.full_path)
timestamp = Time.now
- remote_mirror.update_attributes!(
+ remote_mirror.update!(
last_update_at: timestamp, last_successful_update_at: timestamp, last_error: nil
)
end
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 5f9894f1168..5ed2a7b4068 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -83,7 +83,7 @@ class Repository
@raw_repository&.cleanup
end
- # Return absolute path to repository
+ # Don't use this! It's going away. Use Gitaly to read or write from repos.
def path_to_repo
@path_to_repo ||=
begin
@@ -250,7 +250,7 @@ class Repository
# This will still fail if the file is corrupted (e.g. 0 bytes)
raw_repository.write_ref(keep_around_ref_name(sha), sha, shell: false)
rescue Gitlab::Git::CommandError => ex
- Rails.logger.error "Unable to create keep-around reference for repository #{path}: #{ex}"
+ Rails.logger.error "Unable to create keep-around reference for repository #{disk_path}: #{ex}"
end
def kept_around?(sha)
@@ -462,12 +462,12 @@ class Repository
expire_branches_cache
end
- def method_missing(m, *args, &block)
- if m == :lookup && !block_given?
- lookup_cache[m] ||= {}
- lookup_cache[m][args.join(":")] ||= raw_repository.__send__(m, *args, &block) # rubocop:disable GitlabSecurity/PublicSend
+ def method_missing(msg, *args, &block)
+ if msg == :lookup && !block_given?
+ lookup_cache[msg] ||= {}
+ lookup_cache[msg][args.join(":")] ||= raw_repository.__send__(msg, *args, &block) # rubocop:disable GitlabSecurity/PublicSend
else
- raw_repository.__send__(m, *args, &block) # rubocop:disable GitlabSecurity/PublicSend
+ raw_repository.__send__(msg, *args, &block) # rubocop:disable GitlabSecurity/PublicSend
end
end
@@ -564,7 +564,7 @@ class Repository
end
def rendered_readme
- MarkupHelper.markup_unsafe(readme.name, readme.data, project: project) if readme
+ MarkupHelper.markup_unsafe(readme.name, readme.data, project: project, markdown_engine: :redcarpet) if readme
end
cache_method :rendered_readme
diff --git a/app/models/user.rb b/app/models/user.rb
index 27a5d0278b7..1c5d39db118 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -496,7 +496,7 @@ class User < ActiveRecord::Base
def disable_two_factor!
transaction do
- update_attributes(
+ update(
otp_required_for_login: false,
encrypted_otp_secret: nil,
encrypted_otp_secret_iv: nil,
@@ -1053,7 +1053,7 @@ class User < ActiveRecord::Base
return @global_notification_setting if defined?(@global_notification_setting)
@global_notification_setting = notification_settings.find_or_initialize_by(source: nil)
- @global_notification_setting.update_attributes(level: NotificationSetting.levels[DEFAULT_NOTIFICATION_LEVEL]) unless @global_notification_setting.persisted?
+ @global_notification_setting.update(level: NotificationSetting.levels[DEFAULT_NOTIFICATION_LEVEL]) unless @global_notification_setting.persisted?
@global_notification_setting
end
@@ -1333,8 +1333,8 @@ class User < ActiveRecord::Base
end
end
- def self.unique_internal(scope, username, email_pattern, &b)
- scope.first || create_unique_internal(scope, username, email_pattern, &b)
+ def self.unique_internal(scope, username, email_pattern, &block)
+ scope.first || create_unique_internal(scope, username, email_pattern, &block)
end
def self.create_unique_internal(scope, username, email_pattern, &creation_block)
diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb
index cde79b95062..4b49edb01a5 100644
--- a/app/models/wiki_page.rb
+++ b/app/models/wiki_page.rb
@@ -1,3 +1,4 @@
+# rubocop:disable Rails/ActiveRecordAliases
class WikiPage
PageChangedError = Class.new(StandardError)
PageRenameError = Class.new(StandardError)
diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb
index 520710b757d..ded9fe30eff 100644
--- a/app/policies/group_policy.rb
+++ b/app/policies/group_policy.rb
@@ -72,6 +72,19 @@ class GroupPolicy < BasePolicy
enable :change_visibility_level
end
+ rule { can?(:read_nested_project_resources) }.policy do
+ enable :read_group_activity
+ enable :read_group_issues
+ enable :read_group_boards
+ enable :read_group_labels
+ enable :read_group_milestones
+ enable :read_group_merge_requests
+ end
+
+ rule { can?(:read_cross_project) & can?(:read_group) }.policy do
+ enable :read_nested_project_resources
+ end
+
rule { owner & nested_groups_supported }.enable :create_subgroup
rule { public_group | logged_in_viewable }.enable :view_globally
diff --git a/app/serializers/note_entity.rb b/app/serializers/note_entity.rb
index ce0c31b5806..0e1f94a9f61 100644
--- a/app/serializers/note_entity.rb
+++ b/app/serializers/note_entity.rb
@@ -62,6 +62,8 @@ class NoteEntity < API::Entities::Note
expose :attachment, using: NoteAttachmentEntity, if: -> (note, _) { note.attachment? }
+ expose :cached_markdown_version
+
private
def current_user
diff --git a/app/services/badges/update_service.rb b/app/services/badges/update_service.rb
index 7ca84b5df31..495a4a2c99d 100644
--- a/app/services/badges/update_service.rb
+++ b/app/services/badges/update_service.rb
@@ -3,7 +3,7 @@ module Badges
# returns the updated badge
def execute(badge)
if params.present?
- badge.update_attributes(params)
+ badge.update(params)
end
badge
diff --git a/app/services/commits/change_service.rb b/app/services/commits/change_service.rb
index b9d0173a2d0..1ce6ab36cbf 100644
--- a/app/services/commits/change_service.rb
+++ b/app/services/commits/change_service.rb
@@ -13,8 +13,6 @@ module Commits
# rubocop:disable GitlabSecurity/PublicSend
message = @commit.public_send(:"#{action}_message", current_user)
-
- # rubocop:disable GitlabSecurity/PublicSend
repository.public_send(
action,
current_user,
diff --git a/app/services/import_export_clean_up_service.rb b/app/services/import_export_clean_up_service.rb
index 74088b970c9..3702c3742ef 100644
--- a/app/services/import_export_clean_up_service.rb
+++ b/app/services/import_export_clean_up_service.rb
@@ -10,7 +10,9 @@ class ImportExportCleanUpService
def execute
Gitlab::Metrics.measure(:import_export_clean_up) do
- next unless File.directory?(path)
+ clean_up_export_object_files
+
+ break unless File.directory?(path)
clean_up_export_files
end
@@ -21,4 +23,11 @@ class ImportExportCleanUpService
def clean_up_export_files
Gitlab::Popen.popen(%W(find #{path} -not -path #{path} -mmin +#{mmin} -delete))
end
+
+ def clean_up_export_object_files
+ ImportExportUpload.where('updated_at < ?', mmin.minutes.ago).each do |upload|
+ upload.remove_export_file!
+ upload.save!
+ end
+ end
end
diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb
index 683f64e82ad..5e06e0c61cf 100644
--- a/app/services/issuable_base_service.rb
+++ b/app/services/issuable_base_service.rb
@@ -130,7 +130,7 @@ class IssuableBaseService < BaseService
def create_issuable(issuable, attributes, label_ids:)
issuable.with_transaction_returning_status do
if issuable.save
- issuable.update_attributes(label_ids: label_ids)
+ issuable.update(label_ids: label_ids)
end
end
end
diff --git a/app/services/members/update_service.rb b/app/services/members/update_service.rb
index 48b3d59f7bd..cb19cf01dd7 100644
--- a/app/services/members/update_service.rb
+++ b/app/services/members/update_service.rb
@@ -6,7 +6,7 @@ module Members
old_access_level = member.human_access
- if member.update_attributes(params)
+ if member.update(params)
after_execute(action: permission, old_access_level: old_access_level, member: member)
end
diff --git a/app/services/merge_requests/rebase_service.rb b/app/services/merge_requests/rebase_service.rb
index 5b4bc86b9ba..c741e913860 100644
--- a/app/services/merge_requests/rebase_service.rb
+++ b/app/services/merge_requests/rebase_service.rb
@@ -26,7 +26,7 @@ module MergeRequests
Gitlab::GitLogger.info("#{log_prefix} rebased to #{rebase_sha}")
- merge_request.update_attributes(rebase_commit_sha: rebase_sha)
+ merge_request.update(rebase_commit_sha: rebase_sha)
Gitlab::GitLogger.info("#{log_prefix} rebase SHA saved: #{rebase_sha}")
diff --git a/app/services/milestones/update_service.rb b/app/services/milestones/update_service.rb
index 31b441ed476..74edbf9b41d 100644
--- a/app/services/milestones/update_service.rb
+++ b/app/services/milestones/update_service.rb
@@ -11,7 +11,7 @@ module Milestones
end
if params.present?
- milestone.update_attributes(params.except(:state_event))
+ milestone.update(params.except(:state_event))
end
milestone
diff --git a/app/services/notes/update_service.rb b/app/services/notes/update_service.rb
index 75fd08ea0a9..e16ef398184 100644
--- a/app/services/notes/update_service.rb
+++ b/app/services/notes/update_service.rb
@@ -5,7 +5,7 @@ module Notes
old_mentioned_users = note.mentioned_users.to_a
- note.update_attributes(params.merge(updated_by: current_user))
+ note.update(params.merge(updated_by: current_user))
note.create_new_cross_references!(current_user)
if note.previous_changes.include?('note')
diff --git a/app/services/notification_recipient_service.rb b/app/services/notification_recipient_service.rb
index 4fa38665abc..d9834fd0ccc 100644
--- a/app/services/notification_recipient_service.rb
+++ b/app/services/notification_recipient_service.rb
@@ -10,16 +10,16 @@ module NotificationRecipientService
NotificationRecipient.new(user, *args).notifiable?
end
- def self.build_recipients(*a)
- Builder::Default.new(*a).notification_recipients
+ def self.build_recipients(*args)
+ Builder::Default.new(*args).notification_recipients
end
- def self.build_new_note_recipients(*a)
- Builder::NewNote.new(*a).notification_recipients
+ def self.build_new_note_recipients(*args)
+ Builder::NewNote.new(*args).notification_recipients
end
- def self.build_merge_request_unmergeable_recipients(*a)
- Builder::MergeRequestUnmergeable.new(*a).notification_recipients
+ def self.build_merge_request_unmergeable_recipients(*args)
+ Builder::MergeRequestUnmergeable.new(*args).notification_recipients
end
module Builder
@@ -44,7 +44,6 @@ module NotificationRecipientService
raise 'abstract'
end
- # rubocop:disable Rails/Delegate
def project
target.project
end
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index 636cfbf5b45..8c6221af788 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -135,6 +135,8 @@ class NotificationService
# * watchers of the mr's labels
# * users with custom level checked with "new merge request"
#
+ # In EE, approvers of the merge request are also included
+ #
def new_merge_request(merge_request, current_user)
new_resource_email(merge_request, :new_merge_request_email)
end
@@ -256,6 +258,10 @@ class NotificationService
# ignore gitlab service messages
return true if note.cross_reference? && note.system?
+ send_new_note_notifications(note)
+ end
+
+ def send_new_note_notifications(note)
notify_method = "note_#{note.to_ability_name}_email".to_sym
recipients = NotificationRecipientService.build_new_note_recipients(note)
diff --git a/app/services/preview_markdown_service.rb b/app/services/preview_markdown_service.rb
index 4ee2c1796bd..6da4d9523cf 100644
--- a/app/services/preview_markdown_service.rb
+++ b/app/services/preview_markdown_service.rb
@@ -6,7 +6,8 @@ class PreviewMarkdownService < BaseService
success(
text: text,
users: users,
- commands: commands.join(' ')
+ commands: commands.join(' '),
+ markdown_engine: markdown_engine
)
end
@@ -42,4 +43,8 @@ class PreviewMarkdownService < BaseService
def commands_target_id
params[:quick_actions_target_id]
end
+
+ def markdown_engine
+ CacheMarkdownField::MarkdownEngine.from_version(params[:markdown_version].to_i)
+ end
end
diff --git a/app/services/projects/autocomplete_service.rb b/app/services/projects/autocomplete_service.rb
index aa60661f7f2..9d0eaaf3152 100644
--- a/app/services/projects/autocomplete_service.rb
+++ b/app/services/projects/autocomplete_service.rb
@@ -20,24 +20,28 @@ module Projects
MergeRequestsFinder.new(current_user, project_id: project.id, state: 'opened').execute.select([:iid, :title])
end
- def labels(target = nil)
- labels = LabelsFinder.new(current_user, project_id: project.id, include_ancestor_groups: true)
- .execute.select([:color, :title])
-
- return labels unless target&.respond_to?(:labels)
-
- issuable_label_titles = target.labels.pluck(:title)
-
- if issuable_label_titles
- labels = labels.as_json(only: [:title, :color])
-
- issuable_label_titles.each do |issuable_label_title|
- found_label = labels.find { |label| label['title'] == issuable_label_title }
- found_label[:set] = true if found_label
+ def labels_as_hash(target = nil)
+ available_labels = LabelsFinder.new(
+ current_user,
+ project_id: project.id,
+ include_ancestor_groups: true
+ ).execute
+
+ label_hashes = available_labels.as_json(only: [:title, :color])
+
+ if target&.respond_to?(:labels)
+ already_set_labels = available_labels & target.labels
+ if already_set_labels.present?
+ titles = already_set_labels.map(&:title)
+ label_hashes.each do |hash|
+ if titles.include?(hash['title'])
+ hash[:set] = true
+ end
+ end
end
end
- labels
+ label_hashes
end
def commands(noteable, type)
diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb
index 02769e72229..87173cc79ec 100644
--- a/app/services/projects/destroy_service.rb
+++ b/app/services/projects/destroy_service.rb
@@ -124,7 +124,7 @@ module Projects
# It's possible that the project was destroyed, but some after_commit
# hook failed and caused us to end up here. A destroyed model will be a frozen hash,
# which cannot be altered.
- project.update_attributes(delete_error: message, pending_delete: false) unless project.destroyed?
+ project.update(delete_error: message, pending_delete: false) unless project.destroyed?
log_error("Deletion failed on #{project.full_path} with the following message: #{message}")
end
diff --git a/app/services/projects/fork_service.rb b/app/services/projects/fork_service.rb
index 348eb0bf8d8..a8aafa9fb4f 100644
--- a/app/services/projects/fork_service.rb
+++ b/app/services/projects/fork_service.rb
@@ -37,7 +37,7 @@ module Projects
return new_project unless new_project.persisted?
builds_access_level = @project.project_feature.builds_access_level
- new_project.project_feature.update_attributes(builds_access_level: builds_access_level)
+ new_project.project_feature.update(builds_access_level: builds_access_level)
link_fork_network(new_project)
diff --git a/app/services/projects/lfs_pointers/lfs_download_service.rb b/app/services/projects/lfs_pointers/lfs_download_service.rb
index 6ea43561d61..618c30b971f 100644
--- a/app/services/projects/lfs_pointers/lfs_download_service.rb
+++ b/app/services/projects/lfs_pointers/lfs_download_service.rb
@@ -22,7 +22,7 @@ module Projects
private
def download_and_save_file(file, sanitized_uri)
- IO.copy_stream(open(sanitized_uri.sanitized_url, headers(sanitized_uri)), file)
+ IO.copy_stream(open(sanitized_uri.sanitized_url, headers(sanitized_uri)), file) # rubocop:disable Security/Open
end
def headers(sanitized_uri)
diff --git a/app/services/projects/update_service.rb b/app/services/projects/update_service.rb
index d8250cd8102..f4fbaacc08b 100644
--- a/app/services/projects/update_service.rb
+++ b/app/services/projects/update_service.rb
@@ -22,7 +22,7 @@ module Projects
# If the block added errors, don't try to save the project
return validation_failed! if project.errors.any?
- if project.update_attributes(params.except(:default_branch))
+ if project.update(params.except(:default_branch))
if project.previous_changes.include?('path')
project.rename_repo
else
diff --git a/app/services/update_release_service.rb b/app/services/update_release_service.rb
index b7c36651968..dc696e9c440 100644
--- a/app/services/update_release_service.rb
+++ b/app/services/update_release_service.rb
@@ -7,7 +7,7 @@ class UpdateReleaseService < BaseService
release = project.releases.find_by(tag: tag_name)
if release
- release.update_attributes(description: release_description)
+ release.update(description: release_description)
success(release)
else
diff --git a/app/uploaders/import_export_uploader.rb b/app/uploaders/import_export_uploader.rb
new file mode 100644
index 00000000000..213ac5c8011
--- /dev/null
+++ b/app/uploaders/import_export_uploader.rb
@@ -0,0 +1,15 @@
+class ImportExportUploader < AttachmentUploader
+ EXTENSION_WHITELIST = %w[tar.gz].freeze
+
+ def extension_whitelist
+ EXTENSION_WHITELIST
+ end
+
+ def move_to_store
+ true
+ end
+
+ def move_to_cache
+ false
+ end
+end
diff --git a/app/views/import/_githubish_status.html.haml b/app/views/import/_githubish_status.html.haml
index f7094375023..5e7be5cd37b 100644
--- a/app/views/import/_githubish_status.html.haml
+++ b/app/views/import/_githubish_status.html.haml
@@ -40,7 +40,7 @@
= project.human_import_status_name
- @repos.each do |repo|
- %tr{ id: "repo_#{repo.id}" }
+ %tr{ id: "repo_#{repo.id}", data: { qa: { repo_path: repo.full_name } } }
%td
= provider_project_link(provider, repo.full_name)
%td.import-target
@@ -50,7 +50,7 @@
- if current_user.can_select_namespace?
- selected = params[:namespace_id] || :current_user
- opts = current_user.can_create_group? ? { extra_group: Group.new(name: repo.owner.login, path: repo.owner.login) } : {}
- = select_tag :namespace_id, namespaces_options(selected, opts.merge({ display_path: true })), { class: 'input-group-text select2 js-select-namespace', tabindex: 1 }
+ = select_tag :namespace_id, namespaces_options(selected, opts.merge({ display_path: true })), { class: 'input-group-text select2 js-select-namespace qa-project-namespace-select', tabindex: 1 }
- else
= text_field_tag :path, current_user.namespace_path, class: "input-group-text input-large form-control", tabindex: 1, disabled: true
%span.input-group-prepend
diff --git a/app/views/layouts/header/_current_user_dropdown.html.haml b/app/views/layouts/header/_current_user_dropdown.html.haml
index a74ea246eaf..9ed05d6e3d0 100644
--- a/app/views/layouts/header/_current_user_dropdown.html.haml
+++ b/app/views/layouts/header/_current_user_dropdown.html.haml
@@ -17,11 +17,7 @@
= link_to _("Help"), help_path
- if current_user_menu?(:help) || current_user_menu?(:settings) || current_user_menu?(:profile)
%li.divider
- %li
- = link_to "https://about.gitlab.com/contributing", target: '_blank', class: 'text-nowrap' do
- = _("Contribute to GitLab")
- = sprite_icon('external-link', size: 16)
- %li.divider
+ = render 'shared/user_dropdown_contributing_link'
- if current_user_menu?(:sign_out)
%li
= link_to _("Sign out"), destroy_user_session_path, class: "sign-out-link"
diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml
index d8e32651b36..3aa8eb18bf3 100644
--- a/app/views/layouts/header/_default.html.haml
+++ b/app/views/layouts/header/_default.html.haml
@@ -61,7 +61,9 @@
- if header_link?(:sign_in)
%li.nav-item
%div
- = link_to "Sign in / Register", new_session_path(:user, redirect_to_referer: 'yes'), class: 'btn btn-sign-in'
+ - sign_in_text = allow_signup? ? 'Sign in / Register' : 'Sign in'
+ = link_to sign_in_text, new_session_path(:user, redirect_to_referer: 'yes'), class: 'btn btn-sign-in'
+
%button.navbar-toggler.d-block.d-sm-none{ type: 'button' }
%span.sr-only Toggle navigation
diff --git a/app/views/layouts/header/_new_dropdown.haml b/app/views/layouts/header/_new_dropdown.haml
index d35df706036..792291bde75 100644
--- a/app/views/layouts/header/_new_dropdown.haml
+++ b/app/views/layouts/header/_new_dropdown.haml
@@ -37,7 +37,7 @@
%li.dropdown-bold-header GitLab
- if current_user.can_create_project?
%li
- = link_to 'New project', new_project_path
+ = link_to 'New project', new_project_path, class: 'qa-global-new-project-link'
- if current_user.can_create_group?
%li
= link_to 'New group', new_group_path
diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml
index 7647e25e804..4029287fc0e 100644
--- a/app/views/layouts/nav/_dashboard.html.haml
+++ b/app/views/layouts/nav/_dashboard.html.haml
@@ -1,16 +1,19 @@
%ul.list-unstyled.navbar-sub-nav
- if dashboard_nav_link?(:projects)
= nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: { id: 'nav-projects-dropdown', class: "home dropdown header-projects qa-projects-dropdown" }) do
- %a{ href: "#", data: { toggle: "dropdown" } }
+ %button{ type: 'button', data: { toggle: "dropdown" } }
Projects
= sprite_icon('angle-down', css_class: 'caret-down')
- .dropdown-menu.projects-dropdown-menu
+ .dropdown-menu.frequent-items-dropdown-menu
= render "layouts/nav/projects_dropdown/show"
- if dashboard_nav_link?(:groups)
- = nav_link(controller: ['dashboard/groups', 'explore/groups'], html_options: { class: "d-none d-sm-block" }) do
- = link_to dashboard_groups_path, class: 'dashboard-shortcuts-groups qa-groups-link', title: 'Groups' do
+ = nav_link(controller: ['dashboard/groups', 'explore/groups'], html_options: { id: 'nav-groups-dropdown', class: "home dropdown header-groups qa-groups-dropdown" }) do
+ %button{ type: 'button', data: { toggle: "dropdown" } }
Groups
+ = sprite_icon('angle-down', css_class: 'caret-down')
+ .dropdown-menu.frequent-items-dropdown-menu
+ = render "layouts/nav/groups_dropdown/show"
- if dashboard_nav_link?(:activity)
= nav_link(path: 'dashboard#activity', html_options: { class: "d-none d-lg-block d-xl-block" }) do
@@ -34,11 +37,6 @@
= sprite_icon('angle-down', css_class: 'caret-down')
.dropdown-menu
%ul
- - if dashboard_nav_link?(:groups)
- = nav_link(controller: ['dashboard/groups', 'explore/groups'], html_options: { class: "d-block d-sm-none" }) do
- = link_to dashboard_groups_path, class: 'dashboard-shortcuts-groups', title: 'Groups' do
- Groups
-
- if dashboard_nav_link?(:activity)
= nav_link(path: 'dashboard#activity') do
= link_to activity_dashboard_path, title: 'Activity' do
diff --git a/app/views/layouts/nav/groups_dropdown/_show.html.haml b/app/views/layouts/nav/groups_dropdown/_show.html.haml
new file mode 100644
index 00000000000..3ce1fa6bcca
--- /dev/null
+++ b/app/views/layouts/nav/groups_dropdown/_show.html.haml
@@ -0,0 +1,12 @@
+- group_meta = { id: @group.id, name: @group.name, namespace: @group.full_name, web_url: group_path(@group), avatar_url: @group.avatar_url } if @group&.persisted?
+.frequent-items-dropdown-container
+ .frequent-items-dropdown-sidebar.qa-groups-dropdown-sidebar
+ %ul
+ = nav_link(path: 'dashboard/groups#index') do
+ = link_to dashboard_groups_path, class: 'qa-your-groups-link' do
+ = _('Your groups')
+ = nav_link(path: 'groups#explore') do
+ = link_to explore_groups_path do
+ = _('Explore groups')
+ .frequent-items-dropdown-content
+ #js-groups-dropdown{ data: { user_name: current_user.username, group: group_meta } }
diff --git a/app/views/layouts/nav/projects_dropdown/_show.html.haml b/app/views/layouts/nav/projects_dropdown/_show.html.haml
index 5809d6f7fea..f2170f71532 100644
--- a/app/views/layouts/nav/projects_dropdown/_show.html.haml
+++ b/app/views/layouts/nav/projects_dropdown/_show.html.haml
@@ -1,6 +1,6 @@
- project_meta = { id: @project.id, name: @project.name, namespace: @project.full_name, web_url: project_path(@project), avatar_url: @project.avatar_url } if @project&.persisted?
-.projects-dropdown-container
- .project-dropdown-sidebar.qa-projects-dropdown-sidebar
+.frequent-items-dropdown-container
+ .frequent-items-dropdown-sidebar.qa-projects-dropdown-sidebar
%ul
= nav_link(path: 'dashboard/projects#index') do
= link_to dashboard_projects_path, class: 'qa-your-projects-link' do
@@ -11,5 +11,5 @@
= nav_link(path: 'projects#trending') do
= link_to explore_root_path do
= _('Explore projects')
- .project-dropdown-content
+ .frequent-items-dropdown-content
#js-projects-dropdown{ data: { user_name: current_user.username, project: project_meta } }
diff --git a/app/views/profiles/keys/_form.html.haml b/app/views/profiles/keys/_form.html.haml
index c14700794ce..43a2d53b84d 100644
--- a/app/views/profiles/keys/_form.html.haml
+++ b/app/views/profiles/keys/_form.html.haml
@@ -5,11 +5,18 @@
.form-group
= f.label :key, class: 'label-light'
%p= _("Paste your public SSH key, which is usually contained in the file '~/.ssh/id_rsa.pub' and begins with 'ssh-rsa'. Don't use your private SSH key.")
- = f.text_area :key, class: "form-control", rows: 8, required: true, placeholder: 'Typically starts with "ssh-rsa …"'
+ = f.text_area :key, class: "form-control js-add-ssh-key-validation-input", rows: 8, required: true, placeholder: s_('Profiles|Typically starts with "ssh-rsa …"')
.form-group
= f.label :title, class: 'label-light'
- = f.text_field :title, class: "form-control", required: true, placeholder: 'e.g. My MacBook key'
+ = f.text_field :title, class: "form-control input-lg", required: true, placeholder: s_('Profiles|e.g. My MacBook key')
%p.form-text.text-muted= _('Name your individual key via a title')
+ .js-add-ssh-key-validation-warning.hide
+ .bs-callout.bs-callout-warning{ role: 'alert', aria_live: 'assertive' }
+ %strong= _('Oops, are you sure?')
+ %p= s_("Profiles|This doesn't look like a public SSH key, are you sure you want to add it?")
+
+ %button.btn.btn-create.js-add-ssh-key-validation-confirm-submit= _("Yes, add it")
+
.prepend-top-default
- = f.submit 'Add key', class: "btn btn-create"
+ = f.submit s_('Profiles|Add key'), class: "btn btn-create js-add-ssh-key-validation-original-submit"
diff --git a/app/views/projects/_export.html.haml b/app/views/projects/_export.html.haml
index f4d4888bd15..aa980da7e95 100644
--- a/app/views/projects/_export.html.haml
+++ b/app/views/projects/_export.html.haml
@@ -31,7 +31,7 @@
%li Any encrypted tokens
%p
Once the exported file is ready, you will receive a notification email with a download link, or you can download it from this page.
- - if project.export_project_path
+ - if project.export_status == :finished
= link_to 'Download export', download_export_project_path(project),
rel: 'nofollow', download: '', method: :get, class: "btn btn-default"
= link_to 'Generate new export', generate_new_export_project_path(project),
diff --git a/app/views/projects/issues/_form.html.haml b/app/views/projects/issues/_form.html.haml
index 2c5ffd85372..1e4e9450ffa 100644
--- a/app/views/projects/issues/_form.html.haml
+++ b/app/views/projects/issues/_form.html.haml
@@ -1,2 +1,4 @@
-= form_for [@project.namespace.becomes(Namespace), @project, @issue], html: { class: 'issue-form common-note-form js-quick-submit js-requires-input' } do |f|
+= form_for [@project.namespace.becomes(Namespace), @project, @issue],
+ html: { class: 'issue-form common-note-form js-quick-submit js-requires-input' },
+ data: { markdown_version: @issue.cached_markdown_version } do |f|
= render 'shared/issuable/form', f: f, issuable: @issue
diff --git a/app/views/projects/merge_requests/_form.html.haml b/app/views/projects/merge_requests/_form.html.haml
index 179c1fcc684..5a59f956cb5 100644
--- a/app/views/projects/merge_requests/_form.html.haml
+++ b/app/views/projects/merge_requests/_form.html.haml
@@ -1,2 +1,4 @@
-= form_for [@project.namespace.becomes(Namespace), @project, @merge_request], html: { class: 'merge-request-form common-note-form js-requires-input js-quick-submit' } do |f|
+= form_for [@project.namespace.becomes(Namespace), @project, @merge_request],
+ html: { class: 'merge-request-form common-note-form js-requires-input js-quick-submit' },
+ data: { markdown_version: @merge_request.cached_markdown_version } do |f|
= render 'shared/issuable/form', f: f, issuable: @merge_request
diff --git a/app/views/projects/milestones/_form.html.haml b/app/views/projects/milestones/_form.html.haml
index 4cc59718715..ace094a671a 100644
--- a/app/views/projects/milestones/_form.html.haml
+++ b/app/views/projects/milestones/_form.html.haml
@@ -1,4 +1,6 @@
-= form_for [@project.namespace.becomes(Namespace), @project, @milestone], html: {class: 'milestone-form common-note-form js-quick-submit js-requires-input'} do |f|
+= form_for [@project.namespace.becomes(Namespace), @project, @milestone],
+ html: {class: 'milestone-form common-note-form js-quick-submit js-requires-input'},
+ data: { markdown_version: @milestone.cached_markdown_version } do |f|
= form_errors(@milestone)
.row
.col-md-6
diff --git a/app/views/projects/releases/edit.html.haml b/app/views/projects/releases/edit.html.haml
index d6f758608a0..8093cc2c2d7 100644
--- a/app/views/projects/releases/edit.html.haml
+++ b/app/views/projects/releases/edit.html.haml
@@ -11,7 +11,9 @@
%strong= @tag.name
- = form_for(@release, method: :put, url: project_tag_release_path(@project, @tag.name), html: { class: 'common-note-form release-form js-quick-submit' }) do |f|
+ = form_for(@release, method: :put, url: project_tag_release_path(@project, @tag.name),
+ html: { class: 'common-note-form release-form js-quick-submit' },
+ data: { markdown_version: @release.cached_markdown_version }) do |f|
= render layout: 'projects/md_preview', locals: { url: preview_markdown_path(@project), referenced_users: true } do
= render 'projects/zen', f: f, attr: :description, classes: 'note-textarea', placeholder: "Write your release notes or drag files here…"
= render 'shared/notes/hints'
diff --git a/app/views/projects/wikis/_form.html.haml b/app/views/projects/wikis/_form.html.haml
index 26fe1de31fe..de692466fe5 100644
--- a/app/views/projects/wikis/_form.html.haml
+++ b/app/views/projects/wikis/_form.html.haml
@@ -1,7 +1,9 @@
- commit_message = @page.persisted? ? s_("WikiPageEdit|Update %{page_title}") : s_("WikiPageCreate|Create %{page_title}")
- commit_message = commit_message % { page_title: @page.title }
-= form_for [@project.namespace.becomes(Namespace), @project, @page], method: @page.persisted? ? :put : :post, html: { class: 'wiki-form common-note-form prepend-top-default js-quick-submit' } do |f|
+= form_for [@project.namespace.becomes(Namespace), @project, @page], method: @page.persisted? ? :put : :post,
+ html: { class: 'wiki-form common-note-form prepend-top-default js-quick-submit' },
+ data: { markdown_version: CacheMarkdownField::CACHE_REDCARPET_VERSION } do |f|
= form_errors(@page)
- if @page.persisted?
diff --git a/app/views/shared/_user_dropdown_contributing_link.html.haml b/app/views/shared/_user_dropdown_contributing_link.html.haml
new file mode 100644
index 00000000000..333d6fa3489
--- /dev/null
+++ b/app/views/shared/_user_dropdown_contributing_link.html.haml
@@ -0,0 +1,5 @@
+%li
+ = link_to "https://about.gitlab.com/contributing", target: '_blank', class: 'text-nowrap' do
+ = _("Contribute to GitLab")
+ = sprite_icon('external-link', size: 16)
+%li.divider
diff --git a/app/views/shared/hook_logs/_content.html.haml b/app/views/shared/hook_logs/_content.html.haml
index 532712ee6d1..f3b56df0c96 100644
--- a/app/views/shared/hook_logs/_content.html.haml
+++ b/app/views/shared/hook_logs/_content.html.haml
@@ -30,7 +30,7 @@
%h5 Request body:
%pre
- :plain
+ :escaped
#{JSON.pretty_generate(hook_log.request_data)}
%h5 Response headers:
%pre
@@ -40,5 +40,5 @@
%h5 Response body:
%pre
- :plain
+ :escaped
#{hook_log.response_body}
diff --git a/app/views/shared/notes/_note.html.haml b/app/views/shared/notes/_note.html.haml
index d4e8f30e458..f5464058bc0 100644
--- a/app/views/shared/notes/_note.html.haml
+++ b/app/views/shared/notes/_note.html.haml
@@ -52,7 +52,7 @@
.note-text.md
= markdown_field(note, :note)
= edited_time_ago_with_tooltip(note, placement: 'bottom', html_class: 'note_edited_ago')
- .original-note-content.hidden{ data: { post_url: note_url(note), target_id: note.noteable.id, target_type: note.noteable.class.name.underscore } }
+ .original-note-content.hidden{ data: { post_url: note_url(note), target_id: note.noteable.id, target_type: note.noteable.class.name.underscore, markdown_version: note.cached_markdown_version } }
#{note.note}
- if note_editable
= render 'shared/notes/edit', note: note
diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml
index 88f0675f795..6be1fb485a4 100644
--- a/app/views/shared/projects/_project.html.haml
+++ b/app/views/shared/projects/_project.html.haml
@@ -4,7 +4,7 @@
- ci = false unless local_assigns[:ci] == true
- skip_namespace = false unless local_assigns[:skip_namespace] == true
- user = local_assigns[:user]
-- access = user&.max_member_access_for_project(project.id) unless user.nil?
+- access = max_project_member_access(project)
- css_class = '' unless local_assigns[:css_class]
- show_last_commit_as_description = false unless local_assigns[:show_last_commit_as_description] == true && can_show_last_commit_in_list?(project)
- css_class += " no-description" if project.description.blank? && !show_last_commit_as_description
diff --git a/app/views/shared/snippets/_form.html.haml b/app/views/shared/snippets/_form.html.haml
index 858adc8be37..5e5c050d5c3 100644
--- a/app/views/shared/snippets/_form.html.haml
+++ b/app/views/shared/snippets/_form.html.haml
@@ -2,7 +2,9 @@
= page_specific_javascript_tag('lib/ace.js')
.snippet-form-holder
- = form_for @snippet, url: url, html: { class: "snippet-form js-requires-input js-quick-submit common-note-form" } do |f|
+ = form_for @snippet, url: url,
+ html: { class: "snippet-form js-requires-input js-quick-submit common-note-form" },
+ data: { markdown_version: @snippet.cached_markdown_version } do |f|
= form_errors(@snippet)
.form-group.row
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index b8b854853b7..d4be1ccfcfa 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -46,7 +46,6 @@
- mail_scheduler:mail_scheduler_issue_due
- mail_scheduler:mail_scheduler_notification_service
-- object_storage_upload
- object_storage:object_storage_background_move
- object_storage:object_storage_migrate_uploads
diff --git a/app/workers/archive_trace_worker.rb b/app/workers/archive_trace_worker.rb
index 9169f21af2a..c6f89a17729 100644
--- a/app/workers/archive_trace_worker.rb
+++ b/app/workers/archive_trace_worker.rb
@@ -5,7 +5,7 @@ class ArchiveTraceWorker
include PipelineBackgroundQueue
def perform(job_id)
- Ci::Build.find_by(id: job_id).try do |job|
+ Ci::Build.without_archived_trace.find_by(id: job_id).try do |job|
job.trace.archive!
end
end
diff --git a/app/workers/ci/archive_traces_cron_worker.rb b/app/workers/ci/archive_traces_cron_worker.rb
index 7016edde698..7d4e9660a4e 100644
--- a/app/workers/ci/archive_traces_cron_worker.rb
+++ b/app/workers/ci/archive_traces_cron_worker.rb
@@ -12,6 +12,7 @@ module Ci
Ci::Build.finished.with_live_trace.find_each(batch_size: 100) do |build|
begin
build.trace.archive!
+ rescue ::Gitlab::Ci::Trace::AlreadyArchivedError
rescue => e
failed_archive_counter.increment
Rails.logger.error "Failed to archive stale live trace. id: #{build.id} message: #{e.message}"
diff --git a/app/workers/ci/build_trace_chunk_flush_worker.rb b/app/workers/ci/build_trace_chunk_flush_worker.rb
index 6376c6d32cf..9dbf2e5e1ac 100644
--- a/app/workers/ci/build_trace_chunk_flush_worker.rb
+++ b/app/workers/ci/build_trace_chunk_flush_worker.rb
@@ -7,7 +7,7 @@ module Ci
def perform(build_trace_chunk_id)
::Ci::BuildTraceChunk.find_by(id: build_trace_chunk_id).try do |build_trace_chunk|
- build_trace_chunk.use_database!
+ build_trace_chunk.persist_data!
end
end
end
diff --git a/app/workers/email_receiver_worker.rb b/app/workers/email_receiver_worker.rb
index f9f0efb302a..12706613ac2 100644
--- a/app/workers/email_receiver_worker.rb
+++ b/app/workers/email_receiver_worker.rb
@@ -15,14 +15,14 @@ class EmailReceiverWorker
private
- def handle_failure(raw, e)
- Rails.logger.warn("Email can not be processed: #{e}\n\n#{raw}")
+ def handle_failure(raw, error)
+ Rails.logger.warn("Email can not be processed: #{error}\n\n#{raw}")
return unless raw.present?
can_retry = false
reason =
- case e
+ case error
when Gitlab::Email::UnknownIncomingEmail
"We couldn't figure out what the email is for. Please create your issue or comment through the web interface."
when Gitlab::Email::SentNotificationNotFoundError
@@ -42,7 +42,7 @@ class EmailReceiverWorker
"The thread you are replying to no longer exists, perhaps it was deleted? If you believe this is in error, contact a staff member."
when Gitlab::Email::InvalidRecordError
can_retry = true
- e.message
+ error.message
end
if reason
diff --git a/app/workers/object_storage/migrate_uploads_worker.rb b/app/workers/object_storage/migrate_uploads_worker.rb
index a3ecfa8e711..01d03ec7888 100644
--- a/app/workers/object_storage/migrate_uploads_worker.rb
+++ b/app/workers/object_storage/migrate_uploads_worker.rb
@@ -1,6 +1,4 @@
# frozen_string_literal: true
-# rubocop:disable Metrics/LineLength
-# rubocop:disable Style/Documentation
module ObjectStorage
class MigrateUploadsWorker
diff --git a/app/workers/object_storage_upload_worker.rb b/app/workers/object_storage_upload_worker.rb
deleted file mode 100644
index f17980a83d8..00000000000
--- a/app/workers/object_storage_upload_worker.rb
+++ /dev/null
@@ -1,23 +0,0 @@
-# frozen_string_literal: true
-
-# @Deprecated - remove once the `object_storage_upload` queue is empty
-# The queue has been renamed `object_storage:object_storage_background_upload`
-#
-class ObjectStorageUploadWorker
- include ApplicationWorker
-
- sidekiq_options retry: 5
-
- def perform(uploader_class_name, subject_class_name, file_field, subject_id)
- uploader_class = uploader_class_name.constantize
- subject_class = subject_class_name.constantize
-
- return unless uploader_class < ObjectStorage::Concern
- return unless uploader_class.object_store_enabled?
- return unless uploader_class.background_upload_enabled?
-
- subject = subject_class.find(subject_id)
- uploader = subject.public_send(file_field) # rubocop:disable GitlabSecurity/PublicSend
- uploader.migrate!(ObjectStorage::Store::REMOTE)
- end
-end
diff --git a/app/workers/repository_check/batch_worker.rb b/app/workers/repository_check/batch_worker.rb
index 051382a08a9..07559ea479b 100644
--- a/app/workers/repository_check/batch_worker.rb
+++ b/app/workers/repository_check/batch_worker.rb
@@ -4,9 +4,11 @@ module RepositoryCheck
class BatchWorker
include ApplicationWorker
include RepositoryCheckQueue
+ include ExclusiveLeaseGuard
RUN_TIME = 3600
BATCH_SIZE = 10_000
+ LEASE_TIMEOUT = 1.hour
attr_reader :shard_name
@@ -16,6 +18,20 @@ module RepositoryCheck
return unless Gitlab::CurrentSettings.repository_checks_enabled
return unless Gitlab::ShardHealthCache.healthy_shard?(shard_name)
+ try_obtain_lease do
+ perform_repository_checks
+ end
+ end
+
+ def lease_timeout
+ LEASE_TIMEOUT
+ end
+
+ def lease_key
+ "repository_check_batch_worker:#{shard_name}"
+ end
+
+ def perform_repository_checks
start = Time.now
# This loop will break after a little more than one hour ('a little
@@ -26,7 +42,7 @@ module RepositoryCheck
project_ids.each do |project_id|
break if Time.now - start >= RUN_TIME
- next unless try_obtain_lease(project_id)
+ next unless try_obtain_lease_for_project(project_id)
SingleRepositoryWorker.new.perform(project_id)
end
@@ -60,7 +76,7 @@ module RepositoryCheck
Project.where(repository_storage: shard_name)
end
- def try_obtain_lease(id)
+ def try_obtain_lease_for_project(id)
# Use a 24-hour timeout because on servers/projects where 'git fsck' is
# super slow we definitely do not want to run it twice in parallel.
Gitlab::ExclusiveLease.new(
diff --git a/app/workers/repository_check/dispatch_worker.rb b/app/workers/repository_check/dispatch_worker.rb
index 891a273afd7..96634f09a15 100644
--- a/app/workers/repository_check/dispatch_worker.rb
+++ b/app/workers/repository_check/dispatch_worker.rb
@@ -3,13 +3,22 @@ module RepositoryCheck
include ApplicationWorker
include CronjobQueue
include ::EachShardWorker
+ include ExclusiveLeaseGuard
+
+ LEASE_TIMEOUT = 1.hour
def perform
return unless Gitlab::CurrentSettings.repository_checks_enabled
- each_eligible_shard do |shard_name|
- RepositoryCheck::BatchWorker.perform_async(shard_name)
+ try_obtain_lease do
+ each_eligible_shard do |shard_name|
+ RepositoryCheck::BatchWorker.perform_async(shard_name)
+ end
end
end
+
+ def lease_timeout
+ LEASE_TIMEOUT
+ end
end
end