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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2022-08-18 11:17:02 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2022-08-18 11:17:02 +0300
commitb39512ed755239198a9c294b6a45e65c05900235 (patch)
treed234a3efade1de67c46b9e5a38ce813627726aa7 /app/assets/javascripts/vue_shared
parentd31474cf3b17ece37939d20082b07f6657cc79a9 (diff)
Add latest changes from gitlab-org/gitlab@15-3-stable-eev15.3.0-rc42
Diffstat (limited to 'app/assets/javascripts/vue_shared')
-rw-r--r--app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger.vue1
-rw-r--r--app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger_modal.vue11
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/store/modules/filters/actions.js4
-rw-r--r--app/assets/javascripts/vue_shared/components/gitlab_version_check.vue3
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/header.vue95
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/toolbar.vue12
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/namespace_select/namespace_select.vue61
-rw-r--r--app/assets/javascripts/vue_shared/components/project_avatar.vue13
-rw-r--r--app/assets/javascripts/vue_shared/components/project_selector/project_list_item.vue1
-rw-r--r--app/assets/javascripts/vue_shared/components/rich_timestamp_tooltip.vue42
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_assignees.query.graphql6
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_participants.query.graphql2
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_timelogs.query.graphql1
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/queries/get_merge_request_reviewers.query.graphql26
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_assignees.query.graphql7
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_timelogs.query.graphql1
-rw-r--r--app/assets/javascripts/vue_shared/components/source_editor.vue10
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/components/chunk.vue5
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_line.vue35
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/constants.js1
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/plugins/link_dependencies.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/dependency_linker_util.js5
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/gemspec_linker.js39
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/package_json_linker.js13
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue11
-rw-r--r--app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image_new.vue12
-rw-r--r--app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue11
-rw-r--r--app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link_new.vue7
-rw-r--r--app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_list.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue12
-rw-r--r--app/assets/javascripts/vue_shared/components/user_select/user_select.vue54
-rw-r--r--app/assets/javascripts/vue_shared/components/web_ide_link.vue30
-rw-r--r--app/assets/javascripts/vue_shared/constants.js27
-rw-r--r--app/assets/javascripts/vue_shared/issuable/list/components/issuable_item.vue12
-rw-r--r--app/assets/javascripts/vue_shared/issuable/list/components/issuable_list_root.vue6
-rw-r--r--app/assets/javascripts/vue_shared/issuable/list/constants.js7
-rw-r--r--app/assets/javascripts/vue_shared/issuable/show/components/issuable_header.vue24
-rw-r--r--app/assets/javascripts/vue_shared/issuable/show/components/issuable_show_root.vue7
-rw-r--r--app/assets/javascripts/vue_shared/new_namespace/new_namespace_page.vue2
41 files changed, 474 insertions, 150 deletions
diff --git a/app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger.vue b/app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger.vue
index e12e06a2454..5b9efff1c06 100644
--- a/app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger.vue
+++ b/app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger.vue
@@ -53,6 +53,7 @@ export default {
:variant="buttonVariant"
:disabled="disabled"
:data-testid="buttonTestid"
+ data-qa-selector="confirm_danger_button"
>{{ buttonText }}</gl-button
>
<confirm-danger-modal
diff --git a/app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger_modal.vue b/app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger_modal.vue
index 37e480f7e41..7a982bc035a 100644
--- a/app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger_modal.vue
+++ b/app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger_modal.vue
@@ -66,7 +66,13 @@ export default {
actionPrimary() {
return {
text: this.confirmButtonText,
- attributes: [{ variant: 'danger', disabled: !this.isValid, class: 'qa-confirm-button' }],
+ attributes: [
+ {
+ variant: 'danger',
+ disabled: !this.isValid,
+ 'data-qa-selector': 'confirm_danger_modal_button',
+ },
+ ],
};
},
actionCancel() {
@@ -122,7 +128,8 @@ export default {
<gl-form-input
id="confirm_name_input"
v-model="confirmationPhrase"
- class="form-control qa-confirm-input"
+ class="form-control"
+ data-qa-selector="confirm_danger_field"
data-testid="confirm-danger-input"
type="text"
/>
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/store/modules/filters/actions.js b/app/assets/javascripts/vue_shared/components/filtered_search_bar/store/modules/filters/actions.js
index f4317ba90a2..7c4e372dda1 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/store/modules/filters/actions.js
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/store/modules/filters/actions.js
@@ -30,12 +30,12 @@ export function fetchBranches({ commit, state }, search = '') {
});
}
-export const fetchMilestones = ({ commit, state }, search_title = '') => {
+export const fetchMilestones = ({ commit, state }, searchTitle = '') => {
commit(types.REQUEST_MILESTONES);
const { milestonesEndpoint } = state;
return axios
- .get(milestonesEndpoint, { params: { search_title } })
+ .get(milestonesEndpoint, { params: { search_title: searchTitle } })
.then((response) => {
commit(types.RECEIVE_MILESTONES_SUCCESS, response.data);
return response;
diff --git a/app/assets/javascripts/vue_shared/components/gitlab_version_check.vue b/app/assets/javascripts/vue_shared/components/gitlab_version_check.vue
index acddf16bd27..72148a0aa7c 100644
--- a/app/assets/javascripts/vue_shared/components/gitlab_version_check.vue
+++ b/app/assets/javascripts/vue_shared/components/gitlab_version_check.vue
@@ -2,6 +2,7 @@
import { GlBadge } from '@gitlab/ui';
import { s__ } from '~/locale';
import axios from '~/lib/utils/axios_utils';
+import { joinPaths } from '~/lib/utils/url_utility';
const STATUS_TYPES = {
SUCCESS: 'success',
@@ -45,7 +46,7 @@ export default {
methods: {
checkGitlabVersion() {
axios
- .get('/admin/version_check.json')
+ .get(joinPaths('/', gon.relative_url_root, '/admin/version_check.json'))
.then((res) => {
if (res.data) {
this.status = res.data.severity;
diff --git a/app/assets/javascripts/vue_shared/components/markdown/header.vue b/app/assets/javascripts/vue_shared/components/markdown/header.vue
index 4fdf7f45643..1d1b65aa1af 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/header.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/header.vue
@@ -156,6 +156,14 @@ export default {
})
.catch(() => {});
},
+ handleAttachFile(e) {
+ e.preventDefault();
+ const $gfmForm = $(this.$el).closest('.gfm-form');
+ const $gfmTextarea = $gfmForm.find('.js-gfm-input');
+
+ $gfmForm.find('.div-dropzone').click();
+ $gfmTextarea.focus();
+ },
},
shortcuts: {
bold: keysFor(BOLD_TEXT),
@@ -195,6 +203,44 @@ export default {
:class="{ 'gl-display-none!': previewMarkdown }"
class="md-header-toolbar gl-ml-auto gl-pb-3 gl-justify-content-center"
>
+ <template v-if="canSuggest">
+ <toolbar-button
+ ref="suggestButton"
+ :tag="mdSuggestion"
+ :prepend="true"
+ :button-title="__('Insert suggestion')"
+ :cursor-offset="4"
+ :tag-content="lineContent"
+ icon="doc-code"
+ data-qa-selector="suggestion_button"
+ class="js-suggestion-btn"
+ @click="handleSuggestDismissed"
+ />
+ <gl-popover
+ v-if="suggestPopoverVisible"
+ :target="$refs.suggestButton.$el"
+ :css-classes="['diff-suggest-popover']"
+ placement="bottom"
+ :show="suggestPopoverVisible"
+ >
+ <strong>{{ __('New! Suggest changes directly') }}</strong>
+ <p class="mb-2">
+ {{
+ __(
+ 'Suggest code changes which can be immediately applied in one click. Try it out!',
+ )
+ }}
+ </p>
+ <gl-button
+ variant="confirm"
+ category="primary"
+ size="small"
+ @click="handleSuggestDismissed"
+ >
+ {{ __('Got it') }}
+ </gl-button>
+ </gl-popover>
+ </template>
<toolbar-button
tag="**"
:button-title="
@@ -237,44 +283,6 @@ export default {
icon="quote"
@click="handleQuote"
/>
- <template v-if="canSuggest">
- <toolbar-button
- ref="suggestButton"
- :tag="mdSuggestion"
- :prepend="true"
- :button-title="__('Insert suggestion')"
- :cursor-offset="4"
- :tag-content="lineContent"
- icon="doc-code"
- data-qa-selector="suggestion_button"
- class="js-suggestion-btn"
- @click="handleSuggestDismissed"
- />
- <gl-popover
- v-if="suggestPopoverVisible"
- :target="$refs.suggestButton.$el"
- :css-classes="['diff-suggest-popover']"
- placement="bottom"
- :show="suggestPopoverVisible"
- >
- <strong>{{ __('New! Suggest changes directly') }}</strong>
- <p class="mb-2">
- {{
- __(
- 'Suggest code changes which can be immediately applied in one click. Try it out!',
- )
- }}
- </p>
- <gl-button
- variant="confirm"
- category="primary"
- size="small"
- @click="handleSuggestDismissed"
- >
- {{ __('Got it') }}
- </gl-button>
- </gl-popover>
- </template>
<toolbar-button tag="`" tag-block="```" :button-title="__('Insert code')" icon="code" />
<toolbar-button
tag="[{text}](url)"
@@ -306,7 +314,7 @@ export default {
v-if="!restrictedToolBarItems.includes('task-list')"
:prepend="true"
tag="- [ ] "
- :button-title="__('Add a task list')"
+ :button-title="__('Add a checklist')"
icon="list-task"
/>
<toolbar-button
@@ -324,6 +332,15 @@ export default {
:button-title="__('Add a table')"
icon="table"
/>
+ <gl-button
+ v-if="!restrictedToolBarItems.includes('attach-file')"
+ v-gl-tooltip
+ :title="__('Attach a file or image')"
+ data-testid="button-attach-file"
+ category="tertiary"
+ icon="paperclip"
+ @click="handleAttachFile"
+ />
<toolbar-button
v-if="!restrictedToolBarItems.includes('full-screen')"
class="js-zen-enter"
diff --git a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue
index 6c99a749edc..aa325862f06 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue
@@ -74,7 +74,7 @@ export default {
</div>
<span v-if="canAttachFile" class="uploading-container">
<span class="uploading-progress-container hide">
- <gl-icon name="media" />
+ <gl-icon name="paperclip" />
<span class="attaching-file-message"></span>
<!-- eslint-disable-next-line @gitlab/vue-require-i18n-strings -->
<span class="uploading-progress">0%</span>
@@ -82,7 +82,7 @@ export default {
</span>
<span class="uploading-error-container hide">
<span class="uploading-error-icon">
- <gl-icon name="media" />
+ <gl-icon name="paperclip" />
</span>
<span class="uploading-error-message"></span>
@@ -114,14 +114,6 @@ export default {
</gl-sprintf>
</span>
<gl-button
- icon="media"
- variant="link"
- category="primary"
- class="markdown-selector button-attach-file gl-vertical-align-text-bottom"
- >
- {{ __('Attach a file') }}
- </gl-button>
- <gl-button
variant="link"
category="primary"
class="button-cancel-uploading-files gl-vertical-align-baseline hide"
diff --git a/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue b/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue
index 6a83939795c..49217e38a1b 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue
@@ -88,6 +88,6 @@ export default {
category="tertiary"
class="js-md"
data-container="body"
- @click="() => $emit('click')"
+ @click="$emit('click', $event)"
/>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/namespace_select/namespace_select.vue b/app/assets/javascripts/vue_shared/components/namespace_select/namespace_select.vue
index 521b1a1075a..e9f278a5db5 100644
--- a/app/assets/javascripts/vue_shared/components/namespace_select/namespace_select.vue
+++ b/app/assets/javascripts/vue_shared/components/namespace_select/namespace_select.vue
@@ -5,6 +5,8 @@ import {
GlDropdownItem,
GlDropdownSectionHeader,
GlSearchBoxByType,
+ GlIntersectionObserver,
+ GlLoadingIcon,
} from '@gitlab/ui';
import { __ } from '~/locale';
@@ -32,6 +34,8 @@ export default {
GlDropdownItem,
GlDropdownSectionHeader,
GlSearchBoxByType,
+ GlIntersectionObserver,
+ GlLoadingIcon,
},
props: {
groupNamespaces: {
@@ -69,6 +73,26 @@ export default {
required: false,
default: false,
},
+ hasNextPageOfGroups: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ isLoadingMoreGroups: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ isSearchLoading: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ shouldFilterNamespaces: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
},
data() {
return {
@@ -84,10 +108,12 @@ export default {
return this.groupNamespaces.length;
},
filteredGroupNamespaces() {
+ if (!this.shouldFilterNamespaces) return this.groupNamespaces;
if (!this.hasGroupNamespaces) return [];
return filterByName(this.groupNamespaces, this.searchTerm);
},
filteredUserNamespaces() {
+ if (!this.shouldFilterNamespaces) return this.userNamespaces;
if (!this.hasUserNamespaces) return [];
return filterByName(this.userNamespaces, this.searchTerm);
},
@@ -107,9 +133,15 @@ export default {
return emptyNamespaceTitle.toLowerCase().includes(searchTerm.toLowerCase());
},
},
+ watch: {
+ searchTerm() {
+ this.$emit('search', this.searchTerm);
+ },
+ },
methods: {
handleSelect(item) {
this.selectedNamespace = item;
+ this.searchTerm = '';
this.$emit('select', item);
},
handleSelectEmptyNamespace() {
@@ -122,7 +154,11 @@ export default {
<template>
<gl-dropdown :text="selectedNamespaceText" :block="fullWidth" data-qa-selector="namespaces_list">
<template #header>
- <gl-search-box-by-type v-model.trim="searchTerm" />
+ <gl-search-box-by-type
+ v-model.trim="searchTerm"
+ :is-loading="isSearchLoading"
+ data-qa-selector="namespaces_list_search"
+ />
</template>
<div v-if="filteredEmptyNamespaceTitle">
<gl-dropdown-item
@@ -133,29 +169,40 @@ export default {
</gl-dropdown-item>
<gl-dropdown-divider />
</div>
- <div v-if="hasGroupNamespaces" data-qa-selector="namespaces_list_groups">
+ <div
+ v-if="hasUserNamespaces"
+ data-qa-selector="namespaces_list_users"
+ data-testid="namespace-list-users"
+ >
<gl-dropdown-section-header v-if="includeHeaders">{{
- $options.i18n.GROUPS
+ $options.i18n.USERS
}}</gl-dropdown-section-header>
<gl-dropdown-item
- v-for="item in filteredGroupNamespaces"
+ v-for="item in filteredUserNamespaces"
:key="item.id"
data-qa-selector="namespaces_list_item"
@click="handleSelect(item)"
>{{ item.humanName }}</gl-dropdown-item
>
</div>
- <div v-if="hasUserNamespaces" data-qa-selector="namespaces_list_users">
+ <div
+ v-if="hasGroupNamespaces"
+ data-qa-selector="namespaces_list_groups"
+ data-testid="namespace-list-groups"
+ >
<gl-dropdown-section-header v-if="includeHeaders">{{
- $options.i18n.USERS
+ $options.i18n.GROUPS
}}</gl-dropdown-section-header>
<gl-dropdown-item
- v-for="item in filteredUserNamespaces"
+ v-for="item in filteredGroupNamespaces"
:key="item.id"
data-qa-selector="namespaces_list_item"
@click="handleSelect(item)"
>{{ item.humanName }}</gl-dropdown-item
>
</div>
+ <gl-intersection-observer v-if="hasNextPageOfGroups" @appear="$emit('load-more-groups')">
+ <gl-loading-icon v-if="isLoadingMoreGroups" class="gl-mb-3" size="sm" />
+ </gl-intersection-observer>
</gl-dropdown>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/project_avatar.vue b/app/assets/javascripts/vue_shared/components/project_avatar.vue
index 402e75962d2..f65cc8bf2f3 100644
--- a/app/assets/javascripts/vue_shared/components/project_avatar.vue
+++ b/app/assets/javascripts/vue_shared/components/project_avatar.vue
@@ -1,5 +1,6 @@
<script>
import { GlAvatar } from '@gitlab/ui';
+import { getIdFromGraphQLId, isGid } from '~/graphql_shared/utils';
import { AVATAR_SHAPE_OPTION_RECT } from '~/vue_shared/constants';
export default {
@@ -7,6 +8,14 @@ export default {
GlAvatar,
},
props: {
+ projectId: {
+ type: [Number, String],
+ default: 0,
+ required: false,
+ validator(value) {
+ return typeof value === 'string' ? isGid(value) : true;
+ },
+ },
projectName: {
type: String,
required: true,
@@ -31,6 +40,9 @@ export default {
avatarAlt() {
return this.alt ?? this.projectName;
},
+ entityId() {
+ return isGid(this.projectId) ? getIdFromGraphQLId(this.projectId) : this.projectId;
+ },
},
AVATAR_SHAPE_OPTION_RECT,
};
@@ -39,6 +51,7 @@ export default {
<template>
<gl-avatar
:shape="$options.AVATAR_SHAPE_OPTION_RECT"
+ :entity-id="entityId"
:entity-name="projectName"
:src="projectAvatarUrl"
:alt="avatarAlt"
diff --git a/app/assets/javascripts/vue_shared/components/project_selector/project_list_item.vue b/app/assets/javascripts/vue_shared/components/project_selector/project_list_item.vue
index 19ffbe37ce7..66643ff4026 100644
--- a/app/assets/javascripts/vue_shared/components/project_selector/project_list_item.vue
+++ b/app/assets/javascripts/vue_shared/components/project_selector/project_list_item.vue
@@ -53,6 +53,7 @@ export default {
>
<gl-icon v-if="selected" class="js-selected-icon" name="mobile-issue-close" />
<project-avatar
+ :project-id="project.id"
:project-avatar-url="projectAvatarUrl"
:project-name="projectNameWithNamespace"
class="gl-mr-3"
diff --git a/app/assets/javascripts/vue_shared/components/rich_timestamp_tooltip.vue b/app/assets/javascripts/vue_shared/components/rich_timestamp_tooltip.vue
new file mode 100644
index 00000000000..424a11bf88b
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/rich_timestamp_tooltip.vue
@@ -0,0 +1,42 @@
+<script>
+import { GlTooltip } from '@gitlab/ui';
+
+import { formatDate } from '~/lib/utils/datetime_utility';
+import timeagoMixin from '~/vue_shared/mixins/timeago';
+
+export default {
+ components: {
+ GlTooltip,
+ },
+ mixins: [timeagoMixin],
+ props: {
+ target: {
+ type: [Object, HTMLElement, SVGElement, String, Function],
+ required: true,
+ },
+ rawTimestamp: {
+ type: String,
+ required: true,
+ },
+ timestampTypeText: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ timestampInWords() {
+ return this.rawTimestamp ? this.timeFormatted(this.rawTimestamp) : '';
+ },
+ timestamp() {
+ return this.rawTimestamp ? formatDate(new Date(this.rawTimestamp)) : '';
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-tooltip :target="target">
+ <div class="bold" data-testid="header-text">{{ timestampTypeText }} {{ timestampInWords }}</div>
+ <div class="text-tertiary" data-testid="body-text">{{ timestamp }}</div>
+ </gl-tooltip>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_assignees.query.graphql b/app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_assignees.query.graphql
index be270e440ed..4af07366a6d 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_assignees.query.graphql
+++ b/app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_assignees.query.graphql
@@ -3,11 +3,13 @@
query issueAssignees($fullPath: ID!, $iid: String!) {
workspace: project(fullPath: $fullPath) {
- __typename
id
issuable: issue(iid: $iid) {
- __typename
id
+ author {
+ ...User
+ ...UserAvailability
+ }
assignees {
nodes {
...User
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_participants.query.graphql b/app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_participants.query.graphql
index 96a40e597ee..445817d3e52 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_participants.query.graphql
+++ b/app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_participants.query.graphql
@@ -3,10 +3,8 @@
query issueParticipants($fullPath: ID!, $iid: String!) {
workspace: project(fullPath: $fullPath) {
- __typename
id
issuable: issue(iid: $iid) {
- __typename
id
participants {
nodes {
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_timelogs.query.graphql b/app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_timelogs.query.graphql
index dffcc053fac..b127b8ec5a9 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_timelogs.query.graphql
+++ b/app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_timelogs.query.graphql
@@ -2,7 +2,6 @@
query issueTimeTrackingReport($id: IssueID!) {
issuable: issue(id: $id) {
- __typename
id
title
timelogs {
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_merge_request_reviewers.query.graphql b/app/assets/javascripts/vue_shared/components/sidebar/queries/get_merge_request_reviewers.query.graphql
new file mode 100644
index 00000000000..05de680ab05
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/sidebar/queries/get_merge_request_reviewers.query.graphql
@@ -0,0 +1,26 @@
+#import "~/graphql_shared/fragments/user.fragment.graphql"
+#import "~/graphql_shared/fragments/user_availability.fragment.graphql"
+
+query mergeRequestReviewers($fullPath: ID!, $iid: String!) {
+ workspace: project(fullPath: $fullPath) {
+ id
+ issuable: mergeRequest(iid: $iid) {
+ id
+ reviewers {
+ nodes {
+ ...User
+ ...UserAvailability
+ mergeRequestInteraction {
+ canMerge
+ canUpdate
+ approved
+ reviewed
+ }
+ }
+ }
+ userPermissions {
+ updateMergeRequest
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_assignees.query.graphql b/app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_assignees.query.graphql
index 7127940bb05..f70cd723f2e 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_assignees.query.graphql
+++ b/app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_assignees.query.graphql
@@ -6,6 +6,13 @@ query getMrAssignees($fullPath: ID!, $iid: String!) {
id
issuable: mergeRequest(iid: $iid) {
id
+ author {
+ ...User
+ ...UserAvailability
+ mergeRequestInteraction {
+ canMerge
+ }
+ }
assignees {
nodes {
...User
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_timelogs.query.graphql b/app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_timelogs.query.graphql
index ede9b75d765..17f548b44b5 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_timelogs.query.graphql
+++ b/app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_timelogs.query.graphql
@@ -2,7 +2,6 @@
query mrTimeTrackingReport($id: MergeRequestID!) {
issuable: mergeRequest(id: $id) {
- __typename
id
title
timelogs {
diff --git a/app/assets/javascripts/vue_shared/components/source_editor.vue b/app/assets/javascripts/vue_shared/components/source_editor.vue
index 6a0bf07c8b4..1925c5d4064 100644
--- a/app/assets/javascripts/vue_shared/components/source_editor.vue
+++ b/app/assets/javascripts/vue_shared/components/source_editor.vue
@@ -1,5 +1,5 @@
<script>
-import { debounce } from 'lodash';
+import { debounce, isEmpty } from 'lodash';
import { CONTENT_UPDATE_DEBOUNCE, EDITOR_READY_EVENT } from '~/editor/constants';
import Editor from '~/editor/source_editor';
@@ -37,9 +37,9 @@ export default {
default: '',
},
extensions: {
- type: [String, Array],
+ type: [Object, Array],
required: false,
- default: () => null,
+ default: () => ({}),
},
editorOptions: {
type: Object,
@@ -74,11 +74,13 @@ export default {
blobPath: this.fileName,
blobContent: this.value,
blobGlobalId: this.fileGlobalId,
- extensions: this.extensions,
...this.editorOptions,
});
this.editor.onDidChangeModelContent(debounce(this.onFileChange.bind(this), this.debounceValue));
+ if (!isEmpty(this.extensions)) {
+ this.editor.use(this.extensions);
+ }
},
beforeDestroy() {
this.editor.dispose();
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk.vue b/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk.vue
index 6babbca58c3..9683288f937 100644
--- a/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk.vue
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk.vue
@@ -51,6 +51,10 @@ export default {
required: false,
default: null,
},
+ blamePath: {
+ type: String,
+ required: true,
+ },
},
computed: {
lines() {
@@ -76,6 +80,7 @@ export default {
:number="startingFrom + index + 1"
:content="line"
:language="language"
+ :blame-path="blamePath"
/>
</div>
<div v-else class="gl-display-flex">
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_line.vue b/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_line.vue
index 7b62f0cdb7d..257b9f57222 100644
--- a/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_line.vue
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_line.vue
@@ -1,15 +1,14 @@
<script>
-import { GlLink, GlSafeHtmlDirective } from '@gitlab/ui';
+import { GlSafeHtmlDirective } from '@gitlab/ui';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { setAttributes } from '~/lib/utils/dom_utils';
import { BIDI_CHARS, BIDI_CHARS_CLASS_LIST, BIDI_CHAR_TOOLTIP } from '../constants';
export default {
- components: {
- GlLink,
- },
directives: {
SafeHtml: GlSafeHtmlDirective,
},
+ mixins: [glFeatureFlagMixin()],
props: {
number: {
type: Number,
@@ -23,6 +22,10 @@ export default {
type: String,
required: true,
},
+ blamePath: {
+ type: String,
+ required: true,
+ },
},
computed: {
formattedContent() {
@@ -36,9 +39,6 @@ export default {
return content;
},
- firstLineClass() {
- return { 'gl-mt-3!': this.number === 1 };
- },
},
methods: {
wrapBidiChar(bidiChar) {
@@ -59,21 +59,26 @@ export default {
</script>
<template>
<div class="gl-display-flex">
- <div class="gl-p-0! gl-absolute gl-z-index-3 gl-border-r diff-line-num line-numbers">
- <gl-link
+ <div
+ class="gl-p-0! gl-absolute gl-z-index-3 diff-line-num gl-border-r gl-display-flex line-links line-numbers"
+ >
+ <a
+ v-if="glFeatures.fileLineBlame"
+ class="gl-user-select-none gl-shadow-none! file-line-blame"
+ :href="`${blamePath}#L${number}`"
+ ></a>
+ <a
:id="`L${number}`"
- class="gl-user-select-none gl-ml-5 gl-pr-3 gl-shadow-none! file-line-num diff-line-num"
- :class="firstLineClass"
- :to="`#L${number}`"
+ class="gl-user-select-none gl-shadow-none! file-line-num"
+ :href="`#L${number}`"
:data-line-number="number"
>
{{ number }}
- </gl-link>
+ </a>
</div>
<pre
- class="gl-p-0! gl-w-full gl-overflow-visible! gl-ml-11! gl-border-none! code highlight gl-line-height-normal"
- :class="firstLineClass"
+ class="gl-p-0! gl-w-full gl-overflow-visible! gl-border-none! code highlight gl-line-height-normal"
><code><span :id="`LC${number}`" v-safe-html="formattedContent" :lang="language" class="line" data-testid="content"></span></code></pre>
</div>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/constants.js b/app/assets/javascripts/vue_shared/components/source_viewer/constants.js
index 3ac35abcf3a..cc930d67fa4 100644
--- a/app/assets/javascripts/vue_shared/components/source_viewer/constants.js
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/constants.js
@@ -147,3 +147,4 @@ export const HLJS_COMMENT_SELECTOR = 'hljs-comment';
export const HLJS_ON_AFTER_HIGHLIGHT = 'after:highlight';
export const NPM_URL = 'https://npmjs.com/package';
+export const GEM_URL = 'https://rubygems.org/gems';
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/plugins/link_dependencies.js b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/link_dependencies.js
index 5b7650c56ae..d957990fe7f 100644
--- a/app/assets/javascripts/vue_shared/components/source_viewer/plugins/link_dependencies.js
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/link_dependencies.js
@@ -1,7 +1,9 @@
import packageJsonLinker from './utils/package_json_linker';
+import gemspecLinker from './utils/gemspec_linker';
const DEPENDENCY_LINKERS = {
package_json: packageJsonLinker,
+ gemspec: gemspecLinker,
};
/**
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/dependency_linker_util.js b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/dependency_linker_util.js
index 56ad55ef553..dbe6812cf16 100644
--- a/app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/dependency_linker_util.js
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/dependency_linker_util.js
@@ -7,9 +7,10 @@ export const createLink = (href, innerText) => {
const link = document.createElement('a');
setAttributes(link, { href: escape(href), rel });
- link.innerText = escape(innerText);
+ link.textContent = innerText;
return link.outerHTML;
};
-export const generateHLJSOpenTag = (type) => `<span class="hljs-${escape(type)}">&quot;`;
+export const generateHLJSOpenTag = (type, delimiter = '&quot;') =>
+ `<span class="hljs-${escape(type)}">${delimiter}`;
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/gemspec_linker.js b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/gemspec_linker.js
new file mode 100644
index 00000000000..35de8fd13d6
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/gemspec_linker.js
@@ -0,0 +1,39 @@
+import { joinPaths } from '~/lib/utils/url_utility';
+import { GEM_URL } from '../../constants';
+import { createLink, generateHLJSOpenTag } from './dependency_linker_util';
+
+const methodRegex = '.*add_dependency.*|.*add_runtime_dependency.*|.*add_development_dependency.*';
+const openTagRegex = generateHLJSOpenTag('string', '(&.*;)');
+const closeTagRegex = '&.*</span>';
+
+const DEPENDENCY_REGEX = new RegExp(
+ /*
+ * Detects gemspec dependencies inside of content that is highlighted by Highlight.js
+ * Example: s.add_dependency(<span class="hljs-string">&#x27;rugged&#x27;</span>, <span class="hljs-string">&#x27;~&gt; 0.24.0&#x27;</span>)
+ *
+ * Group 1 (method) : s.add_dependency(
+ * Group 2 (delimiter) : &#x27;
+ * Group 3 (packageName): rugged
+ * Group 4 (closeTag) : &#x27;</span>
+ * Group 5 (rest) : , <span class="hljs-string">&#x27;~&gt; 0.24.0&#x27;</span>)
+ */
+ `(${methodRegex})${openTagRegex}(.*)(${closeTagRegex})(.*${closeTagRegex})`,
+ 'gm',
+);
+
+const handleReplace = (method, delimiter, packageName, closeTag, rest) => {
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ const openTag = generateHLJSOpenTag('string linked', delimiter);
+ const href = joinPaths(GEM_URL, packageName);
+ const packageLink = createLink(href, packageName);
+
+ return `${method}${openTag}${packageLink}${closeTag}${rest}`;
+};
+
+export default (result) => {
+ return result.value.replace(
+ DEPENDENCY_REGEX,
+ (_, method, delimiter, packageName, closeTag, rest) =>
+ handleReplace(method, delimiter, packageName, closeTag, rest),
+ );
+};
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/package_json_linker.js b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/package_json_linker.js
index d013d077ba3..3c6fc23c138 100644
--- a/app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/package_json_linker.js
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/package_json_linker.js
@@ -1,3 +1,4 @@
+import { unescape } from 'lodash';
import { joinPaths } from '~/lib/utils/url_utility';
import { NPM_URL } from '../../constants';
import { createLink, generateHLJSOpenTag } from './dependency_linker_util';
@@ -17,13 +18,15 @@ const DEPENDENCY_REGEX = new RegExp(
);
const handleReplace = (original, packageName, version, dependenciesToLink) => {
- const href = joinPaths(NPM_URL, packageName);
- const packageLink = createLink(href, packageName);
- const versionLink = createLink(href, version);
+ const unescapedPackageName = unescape(packageName);
+ const unescapedVersion = unescape(version);
+ const href = joinPaths(NPM_URL, unescapedPackageName);
+ const packageLink = createLink(href, unescapedPackageName);
+ const versionLink = createLink(href, unescapedVersion);
const closeAndOpenTag = `${closeTag}: ${attrOpenTag}`;
- const dependencyToLink = dependenciesToLink[packageName];
+ const dependencyToLink = dependenciesToLink[unescapedPackageName];
- if (dependencyToLink && dependencyToLink === version) {
+ if (dependencyToLink && dependencyToLink === unescapedVersion) {
return `${attrOpenTag}${packageLink}${closeAndOpenTag}${versionLink}${closeTag}`;
}
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue b/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue
index 1bdae40332f..ccc8b44942a 100644
--- a/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue
@@ -199,6 +199,7 @@ export default {
:starting-from="firstChunk.startingFrom"
:is-highlighted="firstChunk.isHighlighted"
:language="firstChunk.language"
+ :blame-path="blob.blamePath"
/>
<gl-loading-icon v-if="isLoading" size="sm" class="gl-my-5" />
@@ -213,6 +214,7 @@ export default {
:is-highlighted="chunk.isHighlighted"
:chunk-index="index"
:language="chunk.language"
+ :blame-path="blob.blamePath"
@appear="highlightChunk"
/>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue
index d07f65cf5c1..c1e618620d8 100644
--- a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue
+++ b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue
@@ -50,7 +50,7 @@ export default {
default: __('user avatar'),
},
size: {
- type: Number,
+ type: [Number, Object],
required: false,
default: 20,
},
@@ -64,12 +64,19 @@ export default {
required: false,
default: 'top',
},
+ enforceGlAvatar: {
+ type: Boolean,
+ required: false,
+ },
},
};
</script>
<template>
- <user-avatar-image-new v-if="glFeatures.glAvatarForAllUserAvatars" v-bind="$props">
+ <user-avatar-image-new
+ v-if="glFeatures.glAvatarForAllUserAvatars || enforceGlAvatar"
+ v-bind="$props"
+ >
<slot></slot>
</user-avatar-image-new>
<user-avatar-image-old v-else v-bind="$props">
diff --git a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image_new.vue b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image_new.vue
index 707b0bbec67..cd610314292 100644
--- a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image_new.vue
+++ b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image_new.vue
@@ -16,6 +16,7 @@
*/
import { GlTooltip, GlAvatar } from '@gitlab/ui';
+import { isObject } from 'lodash';
import defaultAvatarUrl from 'images/no_avatar.png';
import { __ } from '~/locale';
import { placeholderImage } from '~/lazy_loader';
@@ -48,7 +49,7 @@ export default {
default: __('user avatar'),
},
size: {
- type: Number,
+ type: [Number, Object],
required: false,
default: 20,
},
@@ -71,9 +72,16 @@ export default {
let baseSrc = this.imgSrc === '' || this.imgSrc === null ? defaultAvatarUrl : this.imgSrc;
// Only adds the width to the URL if its not a base64 data image
if (!(baseSrc.indexOf('data:') === 0) && !baseSrc.includes('?'))
- baseSrc += `?width=${this.size}`;
+ baseSrc += `?width=${this.maximumSize}`;
return baseSrc;
},
+ maximumSize() {
+ if (isObject(this.size)) {
+ return Math.max(...Object.values(this.size));
+ }
+
+ return this.size;
+ },
resultantSrcAttribute() {
return this.lazy ? placeholderImage : this.sanitizedSource;
},
diff --git a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue
index 887deff17c9..f80abed4d69 100644
--- a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue
+++ b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue
@@ -55,7 +55,7 @@ export default {
default: '',
},
imgSize: {
- type: Number,
+ type: [Number, Object],
required: false,
default: 20,
},
@@ -74,12 +74,19 @@ export default {
required: false,
default: '',
},
+ enforceGlAvatar: {
+ type: Boolean,
+ required: false,
+ },
},
};
</script>
<template>
- <user-avatar-link-new v-if="glFeatures.glAvatarForAllUserAvatars" v-bind="$props">
+ <user-avatar-link-new
+ v-if="glFeatures.glAvatarForAllUserAvatars || enforceGlAvatar"
+ v-bind="$props"
+ >
<slot></slot>
<template #avatar-badge>
<slot name="avatar-badge"></slot>
diff --git a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link_new.vue b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link_new.vue
index 3b459569274..83551c689c4 100644
--- a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link_new.vue
+++ b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link_new.vue
@@ -56,7 +56,7 @@ export default {
default: '',
},
imgSize: {
- type: Number,
+ type: [Number, Object],
required: false,
default: 20,
},
@@ -75,6 +75,10 @@ export default {
required: false,
default: '',
},
+ enforceGlAvatar: {
+ type: Boolean,
+ required: false,
+ },
},
computed: {
shouldShowUsername() {
@@ -97,6 +101,7 @@ export default {
:tooltip-text="avatarTooltipText"
:tooltip-placement="tooltipPlacement"
:lazy="lazy"
+ :enforce-gl-avatar="enforceGlAvatar"
>
<slot></slot>
</user-avatar-image>
diff --git a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_list.vue b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_list.vue
index 60b26d688b2..9da298ad705 100644
--- a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_list.vue
+++ b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_list.vue
@@ -21,7 +21,7 @@ export default {
default: 10,
},
imgSize: {
- type: Number,
+ type: [Number, Object],
required: false,
default: 20,
},
diff --git a/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue b/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue
index a0d8ca117a4..2b9804796ae 100644
--- a/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue
+++ b/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue
@@ -14,6 +14,7 @@ import { glEmojiTag } from '~/emoji';
import createFlash from '~/flash';
import { followUser, unfollowUser } from '~/rest_api';
import { isUserBusy } from '~/set_status_modal/utils';
+import Tracking from '~/tracking';
import { USER_POPOVER_DELAY } from './constants';
const MAX_SKELETON_LINES = 4;
@@ -37,6 +38,7 @@ export default {
directives: {
SafeHtml: GlSafeHtmlDirective,
},
+ mixins: [Tracking.mixin()],
props: {
target: {
type: HTMLElement,
@@ -117,6 +119,11 @@ export default {
},
async follow() {
this.toggleFollowLoading = true;
+
+ this.track('click_button', {
+ label: 'follow_from_user_popover',
+ });
+
try {
await followUser(this.user.id);
this.$emit('follow');
@@ -132,6 +139,11 @@ export default {
},
async unfollow() {
this.toggleFollowLoading = true;
+
+ this.track('click_button', {
+ label: 'unfollow_from_user_popover',
+ });
+
try {
await unfollowUser(this.user.id);
this.$emit('unfollow');
diff --git a/app/assets/javascripts/vue_shared/components/user_select/user_select.vue b/app/assets/javascripts/vue_shared/components/user_select/user_select.vue
index 91f20863089..43a590c2367 100644
--- a/app/assets/javascripts/vue_shared/components/user_select/user_select.vue
+++ b/app/assets/javascripts/vue_shared/components/user_select/user_select.vue
@@ -77,6 +77,11 @@ export default {
required: false,
default: null,
},
+ issuableAuthor: {
+ type: Object,
+ required: false,
+ default: null,
+ },
},
data() {
return {
@@ -178,7 +183,7 @@ export default {
[],
);
- return this.moveCurrentUserToStart(mergedSearchResults);
+ return this.moveCurrentUserAndAuthorToStart(mergedSearchResults);
},
isSearchEmpty() {
return this.search === '';
@@ -196,14 +201,21 @@ export default {
showCurrentUser() {
return this.currentUser.username && !this.isCurrentUserInList && this.isSearchEmpty;
},
+ showAuthor() {
+ return (
+ this.issuableAuthor &&
+ !this.users.some((user) => user.id === this.issuableAuthor.id) &&
+ this.isSearchEmpty
+ );
+ },
selectedFiltered() {
if (this.shouldShowParticipants) {
- return this.moveCurrentUserToStart(this.value);
+ return this.moveCurrentUserAndAuthorToStart(this.value);
}
const foundUsernames = this.users.map(({ username }) => username);
const filtered = this.value.filter(({ username }) => foundUsernames.includes(username));
- return this.moveCurrentUserToStart(filtered);
+ return this.moveCurrentUserAndAuthorToStart(filtered);
},
selectedUserNames() {
return this.value.map(({ username }) => username);
@@ -254,20 +266,22 @@ export default {
showDivider(list) {
return list.length > 0 && this.isSearchEmpty;
},
- moveCurrentUserToStart(users) {
- if (!users) {
- return [];
+ moveCurrentUserAndAuthorToStart(users = []) {
+ let sortedUsers = [...users];
+
+ const author = sortedUsers.find((user) => user.id === this.issuableAuthor?.id);
+ if (author) {
+ sortedUsers = [author, ...sortedUsers.filter((user) => user.id !== author.id)];
}
- const usersCopy = [...users];
- const currentUser = usersCopy.find((user) => user.username === this.currentUser.username);
+
+ const currentUser = sortedUsers.find((user) => user.username === this.currentUser.username);
if (currentUser) {
currentUser.canMerge = this.currentUser.canMerge;
- const index = usersCopy.indexOf(currentUser);
- usersCopy.splice(0, 0, usersCopy.splice(index, 1)[0]);
+ sortedUsers = [currentUser, ...sortedUsers.filter((user) => user.id !== currentUser.id)];
}
- return usersCopy;
+ return sortedUsers;
},
setSearchKey(value) {
this.search = value.trim();
@@ -298,7 +312,7 @@ export default {
<gl-loading-icon
v-if="isLoading"
data-testid="loading-participants"
- size="lg"
+ size="md"
class="gl-absolute gl-left-0 gl-top-0 gl-right-0"
/>
<template v-else>
@@ -312,8 +326,8 @@ export default {
>
<span :class="selectedIsEmpty ? 'gl-pl-0' : 'gl-pl-6'" class="gl-font-weight-bold">{{
$options.i18n.unassigned
- }}</span></gl-dropdown-item
- >
+ }}</span>
+ </gl-dropdown-item>
</template>
<gl-dropdown-divider v-if="showDivider(selectedFiltered)" />
<gl-dropdown-item
@@ -342,7 +356,17 @@ export default {
/>
</gl-dropdown-item>
</template>
- <gl-dropdown-divider v-if="showDivider(unselectedFiltered)" />
+ <gl-dropdown-item
+ v-if="showAuthor"
+ data-testid="issuable-author"
+ @click.native.capture.stop="selectAssignee(issuableAuthor)"
+ >
+ <sidebar-participant
+ :user="issuableAuthor"
+ :issuable-type="issuableType"
+ class="gl-pl-6!"
+ />
+ </gl-dropdown-item>
<gl-dropdown-item
v-for="unselectedUser in unselectedFiltered"
:key="unselectedUser.id"
diff --git a/app/assets/javascripts/vue_shared/components/web_ide_link.vue b/app/assets/javascripts/vue_shared/components/web_ide_link.vue
index cac0d5a45c9..6d179b3dc92 100644
--- a/app/assets/javascripts/vue_shared/components/web_ide_link.vue
+++ b/app/assets/javascripts/vue_shared/components/web_ide_link.vue
@@ -10,6 +10,21 @@ const KEY_WEB_IDE = 'webide';
const KEY_GITPOD = 'gitpod';
const KEY_PIPELINE_EDITOR = 'pipeline_editor';
+export const i18n = {
+ modal: {
+ title: __('Enable Gitpod?'),
+ content: s__(
+ 'Gitpod|To use Gitpod you must first enable the feature in the integrations section of your %{linkStart}user preferences%{linkEnd}.',
+ ),
+ actionCancelText: __('Cancel'),
+ actionPrimaryText: __('Enable Gitpod'),
+ },
+ webIdeText: s__('WebIDE|Quickly and easily edit multiple files in your project.'),
+ webIdeTooltip: s__(
+ 'WebIDE|Quickly and easily edit multiple files in your project. Press . to open',
+ ),
+};
+
export default {
components: {
ActionsButton,
@@ -19,16 +34,7 @@ export default {
GlLink,
ConfirmForkModal,
},
- i18n: {
- modal: {
- title: __('Enable Gitpod?'),
- content: s__(
- 'Gitpod|To use Gitpod you must first enable the feature in the integrations section of your %{linkStart}user preferences%{linkEnd}.',
- ),
- actionCancelText: __('Cancel'),
- actionPrimaryText: __('Enable Gitpod'),
- },
- },
+ i18n,
props: {
isFork: {
type: Boolean,
@@ -207,8 +213,8 @@ export default {
return {
key: KEY_WEB_IDE,
text: this.webIdeActionText,
- secondaryText: __('Quickly and easily edit multiple files in your project.'),
- tooltip: '',
+ secondaryText: this.$options.i18n.webIdeText,
+ tooltip: this.$options.i18n.webIdeTooltip,
attrs: {
'data-qa-selector': 'web_ide_button',
'data-track-action': 'click_consolidated_edit_ide',
diff --git a/app/assets/javascripts/vue_shared/constants.js b/app/assets/javascripts/vue_shared/constants.js
index 14328b1f25f..b6d69faebb5 100644
--- a/app/assets/javascripts/vue_shared/constants.js
+++ b/app/assets/javascripts/vue_shared/constants.js
@@ -1,4 +1,4 @@
-import { __, sprintf } from '~/locale';
+import { __, n__, sprintf } from '~/locale';
import { IssuableType, WorkspaceType } from '~/issues/constants';
const INTERVALS = {
@@ -15,51 +15,62 @@ export const ISO_SHORT_FORMAT = 'yyyy-mm-dd';
export const DATE_FORMATS = [SHORT_DATE_FORMAT, ISO_SHORT_FORMAT];
+const getTimeLabel = (days) => n__('1 day', '%d days', days);
+
+/* eslint-disable @gitlab/require-i18n-strings */
export const timeRanges = [
{
- label: __('30 minutes'),
+ label: n__('1 minute', '%d minutes', 30),
+ shortcut: '30_minutes',
duration: { seconds: 60 * 30 },
name: 'thirtyMinutes',
interval: INTERVALS.minute,
},
{
- label: __('3 hours'),
+ label: n__('1 hour', '%d hours', 3),
+ shortcut: '3_hours',
duration: { seconds: 60 * 60 * 3 },
name: 'threeHours',
interval: INTERVALS.hour,
},
{
- label: __('8 hours'),
+ label: n__('1 hour', '%d hours', 8),
+ shortcut: '8_hours',
duration: { seconds: 60 * 60 * 8 },
name: 'eightHours',
default: true,
interval: INTERVALS.hour,
},
{
- label: __('1 day'),
+ label: getTimeLabel(1),
+ shortcut: '1_day',
duration: { seconds: 60 * 60 * 24 * 1 },
name: 'oneDay',
interval: INTERVALS.hour,
},
{
- label: __('3 days'),
+ label: getTimeLabel(3),
+ shortcut: '3_days',
duration: { seconds: 60 * 60 * 24 * 3 },
name: 'threeDays',
interval: INTERVALS.hour,
},
{
- label: __('7 days'),
+ label: getTimeLabel(7),
+ shortcut: '7_days',
duration: { seconds: 60 * 60 * 24 * 7 * 1 },
name: 'oneWeek',
interval: INTERVALS.day,
},
{
- label: __('30 days'),
+ label: getTimeLabel(30),
+ shortcut: '30_days',
duration: { seconds: 60 * 60 * 24 * 30 },
name: 'oneMonth',
interval: INTERVALS.day,
},
];
+/* eslint-enable @gitlab/require-i18n-strings */
export const defaultTimeRange = timeRanges.find((tr) => tr.default);
export const getTimeWindow = (timeWindowName) =>
diff --git a/app/assets/javascripts/vue_shared/issuable/list/components/issuable_item.vue b/app/assets/javascripts/vue_shared/issuable/list/components/issuable_item.vue
index b616b390032..38083327593 100644
--- a/app/assets/javascripts/vue_shared/issuable/list/components/issuable_item.vue
+++ b/app/assets/javascripts/vue_shared/issuable/list/components/issuable_item.vue
@@ -7,6 +7,7 @@ import { differenceInSeconds, getTimeago, SECONDS_IN_DAY } from '~/lib/utils/dat
import { isExternal, setUrlFragment } from '~/lib/utils/url_utility';
import { __, n__, sprintf } from '~/locale';
import IssuableAssignees from '~/issuable/components/issue_assignees.vue';
+import WorkItemTypeIcon from '~/work_items/components/work_item_type_icon.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago';
export default {
@@ -17,6 +18,7 @@ export default {
GlFormCheckbox,
GlSprintf,
IssuableAssignees,
+ WorkItemTypeIcon,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -50,6 +52,11 @@ export default {
required: false,
default: false,
},
+ showWorkItemTypeIcon: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
computed: {
issuableId() {
@@ -118,8 +125,8 @@ export default {
return sprintf(
n__(
- '%{completedCount} of %{count} task completed',
- '%{completedCount} of %{count} tasks completed',
+ '%{completedCount} of %{count} checklist item completed',
+ '%{completedCount} of %{count} checklist items completed',
count,
),
{ completedCount, count },
@@ -225,6 +232,7 @@ export default {
</span>
</div>
<div class="issuable-info">
+ <work-item-type-icon v-if="showWorkItemTypeIcon" :work-item-type="issuable.type" />
<slot v-if="hasSlotContents('reference')" name="reference"></slot>
<span v-else data-testid="issuable-reference" class="issuable-reference">
{{ reference }}
diff --git a/app/assets/javascripts/vue_shared/issuable/list/components/issuable_list_root.vue b/app/assets/javascripts/vue_shared/issuable/list/components/issuable_list_root.vue
index 189bbb56432..bc10f84b819 100644
--- a/app/assets/javascripts/vue_shared/issuable/list/components/issuable_list_root.vue
+++ b/app/assets/javascripts/vue_shared/issuable/list/components/issuable_list_root.vue
@@ -182,6 +182,11 @@ export default {
required: false,
default: false,
},
+ showWorkItemTypeIcon: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
@@ -344,6 +349,7 @@ export default {
:label-filter-param="labelFilterParam"
:show-checkbox="showBulkEditSidebar"
:checked="issuableChecked(issuable)"
+ :show-work-item-type-icon="showWorkItemTypeIcon"
@checked-input="handleIssuableCheckedInput(issuable, $event)"
>
<template #reference>
diff --git a/app/assets/javascripts/vue_shared/issuable/list/constants.js b/app/assets/javascripts/vue_shared/issuable/list/constants.js
index 507f333a34e..f6b864dfde0 100644
--- a/app/assets/javascripts/vue_shared/issuable/list/constants.js
+++ b/app/assets/javascripts/vue_shared/issuable/list/constants.js
@@ -46,13 +46,6 @@ export const AvailableSortOptions = [
},
];
-export const IssuableTypes = {
- Issue: 'ISSUE',
- Incident: 'INCIDENT',
- TestCase: 'TEST_CASE',
- Requirement: 'REQUIREMENT',
-};
-
export const DEFAULT_PAGE_SIZE = 20;
export const DEFAULT_SKELETON_COUNT = 5;
diff --git a/app/assets/javascripts/vue_shared/issuable/show/components/issuable_header.vue b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_header.vue
index cdc5903b934..1f23fdfaafd 100644
--- a/app/assets/javascripts/vue_shared/issuable/show/components/issuable_header.vue
+++ b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_header.vue
@@ -12,6 +12,7 @@ import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { isExternal } from '~/lib/utils/url_utility';
import { n__, sprintf } from '~/locale';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
+import WorkItemTypeIcon from '~/work_items/components/work_item_type_icon.vue';
import { IssuableStates } from '~/vue_shared/issuable/list/constants';
export default {
@@ -22,6 +23,7 @@ export default {
GlAvatarLink,
GlAvatarLabeled,
TimeAgoTooltip,
+ WorkItemTypeIcon,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -65,6 +67,16 @@ export default {
required: false,
default: null,
},
+ issuableType: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ showWorkItemTypeIcon: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
computed: {
badgeVariant() {
@@ -81,8 +93,8 @@ export default {
return sprintf(
n__(
- '%{completedCount} of %{count} task completed',
- '%{completedCount} of %{count} tasks completed',
+ '%{completedCount} of %{count} checklist item completed',
+ '%{completedCount} of %{count} checklist items completed',
count,
),
{ completedCount, count },
@@ -122,7 +134,13 @@ export default {
</div>
</div>
<span>
- {{ __('Created') }}
+ <template v-if="showWorkItemTypeIcon">
+ <work-item-type-icon :work-item-type="issuableType" show-text />
+ {{ __('created') }}
+ </template>
+ <template v-else>
+ {{ __('Created') }}
+ </template>
<time-ago-tooltip data-testid="startTimeItem" :time="createdAt" />
{{ __('by') }}
</span>
diff --git a/app/assets/javascripts/vue_shared/issuable/show/components/issuable_show_root.vue b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_show_root.vue
index 7ed93c042f8..2bc57ecba55 100644
--- a/app/assets/javascripts/vue_shared/issuable/show/components/issuable_show_root.vue
+++ b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_show_root.vue
@@ -87,6 +87,11 @@ export default {
required: false,
default: 0,
},
+ showWorkItemTypeIcon: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
methods: {
handleKeydownTitle(e, issuableMeta) {
@@ -110,6 +115,8 @@ export default {
:created-at="issuable.createdAt"
:author="issuable.author"
:task-completion-status="taskCompletionStatus"
+ :issuable-type="issuable.type"
+ :show-work-item-type-icon="showWorkItemTypeIcon"
>
<template #status-badge>
<slot name="status-badge"></slot>
diff --git a/app/assets/javascripts/vue_shared/new_namespace/new_namespace_page.vue b/app/assets/javascripts/vue_shared/new_namespace/new_namespace_page.vue
index 8e9b8ef3e6f..232749a2d01 100644
--- a/app/assets/javascripts/vue_shared/new_namespace/new_namespace_page.vue
+++ b/app/assets/javascripts/vue_shared/new_namespace/new_namespace_page.vue
@@ -125,7 +125,7 @@ export default {
<h4>{{ activePanel.title }}</h4>
<p v-if="hasTextDetails">{{ details }}</p>
- <component :is="details" v-else />
+ <component :is="details" v-else v-bind="activePanel.detailProps || {}" />
<slot name="extra-description"></slot>
</div>