summaryrefslogtreecommitdiff
path: root/app/assets/javascripts
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts')
-rw-r--r--app/assets/javascripts/api.js15
-rw-r--r--app/assets/javascripts/boards/components/board_card.vue1
-rw-r--r--app/assets/javascripts/boards/components/board_list.vue1
-rw-r--r--app/assets/javascripts/boards/components/board_sidebar.js2
-rw-r--r--app/assets/javascripts/boards/components/boards_selector.vue1
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_card_list_item.vue41
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_nav_item.vue88
-rw-r--r--app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js2
-rw-r--r--app/assets/javascripts/diffs/components/app.vue2
-rw-r--r--app/assets/javascripts/diffs/components/diff_expansion_cell.vue12
-rw-r--r--app/assets/javascripts/diffs/components/diff_file_header.vue2
-rw-r--r--app/assets/javascripts/event_tracking/issue_sidebar.js2
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/actions.vue12
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/new_merge_request_option.vue33
-rw-r--r--app/assets/javascripts/ide/stores/getters.js3
-rw-r--r--app/assets/javascripts/ide/stores/modules/commit/actions.js23
-rw-r--r--app/assets/javascripts/ide/stores/modules/commit/getters.js12
-rw-r--r--app/assets/javascripts/ide/stores/modules/commit/mutation_types.js1
-rw-r--r--app/assets/javascripts/ide/stores/modules/commit/mutations.js3
-rw-r--r--app/assets/javascripts/ide/stores/modules/commit/state.js3
-rw-r--r--app/assets/javascripts/ide/stores/utils.js2
-rw-r--r--app/assets/javascripts/issue_show/index.js4
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js60
-rw-r--r--app/assets/javascripts/lib/utils/url_utility.js8
-rw-r--r--app/assets/javascripts/main.js17
-rw-r--r--app/assets/javascripts/members.js2
-rw-r--r--app/assets/javascripts/monitoring/components/charts/area.vue5
-rw-r--r--app/assets/javascripts/monitoring/components/charts/time_series.vue342
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard.vue28
-rw-r--r--app/assets/javascripts/monitoring/components/embed.vue6
-rw-r--r--app/assets/javascripts/monitoring/components/panel_type.vue8
-rw-r--r--app/assets/javascripts/monitoring/constants.js9
-rw-r--r--app/assets/javascripts/notes/stores/getters.js39
-rw-r--r--app/assets/javascripts/pages/projects/wikis/wikis.js42
-rw-r--r--app/assets/javascripts/pipelines/components/pipeline_url.vue4
-rw-r--r--app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue4
-rw-r--r--app/assets/javascripts/registry/components/app.vue101
-rw-r--r--app/assets/javascripts/registry/components/svg_message.vue26
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/assignee_avatar.vue48
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/assignee_avatar_link.vue83
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/assignee_title.vue13
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/assignees.vue212
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/collapsed_assignee.vue27
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/collapsed_assignee_list.vue121
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue96
-rw-r--r--app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue8
-rw-r--r--app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue7
-rw-r--r--app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue3
-rw-r--r--app/assets/javascripts/test_utils/simulate_drag.js6
-rw-r--r--app/assets/javascripts/tracking.js8
-rw-r--r--app/assets/javascripts/users_select.js66
-rw-r--r--app/assets/javascripts/visual_review_toolbar/components/comment.js39
-rw-r--r--app/assets/javascripts/visual_review_toolbar/components/comment_mr_note.js31
-rw-r--r--app/assets/javascripts/visual_review_toolbar/components/comment_post.js145
-rw-r--r--app/assets/javascripts/visual_review_toolbar/components/comment_storage.js20
-rw-r--r--app/assets/javascripts/visual_review_toolbar/components/form_elements.js17
-rw-r--r--app/assets/javascripts/visual_review_toolbar/components/index.js23
-rw-r--r--app/assets/javascripts/visual_review_toolbar/components/login.js47
-rw-r--r--app/assets/javascripts/visual_review_toolbar/components/mr_id.js63
-rw-r--r--app/assets/javascripts/visual_review_toolbar/components/note.js35
-rw-r--r--app/assets/javascripts/visual_review_toolbar/components/utils.js51
-rw-r--r--app/assets/javascripts/visual_review_toolbar/components/wrapper.js79
-rw-r--r--app/assets/javascripts/visual_review_toolbar/components/wrapper_icons.js15
-rw-r--r--app/assets/javascripts/visual_review_toolbar/index.js51
-rw-r--r--app/assets/javascripts/visual_review_toolbar/shared/constants.js56
-rw-r--r--app/assets/javascripts/visual_review_toolbar/shared/index.js55
-rw-r--r--app/assets/javascripts/visual_review_toolbar/shared/storage_utils.js42
-rw-r--r--app/assets/javascripts/visual_review_toolbar/store/events.js73
-rw-r--r--app/assets/javascripts/visual_review_toolbar/store/index.js11
-rw-r--r--app/assets/javascripts/visual_review_toolbar/store/state.js95
-rw-r--r--app/assets/javascripts/visual_review_toolbar/store/utils.js15
-rw-r--r--app/assets/javascripts/visual_review_toolbar/styles/toolbar.css188
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue1
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js3
-rw-r--r--app/assets/javascripts/vue_shared/components/file_row.vue8
-rw-r--r--app/assets/javascripts/vue_shared/components/recaptcha_modal.vue7
-rw-r--r--app/assets/javascripts/vue_shared/directives/autofocusonshow.js39
78 files changed, 1306 insertions, 1569 deletions
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js
index a649c521405..136ffdf8b9d 100644
--- a/app/assets/javascripts/api.js
+++ b/app/assets/javascripts/api.js
@@ -14,6 +14,7 @@ const Api = {
projectPath: '/api/:version/projects/:id',
forkedProjectsPath: '/api/:version/projects/:id/forks',
projectLabelsPath: '/:namespace_path/:project_path/-/labels',
+ projectUsersPath: '/api/:version/projects/:id/users',
projectMergeRequestsPath: '/api/:version/projects/:id/merge_requests',
projectMergeRequestPath: '/api/:version/projects/:id/merge_requests/:mrid',
projectMergeRequestChangesPath: '/api/:version/projects/:id/merge_requests/:mrid/changes',
@@ -108,6 +109,20 @@ const Api = {
});
},
+ projectUsers(projectPath, query = '', options = {}) {
+ const url = Api.buildUrl(this.projectUsersPath).replace(':id', encodeURIComponent(projectPath));
+
+ return axios
+ .get(url, {
+ params: {
+ search: query,
+ per_page: 20,
+ ...options,
+ },
+ })
+ .then(({ data }) => data);
+ },
+
// Return single project
project(projectPath) {
const url = Api.buildUrl(Api.projectPath).replace(':id', encodeURIComponent(projectPath));
diff --git a/app/assets/javascripts/boards/components/board_card.vue b/app/assets/javascripts/boards/components/board_card.vue
index 179148b6887..faf722f61af 100644
--- a/app/assets/javascripts/boards/components/board_card.vue
+++ b/app/assets/javascripts/boards/components/board_card.vue
@@ -83,6 +83,7 @@ export default {
}"
:index="index"
:data-issue-id="issue.id"
+ data-qa-selector="board_card"
class="board-card p-3 rounded"
@mousedown="mouseDown"
@mousemove="mouseMove"
diff --git a/app/assets/javascripts/boards/components/board_list.vue b/app/assets/javascripts/boards/components/board_list.vue
index 03a8a92575e..de41698ca04 100644
--- a/app/assets/javascripts/boards/components/board_list.vue
+++ b/app/assets/javascripts/boards/components/board_list.vue
@@ -227,6 +227,7 @@ export default {
<div
:class="{ 'd-none': !list.isExpanded, 'd-flex flex-column': list.isExpanded }"
class="board-list-component position-relative h-100"
+ data-qa-selector="board_list_cards_area"
>
<div v-if="loading" class="board-list-loading text-center" :aria-label="__('Loading issues')">
<gl-loading-icon />
diff --git a/app/assets/javascripts/boards/components/board_sidebar.js b/app/assets/javascripts/boards/components/board_sidebar.js
index 2ace0060c42..ba1fe9202fc 100644
--- a/app/assets/javascripts/boards/components/board_sidebar.js
+++ b/app/assets/javascripts/boards/components/board_sidebar.js
@@ -22,6 +22,8 @@ export default Vue.extend({
components: {
AssigneeTitle,
Assignees,
+ SidebarEpicsSelect: () =>
+ import('ee_component/sidebar/components/sidebar_item_epics_select.vue'),
RemoveBtn,
Subscriptions,
TimeTracker,
diff --git a/app/assets/javascripts/boards/components/boards_selector.vue b/app/assets/javascripts/boards/components/boards_selector.vue
index b05de4538f2..7296426549a 100644
--- a/app/assets/javascripts/boards/components/boards_selector.vue
+++ b/app/assets/javascripts/boards/components/boards_selector.vue
@@ -226,6 +226,7 @@ export default {
<div class="boards-switcher js-boards-selector append-right-10">
<span class="boards-selector-wrapper js-boards-selector-wrapper">
<gl-dropdown
+ data-qa-selector="boards_dropdown"
toggle-class="dropdown-menu-toggle js-dropdown-toggle"
menu-class="flex-column dropdown-extended-height"
:text="board.name"
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_card_list_item.vue b/app/assets/javascripts/cycle_analytics/components/stage_card_list_item.vue
new file mode 100644
index 00000000000..d946594a069
--- /dev/null
+++ b/app/assets/javascripts/cycle_analytics/components/stage_card_list_item.vue
@@ -0,0 +1,41 @@
+<script>
+import Icon from '~/vue_shared/components/icon.vue';
+import { GlButton } from '@gitlab/ui';
+
+export default {
+ name: 'StageCardListItem',
+ components: {
+ Icon,
+ GlButton,
+ },
+ props: {
+ isActive: {
+ type: Boolean,
+ required: true,
+ },
+ canEdit: {
+ type: Boolean,
+ default: false,
+ required: false,
+ },
+ },
+};
+</script>
+
+<template>
+ <div :class="{ active: isActive }" class="stage-nav-item d-flex pl-4 pr-4 m-0 mb-1 ml-2 rounded">
+ <slot></slot>
+ <div v-if="canEdit" class="dropdown">
+ <gl-button
+ :title="__('More actions')"
+ class="more-actions-toggle btn btn-transparent p-0"
+ data-toggle="dropdown"
+ >
+ <icon css-classes="icon" name="ellipsis_v" />
+ </gl-button>
+ <ul class="more-actions-dropdown dropdown-menu dropdown-open-left">
+ <slot name="dropdown-options"></slot>
+ </ul>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_nav_item.vue b/app/assets/javascripts/cycle_analytics/components/stage_nav_item.vue
new file mode 100644
index 00000000000..004d335f572
--- /dev/null
+++ b/app/assets/javascripts/cycle_analytics/components/stage_nav_item.vue
@@ -0,0 +1,88 @@
+<script>
+import StageCardListItem from './stage_card_list_item.vue';
+
+export default {
+ name: 'StageNavItem',
+ components: {
+ StageCardListItem,
+ },
+ props: {
+ isDefaultStage: {
+ type: Boolean,
+ default: false,
+ required: false,
+ },
+ isActive: {
+ type: Boolean,
+ default: false,
+ required: false,
+ },
+ isUserAllowed: {
+ type: Boolean,
+ required: true,
+ },
+ title: {
+ type: String,
+ required: true,
+ },
+ value: {
+ type: String,
+ default: '',
+ required: false,
+ },
+ canEdit: {
+ type: Boolean,
+ default: false,
+ required: false,
+ },
+ },
+ computed: {
+ hasValue() {
+ return this.value && this.value.length > 0;
+ },
+ editable() {
+ return this.isUserAllowed && this.canEdit;
+ },
+ },
+};
+</script>
+
+<template>
+ <li @click="$emit('select')">
+ <stage-card-list-item :is-active="isActive" :can-edit="editable">
+ <div class="stage-nav-item-cell stage-name p-0" :class="{ 'font-weight-bold': isActive }">
+ {{ title }}
+ </div>
+ <div class="stage-nav-item-cell stage-median mr-4">
+ <template v-if="isUserAllowed">
+ <span v-if="hasValue">{{ value }}</span>
+ <span v-else class="stage-empty">{{ __('Not enough data') }}</span>
+ </template>
+ <template v-else>
+ <span class="not-available">{{ __('Not available') }}</span>
+ </template>
+ </div>
+ <template v-slot:dropdown-options>
+ <template v-if="isDefaultStage">
+ <li>
+ <button type="button" class="btn-default btn-transparent">
+ {{ __('Hide stage') }}
+ </button>
+ </li>
+ </template>
+ <template v-else>
+ <li>
+ <button type="button" class="btn-default btn-transparent">
+ {{ __('Edit stage') }}
+ </button>
+ </li>
+ <li>
+ <button type="button" class="btn-danger danger">
+ {{ __('Remove stage') }}
+ </button>
+ </li>
+ </template>
+ </template>
+ </stage-card-list-item>
+ </li>
+</template>
diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js
index 671405602cc..b3ae47af750 100644
--- a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js
+++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js
@@ -12,6 +12,7 @@ import stageComponent from './components/stage_component.vue';
import stageReviewComponent from './components/stage_review_component.vue';
import stageStagingComponent from './components/stage_staging_component.vue';
import stageTestComponent from './components/stage_test_component.vue';
+import stageNavItem from './components/stage_nav_item.vue';
import CycleAnalyticsService from './cycle_analytics_service';
import CycleAnalyticsStore from './cycle_analytics_store';
@@ -41,6 +42,7 @@ export default () => {
import('ee_component/analytics/shared/components/projects_dropdown_filter.vue'),
DateRangeDropdown: () =>
import('ee_component/analytics/shared/components/date_range_dropdown.vue'),
+ 'stage-nav-item': stageNavItem,
},
mixins: [filterMixins],
data() {
diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue
index 81da0754752..19b85710710 100644
--- a/app/assets/javascripts/diffs/components/app.vue
+++ b/app/assets/javascripts/diffs/components/app.vue
@@ -305,7 +305,7 @@ export default {
<div
v-show="showTreeList"
:style="{ width: `${treeWidth}px` }"
- class="diff-tree-list js-diff-tree-list"
+ class="diff-tree-list js-diff-tree-list mr-3"
>
<panel-resizer
:size.sync="treeWidth"
diff --git a/app/assets/javascripts/diffs/components/diff_expansion_cell.vue b/app/assets/javascripts/diffs/components/diff_expansion_cell.vue
index 925385fa98a..839ab542377 100644
--- a/app/assets/javascripts/diffs/components/diff_expansion_cell.vue
+++ b/app/assets/javascripts/diffs/components/diff_expansion_cell.vue
@@ -212,19 +212,18 @@ export default {
</script>
<template>
- <td :colspan="colspan">
+ <td :colspan="colspan" class="text-center">
<div class="content js-line-expansion-content">
<a
v-if="canExpandUp"
v-tooltip
- class="cursor-pointer js-unfold unfold-icon"
+ class="cursor-pointer js-unfold unfold-icon d-inline-block pt-2 pb-2"
data-placement="top"
data-container="body"
:title="__('Expand up')"
@click="handleExpandLines(EXPAND_UP)"
>
- <!-- TODO: remove style & replace with correct icon, waiting for MR https://gitlab.com/gitlab-org/gitlab-design/issues/499 -->
- <icon :size="12" name="expand-left" aria-hidden="true" style="transform: rotate(270deg);" />
+ <icon :size="12" name="expand-up" aria-hidden="true" />
</a>
<a class="mx-2 cursor-pointer js-unfold-all" @click="handleExpandLines()">
<span>{{ s__('Diffs|Show all lines') }}</span>
@@ -232,14 +231,13 @@ export default {
<a
v-if="canExpandDown"
v-tooltip
- class="cursor-pointer js-unfold-down has-tooltip unfold-icon"
+ class="cursor-pointer js-unfold-down has-tooltip unfold-icon d-inline-block pt-2 pb-2"
data-placement="top"
data-container="body"
:title="__('Expand down')"
@click="handleExpandLines(EXPAND_DOWN)"
>
- <!-- TODO: remove style & replace with correct icon, waiting for MR https://gitlab.com/gitlab-org/gitlab-design/issues/499 -->
- <icon :size="12" name="expand-left" aria-hidden="true" style="transform: rotate(90deg);" />
+ <icon :size="12" name="expand-down" aria-hidden="true" />
</a>
</div>
</td>
diff --git a/app/assets/javascripts/diffs/components/diff_file_header.vue b/app/assets/javascripts/diffs/components/diff_file_header.vue
index 32fbeaaa905..69ec6ab8600 100644
--- a/app/assets/javascripts/diffs/components/diff_file_header.vue
+++ b/app/assets/javascripts/diffs/components/diff_file_header.vue
@@ -130,7 +130,7 @@ export default {
return `\`${this.diffFile.file_path}\``;
},
isFileRenamed() {
- return this.diffFile.viewer.name === diffViewerModes.renamed;
+ return this.diffFile.renamed_file;
},
isModeChanged() {
return this.diffFile.viewer.name === diffViewerModes.mode_changed;
diff --git a/app/assets/javascripts/event_tracking/issue_sidebar.js b/app/assets/javascripts/event_tracking/issue_sidebar.js
new file mode 100644
index 00000000000..6909f82c66f
--- /dev/null
+++ b/app/assets/javascripts/event_tracking/issue_sidebar.js
@@ -0,0 +1,2 @@
+export const initSidebarTracking = () => {};
+export const trackEvent = () => {};
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/actions.vue b/app/assets/javascripts/ide/components/commit_sidebar/actions.vue
index 685d8a6b245..8b356ee6e97 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/actions.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/actions.vue
@@ -41,10 +41,16 @@ export default {
methods: {
...mapCommitActions(['updateCommitAction']),
updateSelectedCommitAction() {
- if (this.currentBranch && !this.currentBranch.can_push) {
- this.updateCommitAction(consts.COMMIT_TO_NEW_BRANCH);
- } else if (this.containsStagedChanges) {
+ if (!this.currentBranch) {
+ return;
+ }
+
+ const { can_push: canPush = false, default: isDefault = false } = this.currentBranch;
+
+ if (canPush && !isDefault) {
this.updateCommitAction(consts.COMMIT_TO_CURRENT_BRANCH);
+ } else {
+ this.updateCommitAction(consts.COMMIT_TO_NEW_BRANCH);
}
},
},
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/new_merge_request_option.vue b/app/assets/javascripts/ide/components/commit_sidebar/new_merge_request_option.vue
index b2e7b15089c..daa44a42765 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/new_merge_request_option.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/new_merge_request_option.vue
@@ -1,43 +1,36 @@
<script>
-import { mapGetters, createNamespacedHelpers } from 'vuex';
+import { createNamespacedHelpers } from 'vuex';
const {
mapState: mapCommitState,
- mapGetters: mapCommitGetters,
mapActions: mapCommitActions,
+ mapGetters: mapCommitGetters,
} = createNamespacedHelpers('commit');
export default {
computed: {
...mapCommitState(['shouldCreateMR']),
- ...mapCommitGetters(['isCommittingToCurrentBranch', 'isCommittingToDefaultBranch']),
- ...mapGetters(['hasMergeRequest', 'isOnDefaultBranch']),
- currentBranchHasMr() {
- return this.hasMergeRequest && this.isCommittingToCurrentBranch;
- },
- showNewMrOption() {
- return (
- this.isCommittingToDefaultBranch || !this.currentBranchHasMr || this.isCommittingToNewBranch
- );
- },
- },
- mounted() {
- this.setShouldCreateMR();
+ ...mapCommitGetters(['shouldHideNewMrOption']),
},
methods: {
- ...mapCommitActions(['toggleShouldCreateMR', 'setShouldCreateMR']),
+ ...mapCommitActions(['toggleShouldCreateMR']),
},
};
</script>
<template>
- <div v-if="showNewMrOption">
+ <fieldset v-if="!shouldHideNewMrOption">
<hr class="my-2" />
- <label class="mb-0">
- <input :checked="shouldCreateMR" type="checkbox" @change="toggleShouldCreateMR" />
+ <label class="mb-0 js-ide-commit-new-mr">
+ <input
+ :checked="shouldCreateMR"
+ type="checkbox"
+ data-qa-selector="start_new_mr_checkbox"
+ @change="toggleShouldCreateMR"
+ />
<span class="prepend-left-10">
{{ __('Start a new merge request') }}
</span>
</label>
- </div>
+ </fieldset>
</template>
diff --git a/app/assets/javascripts/ide/stores/getters.js b/app/assets/javascripts/ide/stores/getters.js
index 406903129db..85fd45358be 100644
--- a/app/assets/javascripts/ide/stores/getters.js
+++ b/app/assets/javascripts/ide/stores/getters.js
@@ -104,5 +104,8 @@ export const packageJson = state => state.entries[packageJsonPath];
export const isOnDefaultBranch = (_state, getters) =>
getters.currentProject && getters.currentProject.default_branch === getters.branchName;
+export const canPushToBranch = (_state, getters) =>
+ getters.currentBranch && getters.currentBranch.can_push;
+
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
diff --git a/app/assets/javascripts/ide/stores/modules/commit/actions.js b/app/assets/javascripts/ide/stores/modules/commit/actions.js
index ac34491c1ad..23caf2d48ed 100644
--- a/app/assets/javascripts/ide/stores/modules/commit/actions.js
+++ b/app/assets/javascripts/ide/stores/modules/commit/actions.js
@@ -18,34 +18,15 @@ export const discardDraft = ({ commit }) => {
commit(types.UPDATE_COMMIT_MESSAGE, '');
};
-export const updateCommitAction = ({ commit, dispatch }, commitAction) => {
+export const updateCommitAction = ({ commit, getters }, commitAction) => {
commit(types.UPDATE_COMMIT_ACTION, {
commitAction,
});
- dispatch('setShouldCreateMR');
+ commit(types.TOGGLE_SHOULD_CREATE_MR, !getters.shouldHideNewMrOption);
};
export const toggleShouldCreateMR = ({ commit }) => {
commit(types.TOGGLE_SHOULD_CREATE_MR);
- commit(types.INTERACT_WITH_NEW_MR);
-};
-
-export const setShouldCreateMR = ({
- commit,
- getters,
- rootGetters,
- state: { interactedWithNewMR },
-}) => {
- const committingToExistingMR =
- getters.isCommittingToCurrentBranch &&
- rootGetters.hasMergeRequest &&
- !rootGetters.isOnDefaultBranch;
-
- if ((getters.isCommittingToDefaultBranch && !interactedWithNewMR) || committingToExistingMR) {
- commit(types.TOGGLE_SHOULD_CREATE_MR, false);
- } else if (!interactedWithNewMR) {
- commit(types.TOGGLE_SHOULD_CREATE_MR, true);
- }
};
export const updateBranchName = ({ commit }, branchName) => {
diff --git a/app/assets/javascripts/ide/stores/modules/commit/getters.js b/app/assets/javascripts/ide/stores/modules/commit/getters.js
index 64779e9e4df..de289e27199 100644
--- a/app/assets/javascripts/ide/stores/modules/commit/getters.js
+++ b/app/assets/javascripts/ide/stores/modules/commit/getters.js
@@ -20,7 +20,7 @@ export const placeholderBranchName = (state, _, rootState) =>
)}`;
export const branchName = (state, getters, rootState) => {
- if (state.commitAction === consts.COMMIT_TO_NEW_BRANCH) {
+ if (getters.isCreatingNewBranch) {
if (state.newBranchName === '') {
return getters.placeholderBranchName;
}
@@ -48,11 +48,11 @@ export const preBuiltCommitMessage = (state, _, rootState) => {
export const isCreatingNewBranch = state => state.commitAction === consts.COMMIT_TO_NEW_BRANCH;
-export const isCommittingToCurrentBranch = state =>
- state.commitAction === consts.COMMIT_TO_CURRENT_BRANCH;
-
-export const isCommittingToDefaultBranch = (_state, getters, _rootState, rootGetters) =>
- getters.isCommittingToCurrentBranch && rootGetters.isOnDefaultBranch;
+export const shouldHideNewMrOption = (_state, getters, _rootState, rootGetters) =>
+ !getters.isCreatingNewBranch &&
+ (rootGetters.hasMergeRequest ||
+ (!rootGetters.hasMergeRequest && rootGetters.isOnDefaultBranch)) &&
+ rootGetters.canPushToBranch;
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
diff --git a/app/assets/javascripts/ide/stores/modules/commit/mutation_types.js b/app/assets/javascripts/ide/stores/modules/commit/mutation_types.js
index b81918156b0..7ad8f3570b7 100644
--- a/app/assets/javascripts/ide/stores/modules/commit/mutation_types.js
+++ b/app/assets/javascripts/ide/stores/modules/commit/mutation_types.js
@@ -3,4 +3,3 @@ export const UPDATE_COMMIT_ACTION = 'UPDATE_COMMIT_ACTION';
export const UPDATE_NEW_BRANCH_NAME = 'UPDATE_NEW_BRANCH_NAME';
export const UPDATE_LOADING = 'UPDATE_LOADING';
export const TOGGLE_SHOULD_CREATE_MR = 'TOGGLE_SHOULD_CREATE_MR';
-export const INTERACT_WITH_NEW_MR = 'INTERACT_WITH_NEW_MR';
diff --git a/app/assets/javascripts/ide/stores/modules/commit/mutations.js b/app/assets/javascripts/ide/stores/modules/commit/mutations.js
index 14957d283bb..73b618e250f 100644
--- a/app/assets/javascripts/ide/stores/modules/commit/mutations.js
+++ b/app/assets/javascripts/ide/stores/modules/commit/mutations.js
@@ -24,7 +24,4 @@ export default {
shouldCreateMR: shouldCreateMR === undefined ? !state.shouldCreateMR : shouldCreateMR,
});
},
- [types.INTERACT_WITH_NEW_MR](state) {
- Object.assign(state, { interactedWithNewMR: true });
- },
};
diff --git a/app/assets/javascripts/ide/stores/modules/commit/state.js b/app/assets/javascripts/ide/stores/modules/commit/state.js
index 53647a7e3e3..259577e48e0 100644
--- a/app/assets/javascripts/ide/stores/modules/commit/state.js
+++ b/app/assets/javascripts/ide/stores/modules/commit/state.js
@@ -3,6 +3,5 @@ export default () => ({
commitAction: '1',
newBranchName: '',
submitCommitLoading: false,
- shouldCreateMR: false,
- interactedWithNewMR: false,
+ shouldCreateMR: true,
});
diff --git a/app/assets/javascripts/ide/stores/utils.js b/app/assets/javascripts/ide/stores/utils.js
index 04e86afb268..52200ce7847 100644
--- a/app/assets/javascripts/ide/stores/utils.js
+++ b/app/assets/javascripts/ide/stores/utils.js
@@ -129,7 +129,7 @@ export const commitActionForFile = file => {
export const getCommitFiles = stagedFiles =>
stagedFiles.reduce((acc, file) => {
- if (file.moved) return acc;
+ if (file.moved || file.type === 'tree') return acc;
return acc.concat({
...file,
diff --git a/app/assets/javascripts/issue_show/index.js b/app/assets/javascripts/issue_show/index.js
index 529b6386221..5a9dd91817e 100644
--- a/app/assets/javascripts/issue_show/index.js
+++ b/app/assets/javascripts/issue_show/index.js
@@ -1,4 +1,5 @@
import Vue from 'vue';
+import { initSidebarTracking } from 'ee_else_ce/event_tracking/issue_sidebar';
import issuableApp from './components/app.vue';
import { parseIssuableData } from './utils/parse_data';
import '../vue_shared/vue_resource_interceptor';
@@ -9,6 +10,9 @@ export default function initIssueableApp() {
components: {
issuableApp,
},
+ mounted() {
+ initSidebarTracking();
+ },
render(createElement) {
return createElement('issuable-app', {
props: parseIssuableData(),
diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js
index 31c4a920bbe..6e8f63a10a4 100644
--- a/app/assets/javascripts/lib/utils/common_utils.js
+++ b/app/assets/javascripts/lib/utils/common_utils.js
@@ -732,6 +732,66 @@ export const NavigationType = {
};
/**
+ * Method to perform case-insensitive search for a string
+ * within multiple properties and return object containing
+ * properties in case there are multiple matches or `null`
+ * if there's no match.
+ *
+ * Eg; Suppose we want to allow user to search using for a string
+ * within `iid`, `title`, `url` or `reference` props of a target object;
+ *
+ * const objectToSearch = {
+ * "iid": 1,
+ * "title": "Error omnis quos consequatur ullam a vitae sed omnis libero cupiditate. &3",
+ * "url": "/groups/gitlab-org/-/epics/1",
+ * "reference": "&1",
+ * };
+ *
+ * Following is how we call searchBy and the return values it will yield;
+ *
+ * - `searchBy('omnis', objectToSearch);`: This will return `{ title: ... }` as our
+ * query was found within title prop we only return that.
+ * - `searchBy('1', objectToSearch);`: This will return `{ "iid": ..., "reference": ..., "url": ... }`.
+ * - `searchBy('https://gitlab.com/groups/gitlab-org/-/epics/1', objectToSearch);`:
+ * This will return `{ "url": ... }`.
+ * - `searchBy('foo', objectToSearch);`: This will return `null` as no property value
+ * matched with our query.
+ *
+ * You can learn more about behaviour of this method by referring to tests
+ * within `spec/javascripts/lib/utils/common_utils_spec.js`.
+ *
+ * @param {string} query String to search for
+ * @param {object} searchSpace Object containing properties to search in for `query`
+ */
+export const searchBy = (query = '', searchSpace = {}) => {
+ const targetKeys = searchSpace !== null ? Object.keys(searchSpace) : [];
+
+ if (!query || !targetKeys.length) {
+ return null;
+ }
+
+ const normalizedQuery = query.toLowerCase();
+ const matches = targetKeys
+ .filter(item => {
+ const searchItem = `${searchSpace[item]}`.toLowerCase();
+
+ return (
+ searchItem.indexOf(normalizedQuery) > -1 ||
+ normalizedQuery.indexOf(searchItem) > -1 ||
+ normalizedQuery === searchItem
+ );
+ })
+ .reduce((acc, prop) => {
+ const match = acc;
+ match[prop] = searchSpace[prop];
+
+ return acc;
+ }, {});
+
+ return Object.keys(matches).length ? matches : null;
+};
+
+/**
* Checks if the given Label has a special syntax `::` in
* it's title.
*
diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js
index 1336b6a5461..7ead9d46fbb 100644
--- a/app/assets/javascripts/lib/utils/url_utility.js
+++ b/app/assets/javascripts/lib/utils/url_utility.js
@@ -1,5 +1,11 @@
import { join as joinPaths } from 'path';
+// Returns a decoded url parameter value
+// - Treats '+' as '%20'
+function decodeUrlParameter(val) {
+ return decodeURIComponent(val.replace(/\+/g, '%20'));
+}
+
// Returns an array containing the value(s) of the
// of the key passed as an argument
export function getParameterValues(sParam, url = window.location) {
@@ -30,7 +36,7 @@ export function mergeUrlParams(params, url) {
.forEach(part => {
if (part.length) {
const kv = part.split('=');
- merged[decodeURIComponent(kv[0])] = decodeURIComponent(kv.slice(1).join('='));
+ merged[decodeUrlParameter(kv[0])] = decodeUrlParameter(kv.slice(1).join('='));
}
});
}
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index ba33d72b1f3..39f2097c174 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -9,7 +9,11 @@ import './commons';
import './behaviors';
// lib/utils
-import { handleLocationHash, addSelectOnFocusBehaviour } from './lib/utils/common_utils';
+import {
+ handleLocationHash,
+ addSelectOnFocusBehaviour,
+ getCspNonceValue,
+} from './lib/utils/common_utils';
import { localTimeAgo } from './lib/utils/datetime_utility';
import { getLocationHash, visitUrl } from './lib/utils/url_utility';
@@ -39,6 +43,17 @@ import 'ee_else_ce/main_ee';
window.jQuery = jQuery;
window.$ = jQuery;
+// Add nonce to jQuery script handler
+jQuery.ajaxSetup({
+ converters: {
+ // eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings, func-names
+ 'text script': function(text) {
+ jQuery.globalEval(text, { nonce: getCspNonceValue() });
+ return text;
+ },
+ },
+});
+
// inject test utilities if necessary
if (process.env.NODE_ENV !== 'production' && gon && gon.test_env) {
$.fx.off = true;
diff --git a/app/assets/javascripts/members.js b/app/assets/javascripts/members.js
index af2697444f2..d719fd8748d 100644
--- a/app/assets/javascripts/members.js
+++ b/app/assets/javascripts/members.js
@@ -17,6 +17,8 @@ export default class Members {
}
dropdownClicked(options) {
+ options.e.preventDefault();
+
this.formSubmit(null, options.$el);
}
diff --git a/app/assets/javascripts/monitoring/components/charts/area.vue b/app/assets/javascripts/monitoring/components/charts/area.vue
index 90c764587a3..cac10474d06 100644
--- a/app/assets/javascripts/monitoring/components/charts/area.vue
+++ b/app/assets/javascripts/monitoring/components/charts/area.vue
@@ -12,6 +12,9 @@ import { graphDataValidatorForValues } from '../../utils';
let debouncedResize;
+// TODO: Remove this component in favor of the more general time_series.vue
+// Please port all changes here to time_series.vue as well.
+
export default {
components: {
GlAreaChart,
@@ -123,7 +126,7 @@ export default {
},
},
series: this.scatterSeries,
- dataZoom: this.dataZoomConfig,
+ dataZoom: [this.dataZoomConfig],
};
},
dataZoomConfig() {
diff --git a/app/assets/javascripts/monitoring/components/charts/time_series.vue b/app/assets/javascripts/monitoring/components/charts/time_series.vue
new file mode 100644
index 00000000000..02e7a7ba0a6
--- /dev/null
+++ b/app/assets/javascripts/monitoring/components/charts/time_series.vue
@@ -0,0 +1,342 @@
+<script>
+import { __ } from '~/locale';
+import { mapState } from 'vuex';
+import { GlLink, GlButton } from '@gitlab/ui';
+import { GlAreaChart, GlLineChart, GlChartSeriesLabel } from '@gitlab/ui/dist/charts';
+import dateFormat from 'dateformat';
+import { debounceByAnimationFrame, roundOffFloat } from '~/lib/utils/common_utils';
+import { getSvgIconPathContent } from '~/lib/utils/icon_utils';
+import Icon from '~/vue_shared/components/icon.vue';
+import { chartHeight, graphTypes, lineTypes, symbolSizes, dateFormats } from '../../constants';
+import { makeDataSeries } from '~/helpers/monitor_helper';
+import { graphDataValidatorForValues } from '../../utils';
+
+let debouncedResize;
+
+export default {
+ components: {
+ GlAreaChart,
+ GlLineChart,
+ GlButton,
+ GlChartSeriesLabel,
+ GlLink,
+ Icon,
+ },
+ inheritAttrs: false,
+ props: {
+ graphData: {
+ type: Object,
+ required: true,
+ validator: graphDataValidatorForValues.bind(null, false),
+ },
+ containerWidth: {
+ type: Number,
+ required: true,
+ },
+ deploymentData: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ projectPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ showBorder: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ singleEmbed: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ thresholds: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ },
+ data() {
+ return {
+ tooltip: {
+ title: '',
+ content: [],
+ commitUrl: '',
+ isDeployment: false,
+ sha: '',
+ },
+ width: 0,
+ height: chartHeight,
+ svgs: {},
+ primaryColor: null,
+ };
+ },
+ computed: {
+ ...mapState('monitoringDashboard', ['exportMetricsToCsvEnabled']),
+ chartData() {
+ // Transforms & supplements query data to render appropriate labels & styles
+ // Input: [{ queryAttributes1 }, { queryAttributes2 }]
+ // Output: [{ seriesAttributes1 }, { seriesAttributes2 }]
+ return this.graphData.queries.reduce((acc, query) => {
+ const { appearance } = query;
+ const lineType =
+ appearance && appearance.line && appearance.line.type
+ ? appearance.line.type
+ : lineTypes.default;
+ const lineWidth =
+ appearance && appearance.line && appearance.line.width
+ ? appearance.line.width
+ : undefined;
+ const areaStyle = {
+ opacity:
+ appearance && appearance.area && typeof appearance.area.opacity === 'number'
+ ? appearance.area.opacity
+ : undefined,
+ };
+
+ const series = makeDataSeries(query.result, {
+ name: this.formatLegendLabel(query),
+ lineStyle: {
+ type: lineType,
+ width: lineWidth,
+ },
+ showSymbol: false,
+ areaStyle: this.graphData.type === 'area-chart' ? areaStyle : undefined,
+ });
+
+ return acc.concat(series);
+ }, []);
+ },
+ chartOptions() {
+ return {
+ xAxis: {
+ name: __('Time'),
+ type: 'time',
+ axisLabel: {
+ formatter: date => dateFormat(date, dateFormats.timeOfDay),
+ },
+ axisPointer: {
+ snap: true,
+ },
+ },
+ yAxis: {
+ name: this.yAxisLabel,
+ axisLabel: {
+ formatter: num => roundOffFloat(num, 3).toString(),
+ },
+ },
+ series: this.scatterSeries,
+ dataZoom: this.dataZoomConfig,
+ };
+ },
+ dataZoomConfig() {
+ const handleIcon = this.svgs['scroll-handle'];
+
+ return handleIcon ? { handleIcon } : {};
+ },
+ earliestDatapoint() {
+ return this.chartData.reduce((acc, series) => {
+ const { data } = series;
+ const { length } = data;
+ if (!length) {
+ return acc;
+ }
+
+ const [first] = data[0];
+ const [last] = data[length - 1];
+ const seriesEarliest = first < last ? first : last;
+
+ return seriesEarliest < acc || acc === null ? seriesEarliest : acc;
+ }, null);
+ },
+ glChartComponent() {
+ const chartTypes = {
+ 'area-chart': GlAreaChart,
+ 'line-chart': GlLineChart,
+ };
+ return chartTypes[this.graphData.type] || GlAreaChart;
+ },
+ isMultiSeries() {
+ return this.tooltip.content.length > 1;
+ },
+ recentDeployments() {
+ return this.deploymentData.reduce((acc, deployment) => {
+ if (deployment.created_at >= this.earliestDatapoint) {
+ const { id, created_at, sha, ref, tag } = deployment;
+ acc.push({
+ id,
+ createdAt: created_at,
+ sha,
+ commitUrl: `${this.projectPath}/commit/${sha}`,
+ tag,
+ tagUrl: tag ? `${this.tagsPath}/${ref.name}` : null,
+ ref: ref.name,
+ showDeploymentFlag: false,
+ });
+ }
+
+ return acc;
+ }, []);
+ },
+ scatterSeries() {
+ return {
+ type: graphTypes.deploymentData,
+ data: this.recentDeployments.map(deployment => [deployment.createdAt, 0]),
+ symbol: this.svgs.rocket,
+ symbolSize: symbolSizes.default,
+ itemStyle: {
+ color: this.primaryColor,
+ },
+ };
+ },
+ yAxisLabel() {
+ return `${this.graphData.y_label}`;
+ },
+ csvText() {
+ const chartData = this.chartData[0].data;
+ const header = `timestamp,${this.graphData.y_label}\r\n`; // eslint-disable-line @gitlab/i18n/no-non-i18n-strings
+ return chartData.reduce((csv, data) => {
+ const row = data.join(',');
+ return `${csv}${row}\r\n`;
+ }, header);
+ },
+ downloadLink() {
+ const data = new Blob([this.csvText], { type: 'text/plain' });
+ return window.URL.createObjectURL(data);
+ },
+ },
+ watch: {
+ containerWidth: 'onResize',
+ },
+ beforeDestroy() {
+ window.removeEventListener('resize', debouncedResize);
+ },
+ created() {
+ debouncedResize = debounceByAnimationFrame(this.onResize);
+ window.addEventListener('resize', debouncedResize);
+ this.setSvg('rocket');
+ this.setSvg('scroll-handle');
+ },
+ methods: {
+ formatLegendLabel(query) {
+ return `${query.label}`;
+ },
+ formatTooltipText(params) {
+ this.tooltip.title = dateFormat(params.value, dateFormats.default);
+ this.tooltip.content = [];
+ params.seriesData.forEach(dataPoint => {
+ const [xVal, yVal] = dataPoint.value;
+ this.tooltip.isDeployment = dataPoint.componentSubType === graphTypes.deploymentData;
+ if (this.tooltip.isDeployment) {
+ const [deploy] = this.recentDeployments.filter(
+ deployment => deployment.createdAt === xVal,
+ );
+ this.tooltip.sha = deploy.sha.substring(0, 8);
+ this.tooltip.commitUrl = deploy.commitUrl;
+ } else {
+ const { seriesName, color } = dataPoint;
+ const value = yVal.toFixed(3);
+ this.tooltip.content.push({
+ name: seriesName,
+ value,
+ color,
+ });
+ }
+ });
+ },
+ setSvg(name) {
+ getSvgIconPathContent(name)
+ .then(path => {
+ if (path) {
+ this.$set(this.svgs, name, `path://${path}`);
+ }
+ })
+ .catch(e => {
+ // eslint-disable-next-line no-console, @gitlab/i18n/no-non-i18n-strings
+ console.error('SVG could not be rendered correctly: ', e);
+ });
+ },
+ onChartUpdated(chart) {
+ [this.primaryColor] = chart.getOption().color;
+ },
+ onResize() {
+ if (!this.$refs.chart) return;
+ const { width } = this.$refs.chart.$el.getBoundingClientRect();
+ this.width = width;
+ },
+ },
+};
+</script>
+
+<template>
+ <div
+ class="prometheus-graph col-12"
+ :class="[showBorder ? 'p-2' : 'p-0', { 'col-lg-6': !singleEmbed }]"
+ >
+ <div :class="{ 'prometheus-graph-embed w-100 p-3': showBorder }">
+ <div class="prometheus-graph-header">
+ <h5 class="prometheus-graph-title js-graph-title">{{ graphData.title }}</h5>
+ <gl-button
+ v-if="exportMetricsToCsvEnabled"
+ :href="downloadLink"
+ :title="__('Download CSV')"
+ :aria-label="__('Download CSV')"
+ style="margin-left: 200px;"
+ download="chart_metrics.csv"
+ >
+ {{ __('Download CSV') }}
+ </gl-button>
+ <div class="prometheus-graph-widgets js-graph-widgets">
+ <slot></slot>
+ </div>
+ </div>
+
+ <component
+ :is="glChartComponent"
+ ref="chart"
+ v-bind="$attrs"
+ :data="chartData"
+ :option="chartOptions"
+ :format-tooltip-text="formatTooltipText"
+ :thresholds="thresholds"
+ :width="width"
+ :height="height"
+ @updated="onChartUpdated"
+ >
+ <template v-if="tooltip.isDeployment">
+ <template slot="tooltipTitle">
+ {{ __('Deployed') }}
+ </template>
+ <div slot="tooltipContent" class="d-flex align-items-center">
+ <icon name="commit" class="mr-2" />
+ <gl-link :href="tooltip.commitUrl">{{ tooltip.sha }}</gl-link>
+ </div>
+ </template>
+ <template v-else>
+ <template slot="tooltipTitle">
+ <div class="text-nowrap">
+ {{ tooltip.title }}
+ </div>
+ </template>
+ <template slot="tooltipContent">
+ <div
+ v-for="(content, key) in tooltip.content"
+ :key="key"
+ class="d-flex justify-content-between"
+ >
+ <gl-chart-series-label :color="isMultiSeries ? content.color : ''">
+ {{ content.name }}
+ </gl-chart-series-label>
+ <div class="prepend-left-32">
+ {{ content.value }}
+ </div>
+ </div>
+ </template>
+ </template>
+ </component>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue
index dfeeba238ca..d330ceb836c 100644
--- a/app/assets/javascripts/monitoring/components/dashboard.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard.vue
@@ -14,9 +14,9 @@ import { __, s__ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
import { getParameterValues, mergeUrlParams } from '~/lib/utils/url_utility';
import invalidUrl from '~/lib/utils/invalid_url';
-import MonitorAreaChart from './charts/area.vue';
+import PanelType from 'ee_else_ce/monitoring/components/panel_type.vue';
+import MonitorTimeSeriesChart from './charts/time_series.vue';
import MonitorSingleStatChart from './charts/single_stat.vue';
-import PanelType from './panel_type.vue';
import GraphGroup from './graph_group.vue';
import EmptyState from './empty_state.vue';
import { sidebarAnimationDuration, timeWindows } from '../constants';
@@ -26,7 +26,7 @@ let sidebarMutationObserver;
export default {
components: {
- MonitorAreaChart,
+ MonitorTimeSeriesChart,
MonitorSingleStatChart,
PanelType,
GraphGroup,
@@ -141,6 +141,16 @@ export default {
required: false,
default: false,
},
+ alertsEndpoint: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ prometheusAlertsAvailable: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
@@ -264,12 +274,12 @@ export default {
showToast() {
this.$toast.show(__('Link copied to clipboard'));
},
+ // TODO: END
generateLink(group, title, yLabel) {
const dashboard = this.currentDashboard || this.firstDashboard.path;
- const params = { dashboard, group, title, y_label: yLabel };
+ const params = _.pick({ dashboard, group, title, y_label: yLabel }, value => value != null);
return mergeUrlParams(params, window.location.href);
},
- // TODO: END
hideAddMetricModal() {
this.$refs.addMetricModal.hide();
},
@@ -449,11 +459,13 @@ export default {
:clipboard-text="generateLink(groupData.group, graphData.title, graphData.y_label)"
:graph-data="graphData"
:dashboard-width="elWidth"
+ :alerts-endpoint="alertsEndpoint"
+ :prometheus-alerts-available="prometheusAlertsAvailable"
:index="`${index}-${graphIndex}`"
/>
</template>
<template v-else>
- <monitor-area-chart
+ <monitor-time-series-chart
v-for="(graphData, graphIndex) in chartsWithData(groupData.metrics)"
:key="graphIndex"
:graph-data="graphData"
@@ -461,7 +473,7 @@ export default {
:thresholds="getGraphAlertValues(graphData.queries)"
:container-width="elWidth"
:project-path="projectPath"
- group-id="monitor-area-chart"
+ group-id="monitor-time-series-chart"
>
<div class="d-flex align-items-center">
<alert-widget
@@ -503,7 +515,7 @@ export default {
</gl-dropdown-item>
</gl-dropdown>
</div>
- </monitor-area-chart>
+ </monitor-time-series-chart>
</template>
</graph-group>
</div>
diff --git a/app/assets/javascripts/monitoring/components/embed.vue b/app/assets/javascripts/monitoring/components/embed.vue
index e3256147618..b516a82c170 100644
--- a/app/assets/javascripts/monitoring/components/embed.vue
+++ b/app/assets/javascripts/monitoring/components/embed.vue
@@ -2,7 +2,7 @@
import { mapActions, mapState } from 'vuex';
import { getParameterValues, removeParams } from '~/lib/utils/url_utility';
import GraphGroup from './graph_group.vue';
-import MonitorAreaChart from './charts/area.vue';
+import MonitorTimeSeriesChart from './charts/time_series.vue';
import { sidebarAnimationDuration } from '../constants';
import { getTimeDiff } from '../utils';
@@ -11,7 +11,7 @@ let sidebarMutationObserver;
export default {
components: {
GraphGroup,
- MonitorAreaChart,
+ MonitorTimeSeriesChart,
},
props: {
dashboardUrl: {
@@ -92,7 +92,7 @@ export default {
<template>
<div class="metrics-embed" :class="{ 'd-inline-flex col-lg-6 p-0': isSingleChart }">
<div v-if="charts.length" class="row w-100 m-n2 pb-4">
- <monitor-area-chart
+ <monitor-time-series-chart
v-for="graphData in charts"
:key="graphData.title"
:graph-data="graphData"
diff --git a/app/assets/javascripts/monitoring/components/panel_type.vue b/app/assets/javascripts/monitoring/components/panel_type.vue
index 96f62bc85ee..73ff651d510 100644
--- a/app/assets/javascripts/monitoring/components/panel_type.vue
+++ b/app/assets/javascripts/monitoring/components/panel_type.vue
@@ -10,14 +10,14 @@ import {
GlTooltipDirective,
} from '@gitlab/ui';
import Icon from '~/vue_shared/components/icon.vue';
-import MonitorAreaChart from './charts/area.vue';
+import MonitorTimeSeriesChart from './charts/time_series.vue';
import MonitorSingleStatChart from './charts/single_stat.vue';
import MonitorEmptyChart from './charts/empty_chart.vue';
export default {
components: {
- MonitorAreaChart,
MonitorSingleStatChart,
+ MonitorTimeSeriesChart,
MonitorEmptyChart,
Icon,
GlDropdown,
@@ -92,7 +92,7 @@ export default {
v-if="isPanelType('single-stat') && graphDataHasMetrics"
:graph-data="graphData"
/>
- <monitor-area-chart
+ <monitor-time-series-chart
v-else-if="graphDataHasMetrics"
:graph-data="graphData"
:deployment-data="deploymentData"
@@ -136,6 +136,6 @@ export default {
</gl-dropdown-item>
</gl-dropdown>
</div>
- </monitor-area-chart>
+ </monitor-time-series-chart>
<monitor-empty-chart v-else :graph-title="graphData.title" />
</template>
diff --git a/app/assets/javascripts/monitoring/constants.js b/app/assets/javascripts/monitoring/constants.js
index d7d89522732..13aba3d9f44 100644
--- a/app/assets/javascripts/monitoring/constants.js
+++ b/app/assets/javascripts/monitoring/constants.js
@@ -8,6 +8,10 @@ export const graphTypes = {
deploymentData: 'scatter',
};
+export const symbolSizes = {
+ default: 14,
+};
+
export const lineTypes = {
default: 'solid',
};
@@ -21,6 +25,11 @@ export const timeWindows = {
oneWeek: __('1 week'),
};
+export const dateFormats = {
+ timeOfDay: 'h:MM TT',
+ default: 'dd mmm yyyy, h:MMTT',
+};
+
export const secondsIn = {
thirtyMinutes: 60 * 30,
threeHours: 60 * 60 * 3,
diff --git a/app/assets/javascripts/notes/stores/getters.js b/app/assets/javascripts/notes/stores/getters.js
index 52410f18d4a..3d0ec8cd3a7 100644
--- a/app/assets/javascripts/notes/stores/getters.js
+++ b/app/assets/javascripts/notes/stores/getters.js
@@ -171,26 +171,33 @@ export const isLastUnresolvedDiscussion = (state, getters) => (discussionId, dif
return lastDiscussionId === discussionId;
};
-// Gets the ID of the discussion following the one provided, respecting order (diff or date)
-// @param {Boolean} discussionId - id of the current discussion
-// @param {Boolean} diffOrder - is ordered by diff?
-export const nextUnresolvedDiscussionId = (state, getters) => (discussionId, diffOrder) => {
- const idsOrdered = getters.unresolvedDiscussionsIdsOrdered(diffOrder);
- const currentIndex = idsOrdered.indexOf(discussionId);
- const slicedIds = idsOrdered.slice(currentIndex + 1, currentIndex + 2);
+export const findUnresolvedDiscussionIdNeighbor = (state, getters) => ({
+ discussionId,
+ diffOrder,
+ step,
+}) => {
+ const ids = getters.unresolvedDiscussionsIdsOrdered(diffOrder);
+ const index = ids.indexOf(discussionId) + step;
+
+ if (index < 0 && step < 0) {
+ return ids[ids.length - 1];
+ }
+
+ if (index === ids.length && step > 0) {
+ return ids[0];
+ }
- // Get the first ID if there is none after the currentIndex
- return slicedIds.length ? idsOrdered.slice(currentIndex + 1, currentIndex + 2)[0] : idsOrdered[0];
+ return ids[index];
};
-export const previousUnresolvedDiscussionId = (state, getters) => (discussionId, diffOrder) => {
- const idsOrdered = getters.unresolvedDiscussionsIdsOrdered(diffOrder);
- const currentIndex = idsOrdered.indexOf(discussionId);
- const slicedIds = idsOrdered.slice(currentIndex - 1, currentIndex);
+// Gets the ID of the discussion following the one provided, respecting order (diff or date)
+// @param {Boolean} discussionId - id of the current discussion
+// @param {Boolean} diffOrder - is ordered by diff?
+export const nextUnresolvedDiscussionId = (state, getters) => (discussionId, diffOrder) =>
+ getters.findUnresolvedDiscussionIdNeighbor({ discussionId, diffOrder, step: 1 });
- // Get the last ID if there is none after the currentIndex
- return slicedIds.length ? slicedIds[0] : idsOrdered[idsOrdered.length - 1];
-};
+export const previousUnresolvedDiscussionId = (state, getters) => (discussionId, diffOrder) =>
+ getters.findUnresolvedDiscussionIdNeighbor({ discussionId, diffOrder, step: -1 });
// @param {Boolean} diffOrder - is ordered by diff?
export const firstUnresolvedDiscussionId = (state, getters) => diffOrder => {
diff --git a/app/assets/javascripts/pages/projects/wikis/wikis.js b/app/assets/javascripts/pages/projects/wikis/wikis.js
index 9b58d42b47d..d41199f6374 100644
--- a/app/assets/javascripts/pages/projects/wikis/wikis.js
+++ b/app/assets/javascripts/pages/projects/wikis/wikis.js
@@ -1,6 +1,5 @@
import bp from '../../../breakpoints';
-import { parseQueryStringIntoObject } from '../../../lib/utils/common_utils';
-import { mergeUrlParams, redirectTo } from '../../../lib/utils/url_utility';
+import { s__, sprintf } from '~/locale';
export default class Wikis {
constructor() {
@@ -12,32 +11,37 @@ export default class Wikis {
sidebarToggles[i].addEventListener('click', e => this.handleToggleSidebar(e));
}
- this.newWikiForm = document.querySelector('form.new-wiki-page');
- if (this.newWikiForm) {
- this.newWikiForm.addEventListener('submit', e => this.handleNewWikiSubmit(e));
+ this.isNewWikiPage = Boolean(document.querySelector('.js-new-wiki-page'));
+ this.editTitleInput = document.querySelector('form.wiki-form #wiki_title');
+ this.commitMessageInput = document.querySelector('form.wiki-form #wiki_message');
+ this.commitMessageI18n = this.isNewWikiPage
+ ? s__('WikiPageCreate|Create %{pageTitle}')
+ : s__('WikiPageEdit|Update %{pageTitle}');
+
+ if (this.editTitleInput) {
+ // Initialize the commit message on load
+ if (this.editTitleInput.value) this.setWikiCommitMessage(this.editTitleInput.value);
+
+ // Set the commit message as the page title is changed
+ this.editTitleInput.addEventListener('keyup', e => this.handleWikiTitleChange(e));
}
window.addEventListener('resize', () => this.renderSidebar());
this.renderSidebar();
}
- handleNewWikiSubmit(e) {
- if (!this.newWikiForm) return;
-
- const slugInput = this.newWikiForm.querySelector('#new_wiki_path');
-
- const slug = slugInput.value;
+ handleWikiTitleChange(e) {
+ this.setWikiCommitMessage(e.target.value);
+ }
- if (slug.length > 0) {
- const wikisPath = slugInput.getAttribute('data-wikis-path');
+ setWikiCommitMessage(rawTitle) {
+ let title = rawTitle;
- // If the wiki is empty, we need to merge the current URL params to keep the "create" view.
- const params = parseQueryStringIntoObject(window.location.search.substr(1));
- const url = mergeUrlParams(params, `${wikisPath}/${slug}`);
- redirectTo(url);
+ // Replace hyphens with spaces
+ if (title) title = title.replace(/-+/g, ' ');
- e.preventDefault();
- }
+ const newCommitMessage = sprintf(this.commitMessageI18n, { pageTitle: title });
+ this.commitMessageInput.value = newCommitMessage;
}
handleToggleSidebar(e) {
diff --git a/app/assets/javascripts/pipelines/components/pipeline_url.vue b/app/assets/javascripts/pipelines/components/pipeline_url.vue
index e01080b04d6..a08f732dda7 100644
--- a/app/assets/javascripts/pipelines/components/pipeline_url.vue
+++ b/app/assets/javascripts/pipelines/components/pipeline_url.vue
@@ -67,7 +67,7 @@ export default {
<span
v-if="pipeline.flags.latest"
v-gl-tooltip
- :title="__('Latest pipeline for this branch')"
+ :title="__('Latest pipeline for the most recent commit on this branch')"
class="js-pipeline-url-latest badge badge-success"
>
{{ __('latest') }}
@@ -104,7 +104,7 @@ export default {
v-gl-tooltip
:title="
__(
- 'The code of a detached pipeline is tested against the source branch instead of merged results',
+ 'Pipelines for merge requests are configured. A detached pipeline runs in the context of the merge request, and not against the merged result. Learn more on the documentation for Pipelines for Merged Results.',
)
"
class="js-pipeline-url-detached badge badge-info"
diff --git a/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue b/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue
index 03281aa1317..12ee1ce2f0c 100644
--- a/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue
+++ b/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue
@@ -38,7 +38,9 @@ export default {
},
computed: {
statusTitle() {
- return sprintf(s__('Commits|Commit: %{commitText}'), { commitText: this.ciStatus.text });
+ return sprintf(s__('PipelineStatusTooltip|Pipeline: %{ciStatus}'), {
+ ciStatus: this.ciStatus.text,
+ });
},
},
mounted() {
diff --git a/app/assets/javascripts/registry/components/app.vue b/app/assets/javascripts/registry/components/app.vue
index efbf0a4e3cf..346dc470a59 100644
--- a/app/assets/javascripts/registry/components/app.vue
+++ b/app/assets/javascripts/registry/components/app.vue
@@ -1,10 +1,9 @@
<script>
import { mapGetters, mapActions } from 'vuex';
-import { GlLoadingIcon } from '@gitlab/ui';
+import { GlLoadingIcon, GlEmptyState } from '@gitlab/ui';
import store from '../stores';
import clipboardButton from '../../vue_shared/components/clipboard_button.vue';
import CollapsibleContainer from './collapsible_container.vue';
-import SvgMessage from './svg_message.vue';
import { s__, sprintf } from '../../locale';
export default {
@@ -12,8 +11,8 @@ export default {
components: {
clipboardButton,
CollapsibleContainer,
+ GlEmptyState,
GlLoadingIcon,
- SvgMessage,
},
props: {
endpoint: {
@@ -93,7 +92,9 @@ export default {
this.setMainEndpoint(this.endpoint);
},
mounted() {
- this.fetchRepos();
+ if (!this.characterError) {
+ this.fetchRepos();
+ }
},
methods: {
...mapActions(['setMainEndpoint', 'fetchRepos']),
@@ -102,61 +103,63 @@ export default {
</script>
<template>
<div>
- <svg-message v-if="characterError" id="invalid-characters" :svg-path="containersErrorImage">
- <h4>
- {{ s__('ContainerRegistry|Docker connection error') }}
- </h4>
- <p v-html="dockerConnectionErrorText"></p>
- </svg-message>
+ <gl-empty-state
+ v-if="characterError"
+ :title="s__('ContainerRegistry|Docker connection error')"
+ :svg-path="containersErrorImage"
+ >
+ <template #description>
+ <p v-html="dockerConnectionErrorText"></p>
+ </template>
+ </gl-empty-state>
- <gl-loading-icon v-else-if="isLoading && !characterError" size="md" class="prepend-top-16" />
+ <gl-loading-icon v-else-if="isLoading" size="md" class="prepend-top-16" />
- <div v-else-if="!isLoading && !characterError && repos.length">
+ <div v-else-if="!isLoading && repos.length">
<h4>{{ s__('ContainerRegistry|Container Registry') }}</h4>
<p v-html="introText"></p>
<collapsible-container v-for="item in repos" :key="item.id" :repo="item" />
</div>
- <svg-message
- v-else-if="!isLoading && !characterError && !repos.length"
- id="no-container-images"
+ <gl-empty-state
+ v-else
+ :title="s__('ContainerRegistry|There are no container images stored for this project')"
:svg-path="noContainersImage"
+ class="container-message"
>
- <h4>
- {{ s__('ContainerRegistry|There are no container images stored for this project') }}
- </h4>
- <p v-html="noContainerImagesText"></p>
-
- <h5>{{ s__('ContainerRegistry|Quick Start') }}</h5>
- <p>
- {{
- s__(
- 'ContainerRegistry|You can add an image to this registry with the following commands:',
- )
- }}
- </p>
+ <template #description>
+ <p class="js-no-container-images-text" v-html="noContainerImagesText"></p>
+ <h5>{{ s__('ContainerRegistry|Quick Start') }}</h5>
+ <p>
+ {{
+ s__(
+ 'ContainerRegistry|You can add an image to this registry with the following commands:',
+ )
+ }}
+ </p>
- <div class="input-group append-bottom-10">
- <input :value="dockerBuildCommand" type="text" class="form-control monospace" readonly />
- <span class="input-group-append">
- <clipboard-button
- :text="dockerBuildCommand"
- :title="s__('ContainerRegistry|Copy build command to clipboard')"
- class="input-group-text"
- />
- </span>
- </div>
+ <div class="input-group append-bottom-10">
+ <input :value="dockerBuildCommand" type="text" class="form-control monospace" readonly />
+ <span class="input-group-append">
+ <clipboard-button
+ :text="dockerBuildCommand"
+ :title="s__('ContainerRegistry|Copy build command to clipboard')"
+ class="input-group-text"
+ />
+ </span>
+ </div>
- <div class="input-group">
- <input :value="dockerPushCommand" type="text" class="form-control monospace" readonly />
- <span class="input-group-append">
- <clipboard-button
- :text="dockerPushCommand"
- :title="s__('ContainerRegistry|Copy push command to clipboard')"
- class="input-group-text"
- />
- </span>
- </div>
- </svg-message>
+ <div class="input-group">
+ <input :value="dockerPushCommand" type="text" class="form-control monospace" readonly />
+ <span class="input-group-append">
+ <clipboard-button
+ :text="dockerPushCommand"
+ :title="s__('ContainerRegistry|Copy push command to clipboard')"
+ class="input-group-text"
+ />
+ </span>
+ </div>
+ </template>
+ </gl-empty-state>
</div>
</template>
diff --git a/app/assets/javascripts/registry/components/svg_message.vue b/app/assets/javascripts/registry/components/svg_message.vue
deleted file mode 100644
index 617093e054e..00000000000
--- a/app/assets/javascripts/registry/components/svg_message.vue
+++ /dev/null
@@ -1,26 +0,0 @@
-<script>
-export default {
- name: 'RegistrySvgMessage',
- props: {
- id: {
- type: String,
- required: true,
- },
- svgPath: {
- type: String,
- required: true,
- },
- },
-};
-</script>
-
-<template>
- <div :id="id" class="empty-state container-message">
- <div class="svg-content">
- <img :src="svgPath" class="flex-align-self-center" />
- </div>
- <div class="text-content">
- <slot></slot>
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/sidebar/components/assignees/assignee_avatar.vue b/app/assets/javascripts/sidebar/components/assignees/assignee_avatar.vue
new file mode 100644
index 00000000000..71a1fc31315
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/assignees/assignee_avatar.vue
@@ -0,0 +1,48 @@
+<script>
+import { __, sprintf } from '~/locale';
+
+export default {
+ props: {
+ user: {
+ type: Object,
+ required: true,
+ },
+ imgSize: {
+ type: Number,
+ required: true,
+ },
+ issuableType: {
+ type: String,
+ required: false,
+ default: 'issue',
+ },
+ },
+ computed: {
+ assigneeAlt() {
+ return sprintf(__("%{userName}'s avatar"), { userName: this.user.name });
+ },
+ avatarUrl() {
+ return this.user.avatar || this.user.avatar_url || gon.default_avatar_url;
+ },
+ isMergeRequest() {
+ return this.issuableType === 'merge_request';
+ },
+ hasMergeIcon() {
+ return this.isMergeRequest && !this.user.can_merge;
+ },
+ },
+};
+</script>
+
+<template>
+ <span class="position-relative">
+ <img
+ :alt="assigneeAlt"
+ :src="avatarUrl"
+ :width="imgSize"
+ :class="`s${imgSize}`"
+ class="avatar avatar-inline m-0"
+ />
+ <i v-if="hasMergeIcon" aria-hidden="true" class="fa fa-exclamation-triangle merge-icon"></i>
+ </span>
+</template>
diff --git a/app/assets/javascripts/sidebar/components/assignees/assignee_avatar_link.vue b/app/assets/javascripts/sidebar/components/assignees/assignee_avatar_link.vue
new file mode 100644
index 00000000000..6633a63d046
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/assignees/assignee_avatar_link.vue
@@ -0,0 +1,83 @@
+<script>
+import { __, sprintf } from '~/locale';
+import { GlTooltipDirective, GlLink } from '@gitlab/ui';
+import { joinPaths } from '~/lib/utils/url_utility';
+import AssigneeAvatar from './assignee_avatar.vue';
+
+export default {
+ components: {
+ AssigneeAvatar,
+ GlLink,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ props: {
+ user: {
+ type: Object,
+ required: true,
+ },
+ rootPath: {
+ type: String,
+ required: true,
+ },
+ tooltipPlacement: {
+ type: String,
+ default: 'bottom',
+ required: false,
+ },
+ tooltipHasName: {
+ type: Boolean,
+ default: true,
+ required: false,
+ },
+ issuableType: {
+ type: String,
+ default: 'issue',
+ required: false,
+ },
+ },
+ computed: {
+ cannotMerge() {
+ return this.issuableType === 'merge_request' && !this.user.can_merge;
+ },
+ tooltipTitle() {
+ if (this.cannotMerge && this.tooltipHasName) {
+ return sprintf(__('%{userName} (cannot merge)'), { userName: this.user.name });
+ } else if (this.cannotMerge) {
+ return __('Cannot merge');
+ } else if (this.tooltipHasName) {
+ return this.user.name;
+ }
+
+ return '';
+ },
+ tooltipOption() {
+ return {
+ container: 'body',
+ placement: this.tooltipPlacement,
+ boundary: 'viewport',
+ };
+ },
+ assigneeUrl() {
+ return joinPaths(`${this.rootPath}`, `${this.user.username}`);
+ },
+ },
+};
+</script>
+
+<template>
+ <!-- must be `d-inline-block` or parent flex-basis causes width issues -->
+ <gl-link
+ v-gl-tooltip="tooltipOption"
+ :href="assigneeUrl"
+ :title="tooltipTitle"
+ class="d-inline-block"
+ >
+ <!-- use d-flex so that slot can be appropriately styled -->
+ <span class="d-flex">
+ <assignee-avatar :user="user" :img-size="32" :issuable-type="issuableType" />
+ <slot :user="user"></slot>
+ </span>
+ </gl-link>
+</template>
diff --git a/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue b/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue
index fa6b6bfaef1..63b93a80ead 100644
--- a/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue
@@ -1,5 +1,6 @@
<script>
import { n__ } from '~/locale';
+import { trackEvent } from 'ee_else_ce/event_tracking/issue_sidebar';
export default {
name: 'AssigneeTitle',
@@ -29,13 +30,23 @@ export default {
return n__('Assignee', `%d Assignees`, assignees);
},
},
+ methods: {
+ trackEdit() {
+ trackEvent('click_edit_button', 'assignee');
+ },
+ },
};
</script>
<template>
<div class="title hide-collapsed">
{{ assigneeTitle }}
<i v-if="loading" aria-hidden="true" class="fa fa-spinner fa-spin block-loading"></i>
- <a v-if="editable" class="js-sidebar-dropdown-toggle edit-link float-right" href="#">
+ <a
+ v-if="editable"
+ class="js-sidebar-dropdown-toggle edit-link float-right"
+ href="#"
+ @click.prevent="trackEdit"
+ >
{{ __('Edit') }}
</a>
<a
diff --git a/app/assets/javascripts/sidebar/components/assignees/assignees.vue b/app/assets/javascripts/sidebar/components/assignees/assignees.vue
index 631e2e28d4d..d9739e8d197 100644
--- a/app/assets/javascripts/sidebar/components/assignees/assignees.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/assignees.vue
@@ -1,13 +1,14 @@
<script>
-import { __, sprintf } from '~/locale';
-import tooltip from '~/vue_shared/directives/tooltip';
+import CollapsedAssigneeList from '../assignees/collapsed_assignee_list.vue';
+import UncollapsedAssigneeList from '../assignees/uncollapsed_assignee_list.vue';
export default {
// name: 'Assignees' is a false positive: https://gitlab.com/gitlab-org/frontend/eslint-plugin-i18n/issues/26#possible-false-positives
// eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings
name: 'Assignees',
- directives: {
- tooltip,
+ components: {
+ CollapsedAssigneeList,
+ UncollapsedAssigneeList,
},
props: {
rootPath: {
@@ -24,171 +25,34 @@ export default {
},
issuableType: {
type: String,
- require: true,
+ required: false,
default: 'issue',
},
},
- data() {
- return {
- defaultRenderCount: 5,
- defaultMaxCounter: 99,
- showLess: true,
- };
- },
computed: {
- firstUser() {
- return this.users[0];
- },
- hasMoreThanTwoAssignees() {
- return this.users.length > 2;
- },
- hasMoreThanOneAssignee() {
- return this.users.length > 1;
- },
- hasAssignees() {
- return this.users.length > 0;
- },
hasNoUsers() {
return !this.users.length;
},
- hasOneUser() {
- return this.users.length === 1;
- },
- renderShowMoreSection() {
- return this.users.length > this.defaultRenderCount;
- },
- numberOfHiddenAssignees() {
- return this.users.length - this.defaultRenderCount;
- },
- isHiddenAssignees() {
- return this.numberOfHiddenAssignees > 0;
- },
- hiddenAssigneesLabel() {
- const { numberOfHiddenAssignees } = this;
- return sprintf(__('+ %{numberOfHiddenAssignees} more'), { numberOfHiddenAssignees });
- },
- collapsedTooltipTitle() {
- const maxRender = Math.min(this.defaultRenderCount, this.users.length);
- const renderUsers = this.users.slice(0, maxRender);
- const names = renderUsers.map(u => u.name);
-
- if (this.users.length > maxRender) {
- names.push(`+ ${this.users.length - maxRender} more`);
- }
-
- if (!this.users.length) {
- const emptyTooltipLabel = __('Assignee(s)');
- names.push(emptyTooltipLabel);
- }
-
- return names.join(', ');
- },
- sidebarAvatarCounter() {
- let counter = `+${this.users.length - 1}`;
-
- if (this.users.length > this.defaultMaxCounter) {
- counter = `${this.defaultMaxCounter}+`;
- }
+ sortedAssigness() {
+ const canMergeUsers = this.users.filter(user => user.can_merge);
+ const canNotMergeUsers = this.users.filter(user => !user.can_merge);
- return counter;
- },
- mergeNotAllowedTooltipMessage() {
- const assigneesCount = this.users.length;
-
- if (this.issuableType !== 'merge_request' || assigneesCount === 0) {
- return null;
- }
-
- const cannotMergeCount = this.users.filter(u => u.can_merge === false).length;
- const canMergeCount = assigneesCount - cannotMergeCount;
-
- if (canMergeCount === assigneesCount) {
- // Everyone can merge
- return null;
- } else if (cannotMergeCount === assigneesCount && assigneesCount > 1) {
- return __('No one can merge');
- } else if (assigneesCount === 1) {
- return __('Cannot merge');
- }
-
- return sprintf(__('%{canMergeCount}/%{assigneesCount} can merge'), {
- canMergeCount,
- assigneesCount,
- });
+ return [...canMergeUsers, ...canNotMergeUsers];
},
},
methods: {
assignSelf() {
this.$emit('assign-self');
},
- toggleShowLess() {
- this.showLess = !this.showLess;
- },
- renderAssignee(index) {
- return !this.showLess || (index < this.defaultRenderCount && this.showLess);
- },
- avatarUrl(user) {
- return user.avatar || user.avatar_url || gon.default_avatar_url;
- },
- assigneeUrl(user) {
- return `${this.rootPath}${user.username}`;
- },
- assigneeAlt(user) {
- return sprintf(__("%{userName}'s avatar"), { userName: user.name });
- },
- assigneeUsername(user) {
- return `@${user.username}`;
- },
- shouldRenderCollapsedAssignee(index) {
- const firstTwo = this.users.length <= 2 && index <= 2;
-
- return index === 0 || firstTwo;
- },
},
};
</script>
<template>
<div>
- <div
- v-tooltip
- :class="{ 'multiple-users': hasMoreThanOneAssignee }"
- :title="collapsedTooltipTitle"
- class="sidebar-collapsed-icon sidebar-collapsed-user"
- data-container="body"
- data-placement="left"
- data-boundary="viewport"
- >
- <i v-if="hasNoUsers" :aria-label="__('None')" class="fa fa-user"> </i>
- <button
- v-for="(user, index) in users"
- v-if="shouldRenderCollapsedAssignee(index)"
- :key="user.id"
- type="button"
- class="btn-link"
- >
- <img
- :alt="assigneeAlt(user)"
- :src="avatarUrl(user)"
- width="24"
- class="avatar avatar-inline s24"
- />
- <span class="author"> {{ user.name }} </span>
- </button>
- <button v-if="hasMoreThanTwoAssignees" class="btn-link" type="button">
- <span class="avatar-counter sidebar-avatar-counter"> {{ sidebarAvatarCounter }} </span>
- </button>
- </div>
+ <collapsed-assignee-list :users="sortedAssigness" :issuable-type="issuableType" />
+
<div class="value hide-collapsed">
- <span
- v-if="mergeNotAllowedTooltipMessage"
- v-tooltip
- :title="mergeNotAllowedTooltipMessage"
- data-placement="left"
- class="float-right cannot-be-merged"
- >
- <i aria-hidden="true" data-hidden="true" class="fa fa-exclamation-triangle"></i>
- </span>
<template v-if="hasNoUsers">
<span class="assign-yourself no-value qa-assign-yourself">
{{ __('None') }}
@@ -200,51 +64,13 @@ export default {
</template>
</span>
</template>
- <template v-else-if="hasOneUser">
- <a :href="assigneeUrl(firstUser)" class="author-link bold">
- <img
- :alt="assigneeAlt(firstUser)"
- :src="avatarUrl(firstUser)"
- width="32"
- class="avatar avatar-inline s32"
- />
- <span class="author"> {{ firstUser.name }} </span>
- <span class="username"> {{ assigneeUsername(firstUser) }} </span>
- </a>
- </template>
- <template v-else>
- <div class="user-list">
- <div
- v-for="(user, index) in users"
- v-if="renderAssignee(index)"
- :key="user.id"
- class="user-item"
- >
- <a
- :href="assigneeUrl(user)"
- :data-title="user.name"
- class="user-link has-tooltip"
- data-container="body"
- data-placement="bottom"
- >
- <img
- :alt="assigneeAlt(user)"
- :src="avatarUrl(user)"
- width="32"
- class="avatar avatar-inline s32"
- />
- </a>
- </div>
- </div>
- <div v-if="renderShowMoreSection" class="user-list-more">
- <button type="button" class="btn-link" @click="toggleShowLess">
- <template v-if="showLess">
- {{ hiddenAssigneesLabel }}
- </template>
- <template v-else>{{ __('- show less') }}</template>
- </button>
- </div>
- </template>
+
+ <uncollapsed-assignee-list
+ v-else
+ :users="sortedAssigness"
+ :root-path="rootPath"
+ :issuable-type="issuableType"
+ />
</div>
</div>
</template>
diff --git a/app/assets/javascripts/sidebar/components/assignees/collapsed_assignee.vue b/app/assets/javascripts/sidebar/components/assignees/collapsed_assignee.vue
new file mode 100644
index 00000000000..2f654409561
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/assignees/collapsed_assignee.vue
@@ -0,0 +1,27 @@
+<script>
+import AssigneeAvatar from './assignee_avatar.vue';
+
+export default {
+ components: {
+ AssigneeAvatar,
+ },
+ props: {
+ user: {
+ type: Object,
+ required: true,
+ },
+ issuableType: {
+ type: String,
+ required: false,
+ default: 'issue',
+ },
+ },
+};
+</script>
+
+<template>
+ <button type="button" class="btn-link">
+ <assignee-avatar :user="user" :img-size="24" :issuable-type="issuableType" />
+ <span class="author"> {{ user.name }} </span>
+ </button>
+</template>
diff --git a/app/assets/javascripts/sidebar/components/assignees/collapsed_assignee_list.vue b/app/assets/javascripts/sidebar/components/assignees/collapsed_assignee_list.vue
new file mode 100644
index 00000000000..5b4a43399ca
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/assignees/collapsed_assignee_list.vue
@@ -0,0 +1,121 @@
+<script>
+import { __, sprintf } from '~/locale';
+import { GlTooltipDirective } from '@gitlab/ui';
+import CollapsedAssignee from './collapsed_assignee.vue';
+
+const DEFAULT_MAX_COUNTER = 99;
+const DEFAULT_RENDER_COUNT = 5;
+
+export default {
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ components: {
+ CollapsedAssignee,
+ },
+ props: {
+ users: {
+ type: Array,
+ required: true,
+ },
+ issuableType: {
+ type: String,
+ required: false,
+ default: 'issue',
+ },
+ },
+ computed: {
+ isMergeRequest() {
+ return this.issuableType === 'merge_request';
+ },
+ hasNoUsers() {
+ return !this.users.length;
+ },
+ hasMoreThanOneAssignee() {
+ return this.users.length > 1;
+ },
+ hasMoreThanTwoAssignees() {
+ return this.users.length > 2;
+ },
+ allAssigneesCanMerge() {
+ return this.users.every(user => user.can_merge);
+ },
+ sidebarAvatarCounter() {
+ if (this.users.length > DEFAULT_MAX_COUNTER) {
+ return `${DEFAULT_MAX_COUNTER}+`;
+ }
+
+ return `+${this.users.length - 1}`;
+ },
+ collapsedUsers() {
+ const collapsedLength = this.hasMoreThanTwoAssignees ? 1 : this.users.length;
+
+ return this.users.slice(0, collapsedLength);
+ },
+ tooltipTitleMergeStatus() {
+ if (!this.isMergeRequest) {
+ return '';
+ }
+
+ const mergeLength = this.users.filter(u => u.can_merge).length;
+
+ if (mergeLength === this.users.length) {
+ return '';
+ } else if (mergeLength > 0) {
+ return sprintf(__('%{mergeLength}/%{usersLength} can merge'), {
+ mergeLength,
+ usersLength: this.users.length,
+ });
+ }
+
+ return this.users.length === 1 ? __('cannot merge') : __('no one can merge');
+ },
+ tooltipTitle() {
+ const maxRender = Math.min(DEFAULT_RENDER_COUNT, this.users.length);
+ const renderUsers = this.users.slice(0, maxRender);
+ const names = renderUsers.map(u => u.name);
+
+ if (!this.users.length) {
+ return __('Assignee(s)');
+ }
+
+ if (this.users.length > names.length) {
+ names.push(sprintf(__('+ %{amount} more'), { amount: this.users.length - names.length }));
+ }
+
+ const text = names.join(', ');
+
+ return this.tooltipTitleMergeStatus ? `${text} (${this.tooltipTitleMergeStatus})` : text;
+ },
+
+ tooltipOptions() {
+ return { container: 'body', placement: 'left', boundary: 'viewport' };
+ },
+ },
+};
+</script>
+
+<template>
+ <div
+ v-gl-tooltip="tooltipOptions"
+ :class="{ 'multiple-users': hasMoreThanOneAssignee }"
+ :title="tooltipTitle"
+ class="sidebar-collapsed-icon sidebar-collapsed-user"
+ >
+ <i v-if="hasNoUsers" :aria-label="__('None')" class="fa fa-user"> </i>
+ <collapsed-assignee
+ v-for="user in collapsedUsers"
+ :key="user.id"
+ :user="user"
+ :issuable-type="issuableType"
+ />
+ <button v-if="hasMoreThanTwoAssignees" class="btn-link" type="button">
+ <span class="avatar-counter sidebar-avatar-counter"> {{ sidebarAvatarCounter }} </span>
+ <i
+ v-if="isMergeRequest && !allAssigneesCanMerge"
+ aria-hidden="true"
+ class="fa fa-exclamation-triangle merge-icon"
+ ></i>
+ </button>
+ </div>
+</template>
diff --git a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue
index be1e4811856..c6cc04a139f 100644
--- a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue
@@ -29,7 +29,7 @@ export default {
},
issuableType: {
type: String,
- require: true,
+ required: false,
default: 'issue',
},
},
diff --git a/app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue b/app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue
new file mode 100644
index 00000000000..3a4623121f4
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue
@@ -0,0 +1,96 @@
+<script>
+import { __, sprintf } from '~/locale';
+import AssigneeAvatarLink from './assignee_avatar_link.vue';
+
+const DEFAULT_RENDER_COUNT = 5;
+
+export default {
+ components: {
+ AssigneeAvatarLink,
+ },
+ props: {
+ users: {
+ type: Array,
+ required: true,
+ },
+ rootPath: {
+ type: String,
+ required: true,
+ },
+ issuableType: {
+ type: String,
+ required: false,
+ default: 'issue',
+ },
+ },
+ data() {
+ return {
+ showLess: true,
+ };
+ },
+ computed: {
+ firstUser() {
+ return this.users[0];
+ },
+ hasOneUser() {
+ return this.users.length === 1;
+ },
+ hiddenAssigneesLabel() {
+ const { numberOfHiddenAssignees } = this;
+ return sprintf(__('+ %{numberOfHiddenAssignees} more'), { numberOfHiddenAssignees });
+ },
+ renderShowMoreSection() {
+ return this.users.length > DEFAULT_RENDER_COUNT;
+ },
+ numberOfHiddenAssignees() {
+ return this.users.length - DEFAULT_RENDER_COUNT;
+ },
+ uncollapsedUsers() {
+ const uncollapsedLength = this.showLess
+ ? Math.min(this.users.length, DEFAULT_RENDER_COUNT)
+ : this.users.length;
+ return this.showLess ? this.users.slice(0, uncollapsedLength) : this.users;
+ },
+ username() {
+ return `@${this.firstUser.username}`;
+ },
+ },
+ methods: {
+ toggleShowLess() {
+ this.showLess = !this.showLess;
+ },
+ },
+};
+</script>
+
+<template>
+ <assignee-avatar-link
+ v-if="hasOneUser"
+ v-slot="{ user }"
+ tooltip-placement="left"
+ :tooltip-has-name="false"
+ :user="firstUser"
+ :root-path="rootPath"
+ :issuable-type="issuableType"
+ >
+ <div class="ml-2">
+ <span class="author"> {{ user.name }} </span>
+ <span class="username"> {{ username }} </span>
+ </div>
+ </assignee-avatar-link>
+ <div v-else>
+ <div class="user-list">
+ <div v-for="user in uncollapsedUsers" :key="user.id" class="user-item">
+ <assignee-avatar-link :user="user" :root-path="rootPath" :issuable-type="issuableType" />
+ </div>
+ </div>
+ <div v-if="renderShowMoreSection" class="user-list-more">
+ <button type="button" class="btn-link" @click="toggleShowLess">
+ <template v-if="showLess">
+ {{ hiddenAssigneesLabel }}
+ </template>
+ <template v-else>{{ __('- show less') }}</template>
+ </button>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue b/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue
index 597b723a9d9..1c75b6148e8 100644
--- a/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue
+++ b/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue
@@ -5,6 +5,7 @@ import tooltip from '~/vue_shared/directives/tooltip';
import Icon from '~/vue_shared/components/icon.vue';
import eventHub from '~/sidebar/event_hub';
import editForm from './edit_form.vue';
+import { trackEvent } from 'ee_else_ce/event_tracking/issue_sidebar';
export default {
components: {
@@ -51,6 +52,11 @@ export default {
toggleForm() {
this.edit = !this.edit;
},
+ onEditClick() {
+ this.toggleForm();
+
+ trackEvent('click_edit_button', 'confidentiality');
+ },
updateConfidentialAttribute(confidential) {
this.service
.update('issue', { confidential })
@@ -82,7 +88,7 @@ export default {
v-if="isEditable"
class="float-right confidential-edit"
href="#"
- @click.prevent="toggleForm"
+ @click.prevent="onEditClick"
>
{{ __('Edit') }}
</a>
diff --git a/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue b/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue
index c5cfa92f3c8..ec2a7b93a98 100644
--- a/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue
+++ b/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue
@@ -6,6 +6,7 @@ import issuableMixin from '~/vue_shared/mixins/issuable';
import Icon from '~/vue_shared/components/icon.vue';
import eventHub from '~/sidebar/event_hub';
import editForm from './edit_form.vue';
+import { trackEvent } from 'ee_else_ce/event_tracking/issue_sidebar';
export default {
components: {
@@ -65,7 +66,11 @@ export default {
toggleForm() {
this.mediator.store.isLockDialogOpen = !this.mediator.store.isLockDialogOpen;
},
+ onEditClick() {
+ this.toggleForm();
+ trackEvent('click_edit_button', 'lock_issue');
+ },
updateLockedAttribute(locked) {
this.mediator.service
.update(this.issuableType, {
@@ -109,7 +114,7 @@ export default {
v-if="isEditable"
class="float-right lock-edit"
type="button"
- @click.prevent="toggleForm"
+ @click.prevent="onEditClick"
>
{{ __('Edit') }}
</button>
diff --git a/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue b/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue
index 0d1faceef11..1f5f19d1931 100644
--- a/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue
+++ b/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue
@@ -4,6 +4,7 @@ import icon from '~/vue_shared/components/icon.vue';
import toggleButton from '~/vue_shared/components/toggle_button.vue';
import tooltip from '~/vue_shared/directives/tooltip';
import eventHub from '../../event_hub';
+import { trackEvent } from 'ee_else_ce/event_tracking/issue_sidebar';
const ICON_ON = 'notifications';
const ICON_OFF = 'notifications-off';
@@ -63,6 +64,8 @@ export default {
// Component event emission.
this.$emit('toggleSubscription', this.id);
+
+ trackEvent('toggle_button', 'notifications', this.subscribed ? 0 : 1);
},
onClickCollapsedIcon() {
this.$emit('toggleSidebar');
diff --git a/app/assets/javascripts/test_utils/simulate_drag.js b/app/assets/javascripts/test_utils/simulate_drag.js
index be9ebc81c6b..c9bf234fcce 100644
--- a/app/assets/javascripts/test_utils/simulate_drag.js
+++ b/app/assets/javascripts/test_utils/simulate_drag.js
@@ -153,7 +153,11 @@ export default function simulateDrag(options) {
if (progress >= 1) {
if (options.ondragend) options.ondragend();
- simulateEvent(toEl, 'mouseup');
+
+ if (options.performDrop) {
+ simulateEvent(toEl, 'mouseup');
+ }
+
clearInterval(dragInterval);
window.SIMULATE_DRAG_ACTIVE = 0;
}
diff --git a/app/assets/javascripts/tracking.js b/app/assets/javascripts/tracking.js
index 2d0b099cf0b..a852f937eec 100644
--- a/app/assets/javascripts/tracking.js
+++ b/app/assets/javascripts/tracking.js
@@ -15,8 +15,14 @@ const extractData = (el, opts = {}) => {
};
export default class Tracking {
+ static trackable() {
+ return !['1', 'yes'].includes(
+ window.doNotTrack || navigator.doNotTrack || navigator.msDoNotTrack,
+ );
+ }
+
static enabled() {
- return typeof window.snowplow === 'function';
+ return typeof window.snowplow === 'function' && this.trackable();
}
static event(category = document.body.dataset.page, event = 'generic', data = {}) {
diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js
index 33cedf78331..12c939aa70f 100644
--- a/app/assets/javascripts/users_select.js
+++ b/app/assets/javascripts/users_select.js
@@ -62,6 +62,8 @@ function UsersSelect(currentUser, els, options = {}) {
options.showCurrentUser = $dropdown.data('currentUser');
options.todoFilter = $dropdown.data('todoFilter');
options.todoStateFilter = $dropdown.data('todoStateFilter');
+ options.iid = $dropdown.data('iid');
+ options.issuableType = $dropdown.data('issuableType');
showNullUser = $dropdown.data('nullUser');
defaultNullUser = $dropdown.data('nullUserDefault');
showMenuAbove = $dropdown.data('showMenuAbove');
@@ -239,7 +241,7 @@ function UsersSelect(currentUser, els, options = {}) {
'<% if( avatar ) { %> <a class="author-link" href="/<%- username %>"> <img width="24" class="avatar avatar-inline s24" alt="" src="<%- avatar %>"> </a> <% } else { %> <i class="fa fa-user"></i> <% } %>',
);
assigneeTemplate = _.template(
- `<% if (username) { %> <a class="author-link bold" href="/<%- username %>"> <% if( avatar ) { %> <img width="32" class="avatar avatar-inline s32" alt="" src="<%- avatar %>"> <% } %> <span class="author"><%- name %></span> <span class="username"> @<%- username %> </span> </a> <% } else { %> <span class="no-value assign-yourself">
+ `<% if (username) { %> <a class="author-link bold" href="/<%- username %>"> <% if( avatar ) { %> <img width="32" class="avatar avatar-inline s32" alt="" src="<%- avatar %>"> <% } %> <span class="author"><%- name %></span> <span class="username"> @<%- username %> </span> </a> <% } else { %> <span class="no-value assign-yourself">
${sprintf(s__('UsersSelect|No assignee - %{openingTag} assign yourself %{closingTag}'), {
openingTag: '<a href="#" class="js-assign-yourself">',
closingTag: '</a>',
@@ -423,6 +425,8 @@ function UsersSelect(currentUser, els, options = {}) {
const { $el, e, isMarking } = options;
const user = options.selectedObj;
+ $el.tooltip('dispose');
+
if ($dropdown.hasClass('js-multiselect')) {
const isActive = $el.hasClass('is-active');
const previouslySelected = $dropdown
@@ -570,20 +574,11 @@ function UsersSelect(currentUser, els, options = {}) {
user.name,
)}</a></li>`;
} else {
- img = "<img src='" + avatar + "' class='avatar avatar-inline' width='32' />";
+ // 0 margin, because it's now handled by a wrapper
+ img = "<img src='" + avatar + "' class='avatar avatar-inline m-0' width='32' />";
}
- return `
- <li data-user-id=${user.id}>
- <a href='#' class='dropdown-menu-user-link ${selected === true ? 'is-active' : ''}'>
- ${img}
- <strong class='dropdown-menu-user-full-name'>
- ${_.escape(user.name)}
- </strong>
- ${username ? `<span class='dropdown-menu-user-username'>${username}</span>` : ''}
- </a>
- </li>
- `;
+ return _this.renderRow(options.issuableType, user, selected, username, img);
},
});
};
@@ -764,6 +759,11 @@ UsersSelect.prototype.users = function(query, options, callback) {
author_id: options.authorId || null,
skip_users: options.skipUsers || null,
};
+
+ if (options.issuableType === 'merge_request') {
+ params.merge_request_iid = options.iid || null;
+ }
+
return axios.get(url, { params }).then(({ data }) => {
callback(data);
});
@@ -776,4 +776,44 @@ UsersSelect.prototype.buildUrl = function(url) {
return url;
};
+UsersSelect.prototype.renderRow = function(issuableType, user, selected, username, img) {
+ const tooltip = issuableType === 'merge_request' && !user.can_merge ? __('Cannot merge') : '';
+ const tooltipClass = tooltip ? `has-tooltip` : '';
+ const selectedClass = selected === true ? 'is-active' : '';
+ const linkClasses = `${selectedClass} ${tooltipClass}`;
+ const tooltipAttributes = tooltip
+ ? `data-container="body" data-placement="left" data-title="${tooltip}"`
+ : '';
+
+ return `
+ <li data-user-id=${user.id}>
+ <a href="#" class="dropdown-menu-user-link d-flex align-items-center ${linkClasses}" ${tooltipAttributes}>
+ ${this.renderRowAvatar(issuableType, user, img)}
+ <span class="d-flex flex-column overflow-hidden">
+ <strong class="dropdown-menu-user-full-name">
+ ${_.escape(user.name)}
+ </strong>
+ ${username ? `<span class="dropdown-menu-user-username">${username}</span>` : ''}
+ </span>
+ </a>
+ </li>
+ `;
+};
+
+UsersSelect.prototype.renderRowAvatar = function(issuableType, user, img) {
+ if (user.beforeDivider) {
+ return img;
+ }
+
+ const mergeIcon =
+ issuableType === 'merge_request' && !user.can_merge
+ ? '<i class="fa fa-exclamation-triangle merge-icon"></i>'
+ : '';
+
+ return `<span class="position-relative mr-2">
+ ${img}
+ ${mergeIcon}
+ </span>`;
+};
+
export default UsersSelect;
diff --git a/app/assets/javascripts/visual_review_toolbar/components/comment.js b/app/assets/javascripts/visual_review_toolbar/components/comment.js
deleted file mode 100644
index a03dc14b319..00000000000
--- a/app/assets/javascripts/visual_review_toolbar/components/comment.js
+++ /dev/null
@@ -1,39 +0,0 @@
-import { nextView } from '../store';
-import { localStorage, COMMENT_BOX, LOGOUT, STORAGE_MR_ID, STORAGE_TOKEN } from '../shared';
-import { clearNote } from './note';
-import { buttonClearStyles } from './utils';
-import { addForm } from './wrapper';
-import { changeSelectedMr, selectedMrNote } from './comment_mr_note';
-import postComment from './comment_post';
-import { saveComment, getSavedComment } from './comment_storage';
-
-const comment = state => {
- const savedComment = getSavedComment();
-
- return `
- <div>
- <textarea id="${COMMENT_BOX}" name="${COMMENT_BOX}" rows="3" placeholder="Enter your feedback or idea" class="gitlab-input" aria-required="true">${savedComment}</textarea>
- ${selectedMrNote(state)}
- <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-success" style="${buttonClearStyles}" type="button" id="gitlab-comment-button"> Send feedback </button>
- <button class="gitlab-button gitlab-button-secondary" style="${buttonClearStyles}" type="button" id="${LOGOUT}"> Log out </button>
- </div>
- `;
-};
-
-// This function is here becaause it is called only from the comment view
-// If we reach a design where we can logout from multiple views, promote this
-// to it's own package
-const logoutUser = state => {
- localStorage.removeItem(STORAGE_TOKEN);
- localStorage.removeItem(STORAGE_MR_ID);
- state.token = '';
- state.mergeRequestId = '';
-
- clearNote();
- addForm(nextView(state, COMMENT_BOX));
-};
-
-export { changeSelectedMr, comment, logoutUser, postComment, saveComment };
diff --git a/app/assets/javascripts/visual_review_toolbar/components/comment_mr_note.js b/app/assets/javascripts/visual_review_toolbar/components/comment_mr_note.js
deleted file mode 100644
index da67763261c..00000000000
--- a/app/assets/javascripts/visual_review_toolbar/components/comment_mr_note.js
+++ /dev/null
@@ -1,31 +0,0 @@
-import { nextView } from '../store';
-import { localStorage, CHANGE_MR_ID_BUTTON, COMMENT_BOX, STORAGE_MR_ID } from '../shared';
-import { clearNote } from './note';
-import { buttonClearStyles } from './utils';
-import { addForm } from './wrapper';
-
-const selectedMrNote = state => {
- const { mrUrl, projectPath, mergeRequestId } = state;
-
- const mrLink = `${mrUrl}/${projectPath}/merge_requests/${mergeRequestId}`;
-
- return `
- <p class="gitlab-metadata-note">
- This posts to merge request <a class="gitlab-link" href="${mrLink}">!${mergeRequestId}</a>.
- <button style="${buttonClearStyles}" type="button" id="${CHANGE_MR_ID_BUTTON}" class="gitlab-link gitlab-link-button">Change</button>
- </p>
- `;
-};
-
-const clearMrId = state => {
- localStorage.removeItem(STORAGE_MR_ID);
- state.mergeRequestId = '';
-};
-
-const changeSelectedMr = state => {
- clearMrId(state);
- clearNote();
- addForm(nextView(state, COMMENT_BOX));
-};
-
-export { changeSelectedMr, selectedMrNote };
diff --git a/app/assets/javascripts/visual_review_toolbar/components/comment_post.js b/app/assets/javascripts/visual_review_toolbar/components/comment_post.js
deleted file mode 100644
index ee5f2b62425..00000000000
--- a/app/assets/javascripts/visual_review_toolbar/components/comment_post.js
+++ /dev/null
@@ -1,145 +0,0 @@
-import { BLACK, COMMENT_BOX, MUTED } from '../shared';
-import { clearSavedComment } from './comment_storage';
-import { clearNote, postError } from './note';
-import { selectCommentBox, selectCommentButton, selectNote, selectNoteContainer } from './utils';
-
-const resetCommentButton = () => {
- 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;
-};
-
-const resetCommentBox = () => {
- const commentBox = selectCommentBox();
- commentBox.style.pointerEvents = 'auto';
- commentBox.style.color = BLACK;
-};
-
-const resetCommentText = () => {
- const commentBox = selectCommentBox();
- commentBox.value = '';
- clearSavedComment();
-};
-
-const resetComment = () => {
- resetCommentButton();
- resetCommentBox();
- resetCommentText();
-};
-
-const confirmAndClear = feedbackInfo => {
- const commentButton = selectCommentButton();
- const currentNote = selectNote();
- const noteContainer = selectNoteContainer();
-
- /* eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings */
- commentButton.innerText = 'Feedback sent';
- noteContainer.style.visibility = 'visible';
- currentNote.insertAdjacentHTML('beforeend', feedbackInfo);
-
- setTimeout(resetComment, 1000);
- setTimeout(clearNote, 6000);
-};
-
-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 commentErrors = error => {
- switch (error.status) {
- case 401:
- /* eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings */
- return 'Unauthorized. You may have entered an incorrect authentication token.';
- case 404:
- /* eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings */
- return 'Not found. You may have entered an incorrect merge request ID.';
- default:
- /* eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings */
- return `Your comment could not be sent. Please try again. Error: ${error.message}`;
- }
-};
-
-const postComment = ({
- platform,
- browser,
- userAgent,
- innerWidth,
- innerHeight,
- projectId,
- projectPath,
- mergeRequestId,
- mrUrl,
- token,
-}) => {
- // Clear any old errors
- clearNote(COMMENT_BOX);
-
- setInProgressState();
-
- const commentText = selectCommentBox().value.trim();
- // Get the href at the last moment to support SPAs
- const { href } = window.location;
-
- if (!commentText) {
- /* eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings */
- postError('Your comment appears to be empty.', COMMENT_BOX);
- resetCommentBox();
- resetCommentButton();
- 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) {
- return response.json();
- }
-
- throw response;
- })
- .then(data => {
- const commentId = data.notes[0].id;
- const feedbackLink = `${mrUrl}/${projectPath}/merge_requests/${mergeRequestId}#note_${commentId}`;
- const feedbackInfo = `Feedback sent. View at <a class="gitlab-link" href="${feedbackLink}">${projectPath} !${mergeRequestId} (comment ${commentId})</a>`;
- confirmAndClear(feedbackInfo);
- })
- .catch(err => {
- postError(commentErrors(err), COMMENT_BOX);
- resetCommentBox();
- resetCommentButton();
- });
-};
-
-export default postComment;
diff --git a/app/assets/javascripts/visual_review_toolbar/components/comment_storage.js b/app/assets/javascripts/visual_review_toolbar/components/comment_storage.js
deleted file mode 100644
index 49c9400437e..00000000000
--- a/app/assets/javascripts/visual_review_toolbar/components/comment_storage.js
+++ /dev/null
@@ -1,20 +0,0 @@
-import { selectCommentBox } from './utils';
-import { sessionStorage, STORAGE_COMMENT } from '../shared';
-
-const getSavedComment = () => sessionStorage.getItem(STORAGE_COMMENT) || '';
-
-const saveComment = () => {
- const currentComment = selectCommentBox();
-
- // This may be added to any view via top-level beforeunload listener
- // so let's skip if it does not apply
- if (currentComment && currentComment.value) {
- sessionStorage.setItem(STORAGE_COMMENT, currentComment.value);
- }
-};
-
-const clearSavedComment = () => {
- sessionStorage.removeItem(STORAGE_COMMENT);
-};
-
-export { getSavedComment, saveComment, clearSavedComment };
diff --git a/app/assets/javascripts/visual_review_toolbar/components/form_elements.js b/app/assets/javascripts/visual_review_toolbar/components/form_elements.js
deleted file mode 100644
index 608488a6fea..00000000000
--- a/app/assets/javascripts/visual_review_toolbar/components/form_elements.js
+++ /dev/null
@@ -1,17 +0,0 @@
-import { REMEMBER_ITEM } from '../shared';
-import { buttonClearStyles } from './utils';
-
-/* eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings */
-const rememberBox = (rememberText = 'Remember me') => `
- <div class="gitlab-checkbox-wrapper">
- <input type="checkbox" id="${REMEMBER_ITEM}" name="${REMEMBER_ITEM}" value="remember">
- <label for="${REMEMBER_ITEM}" class="gitlab-checkbox-label">${rememberText}</label>
- </div>
-`;
-
-const submitButton = buttonId => `
- <div class="gitlab-button-wrapper">
- <button class="gitlab-button-wide gitlab-button gitlab-button-success" style="${buttonClearStyles}" type="button" id="${buttonId}"> Submit </button>
- </div>
-`;
-export { rememberBox, submitButton };
diff --git a/app/assets/javascripts/visual_review_toolbar/components/index.js b/app/assets/javascripts/visual_review_toolbar/components/index.js
deleted file mode 100644
index e88b3637ad8..00000000000
--- a/app/assets/javascripts/visual_review_toolbar/components/index.js
+++ /dev/null
@@ -1,23 +0,0 @@
-import { changeSelectedMr, comment, logoutUser, postComment, saveComment } from './comment';
-import { authorizeUser, login } from './login';
-import { addMr, mrForm } from './mr_id';
-import { note } from './note';
-import { selectContainer, selectForm } from './utils';
-import { buttonAndForm, toggleForm } from './wrapper';
-
-export {
- addMr,
- authorizeUser,
- buttonAndForm,
- changeSelectedMr,
- comment,
- login,
- logoutUser,
- mrForm,
- note,
- postComment,
- saveComment,
- selectContainer,
- selectForm,
- toggleForm,
-};
diff --git a/app/assets/javascripts/visual_review_toolbar/components/login.js b/app/assets/javascripts/visual_review_toolbar/components/login.js
deleted file mode 100644
index 20ab01bc690..00000000000
--- a/app/assets/javascripts/visual_review_toolbar/components/login.js
+++ /dev/null
@@ -1,47 +0,0 @@
-import { nextView } from '../store';
-import { localStorage, LOGIN, TOKEN_BOX, STORAGE_TOKEN } from '../shared';
-import { clearNote, postError } from './note';
-import { rememberBox, submitButton } from './form_elements';
-import { selectRemember, selectToken } from './utils';
-import { addForm } from './wrapper';
-
-const labelText = `
- Enter your <a class="gitlab-link" href="https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html">personal access token</a>
-`;
-
-const login = `
- <div>
- <label for="${TOKEN_BOX}" class="gitlab-label">${labelText}</label>
- <input class="gitlab-input" type="password" id="${TOKEN_BOX}" name="${TOKEN_BOX}" autocomplete="current-password" aria-required="true">
- </div>
- ${rememberBox()}
- ${submitButton(LOGIN)}
-`;
-
-const storeToken = (token, state) => {
- const rememberMe = selectRemember().checked;
-
- if (rememberMe) {
- localStorage.setItem(STORAGE_TOKEN, token);
- }
-
- 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);
- addForm(nextView(state, LOGIN));
-};
-
-export { authorizeUser, login, storeToken };
diff --git a/app/assets/javascripts/visual_review_toolbar/components/mr_id.js b/app/assets/javascripts/visual_review_toolbar/components/mr_id.js
deleted file mode 100644
index 695b3af8aa0..00000000000
--- a/app/assets/javascripts/visual_review_toolbar/components/mr_id.js
+++ /dev/null
@@ -1,63 +0,0 @@
-import { nextView } from '../store';
-import { MR_ID, MR_ID_BUTTON, STORAGE_MR_ID, localStorage } from '../shared';
-import { clearNote, postError } from './note';
-import { rememberBox, submitButton } from './form_elements';
-import { selectForm, selectMrBox, selectRemember } from './utils';
-import { addForm } from './wrapper';
-
-/* eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings */
-const mrLabel = `Enter your merge request ID`;
-/* eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings */
-const mrRememberText = `Remember this number`;
-
-const mrForm = `
- <div>
- <label for="${MR_ID}" class="gitlab-label">${mrLabel}</label>
- <input class="gitlab-input" type="text" pattern="[1-9][0-9]*" id="${MR_ID}" name="${MR_ID}" placeholder="e.g., 321" aria-required="true">
- </div>
- ${rememberBox(mrRememberText)}
- ${submitButton(MR_ID_BUTTON)}
-`;
-
-const storeMR = (id, state) => {
- const rememberMe = selectRemember().checked;
-
- if (rememberMe) {
- localStorage.setItem(STORAGE_MR_ID, id);
- }
-
- state.mergeRequestId = id;
-};
-
-const getFormError = (mrNumber, form) => {
- if (!mrNumber) {
- /* eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings */
- return 'Please enter your merge request ID number.';
- }
-
- if (!form.checkValidity()) {
- /* eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings */
- return 'Please remove any non-number values from the field.';
- }
-
- return null;
-};
-
-const addMr = state => {
- // Clear any old errors
- clearNote(MR_ID);
-
- const mrNumber = selectMrBox().value;
- const form = selectForm();
- const formError = getFormError(mrNumber, form);
-
- if (formError) {
- postError(formError, MR_ID);
- return;
- }
-
- storeMR(mrNumber, state);
- addForm(nextView(state, MR_ID));
-};
-
-export { addMr, mrForm, storeMR };
diff --git a/app/assets/javascripts/visual_review_toolbar/components/note.js b/app/assets/javascripts/visual_review_toolbar/components/note.js
deleted file mode 100644
index 9cddcb710f2..00000000000
--- a/app/assets/javascripts/visual_review_toolbar/components/note.js
+++ /dev/null
@@ -1,35 +0,0 @@
-import { NOTE, NOTE_CONTAINER, RED } from '../shared';
-import { selectById, selectNote, selectNoteContainer } from './utils';
-
-const note = `
- <div id="${NOTE_CONTAINER}" style="visibility: hidden;">
- <p id="${NOTE}" class="gitlab-message"></p>
- </div>
-`;
-
-const clearNote = inputId => {
- const currentNote = selectNote();
- const noteContainer = selectNoteContainer();
-
- currentNote.innerText = '';
- currentNote.style.color = '';
- noteContainer.style.visibility = 'hidden';
-
- if (inputId) {
- const field = document.getElementById(inputId);
- field.style.borderColor = '';
- }
-};
-
-const postError = (message, inputId) => {
- const currentNote = selectNote();
- const noteContainer = selectNoteContainer();
- const field = selectById(inputId);
- field.style.borderColor = RED;
- currentNote.style.color = RED;
- currentNote.innerText = message;
- noteContainer.style.visibility = 'visible';
- setTimeout(clearNote.bind(null, inputId), 5000);
-};
-
-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
deleted file mode 100644
index 4ec9bd4a32a..00000000000
--- a/app/assets/javascripts/visual_review_toolbar/components/utils.js
+++ /dev/null
@@ -1,51 +0,0 @@
-/* global document */
-
-import {
- COLLAPSE_BUTTON,
- COMMENT_BOX,
- COMMENT_BUTTON,
- FORM,
- FORM_CONTAINER,
- MR_ID,
- NOTE,
- NOTE_CONTAINER,
- REMEMBER_ITEM,
- REVIEW_CONTAINER,
- TOKEN_BOX,
-} from '../shared';
-
-// 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 selectFormContainer = () => document.getElementById(FORM_CONTAINER);
-const selectMrBox = () => document.getElementById(MR_ID);
-const selectNote = () => document.getElementById(NOTE);
-const selectNoteContainer = () => document.getElementById(NOTE_CONTAINER);
-const selectRemember = () => document.getElementById(REMEMBER_ITEM);
-const selectToken = () => document.getElementById(TOKEN_BOX);
-
-export {
- buttonClearStyles,
- selectById,
- selectCollapseButton,
- selectContainer,
- selectCommentBox,
- selectCommentButton,
- selectForm,
- selectFormContainer,
- selectMrBox,
- selectNote,
- selectNoteContainer,
- selectRemember,
- selectToken,
-};
diff --git a/app/assets/javascripts/visual_review_toolbar/components/wrapper.js b/app/assets/javascripts/visual_review_toolbar/components/wrapper.js
deleted file mode 100644
index fdf8ad7c41f..00000000000
--- a/app/assets/javascripts/visual_review_toolbar/components/wrapper.js
+++ /dev/null
@@ -1,79 +0,0 @@
-import { CLEAR, FORM, FORM_CONTAINER, WHITE } from '../shared';
-import {
- selectCollapseButton,
- selectForm,
- selectFormContainer,
- selectNoteContainer,
-} from './utils';
-import { collapseButton, commentIcon, compressIcon } from './wrapper_icons';
-
-const form = content => `
- <form id="${FORM}" novalidate>
- ${content}
- </form>
-`;
-
-const buttonAndForm = content => `
- <div id="${FORM_CONTAINER}" class="gitlab-form-open">
- ${collapseButton}
- ${form(content)}
- </div>
-`;
-
-const addForm = nextForm => {
- const formWrapper = selectForm();
- formWrapper.innerHTML = nextForm;
-};
-
-function toggleForm() {
- const toggleButton = selectCollapseButton();
- const currentForm = selectForm();
- const formContainer = selectFormContainer();
- const noteContainer = selectNoteContainer();
- 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-wrapper-closed', 'gitlab-wrapper-open'];
- 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 = toggleButton.classList.contains('gitlab-collapse-open') ? CLOSED : OPEN;
- const currentVals = stateVals[nextState];
-
- formContainer.classList.replace(...currentVals.containerClasses);
- formContainer.style.backgroundColor = currentVals.backgroundColor;
- formContainer.classList.toggle('gitlab-form-open');
- currentForm.style.display = currentVals.display;
- toggleButton.classList.replace(...currentVals.buttonClasses);
- toggleButton.innerHTML = currentVals.icon;
-
- if (noteContainer && noteContainer.innerText.length > 0) {
- noteContainer.style.display = currentVals.display;
- }
-}
-
-export { addForm, buttonAndForm, 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
deleted file mode 100644
index b686fd4f5c2..00000000000
--- a/app/assets/javascripts/visual_review_toolbar/components/wrapper_icons.js
+++ /dev/null
@@ -1,15 +0,0 @@
-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
deleted file mode 100644
index 67b3fadd772..00000000000
--- a/app/assets/javascripts/visual_review_toolbar/index.js
+++ /dev/null
@@ -1,51 +0,0 @@
-import './styles/toolbar.css';
-
-import { buttonAndForm, note, selectForm, selectContainer } from './components';
-import { REVIEW_CONTAINER } from './shared';
-import { eventLookup, getInitialView, initializeGlobalListeners, initializeState } 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 mainContent = buttonAndForm(getInitialView());
- const container = document.createElement('div');
- container.setAttribute('id', REVIEW_CONTAINER);
- container.insertAdjacentHTML('beforeend', note);
- container.insertAdjacentHTML('beforeend', mainContent);
-
- document.body.insertBefore(container, document.body.firstChild);
-
- selectContainer().addEventListener('click', event => {
- eventLookup(event.target.id)();
- });
-
- selectForm().addEventListener('submit', event => {
- // this is important to prevent the form from adding data
- // as URL params and inadvertently revealing secrets
- event.preventDefault();
-
- const id =
- event.target.querySelector('.gitlab-button-wrapper') &&
- event.target.querySelector('.gitlab-button-wrapper').getElementsByTagName('button')[0] &&
- event.target.querySelector('.gitlab-button-wrapper').getElementsByTagName('button')[0].id;
-
- // even if this is called with false, it's ok; it will get the default no-op
- eventLookup(id)();
- });
-
- initializeGlobalListeners();
-});
diff --git a/app/assets/javascripts/visual_review_toolbar/shared/constants.js b/app/assets/javascripts/visual_review_toolbar/shared/constants.js
deleted file mode 100644
index 0d5b666ab0a..00000000000
--- a/app/assets/javascripts/visual_review_toolbar/shared/constants.js
+++ /dev/null
@@ -1,56 +0,0 @@
-// component selectors
-const CHANGE_MR_ID_BUTTON = 'gitlab-change-mr';
-const COLLAPSE_BUTTON = 'gitlab-collapse';
-const COMMENT_BOX = 'gitlab-comment';
-const COMMENT_BUTTON = 'gitlab-comment-button';
-const FORM = 'gitlab-form';
-const FORM_CONTAINER = 'gitlab-form-wrapper';
-const LOGIN = 'gitlab-login-button';
-const LOGOUT = 'gitlab-logout-button';
-const MR_ID = 'gitlab-submit-mr';
-const MR_ID_BUTTON = 'gitlab-submit-mr-button';
-const NOTE = 'gitlab-validation-note';
-const NOTE_CONTAINER = 'gitlab-note-wrapper';
-const REMEMBER_ITEM = 'gitlab-remember-item';
-const REVIEW_CONTAINER = 'gitlab-review-container';
-const TOKEN_BOX = 'gitlab-token';
-
-// Storage keys
-const STORAGE_PREFIX = '--gitlab'; // Using `--` to make the prefix more unique
-const STORAGE_MR_ID = `${STORAGE_PREFIX}-merge-request-id`;
-const STORAGE_TOKEN = `${STORAGE_PREFIX}-token`;
-const STORAGE_COMMENT = `${STORAGE_PREFIX}-comment`;
-
-// 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(250, 250, 250, 1)';
-
-export {
- CHANGE_MR_ID_BUTTON,
- COLLAPSE_BUTTON,
- COMMENT_BOX,
- COMMENT_BUTTON,
- FORM,
- FORM_CONTAINER,
- LOGIN,
- LOGOUT,
- MR_ID,
- MR_ID_BUTTON,
- NOTE,
- NOTE_CONTAINER,
- REMEMBER_ITEM,
- REVIEW_CONTAINER,
- TOKEN_BOX,
- STORAGE_MR_ID,
- STORAGE_TOKEN,
- STORAGE_COMMENT,
- BLACK,
- CLEAR,
- MUTED,
- RED,
- WHITE,
-};
diff --git a/app/assets/javascripts/visual_review_toolbar/shared/index.js b/app/assets/javascripts/visual_review_toolbar/shared/index.js
deleted file mode 100644
index d8ccb170592..00000000000
--- a/app/assets/javascripts/visual_review_toolbar/shared/index.js
+++ /dev/null
@@ -1,55 +0,0 @@
-import {
- CHANGE_MR_ID_BUTTON,
- COLLAPSE_BUTTON,
- COMMENT_BOX,
- COMMENT_BUTTON,
- FORM,
- FORM_CONTAINER,
- LOGIN,
- LOGOUT,
- MR_ID,
- MR_ID_BUTTON,
- NOTE,
- NOTE_CONTAINER,
- REMEMBER_ITEM,
- REVIEW_CONTAINER,
- TOKEN_BOX,
- STORAGE_MR_ID,
- STORAGE_TOKEN,
- STORAGE_COMMENT,
- BLACK,
- CLEAR,
- MUTED,
- RED,
- WHITE,
-} from './constants';
-
-import { localStorage, sessionStorage } from './storage_utils';
-
-export {
- localStorage,
- sessionStorage,
- CHANGE_MR_ID_BUTTON,
- COLLAPSE_BUTTON,
- COMMENT_BOX,
- COMMENT_BUTTON,
- FORM,
- FORM_CONTAINER,
- LOGIN,
- LOGOUT,
- MR_ID,
- MR_ID_BUTTON,
- NOTE,
- NOTE_CONTAINER,
- REMEMBER_ITEM,
- REVIEW_CONTAINER,
- TOKEN_BOX,
- STORAGE_MR_ID,
- STORAGE_TOKEN,
- STORAGE_COMMENT,
- BLACK,
- CLEAR,
- MUTED,
- RED,
- WHITE,
-};
diff --git a/app/assets/javascripts/visual_review_toolbar/shared/storage_utils.js b/app/assets/javascripts/visual_review_toolbar/shared/storage_utils.js
deleted file mode 100644
index 00456d3536e..00000000000
--- a/app/assets/javascripts/visual_review_toolbar/shared/storage_utils.js
+++ /dev/null
@@ -1,42 +0,0 @@
-import { setUsingGracefulStorageFlag } from '../store/state';
-
-const TEST_KEY = 'gitlab-storage-test';
-
-const createStorageStub = () => {
- const items = {};
-
- return {
- getItem(key) {
- return items[key];
- },
- setItem(key, value) {
- items[key] = value;
- },
- removeItem(key) {
- delete items[key];
- },
- };
-};
-
-const hasStorageSupport = storage => {
- // Support test taken from https://stackoverflow.com/a/11214467/1708147
- try {
- storage.setItem(TEST_KEY, TEST_KEY);
- storage.removeItem(TEST_KEY);
- setUsingGracefulStorageFlag(true);
-
- return true;
- } catch (err) {
- setUsingGracefulStorageFlag(false);
- return false;
- }
-};
-
-const useGracefulStorage = storage =>
- // If a browser does not support local storage, let's return a graceful implementation.
- hasStorageSupport(storage) ? storage : createStorageStub();
-
-const localStorage = useGracefulStorage(window.localStorage);
-const sessionStorage = useGracefulStorage(window.sessionStorage);
-
-export { localStorage, sessionStorage };
diff --git a/app/assets/javascripts/visual_review_toolbar/store/events.js b/app/assets/javascripts/visual_review_toolbar/store/events.js
deleted file mode 100644
index c9095c77ef1..00000000000
--- a/app/assets/javascripts/visual_review_toolbar/store/events.js
+++ /dev/null
@@ -1,73 +0,0 @@
-import {
- addMr,
- authorizeUser,
- changeSelectedMr,
- logoutUser,
- postComment,
- saveComment,
- toggleForm,
-} from '../components';
-
-import {
- CHANGE_MR_ID_BUTTON,
- COLLAPSE_BUTTON,
- COMMENT_BUTTON,
- LOGIN,
- LOGOUT,
- MR_ID_BUTTON,
-} from '../shared';
-
-import { state } from './state';
-import debounce from './utils';
-
-const noop = () => {};
-
-// State needs to be bound here to be acted on
-// because these are called by click events and
-// as such are called with only the `event` object
-const eventLookup = id => {
- switch (id) {
- case CHANGE_MR_ID_BUTTON:
- return () => {
- saveComment();
- changeSelectedMr(state);
- };
- case COLLAPSE_BUTTON:
- return toggleForm;
- case COMMENT_BUTTON:
- return postComment.bind(null, state);
- case LOGIN:
- return authorizeUser.bind(null, state);
- case LOGOUT:
- return () => {
- saveComment();
- logoutUser(state);
- };
- case MR_ID_BUTTON:
- return addMr.bind(null, state);
- default:
- return noop;
- }
-};
-
-const updateWindowSize = wind => {
- state.innerWidth = wind.innerWidth;
- state.innerHeight = wind.innerHeight;
-};
-
-const initializeGlobalListeners = () => {
- window.addEventListener('resize', debounce(updateWindowSize.bind(null, window), 200));
- window.addEventListener('beforeunload', event => {
- if (state.usingGracefulStorage) {
- // if there is no browser storage support, reloading will lose the comment; this way, the user will be warned
- // we assign the return value because it is required by Chrome see: https://developer.mozilla.org/en-US/docs/Web/API/WindowEventHandlers/onbeforeunload#Example,
- event.preventDefault();
- /* eslint-disable-next-line no-param-reassign */
- event.returnValue = '';
- }
-
- saveComment();
- });
-};
-
-export { eventLookup, initializeGlobalListeners };
diff --git a/app/assets/javascripts/visual_review_toolbar/store/index.js b/app/assets/javascripts/visual_review_toolbar/store/index.js
deleted file mode 100644
index 07c8dd6f1d2..00000000000
--- a/app/assets/javascripts/visual_review_toolbar/store/index.js
+++ /dev/null
@@ -1,11 +0,0 @@
-import { eventLookup, initializeGlobalListeners } from './events';
-import { nextView, getInitialView, initializeState, setUsingGracefulStorageFlag } from './state';
-
-export {
- eventLookup,
- getInitialView,
- initializeGlobalListeners,
- initializeState,
- nextView,
- setUsingGracefulStorageFlag,
-};
diff --git a/app/assets/javascripts/visual_review_toolbar/store/state.js b/app/assets/javascripts/visual_review_toolbar/store/state.js
deleted file mode 100644
index b7853bb0723..00000000000
--- a/app/assets/javascripts/visual_review_toolbar/store/state.js
+++ /dev/null
@@ -1,95 +0,0 @@
-import { comment, login, mrForm } from '../components';
-import { localStorage, COMMENT_BOX, LOGIN, MR_ID, STORAGE_MR_ID, STORAGE_TOKEN } from '../shared';
-
-const state = {
- browser: '',
- usingGracefulStorage: '',
- 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 nextView = (appState, form = 'none') => {
- const formsList = {
- [COMMENT_BOX]: currentState => (currentState.token ? mrForm : login),
- [LOGIN]: currentState => (currentState.mergeRequestId ? comment(currentState) : mrForm),
- [MR_ID]: currentState => (currentState.token ? comment(currentState) : login),
- none: currentState => {
- if (!currentState.token) {
- return login;
- }
-
- if (currentState.token && !currentState.mergeRequestId) {
- return mrForm;
- }
-
- return comment(currentState);
- },
- };
-
- return formsList[form](appState);
-};
-
-const initializeState = (wind, doc) => {
- const {
- innerWidth,
- innerHeight,
- navigator: { platform, userAgent },
- } = wind;
-
- const browser = getBrowserId(userAgent);
-
- const scriptEl = doc.getElementById('review-app-toolbar-script');
- const { projectId, mergeRequestId, mrUrl, projectPath } = scriptEl.dataset;
-
- // This mutates our default state object above. It's weird but it makes the linter happy.
- Object.assign(state, {
- browser,
- innerWidth,
- innerHeight,
- mergeRequestId,
- mrUrl,
- platform,
- projectId,
- projectPath,
- userAgent,
- });
-
- return state;
-};
-
-const getInitialView = () => {
- const token = localStorage.getItem(STORAGE_TOKEN);
- const mrId = localStorage.getItem(STORAGE_MR_ID);
-
- if (token) {
- state.token = token;
- }
-
- if (mrId) {
- state.mergeRequestId = mrId;
- }
-
- return nextView(state);
-};
-
-const setUsingGracefulStorageFlag = flag => {
- state.usingGracefulStorage = !flag;
-};
-
-export { initializeState, getInitialView, nextView, setUsingGracefulStorageFlag, state };
diff --git a/app/assets/javascripts/visual_review_toolbar/store/utils.js b/app/assets/javascripts/visual_review_toolbar/store/utils.js
deleted file mode 100644
index 5cf145351b3..00000000000
--- a/app/assets/javascripts/visual_review_toolbar/store/utils.js
+++ /dev/null
@@ -1,15 +0,0 @@
-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/visual_review_toolbar/styles/toolbar.css b/app/assets/javascripts/visual_review_toolbar/styles/toolbar.css
deleted file mode 100644
index d1a8d66ef40..00000000000
--- a/app/assets/javascripts/visual_review_toolbar/styles/toolbar.css
+++ /dev/null
@@ -1,188 +0,0 @@
-/*
- As a standalone script, the toolbar has its own css
- */
-
-#gitlab-collapse > * {
- pointer-events: none;
-}
-
-#gitlab-comment {
- background-color: #fafafa;
-}
-
-#gitlab-form {
- display: flex;
- flex-direction: column;
- width: 100%;
- margin-bottom: 0;
-}
-
-#gitlab-note-wrapper {
- display: flex;
- flex-direction: column;
- background-color: #fafafa;
- border-radius: 4px;
- margin-bottom: .5rem;
- padding: 1rem;
-}
-
-#gitlab-form-wrapper {
- overflow: auto;
- display: flex;
- flex-direction: row-reverse;
- border-radius: 4px;
-}
-
-#gitlab-review-container {
- max-width: 22rem;
- max-height: 22rem;
- overflow: auto;
- display: flex;
- flex-direction: column;
- position: fixed;
- bottom: 1rem;
- right: 1rem;
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Noto Sans', Ubuntu, Cantarell,
- 'Helvetica Neue', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
- 'Noto Color Emoji';
- font-size: .8rem;
- font-weight: 400;
- color: #2e2e2e;
- z-index: 9999; /* toolbar should always be on top */
-}
-
-.gitlab-wrapper-open {
- max-width: 22rem;
- max-height: 22rem;
-}
-
-.gitlab-wrapper-closed {
- max-width: 3.4rem;
- max-height: 3.4rem;
-}
-
-.gitlab-button {
- cursor: pointer;
- transition: background-color 100ms linear, border-color 100ms linear, color 100ms linear, box-shadow 100ms linear;
-}
-
-.gitlab-button-secondary {
- background: none #fafafa;
- margin: 0 .5rem;
- border: 1px solid #e3e3e3;
-}
-
-.gitlab-button-secondary:hover {
- background-color: #f0f0f0;
- border-color: #e3e3e3;
- color: #2e2e2e;
-}
-
-.gitlab-button-secondary:active {
- color: #2e2e2e;
- background-color: #e1e1e1;
- border-color: #dadada;
-}
-
-.gitlab-button-success:hover {
- color: #fff;
- background-color: #137e3f;
- border-color: #127339;
-}
-
-.gitlab-button-success:active {
- background-color: #168f48;
- border-color: #12753a;
- color: #fff;
-}
-
-.gitlab-button-success {
- background-color: #1aaa55;
- border: 1px solid #168f48;
- color: #fff;
-}
-
-.gitlab-button-wide {
- width: 100%;
-}
-
-.gitlab-button-wrapper {
- margin-top: 0.5rem;
- display: flex;
- align-items: baseline;
- /*
- this makes sure the hit enter to submit picks the correct button
- on the comment view
- */
- flex-direction: row-reverse;
-}
-
-.gitlab-collapse {
- width: 2.4rem;
- height: 2.2rem;
- margin-left: 1rem;
- padding: .5rem;
-}
-
-.gitlab-collapse-closed {
- align-self: center;
-}
-
-.gitlab-checkbox-label {
- padding: 0 .2rem;
-}
-
-.gitlab-checkbox-wrapper {
- display: flex;
- align-items: baseline;
-}
-
-.gitlab-form-open {
- padding: 1rem;
- background-color: #fafafa;
-}
-
-.gitlab-label {
- font-weight: 600;
- display: inline-block;
- width: 100%;
-}
-
-.gitlab-link {
- color: #1b69b6;
- text-decoration: none;
- background-color: transparent;
- background-image: none;
-}
-
-.gitlab-link:hover {
- text-decoration: underline;
-}
-
-.gitlab-link-button {
- border: none;
- cursor: pointer;
- padding: 0 .15rem;
-}
-
-.gitlab-message {
- padding: .25rem 0;
- margin: 0;
- line-height: 1.2rem;
-}
-
-.gitlab-metadata-note {
- font-size: .7rem;
- line-height: 1rem;
- color: #666;
- margin-bottom: .5rem;
-}
-
-.gitlab-input {
- width: 100%;
- border: 1px solid #dfdfdf;
- border-radius: 4px;
- padding: .1rem .2rem;
- min-height: 2rem;
- max-width: 17rem;
-}
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue
index c7b064b8506..339e154affc 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue
@@ -50,6 +50,7 @@ export default {
startTag: '<span class="label-branch">',
endTag: '</span>',
},
+ false,
);
},
},
diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
index 0f55bebd3fc..7843409f4a7 100644
--- a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
+++ b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
@@ -87,9 +87,6 @@ export default class MergeRequestStore {
this.allowCollaboration = data.allow_collaboration;
this.sourceProjectId = data.source_project_id;
this.targetProjectId = data.target_project_id;
- this.mergePipelinesEnabled = Boolean(data.merge_pipelines_enabled);
- this.mergeTrainsCount = data.merge_trains_count || 0;
- this.mergeTrainIndex = data.merge_train_index;
// CI related
this.hasCI = data.has_ci;
diff --git a/app/assets/javascripts/vue_shared/components/file_row.vue b/app/assets/javascripts/vue_shared/components/file_row.vue
index fe5289ff371..f49e69c473b 100644
--- a/app/assets/javascripts/vue_shared/components/file_row.vue
+++ b/app/assets/javascripts/vue_shared/components/file_row.vue
@@ -146,6 +146,7 @@ export default {
<span ref="textOutput" :style="levelIndentation" class="file-row-name str-truncated">
<file-icon
v-if="!showChangedIcon || file.type === 'tree'"
+ class="file-row-icon"
:file-name="file.name"
:loading="file.loading"
:folder="isTree"
@@ -223,13 +224,8 @@ export default {
white-space: nowrap;
}
-.file-row-name svg {
+.file-row-name .file-row-icon {
margin-right: 2px;
vertical-align: middle;
}
-
-.file-row-name .loading-container {
- display: inline-block;
- margin-right: 4px;
-}
</style>
diff --git a/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue b/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue
index df19906309c..f0aae20477b 100644
--- a/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue
+++ b/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue
@@ -30,9 +30,16 @@ export default {
},
mounted() {
+ if (window.recaptchaDialogCallback) {
+ throw new Error('recaptchaDialogCallback is already defined!');
+ }
window.recaptchaDialogCallback = this.submit.bind(this);
},
+ beforeDestroy() {
+ window.recaptchaDialogCallback = null;
+ },
+
methods: {
appendRecaptchaScript() {
this.removeRecaptchaScript();
diff --git a/app/assets/javascripts/vue_shared/directives/autofocusonshow.js b/app/assets/javascripts/vue_shared/directives/autofocusonshow.js
new file mode 100644
index 00000000000..4659ec20ceb
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/directives/autofocusonshow.js
@@ -0,0 +1,39 @@
+/**
+ * Input/Textarea Autofocus Directive for Vue
+ */
+export default {
+ /**
+ * Set focus when element is rendered, but
+ * is not visible, using IntersectionObserver
+ *
+ * @param {Element} el Target element
+ */
+ inserted(el) {
+ if ('IntersectionObserver' in window) {
+ // Element visibility is dynamic, so we attach observer
+ el.visibilityObserver = new IntersectionObserver(entries => {
+ entries.forEach(entry => {
+ // Combining `intersectionRatio > 0` and
+ // element's `offsetParent` presence will
+ // deteremine if element is truely visible
+ if (entry.intersectionRatio > 0 && entry.target.offsetParent) {
+ entry.target.focus();
+ }
+ });
+ });
+
+ // Bind the observer.
+ el.visibilityObserver.observe(el, { root: document.documentElement });
+ }
+ },
+ /**
+ * Detach observer on unbind hook.
+ *
+ * @param {Element} el Target element
+ */
+ unbind(el) {
+ if (el.visibilityObserver) {
+ el.visibilityObserver.disconnect();
+ }
+ },
+};