diff options
Diffstat (limited to 'app/assets/javascripts/vue_shared')
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)}">"`; +export const generateHLJSOpenTag = (type, delimiter = '"') => + `<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">'rugged'</span>, <span class="hljs-string">'~> 0.24.0'</span>) + * + * Group 1 (method) : s.add_dependency( + * Group 2 (delimiter) : ' + * Group 3 (packageName): rugged + * Group 4 (closeTag) : '</span> + * Group 5 (rest) : , <span class="hljs-string">'~> 0.24.0'</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> |