Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/contextual_sidebar.js2
-rw-r--r--app/assets/javascripts/diffs/components/app.vue17
-rw-r--r--app/assets/javascripts/diffs/components/diff_line_note_form.vue10
-rw-r--r--app/assets/javascripts/diffs/index.js4
-rw-r--r--app/assets/javascripts/diffs/store/actions.js16
-rw-r--r--app/assets/javascripts/diffs/store/modules/diff_state.js2
-rw-r--r--app/assets/javascripts/diffs/store/mutation_types.js2
-rw-r--r--app/assets/javascripts/diffs/store/mutations.js7
-rw-r--r--app/assets/javascripts/filtered_search/available_dropdown_mappings.js16
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js6
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_manager.js2
-rw-r--r--app/assets/javascripts/filtered_search/visual_token_value.js8
-rw-r--r--app/assets/javascripts/issue_show/components/app.vue3
-rw-r--r--app/assets/javascripts/issue_show/components/pinned_links.vue52
-rw-r--r--app/assets/javascripts/jobs/components/stages_dropdown.vue11
-rw-r--r--app/assets/javascripts/merge_request_tabs.js10
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard.vue14
-rw-r--r--app/assets/javascripts/monitoring/stores/actions.js4
-rw-r--r--app/assets/javascripts/notes/components/note_form.vue9
-rw-r--r--app/assets/javascripts/pipelines/components/header_component.vue2
-rw-r--r--app/assets/javascripts/pipelines/components/pipeline_url.vue13
-rw-r--r--app/assets/javascripts/visual_review_toolbar/components/comment.js132
-rw-r--r--app/assets/javascripts/visual_review_toolbar/components/constants.js37
-rw-r--r--app/assets/javascripts/visual_review_toolbar/components/index.js23
-rw-r--r--app/assets/javascripts/visual_review_toolbar/components/login.js52
-rw-r--r--app/assets/javascripts/visual_review_toolbar/components/note.js27
-rw-r--r--app/assets/javascripts/visual_review_toolbar/components/utils.js42
-rw-r--r--app/assets/javascripts/visual_review_toolbar/components/wrapper.js82
-rw-r--r--app/assets/javascripts/visual_review_toolbar/components/wrapper_icons.js15
-rw-r--r--app/assets/javascripts/visual_review_toolbar/index.js37
-rw-r--r--app/assets/javascripts/visual_review_toolbar/store/events.js36
-rw-r--r--app/assets/javascripts/visual_review_toolbar/store/index.js5
-rw-r--r--app/assets/javascripts/visual_review_toolbar/store/state.js77
-rw-r--r--app/assets/javascripts/visual_review_toolbar/store/utils.js15
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue11
-rw-r--r--app/assets/javascripts/vue_shared/components/ci_pipeline_link.vue32
-rw-r--r--app/assets/javascripts/vue_shared/components/header_ci_component.vue20
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/field.vue7
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/header.vue158
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue1
-rw-r--r--app/assets/javascripts/vue_shared/components/pagination/constants.js4
-rw-r--r--app/assets/javascripts/vue_shared/components/pagination_links.vue29
-rw-r--r--app/assets/stylesheets/framework/common.scss1
-rw-r--r--app/assets/stylesheets/framework/forms.scss16
-rw-r--r--app/assets/stylesheets/framework/markdown_area.scss2
-rw-r--r--app/assets/stylesheets/framework/variables.scss2
-rw-r--r--app/assets/stylesheets/pages/detail_page.scss1
-rw-r--r--app/assets/stylesheets/pages/diff.scss14
-rw-r--r--app/graphql/gitlab_schema.rb4
-rw-r--r--app/graphql/types/issue_type.rb4
-rw-r--r--app/graphql/types/merge_request_type.rb4
-rw-r--r--app/graphql/types/notes/diff_position_type.rb46
-rw-r--r--app/graphql/types/notes/discussion_type.rb15
-rw-r--r--app/graphql/types/notes/note_type.rb46
-rw-r--r--app/graphql/types/notes/noteable_type.rb25
-rw-r--r--app/graphql/types/notes/position_type_enum.rb13
-rw-r--r--app/graphql/types/permission_types/note.rb11
-rw-r--r--app/graphql/types/project_statistics_type.rb2
-rw-r--r--app/graphql/types/project_type.rb2
-rw-r--r--app/graphql/types/task_completion_status.rb11
-rw-r--r--app/helpers/dropdowns_helper.rb2
-rw-r--r--app/helpers/environments_helper.rb2
-rw-r--r--app/helpers/markup_helper.rb5
-rw-r--r--app/helpers/search_helper.rb6
-rw-r--r--app/helpers/user_callouts_helper.rb5
-rw-r--r--app/models/concerns/diff_positionable_note.rb9
-rw-r--r--app/models/discussion.rb8
-rw-r--r--app/models/group.rb2
-rw-r--r--app/models/note.rb2
-rw-r--r--app/models/user_callout_enums.rb6
-rw-r--r--app/policies/project_statistics_policy.rb5
-rw-r--r--app/serializers/pipeline_entity.rb1
-rw-r--r--app/views/layouts/_head.html.haml1
-rw-r--r--app/views/layouts/nav/sidebar/_project.html.haml2
-rw-r--r--app/views/projects/ci/builds/_build.html.haml5
-rw-r--r--app/views/projects/commit/_commit_box.html.haml2
-rw-r--r--app/views/projects/merge_requests/show.html.haml4
-rw-r--r--app/views/shared/_sidebar_toggle_button.html.haml2
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml4
79 files changed, 1131 insertions, 213 deletions
diff --git a/app/assets/javascripts/contextual_sidebar.js b/app/assets/javascripts/contextual_sidebar.js
index b62ec8a651b..9263e9b27e4 100644
--- a/app/assets/javascripts/contextual_sidebar.js
+++ b/app/assets/javascripts/contextual_sidebar.js
@@ -78,7 +78,7 @@ export default class ContextualSidebar {
const dbp = ContextualSidebar.isDesktopBreakpoint();
if (this.$sidebar.length) {
- this.$sidebar.toggleClass('sidebar-collapsed-desktop', collapsed);
+ this.$sidebar.toggleClass(`sidebar-collapsed-desktop ${SIDEBAR_COLLAPSED_CLASS}`, collapsed);
this.$sidebar.toggleClass('sidebar-expanded-mobile', !dbp ? !collapsed : false);
this.$page.toggleClass(
'page-with-icon-sidebar',
diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue
index 11d6672cacf..81da0754752 100644
--- a/app/assets/javascripts/diffs/components/app.vue
+++ b/app/assets/javascripts/diffs/components/app.vue
@@ -69,6 +69,16 @@ export default {
required: false,
default: false,
},
+ dismissEndpoint: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ showSuggestPopover: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
const treeWidth =
@@ -141,7 +151,12 @@ export default {
showTreeList: 'adjustView',
},
mounted() {
- this.setBaseConfig({ endpoint: this.endpoint, projectPath: this.projectPath });
+ this.setBaseConfig({
+ endpoint: this.endpoint,
+ projectPath: this.projectPath,
+ dismissEndpoint: this.dismissEndpoint,
+ showSuggestPopover: this.showSuggestPopover,
+ });
if (this.shouldShow) {
this.fetchData();
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 c209b857652..da0cdbe467b 100644
--- a/app/assets/javascripts/diffs/components/diff_line_note_form.vue
+++ b/app/assets/javascripts/diffs/components/diff_line_note_form.vue
@@ -42,6 +42,7 @@ export default {
noteableData: state => state.notes.noteableData,
diffViewType: state => state.diffs.diffViewType,
}),
+ ...mapState('diffs', ['showSuggestPopover']),
...mapGetters('diffs', ['getDiffFileByHash']),
...mapGetters([
'isLoggedIn',
@@ -80,7 +81,12 @@ export default {
}
},
methods: {
- ...mapActions('diffs', ['cancelCommentForm', 'assignDiscussionsToDiff', 'saveDiffDiscussion']),
+ ...mapActions('diffs', [
+ 'cancelCommentForm',
+ 'assignDiscussionsToDiff',
+ 'saveDiffDiscussion',
+ 'setSuggestPopoverDismissed',
+ ]),
handleCancelCommentForm(shouldConfirm, isDirty) {
if (shouldConfirm && isDirty) {
const msg = s__('Notes|Are you sure you want to cancel creating this comment?');
@@ -125,11 +131,13 @@ export default {
:line="line"
:help-page-path="helpPagePath"
:diff-file="diffFile"
+ :show-suggest-popover="showSuggestPopover"
save-button-title="Comment"
class="diff-comment-form"
@handleFormUpdateAddToReview="addToReview"
@cancelForm="handleCancelCommentForm"
@handleFormUpdate="handleSaveNote"
+ @handleSuggestDismissed="setSuggestPopoverDismissed"
/>
</div>
</template>
diff --git a/app/assets/javascripts/diffs/index.js b/app/assets/javascripts/diffs/index.js
index 1d897bca1dd..1e57e9b8a30 100644
--- a/app/assets/javascripts/diffs/index.js
+++ b/app/assets/javascripts/diffs/index.js
@@ -72,6 +72,8 @@ export default function initDiffsApp(store) {
currentUser: JSON.parse(dataset.currentUserData) || {},
changesEmptyStateIllustration: dataset.changesEmptyStateIllustration,
isFluidLayout: parseBoolean(dataset.isFluidLayout),
+ dismissEndpoint: dataset.dismissEndpoint,
+ showSuggestPopover: parseBoolean(dataset.showSuggestPopover),
};
},
computed: {
@@ -99,6 +101,8 @@ export default function initDiffsApp(store) {
shouldShow: this.activeTab === 'diffs',
changesEmptyStateIllustration: this.changesEmptyStateIllustration,
isFluidLayout: this.isFluidLayout,
+ dismissEndpoint: this.dismissEndpoint,
+ showSuggestPopover: this.showSuggestPopover,
},
});
},
diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js
index 479afc50113..88d7b4bba63 100644
--- a/app/assets/javascripts/diffs/store/actions.js
+++ b/app/assets/javascripts/diffs/store/actions.js
@@ -36,8 +36,8 @@ import {
import { diffViewerModes } from '~/ide/constants';
export const setBaseConfig = ({ commit }, options) => {
- const { endpoint, projectPath } = options;
- commit(types.SET_BASE_CONFIG, { endpoint, projectPath });
+ const { endpoint, projectPath, dismissEndpoint, showSuggestPopover } = options;
+ commit(types.SET_BASE_CONFIG, { endpoint, projectPath, dismissEndpoint, showSuggestPopover });
};
export const fetchDiffFiles = ({ state, commit }) => {
@@ -455,5 +455,17 @@ export const toggleFullDiff = ({ dispatch, getters, state }, filePath) => {
export const setFileCollapsed = ({ commit }, { filePath, collapsed }) =>
commit(types.SET_FILE_COLLAPSED, { filePath, collapsed });
+export const setSuggestPopoverDismissed = ({ commit, state }) =>
+ axios
+ .post(state.dismissEndpoint, {
+ feature_name: 'suggest_popover_dismissed',
+ })
+ .then(() => {
+ commit(types.SET_SHOW_SUGGEST_POPOVER);
+ })
+ .catch(() => {
+ createFlash(s__('MergeRequest|Error dismissing suggestion popover. Please try again.'));
+ });
+
// 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
index cf4dd93dbfb..6821c8445ea 100644
--- a/app/assets/javascripts/diffs/store/modules/diff_state.js
+++ b/app/assets/javascripts/diffs/store/modules/diff_state.js
@@ -28,4 +28,6 @@ export default () => ({
renderTreeList: true,
showWhitespace: true,
fileFinderVisible: false,
+ dismissEndpoint: '',
+ showSuggestPopover: true,
});
diff --git a/app/assets/javascripts/diffs/store/mutation_types.js b/app/assets/javascripts/diffs/store/mutation_types.js
index 6bb24c97139..8d6111da500 100644
--- a/app/assets/javascripts/diffs/store/mutation_types.js
+++ b/app/assets/javascripts/diffs/store/mutation_types.js
@@ -33,3 +33,5 @@ export const SET_HIDDEN_VIEW_DIFF_FILE_LINES = 'SET_HIDDEN_VIEW_DIFF_FILE_LINES'
export const SET_CURRENT_VIEW_DIFF_FILE_LINES = 'SET_CURRENT_VIEW_DIFF_FILE_LINES';
export const ADD_CURRENT_VIEW_DIFF_FILE_LINES = 'ADD_CURRENT_VIEW_DIFF_FILE_LINES';
export const TOGGLE_DIFF_FILE_RENDERING_MORE = 'TOGGLE_DIFF_FILE_RENDERING_MORE';
+
+export const SET_SHOW_SUGGEST_POPOVER = 'SET_SHOW_SUGGEST_POPOVER';
diff --git a/app/assets/javascripts/diffs/store/mutations.js b/app/assets/javascripts/diffs/store/mutations.js
index 67bc1724738..00181a63c43 100644
--- a/app/assets/javascripts/diffs/store/mutations.js
+++ b/app/assets/javascripts/diffs/store/mutations.js
@@ -11,8 +11,8 @@ import * as types from './mutation_types';
export default {
[types.SET_BASE_CONFIG](state, options) {
- const { endpoint, projectPath } = options;
- Object.assign(state, { endpoint, projectPath });
+ const { endpoint, projectPath, dismissEndpoint, showSuggestPopover } = options;
+ Object.assign(state, { endpoint, projectPath, dismissEndpoint, showSuggestPopover });
},
[types.SET_LOADING](state, isLoading) {
@@ -302,4 +302,7 @@ export default {
file.renderingLines = !file.renderingLines;
},
+ [types.SET_SHOW_SUGGEST_POPOVER](state) {
+ state.showSuggestPopover = false;
+ },
};
diff --git a/app/assets/javascripts/filtered_search/available_dropdown_mappings.js b/app/assets/javascripts/filtered_search/available_dropdown_mappings.js
index be867a3838d..891086b4142 100644
--- a/app/assets/javascripts/filtered_search/available_dropdown_mappings.js
+++ b/app/assets/javascripts/filtered_search/available_dropdown_mappings.js
@@ -8,9 +8,19 @@ import DropdownUtils from './dropdown_utils';
import { mergeUrlParams } from '../lib/utils/url_utility';
export default class AvailableDropdownMappings {
- constructor(container, baseEndpoint, groupsOnly, includeAncestorGroups, includeDescendantGroups) {
+ constructor(
+ container,
+ baseEndpoint,
+ labelsEndpoint,
+ milestonesEndpoint,
+ groupsOnly,
+ includeAncestorGroups,
+ includeDescendantGroups,
+ ) {
this.container = container;
this.baseEndpoint = baseEndpoint;
+ this.labelsEndpoint = labelsEndpoint;
+ this.milestonesEndpoint = milestonesEndpoint;
this.groupsOnly = groupsOnly;
this.includeAncestorGroups = includeAncestorGroups;
this.includeDescendantGroups = includeDescendantGroups;
@@ -117,11 +127,11 @@ export default class AvailableDropdownMappings {
}
getMilestoneEndpoint() {
- return `${this.baseEndpoint}/milestones.json`;
+ return `${this.milestonesEndpoint}.json`;
}
getLabelsEndpoint() {
- let endpoint = `${this.baseEndpoint}/labels.json?`;
+ let endpoint = `${this.labelsEndpoint}.json?`;
if (this.groupsOnly) {
endpoint = `${endpoint}only_group_labels=true&`;
diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
index cb0a84b490b..1cbfd7f9bb9 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
@@ -9,6 +9,8 @@ import FilteredSearchVisualTokens from './filtered_search_visual_tokens';
export default class FilteredSearchDropdownManager {
constructor({
baseEndpoint = '',
+ labelsEndpoint = '',
+ milestonesEndpoint = '',
tokenizer,
page,
isGroup,
@@ -18,6 +20,8 @@ export default class FilteredSearchDropdownManager {
}) {
this.container = FilteredSearchContainer.container;
this.baseEndpoint = baseEndpoint.replace(/\/$/, '');
+ this.labelsEndpoint = labelsEndpoint.replace(/\/$/, '');
+ this.milestonesEndpoint = milestonesEndpoint.replace(/\/$/, '');
this.tokenizer = tokenizer;
this.filteredSearchTokenKeys = filteredSearchTokenKeys || FilteredSearchTokenKeys;
this.filteredSearchInput = this.container.querySelector('.filtered-search');
@@ -48,6 +52,8 @@ export default class FilteredSearchDropdownManager {
const availableMappings = new AvailableDropdownMappings(
this.container,
this.baseEndpoint,
+ this.labelsEndpoint,
+ this.milestonesEndpoint,
this.groupsOnly,
this.includeAncestorGroups,
this.includeDescendantGroups,
diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js
index 78fbb3696cc..450e0725f2e 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_manager.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js
@@ -86,6 +86,8 @@ export default class FilteredSearchManager {
this.tokenizer = FilteredSearchTokenizer;
this.dropdownManager = new FilteredSearchDropdownManager({
baseEndpoint: this.filteredSearchInput.getAttribute('data-base-endpoint') || '',
+ labelsEndpoint: this.filteredSearchInput.getAttribute('data-labels-endpoint') || '',
+ milestonesEndpoint: this.filteredSearchInput.getAttribute('data-milestones-endpoint') || '',
tokenizer: this.tokenizer,
page: this.page,
isGroup: this.isGroup,
diff --git a/app/assets/javascripts/filtered_search/visual_token_value.js b/app/assets/javascripts/filtered_search/visual_token_value.js
index 38327472cb3..a54b445fb0a 100644
--- a/app/assets/javascripts/filtered_search/visual_token_value.js
+++ b/app/assets/javascripts/filtered_search/visual_token_value.js
@@ -56,13 +56,13 @@ export default class VisualTokenValue {
updateLabelTokenColor(tokenValueContainer) {
const { tokenValue } = this;
const filteredSearchInput = FilteredSearchContainer.container.querySelector('.filtered-search');
- const { baseEndpoint } = filteredSearchInput.dataset;
- const labelsEndpoint = FilteredSearchVisualTokens.getEndpointWithQueryParams(
- `${baseEndpoint}/labels.json`,
+ const { labelsEndpoint } = filteredSearchInput.dataset;
+ const labelsEndpointWithParams = FilteredSearchVisualTokens.getEndpointWithQueryParams(
+ `${labelsEndpoint}.json`,
filteredSearchInput.dataset.endpointQueryParams,
);
- return AjaxCache.retrieve(labelsEndpoint)
+ return AjaxCache.retrieve(labelsEndpointWithParams)
.then(labels => {
const matchingLabel = (labels || []).find(
label => `~${DropdownUtils.getEscapedText(label.title)}` === tokenValue,
diff --git a/app/assets/javascripts/issue_show/components/app.vue b/app/assets/javascripts/issue_show/components/app.vue
index e88ca4747c5..de2a9664cde 100644
--- a/app/assets/javascripts/issue_show/components/app.vue
+++ b/app/assets/javascripts/issue_show/components/app.vue
@@ -11,6 +11,7 @@ import titleComponent from './title.vue';
import descriptionComponent from './description.vue';
import editedComponent from './edited.vue';
import formComponent from './form.vue';
+import PinnedLinks from './pinned_links.vue';
import recaptchaModalImplementor from '../../vue_shared/mixins/recaptcha_modal_implementor';
export default {
@@ -19,6 +20,7 @@ export default {
titleComponent,
editedComponent,
formComponent,
+ PinnedLinks,
},
mixins: [recaptchaModalImplementor],
props: {
@@ -340,6 +342,7 @@ export default {
:title-text="state.titleText"
:show-inline-edit-button="showInlineEditButton"
/>
+ <pinned-links :description-html="state.descriptionHtml" />
<description-component
v-if="state.descriptionHtml"
:can-update="canUpdate"
diff --git a/app/assets/javascripts/issue_show/components/pinned_links.vue b/app/assets/javascripts/issue_show/components/pinned_links.vue
new file mode 100644
index 00000000000..7a54b26bc2b
--- /dev/null
+++ b/app/assets/javascripts/issue_show/components/pinned_links.vue
@@ -0,0 +1,52 @@
+<script>
+import { GlLink } from '@gitlab/ui';
+import Icon from '~/vue_shared/components/icon.vue';
+
+export default {
+ components: {
+ Icon,
+ GlLink,
+ },
+ props: {
+ descriptionHtml: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ linksInDescription() {
+ const el = document.createElement('div');
+ el.innerHTML = this.descriptionHtml;
+ return [...el.querySelectorAll('a')].map(a => a.href);
+ },
+ // Detect links matching the following formats:
+ // Zoom Start links: https://zoom.us/s/<meeting-id>
+ // Zoom Join links: https://zoom.us/j/<meeting-id>
+ // Personal Zoom links: https://zoom.us/my/<meeting-id>
+ // Vanity Zoom links: https://gitlab.zoom.us/j/<meeting-id> (also /s and /my)
+ zoomHref() {
+ const zoomRegex = /^https:\/\/([\w\d-]+\.)?zoom\.us\/(s|j|my)\/.+/;
+ return this.linksInDescription.reduce((acc, currentLink) => {
+ let lastLink = acc;
+ if (zoomRegex.test(currentLink)) {
+ lastLink = currentLink;
+ }
+ return lastLink;
+ }, '');
+ },
+ },
+};
+</script>
+
+<template>
+ <div v-if="zoomHref" class="border-bottom mb-3 mt-n2">
+ <gl-link
+ :href="zoomHref"
+ target="_blank"
+ class="btn btn-inverted btn-secondary btn-sm text-dark mb-3"
+ >
+ <icon name="brand-zoom" :size="14" />
+ <strong class="vertical-align-top">{{ __('Join Zoom meeting') }}</strong>
+ </gl-link>
+ </div>
+</template>
diff --git a/app/assets/javascripts/jobs/components/stages_dropdown.vue b/app/assets/javascripts/jobs/components/stages_dropdown.vue
index cb073a9b04d..6e92b599b0a 100644
--- a/app/assets/javascripts/jobs/components/stages_dropdown.vue
+++ b/app/assets/javascripts/jobs/components/stages_dropdown.vue
@@ -2,7 +2,6 @@
import _ from 'underscore';
import { GlLink } from '@gitlab/ui';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
-import PipelineLink from '~/vue_shared/components/ci_pipeline_link.vue';
import Icon from '~/vue_shared/components/icon.vue';
export default {
@@ -10,7 +9,6 @@ export default {
CiIcon,
Icon,
GlLink,
- PipelineLink,
},
props: {
pipeline: {
@@ -50,12 +48,9 @@ export default {
<ci-icon :status="pipeline.details.status" class="vertical-align-middle" />
<span class="font-weight-bold">{{ s__('Job|Pipeline') }}</span>
- <pipeline-link
- :href="pipeline.path"
- :pipeline-id="pipeline.id"
- :pipeline-iid="pipeline.iid"
- class="js-pipeline-path link-commit qa-pipeline-path"
- />
+ <gl-link :href="pipeline.path" class="js-pipeline-path link-commit qa-pipeline-path"
+ >#{{ pipeline.id }}</gl-link
+ >
<template v-if="hasRef">
{{ s__('Job|for') }}
diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js
index e5cf43e8289..b6868e63716 100644
--- a/app/assets/javascripts/merge_request_tabs.js
+++ b/app/assets/javascripts/merge_request_tabs.js
@@ -147,14 +147,14 @@ export default class MergeRequestTabs {
e.stopImmediatePropagation();
e.preventDefault();
- const { action } = e.currentTarget.dataset;
+ const { action } = e.currentTarget.dataset || {};
- if (action) {
- const href = e.currentTarget.getAttribute('href');
- this.tabShown(action, href);
- } else if (isMetaClick(e)) {
+ if (isMetaClick(e)) {
const targetLink = e.currentTarget.getAttribute('href');
window.open(targetLink, '_blank');
+ } else if (action) {
+ const href = e.currentTarget.getAttribute('href');
+ this.tabShown(action, href);
}
}
}
diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue
index d716fc211ca..0a652329dfe 100644
--- a/app/assets/javascripts/monitoring/components/dashboard.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard.vue
@@ -70,7 +70,7 @@ export default {
type: String,
required: true,
},
- deploymentEndpoint: {
+ deploymentsEndpoint: {
type: String,
required: false,
default: null,
@@ -148,7 +148,7 @@ export default {
this.setEndpoints({
metricsEndpoint: this.metricsEndpoint,
environmentsEndpoint: this.environmentsEndpoint,
- deploymentsEndpoint: this.deploymentEndpoint,
+ deploymentsEndpoint: this.deploymentsEndpoint,
dashboardEndpoint: this.dashboardEndpoint,
});
@@ -280,9 +280,8 @@ export default {
<gl-button
v-gl-modal-directive="$options.addMetric.modalId"
class="js-add-metric-button text-success border-success"
+ >{{ $options.addMetric.title }}</gl-button
>
- {{ $options.addMetric.title }}
- </gl-button>
<gl-modal
ref="addMetricModal"
:modal-id="$options.addMetric.modalId"
@@ -296,16 +295,13 @@ export default {
/>
</form>
<div slot="modal-footer">
- <gl-button @click="hideAddMetricModal">
- {{ __('Cancel') }}
- </gl-button>
+ <gl-button @click="hideAddMetricModal">{{ __('Cancel') }}</gl-button>
<gl-button
:disabled="!formIsValid"
variant="success"
@click="submitCustomMetricsForm"
+ >{{ __('Save changes') }}</gl-button
>
- {{ __('Save changes') }}
- </gl-button>
</div>
</gl-modal>
</div>
diff --git a/app/assets/javascripts/monitoring/stores/actions.js b/app/assets/javascripts/monitoring/stores/actions.js
index 49ce722b838..f41e215cb5d 100644
--- a/app/assets/javascripts/monitoring/stores/actions.js
+++ b/app/assets/javascripts/monitoring/stores/actions.js
@@ -164,10 +164,10 @@ export const fetchPrometheusMetrics = ({ state, commit, dispatch }, params) => {
};
export const fetchDeploymentsData = ({ state, dispatch }) => {
- if (!state.deploymentEndpoint) {
+ if (!state.deploymentsEndpoint) {
return Promise.resolve([]);
}
- return backOffRequest(() => axios.get(state.deploymentEndpoint))
+ return backOffRequest(() => axios.get(state.deploymentsEndpoint))
.then(resp => resp.data)
.then(response => {
if (!response || !response.deployments) {
diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue
index 09ecb695214..042ed196933 100644
--- a/app/assets/javascripts/notes/components/note_form.vue
+++ b/app/assets/javascripts/notes/components/note_form.vue
@@ -77,6 +77,11 @@ export default {
required: false,
default: '',
},
+ showSuggestPopover: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
let updatedNoteBody = this.noteBody;
@@ -247,6 +252,8 @@ export default {
:can-suggest="canSuggest"
:add-spacing-classes="false"
:help-page-path="helpPagePath"
+ :show-suggest-popover="showSuggestPopover"
+ @handleSuggestDismissed="() => $emit('handleSuggestDismissed')"
>
<textarea
id="note_note"
@@ -303,7 +310,7 @@ export default {
{{ __('Add comment now') }}
</button>
<button
- class="btn btn-cancel note-edit-cancel js-close-discussion-note-form"
+ class="btn note-edit-cancel js-close-discussion-note-form"
type="button"
@click="cancelHandler()"
>
diff --git a/app/assets/javascripts/pipelines/components/header_component.vue b/app/assets/javascripts/pipelines/components/header_component.vue
index f3a71ee434c..b2e365e5cde 100644
--- a/app/assets/javascripts/pipelines/components/header_component.vue
+++ b/app/assets/javascripts/pipelines/components/header_component.vue
@@ -83,8 +83,6 @@ export default {
v-if="shouldRenderContent"
:status="status"
:item-id="pipeline.id"
- :item-iid="pipeline.iid"
- :item-id-tooltip="__('Pipeline ID (IID)')"
:time="pipeline.created_at"
:user="pipeline.user"
:actions="actions"
diff --git a/app/assets/javascripts/pipelines/components/pipeline_url.vue b/app/assets/javascripts/pipelines/components/pipeline_url.vue
index 00c02e15562..c41ecab1294 100644
--- a/app/assets/javascripts/pipelines/components/pipeline_url.vue
+++ b/app/assets/javascripts/pipelines/components/pipeline_url.vue
@@ -2,7 +2,6 @@
import { GlLink, GlTooltipDirective } from '@gitlab/ui';
import _ from 'underscore';
import { __, sprintf } from '~/locale';
-import PipelineLink from '~/vue_shared/components/ci_pipeline_link.vue';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import popover from '~/vue_shared/directives/popover';
@@ -20,7 +19,6 @@ export default {
components: {
UserAvatarLink,
GlLink,
- PipelineLink,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -61,13 +59,10 @@ export default {
};
</script>
<template>
- <div class="table-section section-10 d-none d-sm-none d-md-block pipeline-tags section-wrap">
- <pipeline-link
- :href="pipeline.path"
- :pipeline-id="pipeline.id"
- :pipeline-iid="pipeline.iid"
- class="js-pipeline-url-link"
- />
+ <div class="table-section section-10 d-none d-sm-none d-md-block pipeline-tags">
+ <gl-link :href="pipeline.path" class="js-pipeline-url-link">
+ <span class="pipeline-id">#{{ pipeline.id }}</span>
+ </gl-link>
<div class="label-container">
<span
v-if="pipeline.flags.latest"
diff --git a/app/assets/javascripts/visual_review_toolbar/components/comment.js b/app/assets/javascripts/visual_review_toolbar/components/comment.js
new file mode 100644
index 00000000000..2fec96d1435
--- /dev/null
+++ b/app/assets/javascripts/visual_review_toolbar/components/comment.js
@@ -0,0 +1,132 @@
+import { BLACK, COMMENT_BOX, MUTED, LOGOUT } from './constants';
+import { clearNote, note, postError } from './note';
+import { buttonClearStyles, selectCommentBox, selectCommentButton, selectNote } from './utils';
+
+const comment = `
+ <div>
+ <textarea id="${COMMENT_BOX}" name="${COMMENT_BOX}" rows="3" placeholder="Enter your feedback or idea" class="gitlab-input" aria-required="true"></textarea>
+ ${note}
+ <p class="gitlab-metadata-note">Additional metadata will be included: browser, OS, current page, user agent, and viewport dimensions.</p>
+ </div>
+ <div class="gitlab-button-wrapper">
+ <button class="gitlab-button gitlab-button-secondary" style="${buttonClearStyles}" type="button" id="${LOGOUT}"> Logout </button>
+ <button class="gitlab-button gitlab-button-success" style="${buttonClearStyles}" type="button" id="gitlab-comment-button"> Send feedback </button>
+ </div>
+`;
+
+const resetCommentBox = () => {
+ const commentBox = selectCommentBox();
+ const commentButton = selectCommentButton();
+
+ /* eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings */
+ commentButton.innerText = 'Send feedback';
+ commentButton.classList.replace('gitlab-button-secondary', 'gitlab-button-success');
+ commentButton.style.opacity = 1;
+
+ commentBox.style.pointerEvents = 'auto';
+ commentBox.style.color = BLACK;
+};
+
+const resetCommentButton = () => {
+ const commentBox = selectCommentBox();
+ const currentNote = selectNote();
+
+ commentBox.value = '';
+ currentNote.innerText = '';
+};
+
+const resetComment = () => {
+ resetCommentBox();
+ resetCommentButton();
+};
+
+const confirmAndClear = mergeRequestId => {
+ const commentButton = selectCommentButton();
+ const currentNote = selectNote();
+
+ /* eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings */
+ commentButton.innerText = 'Feedback sent';
+ /* eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings */
+ currentNote.innerText = `Your comment was successfully posted to merge request #${mergeRequestId}`;
+ setTimeout(resetComment, 2000);
+};
+
+const setInProgressState = () => {
+ const commentButton = selectCommentButton();
+ const commentBox = selectCommentBox();
+
+ /* eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings */
+ commentButton.innerText = 'Sending feedback';
+ commentButton.classList.replace('gitlab-button-success', 'gitlab-button-secondary');
+ commentButton.style.opacity = 0.5;
+ commentBox.style.color = MUTED;
+ commentBox.style.pointerEvents = 'none';
+};
+
+const postComment = ({
+ href,
+ platform,
+ browser,
+ userAgent,
+ innerWidth,
+ innerHeight,
+ projectId,
+ mergeRequestId,
+ mrUrl,
+ token,
+}) => {
+ // Clear any old errors
+ clearNote(COMMENT_BOX);
+
+ setInProgressState();
+
+ const commentText = selectCommentBox().value.trim();
+
+ if (!commentText) {
+ /* eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings */
+ postError('Your comment appears to be empty.', COMMENT_BOX);
+ resetCommentBox();
+ return;
+ }
+
+ const detailText = `
+ \n
+<details>
+ <summary>Metadata</summary>
+ Posted from ${href} | ${platform} | ${browser} | ${innerWidth} x ${innerHeight}.
+ <br /><br />
+ <em>User agent: ${userAgent}</em>
+</details>
+ `;
+
+ const url = `
+ ${mrUrl}/api/v4/projects/${projectId}/merge_requests/${mergeRequestId}/discussions`;
+
+ const body = `${commentText} ${detailText}`;
+
+ fetch(url, {
+ method: 'POST',
+ headers: {
+ 'PRIVATE-TOKEN': token,
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({ body }),
+ })
+ .then(response => {
+ if (response.ok) {
+ confirmAndClear(mergeRequestId);
+ return;
+ }
+
+ throw new Error(`${response.status}: ${response.statusText}`);
+ })
+ .catch(err => {
+ postError(
+ `Your comment could not be sent. Please try again. Error: ${err.message}`,
+ COMMENT_BOX,
+ );
+ resetCommentBox();
+ });
+};
+
+export { comment, postComment };
diff --git a/app/assets/javascripts/visual_review_toolbar/components/constants.js b/app/assets/javascripts/visual_review_toolbar/components/constants.js
new file mode 100644
index 00000000000..32ed1153515
--- /dev/null
+++ b/app/assets/javascripts/visual_review_toolbar/components/constants.js
@@ -0,0 +1,37 @@
+// component selectors
+const COLLAPSE_BUTTON = 'gitlab-collapse';
+const COMMENT_BOX = 'gitlab-comment';
+const COMMENT_BUTTON = 'gitlab-comment-button';
+const FORM = 'gitlab-form-wrapper';
+const LOGIN = 'gitlab-login';
+const LOGOUT = 'gitlab-logout-button';
+const NOTE = 'gitlab-validation-note';
+const REMEMBER_TOKEN = 'gitlab-remember_token';
+const REVIEW_CONTAINER = 'gitlab-review-container';
+const TOKEN_BOX = 'gitlab-token';
+
+// colors — these are applied programmatically
+// rest of styles belong in ./styles
+const BLACK = 'rgba(46, 46, 46, 1)';
+const CLEAR = 'rgba(255, 255, 255, 0)';
+const MUTED = 'rgba(223, 223, 223, 0.5)';
+const RED = 'rgba(219, 59, 33, 1)';
+const WHITE = 'rgba(255, 255, 255, 1)';
+
+export {
+ COLLAPSE_BUTTON,
+ COMMENT_BOX,
+ COMMENT_BUTTON,
+ FORM,
+ LOGIN,
+ LOGOUT,
+ NOTE,
+ REMEMBER_TOKEN,
+ REVIEW_CONTAINER,
+ TOKEN_BOX,
+ BLACK,
+ CLEAR,
+ MUTED,
+ RED,
+ WHITE,
+};
diff --git a/app/assets/javascripts/visual_review_toolbar/components/index.js b/app/assets/javascripts/visual_review_toolbar/components/index.js
new file mode 100644
index 00000000000..43581818152
--- /dev/null
+++ b/app/assets/javascripts/visual_review_toolbar/components/index.js
@@ -0,0 +1,23 @@
+import { comment, postComment } from './comment';
+import { COLLAPSE_BUTTON, COMMENT_BUTTON, LOGIN, LOGOUT, REVIEW_CONTAINER } from './constants';
+import { authorizeUser, login } from './login';
+import { selectContainer } from './utils';
+import { form, logoutUser, toggleForm } from './wrapper';
+import { collapseButton } from './wrapper_icons';
+
+export {
+ authorizeUser,
+ collapseButton,
+ comment,
+ form,
+ login,
+ logoutUser,
+ postComment,
+ selectContainer,
+ toggleForm,
+ COLLAPSE_BUTTON,
+ COMMENT_BUTTON,
+ LOGIN,
+ LOGOUT,
+ REVIEW_CONTAINER,
+};
diff --git a/app/assets/javascripts/visual_review_toolbar/components/login.js b/app/assets/javascripts/visual_review_toolbar/components/login.js
new file mode 100644
index 00000000000..ce713cdc520
--- /dev/null
+++ b/app/assets/javascripts/visual_review_toolbar/components/login.js
@@ -0,0 +1,52 @@
+import { LOGIN, REMEMBER_TOKEN, TOKEN_BOX } from './constants';
+import { clearNote, note, postError } from './note';
+import { buttonClearStyles, selectRemember, selectToken } from './utils';
+import { addCommentForm } from './wrapper';
+
+const login = `
+ <div>
+ <label for="${TOKEN_BOX}" class="gitlab-label">Enter your <a class="gitlab-link" href="https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html">personal access token</a></label>
+ <input class="gitlab-input" type="password" id="${TOKEN_BOX}" name="${TOKEN_BOX}" aria-required="true" autocomplete="current-password">
+ ${note}
+ </div>
+ <div class="gitlab-checkbox-wrapper">
+ <input type="checkbox" id="${REMEMBER_TOKEN}" name="${REMEMBER_TOKEN}" value="remember">
+ <label for="${REMEMBER_TOKEN}" class="gitlab-checkbox-label">Remember me</label>
+ </div>
+ <div class="gitlab-button-wrapper">
+ <button class="gitlab-button-wide gitlab-button gitlab-button-success" style="${buttonClearStyles}" type="button" id="${LOGIN}"> Submit </button>
+ </div>
+`;
+
+const storeToken = (token, state) => {
+ const { localStorage } = window;
+ const rememberMe = selectRemember().checked;
+
+ // All the browsers we support have localStorage, so let's silently fail
+ // and go on with the rest of the functionality.
+ try {
+ if (rememberMe) {
+ localStorage.setItem('token', token);
+ }
+ } finally {
+ state.token = token;
+ }
+};
+
+const authorizeUser = state => {
+ // Clear any old errors
+ clearNote(TOKEN_BOX);
+
+ const token = selectToken().value;
+
+ if (!token) {
+ /* eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings */
+ postError('Please enter your token.', TOKEN_BOX);
+ return;
+ }
+
+ storeToken(token, state);
+ addCommentForm();
+};
+
+export { authorizeUser, login };
diff --git a/app/assets/javascripts/visual_review_toolbar/components/note.js b/app/assets/javascripts/visual_review_toolbar/components/note.js
new file mode 100644
index 00000000000..dfebf58fd95
--- /dev/null
+++ b/app/assets/javascripts/visual_review_toolbar/components/note.js
@@ -0,0 +1,27 @@
+import { NOTE, RED } from './constants';
+import { selectById, selectNote } from './utils';
+
+const note = `
+ <p id=${NOTE} class='gitlab-message'></p>
+`;
+
+const clearNote = inputId => {
+ const currentNote = selectNote();
+ currentNote.innerText = '';
+ currentNote.style.color = '';
+
+ if (inputId) {
+ const field = document.getElementById(inputId);
+ field.style.borderColor = '';
+ }
+};
+
+const postError = (message, inputId) => {
+ const currentNote = selectNote();
+ const field = selectById(inputId);
+ field.style.borderColor = RED;
+ currentNote.style.color = RED;
+ currentNote.innerText = message;
+};
+
+export { clearNote, note, postError };
diff --git a/app/assets/javascripts/visual_review_toolbar/components/utils.js b/app/assets/javascripts/visual_review_toolbar/components/utils.js
new file mode 100644
index 00000000000..7bc2e5a905b
--- /dev/null
+++ b/app/assets/javascripts/visual_review_toolbar/components/utils.js
@@ -0,0 +1,42 @@
+/* global document */
+
+import {
+ COLLAPSE_BUTTON,
+ COMMENT_BOX,
+ COMMENT_BUTTON,
+ FORM,
+ NOTE,
+ REMEMBER_TOKEN,
+ REVIEW_CONTAINER,
+ TOKEN_BOX,
+} from './constants';
+
+// this style must be applied inline in a handful of components
+/* eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings */
+const buttonClearStyles = `
+ -webkit-appearance: none;
+`;
+
+// selector functions to abstract out a little
+const selectById = id => document.getElementById(id);
+const selectCollapseButton = () => document.getElementById(COLLAPSE_BUTTON);
+const selectCommentBox = () => document.getElementById(COMMENT_BOX);
+const selectCommentButton = () => document.getElementById(COMMENT_BUTTON);
+const selectContainer = () => document.getElementById(REVIEW_CONTAINER);
+const selectForm = () => document.getElementById(FORM);
+const selectNote = () => document.getElementById(NOTE);
+const selectRemember = () => document.getElementById(REMEMBER_TOKEN);
+const selectToken = () => document.getElementById(TOKEN_BOX);
+
+export {
+ buttonClearStyles,
+ selectById,
+ selectCollapseButton,
+ selectContainer,
+ selectCommentBox,
+ selectCommentButton,
+ selectForm,
+ selectNote,
+ selectRemember,
+ selectToken,
+};
diff --git a/app/assets/javascripts/visual_review_toolbar/components/wrapper.js b/app/assets/javascripts/visual_review_toolbar/components/wrapper.js
new file mode 100644
index 00000000000..233b7ec496c
--- /dev/null
+++ b/app/assets/javascripts/visual_review_toolbar/components/wrapper.js
@@ -0,0 +1,82 @@
+import { comment } from './comment';
+import { CLEAR, FORM, WHITE } from './constants';
+import { login } from './login';
+import { selectCollapseButton, selectContainer, selectForm } from './utils';
+import { commentIcon, compressIcon } from './wrapper_icons';
+
+const form = content => `
+ <form id=${FORM}>
+ ${content}
+ </form>
+`;
+
+const addCommentForm = () => {
+ const formWrapper = selectForm();
+ formWrapper.innerHTML = comment;
+};
+
+const addLoginForm = () => {
+ const formWrapper = selectForm();
+ formWrapper.innerHTML = login;
+};
+
+function logoutUser() {
+ const { localStorage } = window;
+
+ // All the browsers we support have localStorage, so let's silently fail
+ // and go on with the rest of the functionality.
+ try {
+ localStorage.removeItem('token');
+ } catch (err) {
+ return;
+ }
+
+ addLoginForm();
+}
+
+function toggleForm() {
+ const container = selectContainer();
+ const collapseButton = selectCollapseButton();
+ const currentForm = selectForm();
+ const OPEN = 'open';
+ const CLOSED = 'closed';
+
+ /*
+ You may wonder why we spread the arrays before we reverse them.
+ In the immortal words of MDN,
+ Careful: reverse is destructive. It also changes the original array
+ */
+
+ const openButtonClasses = ['gitlab-collapse-closed', 'gitlab-collapse-open'];
+ const closedButtonClasses = [...openButtonClasses].reverse();
+ const openContainerClasses = ['gitlab-closed-wrapper', 'gitlab-open-wrapper'];
+ const closedContainerClasses = [...openContainerClasses].reverse();
+
+ const stateVals = {
+ [OPEN]: {
+ buttonClasses: openButtonClasses,
+ containerClasses: openContainerClasses,
+ icon: compressIcon,
+ display: 'flex',
+ backgroundColor: WHITE,
+ },
+ [CLOSED]: {
+ buttonClasses: closedButtonClasses,
+ containerClasses: closedContainerClasses,
+ icon: commentIcon,
+ display: 'none',
+ backgroundColor: CLEAR,
+ },
+ };
+
+ const nextState = collapseButton.classList.contains('gitlab-collapse-open') ? CLOSED : OPEN;
+ const currentVals = stateVals[nextState];
+
+ container.classList.replace(...currentVals.containerClasses);
+ container.style.backgroundColor = currentVals.backgroundColor;
+ currentForm.style.display = currentVals.display;
+ collapseButton.classList.replace(...currentVals.buttonClasses);
+ collapseButton.innerHTML = currentVals.icon;
+}
+
+export { addCommentForm, addLoginForm, form, logoutUser, toggleForm };
diff --git a/app/assets/javascripts/visual_review_toolbar/components/wrapper_icons.js b/app/assets/javascripts/visual_review_toolbar/components/wrapper_icons.js
new file mode 100644
index 00000000000..b686fd4f5c2
--- /dev/null
+++ b/app/assets/javascripts/visual_review_toolbar/components/wrapper_icons.js
@@ -0,0 +1,15 @@
+import { buttonClearStyles } from './utils';
+
+const commentIcon = `
+ <svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><title>icn/comment</title><path d="M4 11.132l1.446-.964A1 1 0 0 1 6 10h5a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1H5a1 1 0 0 0-1 1v6.132zM6.303 12l-2.748 1.832A1 1 0 0 1 2 13V5a3 3 0 0 1 3-3h6a3 3 0 0 1 3 3v4a3 3 0 0 1-3 3H6.303z" id="gitlab-comment-icon"/></svg>
+`;
+
+const compressIcon = `
+ <svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><title>icn/compress</title><path d="M5.27 12.182l-1.562 1.561a1 1 0 0 1-1.414 0h-.001a1 1 0 0 1 0-1.415l1.56-1.56L2.44 9.353a.5.5 0 0 1 .353-.854H7.09a.5.5 0 0 1 .5.5v4.294a.5.5 0 0 1-.853.353l-1.467-1.465zm6.911-6.914l1.464 1.464a.5.5 0 0 1-.353.854H8.999a.5.5 0 0 1-.5-.5V2.793a.5.5 0 0 1 .854-.354l1.414 1.415 1.56-1.561a1 1 0 1 1 1.415 1.414l-1.561 1.56z" id="gitlab-compress-icon"/></svg>
+`;
+
+const collapseButton = `
+ <button id='gitlab-collapse' style='${buttonClearStyles}' class='gitlab-button gitlab-button-secondary gitlab-collapse gitlab-collapse-open'>${compressIcon}</button>
+`;
+
+export { commentIcon, compressIcon, collapseButton };
diff --git a/app/assets/javascripts/visual_review_toolbar/index.js b/app/assets/javascripts/visual_review_toolbar/index.js
index 91d0382feac..941d77e25b4 100644
--- a/app/assets/javascripts/visual_review_toolbar/index.js
+++ b/app/assets/javascripts/visual_review_toolbar/index.js
@@ -1,2 +1,37 @@
import './styles/toolbar.css';
-import 'vendor/visual_review_toolbar';
+
+import { form, selectContainer, REVIEW_CONTAINER } from './components';
+import { debounce, eventLookup, getInitialView, initializeState, updateWindowSize } from './store';
+
+/*
+
+ Welcome to the visual review toolbar files. A few useful notes:
+
+ - These files build a static script that is served from our webpack
+ assets folder. (https://gitlab.com/assets/webpack/visual_review_toolbar.js)
+
+ - To compile this file, run `yarn webpack-vrt`.
+
+ - Vue is not used in these files because we do not want to ask users to
+ install another library at this time. It's all pure vanilla javascript.
+
+*/
+
+window.addEventListener('load', () => {
+ initializeState(window, document);
+
+ const { content, toggleButton } = getInitialView(window);
+ const container = document.createElement('div');
+
+ container.setAttribute('id', REVIEW_CONTAINER);
+ container.insertAdjacentHTML('beforeend', toggleButton);
+ container.insertAdjacentHTML('beforeend', form(content));
+
+ document.body.insertBefore(container, document.body.firstChild);
+
+ selectContainer().addEventListener('click', event => {
+ eventLookup(event)();
+ });
+
+ window.addEventListener('resize', debounce(updateWindowSize.bind(null, window), 200));
+});
diff --git a/app/assets/javascripts/visual_review_toolbar/store/events.js b/app/assets/javascripts/visual_review_toolbar/store/events.js
new file mode 100644
index 00000000000..93996be8473
--- /dev/null
+++ b/app/assets/javascripts/visual_review_toolbar/store/events.js
@@ -0,0 +1,36 @@
+import {
+ authorizeUser,
+ logoutUser,
+ postComment,
+ toggleForm,
+ COLLAPSE_BUTTON,
+ COMMENT_BUTTON,
+ LOGIN,
+ LOGOUT,
+} from '../components';
+
+import { state } from './state';
+
+const noop = () => {};
+
+const eventLookup = ({ target: { id } }) => {
+ switch (id) {
+ case COLLAPSE_BUTTON:
+ return toggleForm;
+ case COMMENT_BUTTON:
+ return postComment.bind(null, state);
+ case LOGIN:
+ return authorizeUser.bind(null, state);
+ case LOGOUT:
+ return logoutUser;
+ default:
+ return noop;
+ }
+};
+
+const updateWindowSize = wind => {
+ state.innerWidth = wind.innerWidth;
+ state.innerHeight = wind.innerHeight;
+};
+
+export { eventLookup, updateWindowSize };
diff --git a/app/assets/javascripts/visual_review_toolbar/store/index.js b/app/assets/javascripts/visual_review_toolbar/store/index.js
new file mode 100644
index 00000000000..7143588c0bf
--- /dev/null
+++ b/app/assets/javascripts/visual_review_toolbar/store/index.js
@@ -0,0 +1,5 @@
+import { eventLookup, updateWindowSize } from './events';
+import { getInitialView, initializeState } from './state';
+import debounce from './utils';
+
+export { debounce, eventLookup, getInitialView, initializeState, updateWindowSize };
diff --git a/app/assets/javascripts/visual_review_toolbar/store/state.js b/app/assets/javascripts/visual_review_toolbar/store/state.js
new file mode 100644
index 00000000000..f5ede6e85b2
--- /dev/null
+++ b/app/assets/javascripts/visual_review_toolbar/store/state.js
@@ -0,0 +1,77 @@
+import { comment, login, collapseButton } from '../components';
+
+const state = {
+ browser: '',
+ href: '',
+ innerWidth: '',
+ innerHeight: '',
+ mergeRequestId: '',
+ mrUrl: '',
+ platform: '',
+ projectId: '',
+ userAgent: '',
+ token: '',
+};
+
+// adapted from https://developer.mozilla.org/en-US/docs/Web/API/Window/navigator#Example_2_Browser_detect_and_return_an_index
+const getBrowserId = sUsrAg => {
+ /* eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings */
+ const aKeys = ['MSIE', 'Edge', 'Firefox', 'Safari', 'Chrome', 'Opera'];
+ let nIdx = aKeys.length - 1;
+
+ for (nIdx; nIdx > -1 && sUsrAg.indexOf(aKeys[nIdx]) === -1; nIdx -= 1);
+ return aKeys[nIdx];
+};
+
+const initializeState = (wind, doc) => {
+ const {
+ innerWidth,
+ innerHeight,
+ location: { href },
+ navigator: { platform, userAgent },
+ } = wind;
+
+ const browser = getBrowserId(userAgent);
+
+ const scriptEl = doc.getElementById('review-app-toolbar-script');
+ const { projectId, mergeRequestId, mrUrl } = scriptEl.dataset;
+
+ // This mutates our default state object above. It's weird but it makes the linter happy.
+ Object.assign(state, {
+ browser,
+ href,
+ innerWidth,
+ innerHeight,
+ mergeRequestId,
+ mrUrl,
+ platform,
+ projectId,
+ userAgent,
+ });
+};
+
+function getInitialView({ localStorage }) {
+ const loginView = {
+ content: login,
+ toggleButton: collapseButton,
+ };
+
+ const commentView = {
+ content: comment,
+ toggleButton: collapseButton,
+ };
+
+ try {
+ const token = localStorage.getItem('token');
+
+ if (token) {
+ state.token = token;
+ return commentView;
+ }
+ return loginView;
+ } catch (err) {
+ return loginView;
+ }
+}
+
+export { initializeState, getInitialView, state };
diff --git a/app/assets/javascripts/visual_review_toolbar/store/utils.js b/app/assets/javascripts/visual_review_toolbar/store/utils.js
new file mode 100644
index 00000000000..5cf145351b3
--- /dev/null
+++ b/app/assets/javascripts/visual_review_toolbar/store/utils.js
@@ -0,0 +1,15 @@
+const debounce = (fn, time) => {
+ let current;
+
+ const debounced = () => {
+ if (current) {
+ clearTimeout(current);
+ }
+
+ current = setTimeout(fn, time);
+ };
+
+ return debounced;
+};
+
+export default debounce;
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 c377c16fb13..f5fa68308bc 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
@@ -5,7 +5,6 @@ import { sprintf, __ } from '~/locale';
import PipelineStage from '~/pipelines/components/stage.vue';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
import Icon from '~/vue_shared/components/icon.vue';
-import PipelineLink from '~/vue_shared/components/ci_pipeline_link.vue';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
import mrWidgetPipelineMixin from 'ee_else_ce/vue_merge_request_widget/mixins/mr_widget_pipeline';
@@ -17,7 +16,6 @@ export default {
Icon,
TooltipOnTruncate,
GlLink,
- PipelineLink,
LinkedPipelinesMiniList: () =>
import('ee_component/vue_shared/components/linked_pipelines_mini_list.vue'),
},
@@ -114,12 +112,9 @@ export default {
<div class="media-body">
<div class="font-weight-bold js-pipeline-info-container">
{{ s__('Pipeline|Pipeline') }}
- <pipeline-link
- :href="pipeline.path"
- :pipeline-id="pipeline.id"
- :pipeline-iid="pipeline.iid"
- class="pipeline-id pipeline-iid font-weight-normal"
- />
+ <gl-link :href="pipeline.path" class="pipeline-id font-weight-normal pipeline-number"
+ >#{{ pipeline.id }}</gl-link
+ >
{{ pipeline.details.status.label }}
<template v-if="hasCommitInfo">
{{ s__('Pipeline|for') }}
diff --git a/app/assets/javascripts/vue_shared/components/ci_pipeline_link.vue b/app/assets/javascripts/vue_shared/components/ci_pipeline_link.vue
deleted file mode 100644
index eae4c06467c..00000000000
--- a/app/assets/javascripts/vue_shared/components/ci_pipeline_link.vue
+++ /dev/null
@@ -1,32 +0,0 @@
-<script>
-import { GlLink, GlTooltipDirective } from '@gitlab/ui';
-
-export default {
- components: {
- GlLink,
- },
- directives: {
- GlTooltip: GlTooltipDirective,
- },
- props: {
- href: {
- type: String,
- required: true,
- },
- pipelineId: {
- type: Number,
- required: true,
- },
- pipelineIid: {
- type: Number,
- required: true,
- },
- },
-};
-</script>
-<template>
- <gl-link v-gl-tooltip :href="href" :title="__('Pipeline ID (IID)')">
- <span class="pipeline-id">#{{ pipelineId }}</span>
- <span class="pipeline-iid">(#{{ pipelineIid }})</span>
- </gl-link>
-</template>
diff --git a/app/assets/javascripts/vue_shared/components/header_ci_component.vue b/app/assets/javascripts/vue_shared/components/header_ci_component.vue
index 0bac63b1062..3f45dc7853b 100644
--- a/app/assets/javascripts/vue_shared/components/header_ci_component.vue
+++ b/app/assets/javascripts/vue_shared/components/header_ci_component.vue
@@ -37,16 +37,6 @@ export default {
type: Number,
required: true,
},
- itemIid: {
- type: Number,
- required: false,
- default: null,
- },
- itemIdTooltip: {
- type: String,
- required: false,
- default: '',
- },
time: {
type: String,
required: true,
@@ -95,12 +85,7 @@ export default {
<section class="header-main-content">
<ci-icon-badge :status="status" />
- <strong v-gl-tooltip :title="itemIdTooltip">
- {{ itemName }} #{{ itemId }}
- <template v-if="itemIid"
- >(#{{ itemIid }})</template
- >
- </strong>
+ <strong> {{ itemName }} #{{ itemId }} </strong>
<template v-if="shouldRenderTriggeredLabel">
triggered
@@ -111,8 +96,9 @@ export default {
<timeago-tooltip :time="time" />
+ by
+
<template v-if="user">
- by
<gl-link
v-gl-tooltip
:href="user.path"
diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue
index 0f3b3568414..3bdc0bb8ebd 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/field.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue
@@ -67,6 +67,11 @@ export default {
required: false,
default: '',
},
+ showSuggestPopover: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
@@ -194,8 +199,10 @@ export default {
:preview-markdown="previewMarkdown"
:line-content="lineContent"
:can-suggest="canSuggest"
+ :show-suggest-popover="showSuggestPopover"
@preview-markdown="showPreviewTab"
@write-markdown="showWriteTab"
+ @handleSuggestDismissed="() => $emit('handleSuggestDismissed')"
/>
<div v-show="!previewMarkdown" class="md-write-holder">
<div class="zen-backdrop">
diff --git a/app/assets/javascripts/vue_shared/components/markdown/header.vue b/app/assets/javascripts/vue_shared/components/markdown/header.vue
index a5a5b2ef415..56a16c9e4d6 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/header.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/header.vue
@@ -1,6 +1,6 @@
<script>
import $ from 'jquery';
-import { GlTooltipDirective } from '@gitlab/ui';
+import { GlPopover, GlButton, GlTooltipDirective } from '@gitlab/ui';
import ToolbarButton from './toolbar_button.vue';
import Icon from '../icon.vue';
@@ -8,6 +8,8 @@ export default {
components: {
ToolbarButton,
Icon,
+ GlPopover,
+ GlButton,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -27,6 +29,11 @@ export default {
required: false,
default: true,
},
+ showSuggestPopover: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
computed: {
mdTable() {
@@ -70,6 +77,9 @@ export default {
this.$emit('write-markdown');
},
+ handleSuggestDismissed() {
+ this.$emit('handleSuggestDismissed');
+ },
},
};
</script>
@@ -93,66 +103,92 @@ export default {
</button>
</li>
<li :class="{ active: !previewMarkdown }" class="md-header-toolbar">
- <toolbar-button tag="**" :button-title="__('Add bold text')" icon="bold" />
- <toolbar-button tag="*" :button-title="__('Add italic text')" icon="italic" />
- <toolbar-button
- :prepend="true"
- tag="> "
- :button-title="__('Insert a quote')"
- icon="quote"
- />
- <toolbar-button tag="`" tag-block="```" :button-title="__('Insert code')" icon="code" />
- <toolbar-button
- tag="[{text}](url)"
- tag-select="url"
- :button-title="__('Add a link')"
- icon="link"
- />
- <toolbar-button
- :prepend="true"
- tag="* "
- :button-title="__('Add a bullet list')"
- icon="list-bulleted"
- />
- <toolbar-button
- :prepend="true"
- tag="1. "
- :button-title="__('Add a numbered list')"
- icon="list-numbered"
- />
- <toolbar-button
- :prepend="true"
- tag="* [ ] "
- :button-title="__('Add a task list')"
- icon="task-done"
- />
- <toolbar-button
- :tag="mdTable"
- :prepend="true"
- :button-title="__('Add a table')"
- icon="table"
- />
- <toolbar-button
- v-if="canSuggest"
- :tag="mdSuggestion"
- :prepend="true"
- :button-title="__('Insert suggestion')"
- :cursor-offset="4"
- :tag-content="lineContent"
- icon="doc-code"
- class="qa-suggestion-btn"
- />
- <button
- v-gl-tooltip
- :aria-label="__('Go full screen')"
- class="toolbar-btn toolbar-fullscreen-btn js-zen-enter"
- data-container="body"
- tabindex="-1"
- :title="__('Go full screen')"
- type="button"
- >
- <icon name="screen-full" />
- </button>
+ <div class="d-inline-block">
+ <toolbar-button tag="**" :button-title="__('Add bold text')" icon="bold" />
+ <toolbar-button tag="*" :button-title="__('Add italic text')" icon="italic" />
+ <toolbar-button
+ :prepend="true"
+ tag="> "
+ :button-title="__('Insert a quote')"
+ icon="quote"
+ />
+ </div>
+ <div class="d-inline-block ml-md-2 ml-0">
+ <template v-if="canSuggest">
+ <toolbar-button
+ ref="suggestButton"
+ :tag="mdSuggestion"
+ :prepend="true"
+ :button-title="__('Insert suggestion')"
+ :cursor-offset="4"
+ :tag-content="lineContent"
+ icon="doc-code"
+ class="qa-suggestion-btn"
+ @click="handleSuggestDismissed"
+ />
+ <gl-popover
+ v-if="showSuggestPopover"
+ :target="() => $refs.suggestButton"
+ :css-classes="['diff-suggest-popover']"
+ placement="bottom"
+ :show="showSuggestPopover"
+ >
+ <strong>{{ __('New! Suggest changes directly') }}</strong>
+ <p class="mb-2">
+ {{ __('Suggest code changes which are immediately applied. Try it out!') }}
+ </p>
+ <gl-button variant="primary" size="sm" @click="handleSuggestDismissed">
+ {{ __('Got it') }}
+ </gl-button>
+ </gl-popover>
+ </template>
+ <toolbar-button tag="`" tag-block="```" :button-title="__('Insert code')" icon="code" />
+ <toolbar-button
+ tag="[{text}](url)"
+ tag-select="url"
+ :button-title="__('Add a link')"
+ icon="link"
+ />
+ </div>
+ <div class="d-inline-block ml-md-2 ml-0">
+ <toolbar-button
+ :prepend="true"
+ tag="* "
+ :button-title="__('Add a bullet list')"
+ icon="list-bulleted"
+ />
+ <toolbar-button
+ :prepend="true"
+ tag="1. "
+ :button-title="__('Add a numbered list')"
+ icon="list-numbered"
+ />
+ <toolbar-button
+ :prepend="true"
+ tag="* [ ] "
+ :button-title="__('Add a task list')"
+ icon="task-done"
+ />
+ <toolbar-button
+ :tag="mdTable"
+ :prepend="true"
+ :button-title="__('Add a table')"
+ icon="table"
+ />
+ </div>
+ <div class="d-inline-block ml-md-2 ml-0">
+ <button
+ v-gl-tooltip
+ :aria-label="__('Go full screen')"
+ class="toolbar-btn toolbar-fullscreen-btn js-zen-enter"
+ data-container="body"
+ tabindex="-1"
+ :title="__('Go full screen')"
+ type="button"
+ >
+ <icon name="screen-full" />
+ </button>
+ </div>
</li>
</ul>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue b/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue
index 4572caa907b..94f78c0c085 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue
@@ -66,6 +66,7 @@ export default {
class="toolbar-btn js-md"
tabindex="-1"
data-container="body"
+ @click="() => $emit('click')"
>
<icon :name="icon" />
</button>
diff --git a/app/assets/javascripts/vue_shared/components/pagination/constants.js b/app/assets/javascripts/vue_shared/components/pagination/constants.js
index c24b142ac7e..748ad178c70 100644
--- a/app/assets/javascripts/vue_shared/components/pagination/constants.js
+++ b/app/assets/javascripts/vue_shared/components/pagination/constants.js
@@ -7,3 +7,7 @@ export const PREV = s__('Pagination|Prev');
export const NEXT = s__('Pagination|Next');
export const FIRST = s__('Pagination|« First');
export const LAST = s__('Pagination|Last »');
+export const LABEL_FIRST_PAGE = s__('Pagination|Go to first page');
+export const LABEL_PREV_PAGE = s__('Pagination|Go to previous page');
+export const LABEL_NEXT_PAGE = s__('Pagination|Go to next page');
+export const LABEL_LAST_PAGE = s__('Pagination|Go to last page');
diff --git a/app/assets/javascripts/vue_shared/components/pagination_links.vue b/app/assets/javascripts/vue_shared/components/pagination_links.vue
index 0b44f8578cb..06097913e91 100644
--- a/app/assets/javascripts/vue_shared/components/pagination_links.vue
+++ b/app/assets/javascripts/vue_shared/components/pagination_links.vue
@@ -1,6 +1,13 @@
<script>
import { GlPagination } from '@gitlab/ui';
-import { s__ } from '../../locale';
+import {
+ PREV,
+ NEXT,
+ LABEL_FIRST_PAGE,
+ LABEL_PREV_PAGE,
+ LABEL_NEXT_PAGE,
+ LABEL_LAST_PAGE,
+} from '~/vue_shared/components/pagination/constants';
export default {
components: {
@@ -16,23 +23,27 @@ export default {
required: true,
},
},
- firstText: s__('Pagination|« First'),
- prevText: s__('Pagination|Prev'),
- nextText: s__('Pagination|Next'),
- lastText: s__('Pagination|Last »'),
+ prevText: PREV,
+ nextText: NEXT,
+ labelFirstPage: LABEL_FIRST_PAGE,
+ labelPrevPage: LABEL_PREV_PAGE,
+ labelNextPage: LABEL_NEXT_PAGE,
+ labelLastPage: LABEL_LAST_PAGE,
};
</script>
<template>
<gl-pagination
v-bind="$attrs"
- :change="change"
- :page="pageInfo.page"
+ :value="pageInfo.page"
:per-page="pageInfo.perPage"
:total-items="pageInfo.total"
- :first-text="$options.firstText"
:prev-text="$options.prevText"
:next-text="$options.nextText"
- :last-text="$options.lastText"
+ :label-first-page="$options.labelFirstPage"
+ :label-prev-page="$options.labelPrevPage"
+ :label-next-page="$options.labelNextPage"
+ :label-last-page="$options.labelLastPage"
+ @input="change"
/>
</template>
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index db09118ba15..1bd5043ed10 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -416,6 +416,7 @@ img.emoji {
.center { text-align: center; }
.block { display: block; }
.flex { display: flex; }
+.vertical-align-top { vertical-align: top; }
.vertical-align-middle { vertical-align: middle; }
.vertical-align-sub { vertical-align: sub; }
.flex-align-self-center { align-self: center; }
diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss
index 2a601afff53..821e6691fe4 100644
--- a/app/assets/stylesheets/framework/forms.scss
+++ b/app/assets/stylesheets/framework/forms.scss
@@ -248,14 +248,24 @@ label {
.gl-form-checkbox {
align-items: baseline;
+ margin-right: 1rem;
+ margin-bottom: 0.25rem;
+
+ .form-check-input {
+ margin-right: 0;
+ }
+
+ .form-check-label {
+ padding-left: $gl-padding-8;
+ }
&.form-check-inline .form-check-input {
align-self: flex-start;
- margin-right: $gl-padding-8;
height: 1.5 * $gl-font-size;
}
- .help-text {
- margin-bottom: 0;
+ .form-check-input:disabled,
+ .form-check-input:disabled ~ .form-check-label {
+ cursor: not-allowed;
}
}
diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss
index bfd96a4bc05..0bf911eec0a 100644
--- a/app/assets/stylesheets/framework/markdown_area.scss
+++ b/app/assets/stylesheets/framework/markdown_area.scss
@@ -154,11 +154,9 @@
}
.toolbar-fullscreen-btn {
- margin-left: $gl-padding;
margin-right: -5px;
@include media-breakpoint-down(xs) {
- margin-left: 0;
margin-right: 0;
}
}
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 07fc655307e..b6a24247d40 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -823,7 +823,7 @@ $issues-analytics-popover-boarder-color: rgba(0, 0, 0, 0.15);
/*
Merge Requests
*/
-$mr-tabs-height: 51px;
+$mr-tabs-height: 48px;
$mr-version-controls-height: 56px;
/*
diff --git a/app/assets/stylesheets/pages/detail_page.scss b/app/assets/stylesheets/pages/detail_page.scss
index c386493231c..62fc7311d94 100644
--- a/app/assets/stylesheets/pages/detail_page.scss
+++ b/app/assets/stylesheets/pages/detail_page.scss
@@ -9,7 +9,6 @@
color: $gl-text-color;
}
- .issue_created_ago,
.author-link {
white-space: nowrap;
}
diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss
index 4ebf1019456..d2d35d91e0b 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/pages/diff.scss
@@ -14,7 +14,7 @@
position: -webkit-sticky;
position: sticky;
top: $mr-file-header-top;
- z-index: 102;
+ z-index: 220;
&::before {
content: '';
@@ -1122,3 +1122,15 @@ table.code {
outline: 0;
}
}
+
+.diff-suggest-popover {
+ &.popover {
+ width: 250px;
+ min-width: 250px;
+ z-index: 210;
+ }
+
+ .popover-header {
+ display: none;
+ }
+}
diff --git a/app/graphql/gitlab_schema.rb b/app/graphql/gitlab_schema.rb
index 2e5bdbd79c8..5615909c4ec 100644
--- a/app/graphql/gitlab_schema.rb
+++ b/app/graphql/gitlab_schema.rb
@@ -7,8 +7,8 @@ class GitlabSchema < GraphQL::Schema
AUTHENTICATED_COMPLEXITY = 250
ADMIN_COMPLEXITY = 300
- DEFAULT_MAX_DEPTH = 10
- AUTHENTICATED_MAX_DEPTH = 15
+ DEFAULT_MAX_DEPTH = 15
+ AUTHENTICATED_MAX_DEPTH = 20
use BatchLoader::GraphQL
use Gitlab::Graphql::Authorize
diff --git a/app/graphql/types/issue_type.rb b/app/graphql/types/issue_type.rb
index dd5133189dc..f2365499eee 100644
--- a/app/graphql/types/issue_type.rb
+++ b/app/graphql/types/issue_type.rb
@@ -4,6 +4,8 @@ module Types
class IssueType < BaseObject
graphql_name 'Issue'
+ implements(Types::Notes::NoteableType)
+
authorize :read_issue
expose_permissions Types::PermissionTypes::Issue
@@ -49,5 +51,7 @@ module Types
field :created_at, Types::TimeType, null: false
field :updated_at, Types::TimeType, null: false
+
+ field :task_completion_status, Types::TaskCompletionStatus, null: false
end
end
diff --git a/app/graphql/types/merge_request_type.rb b/app/graphql/types/merge_request_type.rb
index 85ac3102442..dac4c24cf10 100644
--- a/app/graphql/types/merge_request_type.rb
+++ b/app/graphql/types/merge_request_type.rb
@@ -4,6 +4,8 @@ module Types
class MergeRequestType < BaseObject
graphql_name 'MergeRequest'
+ implements(Types::Notes::NoteableType)
+
authorize :read_merge_request
expose_permissions Types::PermissionTypes::MergeRequest
@@ -53,5 +55,7 @@ module Types
field :head_pipeline, Types::Ci::PipelineType, null: true, method: :actual_head_pipeline
field :pipelines, Types::Ci::PipelineType.connection_type,
resolver: Resolvers::MergeRequestPipelinesResolver
+
+ field :task_completion_status, Types::TaskCompletionStatus, null: false
end
end
diff --git a/app/graphql/types/notes/diff_position_type.rb b/app/graphql/types/notes/diff_position_type.rb
new file mode 100644
index 00000000000..104ccb79bbb
--- /dev/null
+++ b/app/graphql/types/notes/diff_position_type.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+module Types
+ module Notes
+ class DiffPositionType < BaseObject
+ graphql_name 'DiffPosition'
+
+ field :head_sha, GraphQL::STRING_TYPE, null: false,
+ description: "The sha of the head at the time the comment was made"
+ field :base_sha, GraphQL::STRING_TYPE, null: true,
+ description: "The merge base of the branch the comment was made on"
+ field :start_sha, GraphQL::STRING_TYPE, null: false,
+ description: "The sha of the branch being compared against"
+
+ field :file_path, GraphQL::STRING_TYPE, null: false,
+ description: "The path of the file that was changed"
+ field :old_path, GraphQL::STRING_TYPE, null: true,
+ description: "The path of the file on the start sha."
+ field :new_path, GraphQL::STRING_TYPE, null: true,
+ description: "The path of the file on the head sha."
+ field :position_type, Types::Notes::PositionTypeEnum, null: false
+
+ # Fields for text positions
+ field :old_line, GraphQL::INT_TYPE, null: true,
+ description: "The line on start sha that was changed",
+ resolve: -> (position, _args, _ctx) { position.old_line if position.on_text? }
+ field :new_line, GraphQL::INT_TYPE, null: true,
+ description: "The line on head sha that was changed",
+ resolve: -> (position, _args, _ctx) { position.new_line if position.on_text? }
+
+ # Fields for image positions
+ field :x, GraphQL::INT_TYPE, null: true,
+ description: "The X postion on which the comment was made",
+ resolve: -> (position, _args, _ctx) { position.x if position.on_image? }
+ field :y, GraphQL::INT_TYPE, null: true,
+ description: "The Y position on which the comment was made",
+ resolve: -> (position, _args, _ctx) { position.y if position.on_image? }
+ field :width, GraphQL::INT_TYPE, null: true,
+ description: "The total width of the image",
+ resolve: -> (position, _args, _ctx) { position.width if position.on_image? }
+ field :height, GraphQL::INT_TYPE, null: true,
+ description: "The total height of the image",
+ resolve: -> (position, _args, _ctx) { position.height if position.on_image? }
+ end
+ end
+end
diff --git a/app/graphql/types/notes/discussion_type.rb b/app/graphql/types/notes/discussion_type.rb
new file mode 100644
index 00000000000..c4691942f2d
--- /dev/null
+++ b/app/graphql/types/notes/discussion_type.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module Types
+ module Notes
+ class DiscussionType < BaseObject
+ graphql_name 'Discussion'
+
+ authorize :read_note
+
+ field :id, GraphQL::ID_TYPE, null: false
+ field :created_at, Types::TimeType, null: false
+ field :notes, Types::Notes::NoteType.connection_type, null: false, description: "All notes in the discussion"
+ end
+ end
+end
diff --git a/app/graphql/types/notes/note_type.rb b/app/graphql/types/notes/note_type.rb
new file mode 100644
index 00000000000..85c55d16ac2
--- /dev/null
+++ b/app/graphql/types/notes/note_type.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+module Types
+ module Notes
+ class NoteType < BaseObject
+ graphql_name 'Note'
+
+ authorize :read_note
+
+ expose_permissions Types::PermissionTypes::Note
+
+ field :id, GraphQL::ID_TYPE, null: false
+
+ field :project, Types::ProjectType,
+ null: true,
+ description: "The project this note is associated to",
+ resolve: -> (note, args, context) { Gitlab::Graphql::Loaders::BatchModelLoader.new(Project, note.project_id).find }
+
+ field :author, Types::UserType,
+ null: false,
+ description: "The user who wrote this note",
+ resolve: -> (note, args, context) { Gitlab::Graphql::Loaders::BatchModelLoader.new(User, note.author_id).find }
+
+ field :resolved_by, Types::UserType,
+ null: true,
+ description: "The user that resolved the discussion",
+ resolve: -> (note, _args, _context) { Gitlab::Graphql::Loaders::BatchModelLoader.new(User, note.resolved_by_id).find }
+
+ field :system, GraphQL::BOOLEAN_TYPE,
+ null: false,
+ description: "Whether or not this note was created by the system or by a user"
+
+ field :body, GraphQL::STRING_TYPE,
+ null: false,
+ method: :note,
+ description: "The content note itself"
+
+ field :created_at, Types::TimeType, null: false
+ field :updated_at, Types::TimeType, null: false
+ field :discussion, Types::Notes::DiscussionType, null: true, description: "The discussion this note is a part of"
+ field :resolvable, GraphQL::BOOLEAN_TYPE, null: false, method: :resolvable?
+ field :resolved_at, Types::TimeType, null: true, description: "The time the discussion was resolved"
+ field :position, Types::Notes::DiffPositionType, null: true, description: "The position of this note on a diff"
+ end
+ end
+end
diff --git a/app/graphql/types/notes/noteable_type.rb b/app/graphql/types/notes/noteable_type.rb
new file mode 100644
index 00000000000..9f126d67b0d
--- /dev/null
+++ b/app/graphql/types/notes/noteable_type.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module Types
+ module Notes
+ module NoteableType
+ include Types::BaseInterface
+
+ field :notes, Types::Notes::NoteType.connection_type, null: false, description: "All notes on this noteable"
+ field :discussions, Types::Notes::DiscussionType.connection_type, null: false, description: "All discussions on this noteable"
+
+ definition_methods do
+ def resolve_type(object, context)
+ case object
+ when Issue
+ Types::IssueType
+ when MergeRequest
+ Types::MergeRequestType
+ else
+ raise "Unknown GraphQL type for #{object}"
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/notes/position_type_enum.rb b/app/graphql/types/notes/position_type_enum.rb
new file mode 100644
index 00000000000..abdb2cfc804
--- /dev/null
+++ b/app/graphql/types/notes/position_type_enum.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module Types
+ module Notes
+ class PositionTypeEnum < BaseEnum
+ graphql_name 'DiffPositionType'
+ description 'Type of file the position refers to'
+
+ value 'text'
+ value 'image'
+ end
+ end
+end
diff --git a/app/graphql/types/permission_types/note.rb b/app/graphql/types/permission_types/note.rb
new file mode 100644
index 00000000000..a585d3daaa8
--- /dev/null
+++ b/app/graphql/types/permission_types/note.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module Types
+ module PermissionTypes
+ class Note < BasePermissionType
+ graphql_name 'NotePermissions'
+
+ abilities :read_note, :create_note, :admin_note, :resolve_note, :award_emoji
+ end
+ end
+end
diff --git a/app/graphql/types/project_statistics_type.rb b/app/graphql/types/project_statistics_type.rb
index 62537361918..4000c6db280 100644
--- a/app/graphql/types/project_statistics_type.rb
+++ b/app/graphql/types/project_statistics_type.rb
@@ -4,6 +4,8 @@ module Types
class ProjectStatisticsType < BaseObject
graphql_name 'ProjectStatistics'
+ authorize :read_statistics
+
field :commit_count, GraphQL::INT_TYPE, null: false
field :storage_size, GraphQL::INT_TYPE, null: false
diff --git a/app/graphql/types/project_type.rb b/app/graphql/types/project_type.rb
index 2236ffa394d..81914b70c7f 100644
--- a/app/graphql/types/project_type.rb
+++ b/app/graphql/types/project_type.rb
@@ -70,7 +70,7 @@ module Types
field :group, Types::GroupType, null: true
field :statistics, Types::ProjectStatisticsType,
- null: false,
+ null: true,
resolve: -> (obj, _args, _ctx) { Gitlab::Graphql::Loaders::BatchProjectStatisticsLoader.new(obj.id).find }
field :repository, Types::RepositoryType, null: false
diff --git a/app/graphql/types/task_completion_status.rb b/app/graphql/types/task_completion_status.rb
new file mode 100644
index 00000000000..c289802509d
--- /dev/null
+++ b/app/graphql/types/task_completion_status.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module Types
+ class TaskCompletionStatus < BaseObject
+ graphql_name 'TaskCompletionStatus'
+ description 'Completion status of tasks'
+
+ field :count, GraphQL::INT_TYPE, null: false
+ field :completed_count, GraphQL::INT_TYPE, null: false
+ end
+end
diff --git a/app/helpers/dropdowns_helper.rb b/app/helpers/dropdowns_helper.rb
index 8d8c62f1291..64c5fae7d96 100644
--- a/app/helpers/dropdowns_helper.rb
+++ b/app/helpers/dropdowns_helper.rb
@@ -91,7 +91,7 @@ module DropdownsHelper
def dropdown_filter(placeholder, search_id: nil)
content_tag :div, class: "dropdown-input" do
- filter_output = search_field_tag search_id, nil, class: "dropdown-input-field", placeholder: placeholder, autocomplete: 'off'
+ filter_output = search_field_tag search_id, nil, class: "dropdown-input-field qa-dropdown-input-field", placeholder: placeholder, autocomplete: 'off'
filter_output << icon('search', class: "dropdown-input-search")
filter_output << icon('times', class: "dropdown-input-clear js-dropdown-input-clear", role: "button")
diff --git a/app/helpers/environments_helper.rb b/app/helpers/environments_helper.rb
index 855b243cc8a..0f118c235d8 100644
--- a/app/helpers/environments_helper.rb
+++ b/app/helpers/environments_helper.rb
@@ -27,7 +27,7 @@ module EnvironmentsHelper
"empty-unable-to-connect-svg-path" => image_path('illustrations/monitoring/unable_to_connect.svg'),
"metrics-endpoint" => additional_metrics_project_environment_path(project, environment, format: :json),
"dashboard-endpoint" => metrics_dashboard_project_environment_path(project, environment, format: :json),
- "deployment-endpoint" => project_environment_deployments_path(project, environment, format: :json),
+ "deployments-endpoint" => project_environment_deployments_path(project, environment, format: :json),
"environments-endpoint": project_environments_path(project, format: :json),
"project-path" => project_path(project),
"tags-path" => project_tags_path(project),
diff --git a/app/helpers/markup_helper.rb b/app/helpers/markup_helper.rb
index dce4168ad7b..bf894360a2e 100644
--- a/app/helpers/markup_helper.rb
+++ b/app/helpers/markup_helper.rb
@@ -263,6 +263,11 @@ module MarkupHelper
end
def asciidoc_unsafe(text, context = {})
+ context.merge!(
+ commit: @commit,
+ ref: @ref,
+ requested_path: @path
+ )
Gitlab::Asciidoc.render(text, context)
end
diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb
index 4594f5a31b9..dfa34ad7020 100644
--- a/app/helpers/search_helper.rb
+++ b/app/helpers/search_helper.rb
@@ -172,11 +172,17 @@ module SearchHelper
if @project.present?
opts[:data]['project-id'] = @project.id
opts[:data]['base-endpoint'] = project_path(@project)
+ opts[:data]['labels-endpoint'] = project_labels_path(@project)
+ opts[:data]['milestones-endpoint'] = project_milestones_path(@project)
elsif @group.present?
opts[:data]['group-id'] = @group.id
opts[:data]['base-endpoint'] = group_canonical_path(@group)
+ opts[:data]['labels-endpoint'] = group_labels_path(@group)
+ opts[:data]['milestones-endpoint'] = group_milestones_path(@group)
else
opts[:data]['base-endpoint'] = root_dashboard_path
+ opts[:data]['labels-endpoint'] = dashboard_labels_path
+ opts[:data]['milestones-endpoint'] = dashboard_milestones_path
end
opts
diff --git a/app/helpers/user_callouts_helper.rb b/app/helpers/user_callouts_helper.rb
index 5d658d35107..d5e459311f7 100644
--- a/app/helpers/user_callouts_helper.rb
+++ b/app/helpers/user_callouts_helper.rb
@@ -3,6 +3,7 @@
module UserCalloutsHelper
GKE_CLUSTER_INTEGRATION = 'gke_cluster_integration'.freeze
GCP_SIGNUP_OFFER = 'gcp_signup_offer'.freeze
+ SUGGEST_POPOVER_DISMISSED = 'suggest_popover_dismissed'.freeze
def show_gke_cluster_integration_callout?(project)
can?(current_user, :create_cluster, project) &&
@@ -20,6 +21,10 @@ module UserCalloutsHelper
def render_dashboard_gold_trial(user)
end
+ def show_suggest_popover?
+ !user_dismissed?(SUGGEST_POPOVER_DISMISSED)
+ end
+
private
def user_dismissed?(feature_name)
diff --git a/app/models/concerns/diff_positionable_note.rb b/app/models/concerns/diff_positionable_note.rb
index b61bf29e6ad..2d09eff0111 100644
--- a/app/models/concerns/diff_positionable_note.rb
+++ b/app/models/concerns/diff_positionable_note.rb
@@ -3,6 +3,7 @@ module DiffPositionableNote
extend ActiveSupport::Concern
included do
+ delegate :on_text?, :on_image?, to: :position, allow_nil: true
before_validation :set_original_position, on: :create
before_validation :update_position, on: :create, if: :on_text?
@@ -28,14 +29,6 @@ module DiffPositionableNote
end
end
- def on_text?
- position&.position_type == "text"
- end
-
- def on_image?
- position&.position_type == "image"
- end
-
def supported?
for_commit? || self.noteable.has_complete_diff_refs?
end
diff --git a/app/models/discussion.rb b/app/models/discussion.rb
index 32529ebf71d..ae13cdfd85f 100644
--- a/app/models/discussion.rb
+++ b/app/models/discussion.rb
@@ -4,6 +4,7 @@
#
# A discussion of this type can be resolvable.
class Discussion
+ include GlobalID::Identification
include ResolvableDiscussion
attr_reader :notes, :context_noteable
@@ -11,14 +12,19 @@ class Discussion
delegate :created_at,
:project,
:author,
-
:noteable,
:commit_id,
:for_commit?,
:for_merge_request?,
+ :to_ability_name,
+ :editable?,
to: :first_note
+ def declarative_policy_delegate
+ first_note
+ end
+
def project_id
project&.id
end
diff --git a/app/models/group.rb b/app/models/group.rb
index cdb4e6e87f6..dbec211935d 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -423,7 +423,7 @@ class Group < Namespace
def update_two_factor_requirement
return unless saved_change_to_require_two_factor_authentication? || saved_change_to_two_factor_grace_period?
- users.find_each(&:update_two_factor_requirement)
+ members_with_descendants.find_each(&:update_two_factor_requirement)
end
def path_changed_hook
diff --git a/app/models/note.rb b/app/models/note.rb
index 081d6f91230..15271c68a9e 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -342,7 +342,7 @@ class Note < ApplicationRecord
end
def to_ability_name
- for_snippet? ? noteable.class.name.underscore : noteable_type.underscore
+ for_snippet? ? noteable.class.name.underscore : noteable_type.demodulize.underscore
end
def can_be_discussion_note?
diff --git a/app/models/user_callout_enums.rb b/app/models/user_callout_enums.rb
index b9373ae6166..7b68e5076c7 100644
--- a/app/models/user_callout_enums.rb
+++ b/app/models/user_callout_enums.rb
@@ -6,11 +6,15 @@ module UserCalloutEnums
#
# This method is separate from the `UserCallout` model so that it can be
# extended by EE.
+ #
+ # If you are going to add new items to this hash, check that you're not going
+ # to conflict with EE-only values: https://gitlab.com/gitlab-org/gitlab-ee/blob/master/ee/app/models/ee/user_callout_enums.rb
def self.feature_names
{
gke_cluster_integration: 1,
gcp_signup_offer: 2,
- cluster_security_warning: 3
+ cluster_security_warning: 3,
+ suggest_popover_dismissed: 9
}
end
end
diff --git a/app/policies/project_statistics_policy.rb b/app/policies/project_statistics_policy.rb
new file mode 100644
index 00000000000..c0592f1ea13
--- /dev/null
+++ b/app/policies/project_statistics_policy.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+class ProjectStatisticsPolicy < BasePolicy
+ delegate { @subject.project }
+end
diff --git a/app/serializers/pipeline_entity.rb b/app/serializers/pipeline_entity.rb
index ec2698ecbe3..9ef93b2387f 100644
--- a/app/serializers/pipeline_entity.rb
+++ b/app/serializers/pipeline_entity.rb
@@ -4,7 +4,6 @@ class PipelineEntity < Grape::Entity
include RequestAwareEntity
expose :id
- expose :iid
expose :user, using: UserEntity
expose :active?, as: :active
diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml
index c357207054b..7535aee83a3 100644
--- a/app/views/layouts/_head.html.haml
+++ b/app/views/layouts/_head.html.haml
@@ -78,3 +78,4 @@
= render 'layouts/google_analytics' if extra_config.has_key?('google_analytics_id')
= render 'layouts/piwik' if extra_config.has_key?('piwik_url') && extra_config.has_key?('piwik_site_id')
= render_if_exists 'layouts/snowplow'
+ = render_if_exists 'layouts/pendo' if Feature.enabled?(:pendo_tracking) && !Rails.env.test?
diff --git a/app/views/layouts/nav/sidebar/_project.html.haml b/app/views/layouts/nav/sidebar/_project.html.haml
index 9b6551552c7..49ff976f8e8 100644
--- a/app/views/layouts/nav/sidebar/_project.html.haml
+++ b/app/views/layouts/nav/sidebar/_project.html.haml
@@ -9,7 +9,7 @@
= @project.name
%ul.sidebar-top-level-items
= nav_link(path: sidebar_projects_paths, html_options: { class: 'home' }) do
- = link_to project_path(@project), class: 'shortcuts-project' do
+ = link_to project_path(@project), class: 'shortcuts-project qa-link-project' do
.nav-icon-container
= sprite_icon('home')
%span.nav-item-name
diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml
index bdf7b933ab8..f4560404c03 100644
--- a/app/views/projects/ci/builds/_build.html.haml
+++ b/app/views/projects/ci/builds/_build.html.haml
@@ -53,10 +53,9 @@
%span.badge.badge-info= _('manual')
- if pipeline_link
- %td.pipeline-link
- = link_to pipeline_path(pipeline), class: 'has-tooltip', title: _('Pipeline ID (IID)') do
+ %td
+ = link_to pipeline_path(pipeline) do
%span.pipeline-id ##{pipeline.id}
- %span.pipeline-iid (##{pipeline.iid})
%span by
- if pipeline.user
= user_avatar(user: pipeline.user, size: 20)
diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml
index 77ea2c04b28..a766dd51463 100644
--- a/app/views/projects/commit/_commit_box.html.haml
+++ b/app/views/projects/commit/_commit_box.html.haml
@@ -81,7 +81,7 @@
= link_to project_pipeline_path(@project, last_pipeline.id), class: "ci-status-icon-#{last_pipeline.status}" do
= ci_icon_for_status(last_pipeline.status)
#{ _('Pipeline') }
- = link_to "##{last_pipeline.id} (##{last_pipeline.iid})", project_pipeline_path(@project, last_pipeline.id), class: "has-tooltip", title: _('Pipeline ID (IID)')
+ = link_to "##{last_pipeline.id}", project_pipeline_path(@project, last_pipeline.id)
= ci_label_for_status(last_pipeline.status)
- if last_pipeline.stages_count.nonzero?
#{ n_(s_('Pipeline|with stage'), s_('Pipeline|with stages'), last_pipeline.stages_count) }
diff --git a/app/views/projects/merge_requests/show.html.haml b/app/views/projects/merge_requests/show.html.haml
index a201fafb949..f593f4e049e 100644
--- a/app/views/projects/merge_requests/show.html.haml
+++ b/app/views/projects/merge_requests/show.html.haml
@@ -80,7 +80,9 @@
current_user_data: UserSerializer.new(project: @project).represent(current_user, {}, MergeRequestUserEntity).to_json,
project_path: project_path(@merge_request.project),
changes_empty_state_illustration: image_path('illustrations/merge_request_changes_empty.svg'),
- is_fluid_layout: fluid_layout.to_s } }
+ is_fluid_layout: fluid_layout.to_s,
+ dismiss_endpoint: user_callouts_path,
+ show_suggest_popover: show_suggest_popover?.to_s } }
.mr-loading-status
= spinner
diff --git a/app/views/shared/_sidebar_toggle_button.html.haml b/app/views/shared/_sidebar_toggle_button.html.haml
index d90a6d43761..d499bc0a253 100644
--- a/app/views/shared/_sidebar_toggle_button.html.haml
+++ b/app/views/shared/_sidebar_toggle_button.html.haml
@@ -1,4 +1,4 @@
-%a.toggle-sidebar-button.js-toggle-sidebar{ role: "button", type: "button", title: "Toggle sidebar" }
+%a.toggle-sidebar-button.js-toggle-sidebar.qa-toggle-sidebar{ role: "button", type: "button", title: "Toggle sidebar" }
= sprite_icon('angle-double-left', css_class: 'icon-angle-double-left')
= sprite_icon('angle-double-right', css_class: 'icon-angle-double-right')
%span.collapse-text= _("Collapse sidebar")
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index 3a5adb34ad1..e87e560266f 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -102,7 +102,7 @@
= _('Labels')
= icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true')
- if can_edit_issuable
- = link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link float-right'
+ = link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link qa-edit-link-labels float-right'
.value.issuable-show-labels.dont-hide.hide-collapsed.qa-labels-block{ class: ("has-labels" if selected_labels.any?) }
- if selected_labels.any?
- selected_labels.each do |label_hash|
@@ -118,7 +118,7 @@
%span.dropdown-toggle-text{ class: ("is-default" if selected_labels.empty?) }
= multi_label_name(selected_labels, "Labels")
= icon('chevron-down', 'aria-hidden': 'true')
- .dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable.dropdown-extended-height
+ .dropdown-menu.dropdown-select.dropdown-menu-paging.qa-dropdown-menu-labels.dropdown-menu-labels.dropdown-menu-selectable.dropdown-extended-height
= render partial: "shared/issuable/label_page_default"
- if issuable_sidebar.dig(:current_user, :can_admin_label)
= render partial: "shared/issuable/label_page_create"