diff options
Diffstat (limited to 'app/assets/javascripts/vue_shared/components')
42 files changed, 514 insertions, 279 deletions
diff --git a/app/assets/javascripts/vue_shared/components/awards_list.vue b/app/assets/javascripts/vue_shared/components/awards_list.vue index 848295cc984..c0a42e08dee 100644 --- a/app/assets/javascripts/vue_shared/components/awards_list.vue +++ b/app/assets/javascripts/vue_shared/components/awards_list.vue @@ -34,10 +34,21 @@ export default { required: false, default: '', }, + defaultAwards: { + type: Array, + required: false, + default: () => [], + }, }, computed: { + groupedDefaultAwards() { + return this.defaultAwards.reduce((obj, key) => Object.assign(obj, { [key]: [] }), {}); + }, groupedAwards() { - const { thumbsup, thumbsdown, ...rest } = groupBy(this.awards, x => x.name); + const { thumbsup, thumbsdown, ...rest } = { + ...this.groupedDefaultAwards, + ...groupBy(this.awards, x => x.name), + }; return [ ...(thumbsup ? [this.createAwardList('thumbsup', thumbsup)] : []), @@ -73,6 +84,10 @@ export default { }; }, getAwardListTitle(awardsList) { + if (!awardsList.length) { + return ''; + } + const hasReactionByCurrentUser = this.hasReactionByCurrentUser(awardsList); const TOOLTIP_NAME_COUNT = hasReactionByCurrentUser ? 9 : 10; let awardList = awardsList; diff --git a/app/assets/javascripts/vue_shared/components/blob_viewers/rich_viewer.vue b/app/assets/javascripts/vue_shared/components/blob_viewers/rich_viewer.vue index afbfb1e0ee2..52ce05f0d99 100644 --- a/app/assets/javascripts/vue_shared/components/blob_viewers/rich_viewer.vue +++ b/app/assets/javascripts/vue_shared/components/blob_viewers/rich_viewer.vue @@ -1,8 +1,12 @@ <script> +import MarkdownFieldView from '~/vue_shared/components/markdown/field_view.vue'; import ViewerMixin from './mixins'; import { handleBlobRichViewer } from '~/blob/viewer'; export default { + components: { + MarkdownFieldView, + }, mixins: [ViewerMixin], mounted() { handleBlobRichViewer(this.$refs.content, this.type); @@ -10,5 +14,5 @@ export default { }; </script> <template> - <div ref="content" v-html="content"></div> + <markdown-field-view ref="content" v-html="content" /> </template> diff --git a/app/assets/javascripts/vue_shared/components/blob_viewers/simple_viewer.vue b/app/assets/javascripts/vue_shared/components/blob_viewers/simple_viewer.vue index e64c7132117..1eb05780206 100644 --- a/app/assets/javascripts/vue_shared/components/blob_viewers/simple_viewer.vue +++ b/app/assets/javascripts/vue_shared/components/blob_viewers/simple_viewer.vue @@ -44,7 +44,8 @@ export default { </script> <template> <div - class="file-content code js-syntax-highlight qa-file-content" + class="file-content code js-syntax-highlight" + data-qa-selector="file_content" :class="$options.userColorScheme" > <div class="line-numbers"> diff --git a/app/assets/javascripts/vue_shared/components/ci_icon.vue b/app/assets/javascripts/vue_shared/components/ci_icon.vue index 162cfc02959..890dbe86c0d 100644 --- a/app/assets/javascripts/vue_shared/components/ci_icon.vue +++ b/app/assets/javascripts/vue_shared/components/ci_icon.vue @@ -1,5 +1,5 @@ <script> -import Icon from '../../vue_shared/components/icon.vue'; +import Icon from './icon.vue'; /** * Renders CI icon based on API response shared between all places where it is used. diff --git a/app/assets/javascripts/vue_shared/components/clipboard_button.vue b/app/assets/javascripts/vue_shared/components/clipboard_button.vue index d38dd258ce6..0234b6bf848 100644 --- a/app/assets/javascripts/vue_shared/components/clipboard_button.vue +++ b/app/assets/javascripts/vue_shared/components/clipboard_button.vue @@ -67,6 +67,7 @@ export default { <template> <gl-deprecated-button v-gl-tooltip="{ placement: tooltipPlacement, container: tooltipContainer }" + v-gl-tooltip.hover.blur :class="cssClass" :title="title" :data-clipboard-text="clipboardText" diff --git a/app/assets/javascripts/vue_shared/components/clone_dropdown.vue b/app/assets/javascripts/vue_shared/components/clone_dropdown.vue index 7826c179889..ac95c88225e 100644 --- a/app/assets/javascripts/vue_shared/components/clone_dropdown.vue +++ b/app/assets/javascripts/vue_shared/components/clone_dropdown.vue @@ -4,7 +4,6 @@ import { GlNewDropdownHeader, GlFormInputGroup, GlButton, - GlIcon, GlTooltipDirective, } from '@gitlab/ui'; import { __, sprintf } from '~/locale'; @@ -16,7 +15,6 @@ export default { GlNewDropdownHeader, GlFormInputGroup, GlButton, - GlIcon, }, directives: { GlTooltip: GlTooltipDirective, @@ -59,9 +57,10 @@ export default { v-gl-tooltip.hover :title="$options.copyURLTooltip" :data-clipboard-text="sshLink" - > - <gl-icon name="copy-to-clipboard" :title="$options.copyURLTooltip" /> - </gl-button> + data-qa-selector="copy_ssh_url_button" + icon="copy-to-clipboard" + class="d-inline-flex" + /> </template> </gl-form-input-group> </div> @@ -77,9 +76,10 @@ export default { v-gl-tooltip.hover :title="$options.copyURLTooltip" :data-clipboard-text="httpLink" - > - <gl-icon name="copy-to-clipboard" :title="$options.copyURLTooltip" /> - </gl-button> + data-qa-selector="copy_http_url_button" + icon="copy-to-clipboard" + class="d-inline-flex" + /> </template> </gl-form-input-group> </div> diff --git a/app/assets/javascripts/vue_shared/components/code_block.vue b/app/assets/javascripts/vue_shared/components/code_block.vue index 3cca7a86bef..1928bf6dac5 100644 --- a/app/assets/javascripts/vue_shared/components/code_block.vue +++ b/app/assets/javascripts/vue_shared/components/code_block.vue @@ -6,11 +6,26 @@ export default { type: String, required: true, }, + maxHeight: { + type: String, + required: false, + default: 'initial', + }, + }, + computed: { + styleObject() { + const { maxHeight } = this; + const isScrollable = maxHeight !== 'initial'; + const scrollableStyles = { + maxHeight, + overflowY: 'auto', + }; + + return isScrollable ? scrollableStyles : null; + }, }, }; </script> <template> - <pre class="code-block rounded"> - <code class="d-block">{{ code }}</code> - </pre> + <pre class="code-block rounded" :style="styleObject"><code class="d-block">{{ code }}</code></pre> </template> diff --git a/app/assets/javascripts/vue_shared/components/commit.vue b/app/assets/javascripts/vue_shared/components/commit.vue index 356f733fb8c..23bea6c28b4 100644 --- a/app/assets/javascripts/vue_shared/components/commit.vue +++ b/app/assets/javascripts/vue_shared/components/commit.vue @@ -4,7 +4,7 @@ import { GlTooltipDirective, GlLink } from '@gitlab/ui'; import { __, sprintf } from '~/locale'; import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue'; import UserAvatarLink from './user_avatar/user_avatar_link.vue'; -import Icon from '../../vue_shared/components/icon.vue'; +import Icon from './icon.vue'; export default { directives: { diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/content_viewer.vue b/app/assets/javascripts/vue_shared/components/content_viewer/content_viewer.vue index 2f5e5f35064..fe488ab6cfa 100644 --- a/app/assets/javascripts/vue_shared/components/content_viewer/content_viewer.vue +++ b/app/assets/javascripts/vue_shared/components/content_viewer/content_viewer.vue @@ -24,6 +24,11 @@ export default { required: false, default: '', }, + commitSha: { + type: String, + required: false, + default: '', + }, projectPath: { type: String, required: false, @@ -34,6 +39,11 @@ export default { required: false, default: '', }, + images: { + type: Object, + required: false, + default: () => ({}), + }, }, computed: { viewer() { @@ -62,6 +72,8 @@ export default { :file-size="fileSize" :project-path="projectPath" :content="content" + :images="images" + :commit-sha="commitSha" /> </div> </template> diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/lib/viewer_utils.js b/app/assets/javascripts/vue_shared/components/content_viewer/lib/viewer_utils.js index da0b45110e2..b7fa73bc197 100644 --- a/app/assets/javascripts/vue_shared/components/content_viewer/lib/viewer_utils.js +++ b/app/assets/javascripts/vue_shared/components/content_viewer/lib/viewer_utils.js @@ -26,8 +26,8 @@ const fileExtensionViewers = { export function viewerInformationForPath(path) { if (!path) return null; const name = path.split('/').pop(); - const viewerName = - fileNameViewers[name] || fileExtensionViewers[name ? name.split('.').pop() : ''] || ''; + const extension = name.includes('.') && name.split('.').pop(); + const viewerName = fileNameViewers[name] || fileExtensionViewers[extension]; return viewers[viewerName]; } diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue index eb3e489fb8c..1344c766e0e 100644 --- a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue +++ b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue @@ -1,8 +1,11 @@ <script> import $ from 'jquery'; +import '~/behaviors/markdown/render_gfm'; + import { GlSkeletonLoading } from '@gitlab/ui'; import axios from '~/lib/utils/axios_utils'; import { __ } from '~/locale'; +import { forEach, escape } from 'lodash'; const { CancelToken } = axios; let axiosSource; @@ -16,6 +19,11 @@ export default { type: String, required: true, }, + commitSha: { + type: String, + required: false, + default: '', + }, filePath: { type: String, required: false, @@ -25,6 +33,11 @@ export default { type: String, required: true, }, + images: { + type: Object, + required: false, + default: () => ({}), + }, }, data() { return { @@ -55,6 +68,9 @@ export default { text: this.content, path: this.filePath, }; + if (this.commitSha) { + postBody.ref = this.commitSha; + } const postOptions = { cancelToken: axiosSource.token, }; @@ -66,11 +82,19 @@ export default { postOptions, ) .then(({ data }) => { - this.previewContent = data.body; + let previewContent = data.body; + forEach(this.images, ({ src, title = '', alt }, key) => { + previewContent = previewContent.replace( + key, + `<img src="${escape(src)}" title="${escape(title)}" alt="${escape(alt)}">`, + ); + }); + + this.previewContent = previewContent; this.isLoading = false; this.$nextTick(() => { - $(this.$refs['markdown-preview']).renderGFM(); + $(this.$refs.markdownPreview).renderGFM(); }); }) .catch(() => { @@ -84,7 +108,7 @@ export default { </script> <template> - <div ref="markdown-preview" class="md-previewer"> + <div ref="markdownPreview" class="md-previewer"> <gl-skeleton-loading v-if="isLoading" /> <div v-else class="md" v-html="previewContent"></div> </div> diff --git a/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker.vue b/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker.vue index ffc616d7309..07748482204 100644 --- a/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker.vue +++ b/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker.vue @@ -169,15 +169,15 @@ export default { menu-class="date-time-picker-menu" toggle-class="date-time-picker-toggle text-truncate" > - <div class="d-flex justify-content-between gl-p-2"> + <div class="d-flex justify-content-between gl-p-2-deprecated-no-really-do-not-use-me"> <gl-form-group v-if="customEnabled" :label="__('Custom range')" label-for="custom-from-time" - label-class="gl-pb-1" - class="custom-time-range-form-group col-md-7 gl-pl-1 gl-pr-0 m-0" + label-class="gl-pb-1-deprecated-no-really-do-not-use-me" + class="custom-time-range-form-group col-md-7 gl-pl-1-deprecated-no-really-do-not-use-me gl-pr-0 m-0" > - <div class="gl-pt-2"> + <div class="gl-pt-2-deprecated-no-really-do-not-use-me"> <date-time-picker-input id="custom-time-from" v-model="startInput" @@ -198,14 +198,18 @@ export default { </gl-deprecated-button> </gl-form-group> </gl-form-group> - <gl-form-group label-for="group-id-dropdown" class="col-md-5 gl-pl-1 gl-pr-1 m-0"> + <gl-form-group + label-for="group-id-dropdown" + class="col-md-5 gl-pl-1-deprecated-no-really-do-not-use-me gl-pr-1-deprecated-no-really-do-not-use-me m-0" + > <template #label> - <span class="gl-pl-5">{{ __('Quick range') }}</span> + <span class="gl-pl-5-deprecated-no-really-do-not-use-me">{{ __('Quick range') }}</span> </template> <gl-dropdown-item v-for="(option, index) in options" :key="index" + data-qa-selector="quick_range_item" :active="isOptionActive(option)" active-class="active" @click="setQuickRange(option)" diff --git a/app/assets/javascripts/vue_shared/components/file_finder/item.vue b/app/assets/javascripts/vue_shared/components/file_finder/item.vue index 73511879ff2..018e3a84c39 100644 --- a/app/assets/javascripts/vue_shared/components/file_finder/item.vue +++ b/app/assets/javascripts/vue_shared/components/file_finder/item.vue @@ -1,8 +1,8 @@ <script> import fuzzaldrinPlus from 'fuzzaldrin-plus'; import Icon from '~/vue_shared/components/icon.vue'; -import FileIcon from '../../../vue_shared/components/file_icon.vue'; -import ChangedFileIcon from '../../../vue_shared/components/changed_file_icon.vue'; +import FileIcon from '../file_icon.vue'; +import ChangedFileIcon from '../changed_file_icon.vue'; const MAX_PATH_LENGTH = 60; diff --git a/app/assets/javascripts/vue_shared/components/file_icon/file_icon_map.js b/app/assets/javascripts/vue_shared/components/file_icon/file_icon_map.js index 2f6640232dd..9ecae87c1a9 100644 --- a/app/assets/javascripts/vue_shared/components/file_icon/file_icon_map.js +++ b/app/assets/javascripts/vue_shared/components/file_icon/file_icon_map.js @@ -493,6 +493,7 @@ const fileNameIcons = { '.npmignore': 'npm', '.npmrc': 'npm', '.yarnrc': 'yarn', + '.yarnrc.yml': 'yarn', 'yarn.lock': 'yarn', '.yarnclean': 'yarn', '.yarn-integrity': 'yarn', @@ -575,6 +576,7 @@ const fileNameIcons = { '.prettierrc.json': 'prettier', '.prettierrc.yaml': 'prettier', '.prettierrc.yml': 'prettier', + '.prettierignore': 'prettier', 'nodemon.json': 'nodemon', '.sonarrc': 'sonar', browserslist: 'browserlist', diff --git a/app/assets/javascripts/vue_shared/components/file_row.vue b/app/assets/javascripts/vue_shared/components/file_row.vue index 0a5cc7b693c..0cc96309a92 100644 --- a/app/assets/javascripts/vue_shared/components/file_row.vue +++ b/app/assets/javascripts/vue_shared/components/file_row.vue @@ -148,19 +148,6 @@ export default { cursor: pointer; } -.file-row:hover, -.file-row:focus { - background: #f2f2f2; -} - -.file-row:active { - background: #dfdfdf; -} - -.file-row.is-active { - background: #f2f2f2; -} - .file-row-name-container { display: flex; width: 100%; diff --git a/app/assets/javascripts/vue_shared/components/form/title.vue b/app/assets/javascripts/vue_shared/components/form/title.vue index fad69dc1e24..5d6633fa6d7 100644 --- a/app/assets/javascripts/vue_shared/components/form/title.vue +++ b/app/assets/javascripts/vue_shared/components/form/title.vue @@ -6,6 +6,7 @@ export default { GlFormInput, GlFormGroup, }, + inheritAttrs: false, }; </script> <template> diff --git a/app/assets/javascripts/vue_shared/components/gl_mentions.vue b/app/assets/javascripts/vue_shared/components/gl_mentions.vue index bbf293664a6..508f43afe61 100644 --- a/app/assets/javascripts/vue_shared/components/gl_mentions.vue +++ b/app/assets/javascripts/vue_shared/components/gl_mentions.vue @@ -34,7 +34,7 @@ function createMenuItemTemplate({ original }) { return `${avatarTag} ${original.username} - <small class="small font-weight-normal gl-color-inherit">${name}${count}</small> + <small class="small font-weight-normal gl-reset-color">${name}${count}</small> ${icon}`; } diff --git a/app/assets/javascripts/vue_shared/components/identicon.vue b/app/assets/javascripts/vue_shared/components/identicon.vue index 9dd61c8eada..87a995464fa 100644 --- a/app/assets/javascripts/vue_shared/components/identicon.vue +++ b/app/assets/javascripts/vue_shared/components/identicon.vue @@ -4,7 +4,7 @@ import { getIdenticonBackgroundClass, getIdenticonTitle } from '~/helpers/avatar export default { props: { entityId: { - type: Number, + type: [Number, String], required: true, }, entityName: { diff --git a/app/assets/javascripts/vue_shared/components/issue/issue_warning.vue b/app/assets/javascripts/vue_shared/components/issue/issue_warning.vue index 89a8595fc79..cb3cd18e5a7 100644 --- a/app/assets/javascripts/vue_shared/components/issue/issue_warning.vue +++ b/app/assets/javascripts/vue_shared/components/issue/issue_warning.vue @@ -1,11 +1,11 @@ <script> import { GlLink } from '@gitlab/ui'; -import { escape as esc } from 'lodash'; +import { escape } from 'lodash'; import { __, sprintf } from '~/locale'; -import icon from '../../../vue_shared/components/icon.vue'; +import icon from '../icon.vue'; function buildDocsLinkStart(path) { - return `<a href="${esc(path)}" target="_blank" rel="noopener noreferrer">`; + return `<a href="${escape(path)}" target="_blank" rel="noopener noreferrer">`; } export default { diff --git a/app/assets/javascripts/vue_shared/components/issue/related_issuable_item.vue b/app/assets/javascripts/vue_shared/components/issue/related_issuable_item.vue index 5d7e9557aff..4f1b1c758b2 100644 --- a/app/assets/javascripts/vue_shared/components/issue/related_issuable_item.vue +++ b/app/assets/javascripts/vue_shared/components/issue/related_issuable_item.vue @@ -1,9 +1,9 @@ <script> import '~/commons/bootstrap'; -import { GlTooltip, GlTooltipDirective } from '@gitlab/ui'; +import { GlIcon, GlTooltip, GlTooltipDirective } from '@gitlab/ui'; import { sprintf } from '~/locale'; -import IssueMilestone from '../../components/issue/issue_milestone.vue'; -import IssueAssignees from '../../components/issue/issue_assignees.vue'; +import IssueMilestone from './issue_milestone.vue'; +import IssueAssignees from './issue_assignees.vue'; import relatedIssuableMixin from '../../mixins/related_issuable_mixin'; import CiIcon from '../ci_icon.vue'; @@ -13,6 +13,7 @@ export default { IssueMilestone, IssueAssignees, CiIcon, + GlIcon, GlTooltip, }, directives: { @@ -44,6 +45,9 @@ export default { visibility: 'hidden', }; }, + iconClasses() { + return `${this.iconClass} ic-${this.iconName}`; + }, }, }; </script> @@ -54,30 +58,29 @@ export default { 'issuable-info-container': !canReorder, 'card-body': canReorder, }" - class="item-body d-flex align-items-center p-2 p-lg-3 py-xl-2 px-xl-3" + class="item-body d-flex align-items-center py-2 px-3" > <div class="item-contents d-flex align-items-center flex-wrap flex-grow-1 flex-xl-nowrap"> <!-- Title area: Status icon (XL) and title --> - <div class="item-title d-flex align-items-center mb-xl-0"> - <span ref="iconElementXL"> - <icon + <div class="item-title d-flex align-items-xl-center mb-xl-0"> + <div ref="iconElementXL"> + <gl-icon v-if="hasState" ref="iconElementXL" - :class="iconClass" + class="mr-2 d-block" + :class="iconClasses" :name="iconName" - :size="16" :title="stateTitle" :aria-label="state" /> - </span> + </div> <gl-tooltip :target="() => $refs.iconElementXL"> <span v-html="stateTitle"></span> </gl-tooltip> - <icon + <gl-icon v-if="confidential" v-gl-tooltip name="eye-slash" - :size="16" :title="__('Confidential')" class="confidential-icon append-right-4 align-self-baseline align-self-md-auto mt-xl-0" :aria-label="__('Confidential')" @@ -97,17 +100,6 @@ export default { <div class="item-path-area item-path-id d-flex align-items-center mr-2 mt-2 mt-xl-0 ml-xl-2" > - <span ref="iconElement"> - <icon - v-if="hasState" - :class="iconClass" - :name="iconName" - :title="stateTitle" - :aria-label="state" - data-html="true" - class="d-xl-none" - /> - </span> <gl-tooltip :target="() => this.$refs.iconElement"> <span v-html="stateTitle"></span> </gl-tooltip> @@ -159,7 +151,7 @@ export default { v-gl-tooltip :disabled="removeDisabled" type="button" - class="btn btn-default btn-svg btn-item-remove js-issue-item-remove-button mr-xl-0 align-self-xl-center" + class="btn btn-default btn-svg btn-item-remove js-issue-item-remove-button" data-qa-selector="remove_related_issue_button" :title="__('Remove')" :aria-label="__('Remove')" diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue index 26e878d56a0..8007ccb91d5 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/field.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue @@ -1,7 +1,7 @@ <script> import $ from 'jquery'; import '~/behaviors/markdown/render_gfm'; -import { unescape as unesc } from 'lodash'; +import { unescape } from 'lodash'; import { __, sprintf } from '~/locale'; import { stripHtml } from '~/lib/utils/text_utility'; import Flash from '../../../flash'; @@ -115,7 +115,7 @@ export default { return text; } - return unesc(stripHtml(richText).replace(/\n/g, '')); + return unescape(stripHtml(richText).replace(/\n/g, '')); } return ''; diff --git a/app/assets/javascripts/vue_shared/components/markdown/field_view.vue b/app/assets/javascripts/vue_shared/components/markdown/field_view.vue new file mode 100644 index 00000000000..d77123371f2 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/markdown/field_view.vue @@ -0,0 +1,19 @@ +<script> +import $ from 'jquery'; +import '~/behaviors/markdown/render_gfm'; + +export default { + mounted() { + this.renderGFM(); + }, + methods: { + renderGFM() { + $(this.$el).renderGFM(); + }, + }, +}; +</script> + +<template> + <div><slot></slot></div> +</template> diff --git a/app/assets/javascripts/vue_shared/components/recaptcha_eventhub.js b/app/assets/javascripts/vue_shared/components/recaptcha_eventhub.js index a4e004c3341..e193883b6e9 100644 --- a/app/assets/javascripts/vue_shared/components/recaptcha_eventhub.js +++ b/app/assets/javascripts/vue_shared/components/recaptcha_eventhub.js @@ -1,9 +1,9 @@ -import Vue from 'vue'; +import createEventHub from '~/helpers/event_hub_factory'; // see recaptcha_tags in app/views/shared/_recaptcha_form.html.haml export const callbackName = 'recaptchaDialogCallback'; -export const eventHub = new Vue(); +export const eventHub = createEventHub(); const throwDuplicateCallbackError = () => { throw new Error(`${callbackName} is already defined!`); diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/constants.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/constants.js new file mode 100644 index 00000000000..457f1806452 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/constants.js @@ -0,0 +1,37 @@ +import { __ } from '~/locale'; +import { generateToolbarItem } from './toolbar_service'; + +/* eslint-disable @gitlab/require-i18n-strings */ +const TOOLBAR_ITEM_CONFIGS = [ + { icon: 'heading', event: 'openHeadingSelect', classes: 'tui-heading', tooltip: __('Headings') }, + { icon: 'bold', command: 'Bold', tooltip: __('Add bold text') }, + { icon: 'italic', command: 'Italic', tooltip: __('Add italic text') }, + { icon: 'strikethrough', command: 'Strike', tooltip: __('Add strikethrough text') }, + { isDivider: true }, + { icon: 'quote', command: 'Blockquote', tooltip: __('Insert a quote') }, + { icon: 'link', event: 'openPopupAddLink', tooltip: __('Add a link') }, + { icon: 'doc-code', command: 'CodeBlock', tooltip: __('Insert a code block') }, + { isDivider: true }, + { icon: 'list-bulleted', command: 'UL', tooltip: __('Add a bullet list') }, + { icon: 'list-numbered', command: 'OL', tooltip: __('Add a numbered list') }, + { icon: 'list-task', command: 'Task', tooltip: __('Add a task list') }, + { icon: 'list-indent', command: 'Indent', tooltip: __('Indent') }, + { icon: 'list-outdent', command: 'Outdent', tooltip: __('Outdent') }, + { isDivider: true }, + { icon: 'dash', command: 'HR', tooltip: __('Add a line') }, + { icon: 'table', event: 'openPopupAddTable', classes: 'tui-table', tooltip: __('Add a table') }, + { isDivider: true }, + { icon: 'code', command: 'Code', tooltip: __('Insert inline code') }, +]; + +export const EDITOR_OPTIONS = { + toolbarItems: TOOLBAR_ITEM_CONFIGS.map(config => generateToolbarItem(config)), +}; + +export const EDITOR_TYPES = { + wysiwyg: 'wysiwyg', +}; + +export const EDITOR_HEIGHT = '100%'; + +export const EDITOR_PREVIEW_STYLE = 'horizontal'; diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/rich_content_editor.vue b/app/assets/javascripts/vue_shared/components/rich_content_editor/rich_content_editor.vue new file mode 100644 index 00000000000..ba3696c8ad1 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/rich_content_editor.vue @@ -0,0 +1,65 @@ +<script> +import 'codemirror/lib/codemirror.css'; +import '@toast-ui/editor/dist/toastui-editor.css'; + +import { EDITOR_OPTIONS, EDITOR_TYPES, EDITOR_HEIGHT, EDITOR_PREVIEW_STYLE } from './constants'; + +export default { + components: { + ToastEditor: () => + import(/* webpackChunkName: 'toast_editor' */ '@toast-ui/vue-editor').then( + toast => toast.Editor, + ), + }, + props: { + value: { + type: String, + required: true, + }, + options: { + type: Object, + required: false, + default: () => EDITOR_OPTIONS, + }, + initialEditType: { + type: String, + required: false, + default: EDITOR_TYPES.wysiwyg, + }, + height: { + type: String, + required: false, + default: EDITOR_HEIGHT, + }, + previewStyle: { + type: String, + required: false, + default: EDITOR_PREVIEW_STYLE, + }, + }, + computed: { + editorOptions() { + return { ...EDITOR_OPTIONS, ...this.options }; + }, + }, + methods: { + onContentChanged() { + this.$emit('input', this.getMarkdown()); + }, + getMarkdown() { + return this.$refs.editor.invoke('getMarkdown'); + }, + }, +}; +</script> +<template> + <toast-editor + ref="editor" + :initial-value="value" + :options="editorOptions" + :preview-style="previewStyle" + :initial-edit-type="initialEditType" + :height="height" + @change="onContentChanged" + /> +</template> diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/toolbar_item.vue b/app/assets/javascripts/vue_shared/components/rich_content_editor/toolbar_item.vue new file mode 100644 index 00000000000..58aaeef45f2 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/toolbar_item.vue @@ -0,0 +1,20 @@ +<script> +import { GlIcon } from '@gitlab/ui'; + +export default { + components: { + GlIcon, + }, + props: { + icon: { + type: String, + required: true, + }, + }, +}; +</script> +<template> + <button class="p-0 gl-display-flex toolbar-button"> + <gl-icon class="gl-mx-auto" :name="icon" /> + </button> +</template> diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/toolbar_service.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/toolbar_service.js new file mode 100644 index 00000000000..fff90f3e3fb --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/toolbar_service.js @@ -0,0 +1,32 @@ +import Vue from 'vue'; +import ToolbarItem from './toolbar_item.vue'; + +const buildWrapper = propsData => { + const instance = new Vue({ + render(createElement) { + return createElement(ToolbarItem, propsData); + }, + }); + + instance.$mount(); + return instance.$el; +}; + +// eslint-disable-next-line import/prefer-default-export +export const generateToolbarItem = config => { + const { icon, classes, event, command, tooltip, isDivider } = config; + + if (isDivider) { + return 'divider'; + } + + return { + type: 'button', + options: { + el: buildWrapper({ props: { icon }, class: classes }), + event, + command, + tooltip, + }, + }; +}; diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue index 44cc11a6aaa..5eef439aa90 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue @@ -80,11 +80,6 @@ export default { required: false, default: false, }, - scopedLabelsDocumentationLink: { - type: String, - required: false, - default: '#', - }, }, computed: { hiddenInputName() { @@ -136,7 +131,6 @@ export default { <dropdown-value :labels="context.labels" :label-filter-base-path="labelFilterBasePath" - :scoped-labels-documentation-link="scopedLabelsDocumentationLink" :enable-scoped-labels="enableScopedLabels" > <slot></slot> @@ -157,7 +151,6 @@ export default { :namespace="namespace" :labels="context.labels" :show-extra-options="!showCreate" - :scoped-labels-documentation-link="scopedLabelsDocumentationLink" :enable-scoped-labels="enableScopedLabels" /> <div diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button.vue index c3bc61d0053..30f7e6a5980 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button.vue @@ -36,11 +36,6 @@ export default { required: false, default: false, }, - scopedLabelsDocumentationLink: { - type: String, - required: false, - default: '#', - }, }, computed: { dropdownToggleText() { @@ -72,7 +67,6 @@ export default { :data-namespace-path="namespace" :data-show-any="showExtraOptions" :data-scoped-labels="enableScopedLabels" - :data-scoped-labels-documentation-link="scopedLabelsDocumentationLink" type="button" class="dropdown-menu-toggle wide js-label-select js-multiselect js-context-config-modal" data-toggle="dropdown" diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value.vue index fe43f77b1ee..71d7069dd57 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value.vue @@ -20,11 +20,6 @@ export default { required: false, default: false, }, - scopedLabelsDocumentationLink: { - type: String, - required: false, - default: '#', - }, }, computed: { isEmpty() { @@ -64,7 +59,6 @@ export default { :title="label.title" :description="label.description" :scoped="showScopedLabels(label)" - :scoped-labels-documentation-link="scopedLabelsDocumentationLink" /> </template> </div> diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/constants.js b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/constants.js new file mode 100644 index 00000000000..ab652c9356a --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/constants.js @@ -0,0 +1,5 @@ +// eslint-disable-next-line import/prefer-default-export +export const DropdownVariant = { + Sidebar: 'sidebar', + Standalone: 'standalone', +}; diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_button.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_button.vue index 55fa1e4ef9c..f45c14f8344 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_button.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_button.vue @@ -1,21 +1,35 @@ <script> -import { mapGetters } from 'vuex'; -import { GlDeprecatedButton, GlIcon } from '@gitlab/ui'; +import { mapActions, mapGetters } from 'vuex'; +import { GlButton, GlIcon } from '@gitlab/ui'; export default { components: { - GlDeprecatedButton, + GlButton, GlIcon, }, computed: { - ...mapGetters(['dropdownButtonText']), + ...mapGetters(['dropdownButtonText', 'isDropdownVariantStandalone']), + }, + methods: { + ...mapActions(['toggleDropdownContents']), + handleButtonClick(e) { + if (this.isDropdownVariantStandalone) { + this.toggleDropdownContents(); + e.stopPropagation(); + } + }, }, }; </script> <template> - <gl-deprecated-button class="labels-select-dropdown-button w-100 text-left"> - <span class="dropdown-toggle-text">{{ dropdownButtonText }}</span> + <gl-button + class="labels-select-dropdown-button js-dropdown-button w-100 text-left" + @click="handleButtonClick" + > + <span class="dropdown-toggle-text flex-fill"> + {{ dropdownButtonText }} + </span> <gl-icon name="chevron-down" class="pull-right" /> - </gl-deprecated-button> + </gl-button> </template> diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view.vue index 6bb77f6b6f3..ba8d8391952 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view.vue @@ -1,18 +1,10 @@ <script> import { mapState, mapActions } from 'vuex'; -import { - GlTooltipDirective, - GlDeprecatedButton, - GlIcon, - GlFormInput, - GlLink, - GlLoadingIcon, -} from '@gitlab/ui'; +import { GlTooltipDirective, GlButton, GlFormInput, GlLink, GlLoadingIcon } from '@gitlab/ui'; export default { components: { - GlDeprecatedButton, - GlIcon, + GlButton, GlFormInput, GlLink, GlLoadingIcon, @@ -60,25 +52,23 @@ export default { <template> <div class="labels-select-contents-create js-labels-create"> <div class="dropdown-title d-flex align-items-center pt-0 pb-2"> - <gl-deprecated-button + <gl-button :aria-label="__('Go back')" variant="link" - size="sm" + size="small" class="js-btn-back dropdown-header-button p-0" + icon="arrow-left" @click="toggleDropdownContentsCreateView" - > - <gl-icon name="arrow-left" /> - </gl-deprecated-button> + /> <span class="flex-grow-1">{{ labelsCreateTitle }}</span> - <gl-deprecated-button + <gl-button :aria-label="__('Close')" variant="link" - size="sm" + size="small" class="dropdown-header-button p-0" + icon="close" @click="toggleDropdownContents" - > - <gl-icon name="close" /> - </gl-deprecated-button> + /> </div> <div class="dropdown-input"> <gl-form-input @@ -107,21 +97,19 @@ export default { </div> </div> <div class="dropdown-actions clearfix pt-2 px-2"> - <gl-deprecated-button + <gl-button :disabled="disableCreate" - variant="primary" + category="primary" + variant="success" class="pull-left d-flex align-items-center" @click="handleCreateClick" > <gl-loading-icon v-show="labelCreateInProgress" :inline="true" class="mr-1" /> {{ __('Create') }} - </gl-deprecated-button> - <gl-deprecated-button - class="pull-right js-btn-cancel-create" - @click="toggleDropdownContentsCreateView" - > + </gl-button> + <gl-button class="pull-right js-btn-cancel-create" @click="toggleDropdownContentsCreateView"> {{ __('Cancel') }} - </gl-deprecated-button> + </gl-button> </div> </div> </template> diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view.vue index a8e48bfe1a1..1ef2e8b3bed 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view.vue @@ -1,16 +1,18 @@ <script> import { mapState, mapGetters, mapActions } from 'vuex'; -import { GlLoadingIcon, GlDeprecatedButton, GlIcon, GlSearchBoxByType, GlLink } from '@gitlab/ui'; +import { GlLoadingIcon, GlButton, GlSearchBoxByType, GlLink } from '@gitlab/ui'; import { UP_KEY_CODE, DOWN_KEY_CODE, ENTER_KEY_CODE, ESC_KEY_CODE } from '~/lib/utils/keycodes'; +import LabelItem from './label_item.vue'; + export default { components: { GlLoadingIcon, - GlDeprecatedButton, - GlIcon, + GlButton, GlSearchBoxByType, GlLink, + LabelItem, }, data() { return { @@ -20,6 +22,8 @@ export default { }, computed: { ...mapState([ + 'allowLabelCreate', + 'allowMultiselect', 'labelsManagePath', 'labels', 'labelsFetchInProgress', @@ -27,7 +31,7 @@ export default { 'footerCreateLabelTitle', 'footerManageLabelTitle', ]), - ...mapGetters(['selectedLabelsList']), + ...mapGetters(['selectedLabelsList', 'isDropdownVariantSidebar']), visibleLabels() { if (this.searchKey) { return this.labels.filter(label => @@ -56,12 +60,8 @@ export default { 'toggleDropdownContentsCreateView', 'fetchLabels', 'updateSelectedLabels', + 'toggleDropdownContents', ]), - getDropdownLabelBoxStyle(label) { - return { - backgroundColor: label.color, - }; - }, isLabelSelected(label) { return this.selectedLabelsList.includes(label.id); }, @@ -111,6 +111,7 @@ export default { }, handleLabelClick(label) { this.updateSelectedLabels([label]); + if (!this.allowMultiselect) this.toggleDropdownContents(); }, }, }; @@ -123,54 +124,47 @@ export default { class="labels-fetch-loading position-absolute d-flex align-items-center w-100 h-100" size="md" /> - <div class="dropdown-title d-flex align-items-center pt-0 pb-2"> + <div v-if="isDropdownVariantSidebar" class="dropdown-title d-flex align-items-center pt-0 pb-2"> <span class="flex-grow-1">{{ labelsListTitle }}</span> - <gl-deprecated-button + <gl-button :aria-label="__('Close')" variant="link" - size="sm" + size="small" class="dropdown-header-button p-0" + icon="close" @click="toggleDropdownContents" - > - <gl-icon name="close" /> - </gl-deprecated-button> + /> </div> - <div class="dropdown-input"> + <div class="dropdown-input" @click.stop="() => {}"> <gl-search-box-by-type v-model="searchKey" :autofocus="true" /> </div> - <div v-if="!labelsFetchInProgress" ref="labelsListContainer" class="dropdown-content"> + <div v-show="!labelsFetchInProgress" ref="labelsListContainer" class="dropdown-content"> <ul class="list-unstyled mb-0"> <li v-for="(label, index) in visibleLabels" :key="label.id" class="d-block text-left"> - <gl-link - class="d-flex align-items-baseline text-break-word label-item" - :class="{ 'is-focused': index === currentHighlightItem }" - @click="handleLabelClick(label)" - > - <gl-icon v-show="label.set" name="mobile-issue-close" class="mr-2 align-self-center" /> - <span v-show="!label.set" class="mr-3 pr-2"></span> - <span class="dropdown-label-box" :style="getDropdownLabelBoxStyle(label)"></span> - <span>{{ label.title }}</span> - </gl-link> + <label-item + :label="label" + :highlight="index === currentHighlightItem" + @clickLabel="handleLabelClick(label)" + /> </li> - <li v-if="!visibleLabels.length" class="p-2 text-center"> + <li v-show="!visibleLabels.length" class="p-2 text-center"> {{ __('No matching results') }} </li> </ul> </div> - <div class="dropdown-footer"> + <div v-if="isDropdownVariantSidebar" class="dropdown-footer"> <ul class="list-unstyled"> - <li> - <gl-deprecated-button - variant="link" + <li v-if="allowLabelCreate"> + <gl-link class="d-flex w-100 flex-row text-break-word label-item" @click="toggleDropdownContentsCreateView" - >{{ footerCreateLabelTitle }}</gl-deprecated-button + >{{ footerCreateLabelTitle }}</gl-link > </li> <li> - <gl-link :href="labelsManagePath" class="d-flex flex-row text-break-word label-item"> - {{ footerManageLabelTitle }} - </gl-link> + <gl-link :href="labelsManagePath" class="d-flex flex-row text-break-word label-item">{{ + footerManageLabelTitle + }}</gl-link> </li> </ul> </div> diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_value.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_value.vue index 695af775750..12ad2acf308 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_value.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_value.vue @@ -9,12 +9,7 @@ export default { GlLabel, }, computed: { - ...mapState([ - 'selectedLabels', - 'allowScopedLabels', - 'labelsFilterBasePath', - 'scopedLabelsDocumentationPath', - ]), + ...mapState(['selectedLabels', 'allowScopedLabels', 'labelsFilterBasePath']), }, methods: { labelFilterUrl(label) { @@ -45,7 +40,6 @@ export default { :background-color="label.color" :target="labelFilterUrl(label)" :scoped="scopedLabel(label)" - :scoped-labels-documentation-link="scopedLabelsDocumentationPath" tooltip-placement="top" /> </template> diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/label_item.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/label_item.vue new file mode 100644 index 00000000000..c95221d71b5 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/label_item.vue @@ -0,0 +1,52 @@ +<script> +import { GlIcon, GlLink } from '@gitlab/ui'; + +export default { + components: { + GlIcon, + GlLink, + }, + props: { + label: { + type: Object, + required: true, + }, + highlight: { + type: Boolean, + required: false, + default: false, + }, + }, + data() { + return { + isSet: this.label.set, + }; + }, + computed: { + labelBoxStyle() { + return { + backgroundColor: this.label.color, + }; + }, + }, + methods: { + handleClick() { + this.isSet = !this.isSet; + this.$emit('clickLabel', this.label); + }, + }, +}; +</script> + +<template> + <gl-link + class="d-flex align-items-baseline text-break-word label-item" + :class="{ 'is-focused': highlight }" + @click="handleClick" + > + <gl-icon v-show="isSet" name="mobile-issue-close" class="mr-2 align-self-center" /> + <span v-show="!isSet" data-testid="no-icon" class="mr-3 pr-2"></span> + <span class="dropdown-label-box" data-testid="label-color-box" :style="labelBoxStyle"></span> + <span>{{ label.title }}</span> + </gl-link> +</template> diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue index 78102caacf5..f38b66fdfdf 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue @@ -1,7 +1,7 @@ <script> import $ from 'jquery'; import Vue from 'vue'; -import Vuex, { mapState, mapActions } from 'vuex'; +import Vuex, { mapState, mapActions, mapGetters } from 'vuex'; import { __ } from '~/locale'; import DropdownValueCollapsed from '~/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue'; @@ -13,6 +13,8 @@ import DropdownValue from './dropdown_value.vue'; import DropdownButton from './dropdown_button.vue'; import DropdownContents from './dropdown_contents.vue'; +import { DropdownVariant } from './constants'; + Vue.use(Vuex); export default { @@ -33,14 +35,19 @@ export default { type: Boolean, required: true, }, + allowMultiselect: { + type: Boolean, + required: false, + default: false, + }, allowScopedLabels: { type: Boolean, required: true, }, - dropdownOnly: { - type: Boolean, + variant: { + type: String, required: false, - default: false, + default: DropdownVariant.Sidebar, }, selectedLabels: { type: Array, @@ -67,11 +74,6 @@ export default { required: false, default: '', }, - scopedLabelsDocumentationPath: { - type: String, - required: false, - default: '', - }, labelsListTitle: { type: String, required: false, @@ -95,6 +97,10 @@ export default { }, computed: { ...mapState(['showDropdownButton', 'showDropdownContents']), + ...mapGetters(['isDropdownVariantSidebar', 'isDropdownVariantStandalone']), + dropdownButtonVisible() { + return this.isDropdownVariantSidebar ? this.showDropdownButton : true; + }, }, watch: { selectedLabels(selectedLabels) { @@ -105,15 +111,15 @@ export default { }, mounted() { this.setInitialState({ - dropdownOnly: this.dropdownOnly, + variant: this.variant, allowLabelEdit: this.allowLabelEdit, allowLabelCreate: this.allowLabelCreate, + allowMultiselect: this.allowMultiselect, allowScopedLabels: this.allowScopedLabels, selectedLabels: this.selectedLabels, labelsFetchPath: this.labelsFetchPath, labelsManagePath: this.labelsManagePath, labelsFilterBasePath: this.labelsFilterBasePath, - scopedLabelsDocumentationPath: this.scopedLabelsDocumentationPath, labelsListTitle: this.labelsListTitle, labelsCreateTitle: this.labelsCreateTitle, footerCreateLabelTitle: this.footerCreateLabelTitle, @@ -154,13 +160,24 @@ export default { // as the dropdown wrapper is not using `GlDropdown` as // it will also require us to use `BDropdownForm` // which is yet to be implemented in GitLab UI. + const hasExceptionClass = [ + 'js-dropdown-button', + 'js-btn-cancel-create', + 'js-sidebar-dropdown-toggle', + ].some( + className => + target?.classList.contains(className) || + target?.parentElement.classList.contains(className), + ); + + const hadExceptionParent = ['.js-btn-back', '.js-labels-list'].some( + className => $(target).parents(className).length, + ); + if ( - this.showDropdownButton && this.showDropdownContents && - !$(target).parents('.js-btn-back').length && - !$(target).parents('.js-labels-list').length && - !target?.classList.contains('js-btn-cancel-create') && - !target?.classList.contains('js-sidebar-dropdown-toggle') && + !hadExceptionParent && + !hasExceptionClass && !this.$refs.dropdownButtonCollapsed?.$el.contains(target) && !this.$refs.dropdownContents?.$el.contains(target) ) { @@ -181,10 +198,12 @@ export default { </script> <template> - <div class="labels-select-wrapper position-relative"> - <div v-if="!dropdownOnly"> + <div + class="labels-select-wrapper position-relative" + :class="{ 'is-standalone': isDropdownVariantStandalone }" + > + <template v-if="isDropdownVariantSidebar"> <dropdown-value-collapsed - v-if="allowLabelCreate" ref="dropdownButtonCollapsed" :labels="selectedLabels" @onValueClick="handleCollapsedValueClick" @@ -196,8 +215,18 @@ export default { <dropdown-value v-show="!showDropdownButton"> <slot></slot> </dropdown-value> - <dropdown-button v-show="showDropdownButton" /> - <dropdown-contents v-if="showDropdownButton && showDropdownContents" ref="dropdownContents" /> - </div> + <dropdown-button v-show="dropdownButtonVisible" /> + <dropdown-contents + v-if="dropdownButtonVisible && showDropdownContents" + ref="dropdownContents" + /> + </template> + <template v-if="isDropdownVariantStandalone"> + <dropdown-button v-show="dropdownButtonVisible" /> + <dropdown-contents + v-if="dropdownButtonVisible && showDropdownContents" + ref="dropdownContents" + /> + </template> </div> </template> diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/getters.js b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/getters.js index c08a8a8ea58..c39222959a9 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/getters.js +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/getters.js @@ -1,4 +1,5 @@ import { __, s__, sprintf } from '~/locale'; +import { DropdownVariant } from '../constants'; /** * Returns string representing current labels @@ -6,8 +7,11 @@ import { __, s__, sprintf } from '~/locale'; * * @param {object} state */ -export const dropdownButtonText = state => { - const selectedLabels = state.labels.filter(label => label.set); +export const dropdownButtonText = (state, getters) => { + const selectedLabels = getters.isDropdownVariantSidebar + ? state.labels.filter(label => label.set) + : state.selectedLabels; + if (!selectedLabels.length) { return __('Label'); } else if (selectedLabels.length > 1) { @@ -26,5 +30,19 @@ export const dropdownButtonText = state => { */ export const selectedLabelsList = state => state.selectedLabels.map(label => label.id); +/** + * Returns boolean representing whether dropdown variant + * is `sidebar` + * @param {object} state + */ +export const isDropdownVariantSidebar = state => state.variant === DropdownVariant.Sidebar; + +/** + * Returns boolean representing whether dropdown variant + * is `standalone` + * @param {object} state + */ +export const isDropdownVariantStandalone = state => state.variant === DropdownVariant.Standalone; + // prevent babel-plugin-rewire from generating an invalid default during karma tests export default () => {}; diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutations.js b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutations.js index 32a78507e88..54f8c78b4e1 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutations.js +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutations.js @@ -1,4 +1,5 @@ import * as types from './mutation_types'; +import { DropdownVariant } from '../constants'; export default { [types.SET_INITIAL_STATE](state, props) { @@ -10,7 +11,7 @@ export default { }, [types.TOGGLE_DROPDOWN_CONTENTS](state) { - if (!state.dropdownOnly) { + if (state.variant === DropdownVariant.Sidebar) { state.showDropdownButton = !state.showDropdownButton; } state.showDropdownContents = !state.showDropdownContents; @@ -57,20 +58,13 @@ export default { }, [types.UPDATE_SELECTED_LABELS](state, { labels }) { - // Iterate over all the labels and update - // `set` prop value to represent their current state. - const labelIds = labels.map(label => label.id); - state.labels = state.labels.reduce((allLabels, label) => { - if (labelIds.includes(label.id)) { - allLabels.push({ - ...label, - touched: true, - set: !label.set, - }); - } else { - allLabels.push(label); - } - return allLabels; - }, []); + // Find the label to update from all the labels + // and change `set` prop value to represent their current state. + const labelId = labels.pop()?.id; + const candidateLabel = state.labels.find(label => labelId === label.id); + if (candidateLabel) { + candidateLabel.touched = true; + candidateLabel.set = !candidateLabel.set; + } }, }; diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/state.js b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/state.js index ceabc696693..6a6c0b4c0ee 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/state.js +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/state.js @@ -11,13 +11,13 @@ export default () => ({ namespace: '', labelsFetchPath: '', labelsFilterBasePath: '', - scopedLabelsDocumentationPath: '#', // UI Flags + variant: '', allowLabelCreate: false, allowLabelEdit: false, allowScopedLabels: false, - dropdownOnly: false, + allowMultiselect: false, showDropdownButton: false, showDropdownContents: false, showDropdownContentsCreateView: false, diff --git a/app/assets/javascripts/vue_shared/components/user_avatar/badges/gitlab_team_member_badge.vue b/app/assets/javascripts/vue_shared/components/user_avatar/badges/gitlab_team_member_badge.vue deleted file mode 100644 index 527cbd458e2..00000000000 --- a/app/assets/javascripts/vue_shared/components/user_avatar/badges/gitlab_team_member_badge.vue +++ /dev/null @@ -1,27 +0,0 @@ -<script> -import { GlTooltipDirective, GlIcon } from '@gitlab/ui'; -import { __ } from '~/locale'; - -const GITLAB_TEAM_MEMBER_LABEL = __('GitLab Team Member'); - -export default { - name: 'GitlabTeamMemberBadge', - directives: { - GlTooltip: GlTooltipDirective, - }, - components: { GlIcon }, - gitlabTeamMemberLabel: GITLAB_TEAM_MEMBER_LABEL, -}; -</script> - -<template> - <span - v-gl-tooltip.hover - :title="$options.gitlabTeamMemberLabel" - role="img" - :aria-label="$options.gitlabTeamMemberLabel" - class="d-inline-block align-middle" - > - <gl-icon name="tanuki-verified" class="gl-text-purple d-block" /> - </span> -</template> diff --git a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_svg.vue b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_svg.vue deleted file mode 100644 index 7ed4da84120..00000000000 --- a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_svg.vue +++ /dev/null @@ -1,38 +0,0 @@ -<script> -/* This is a re-usable vue component for rendering a user avatar svg (typically - for a blank state). It will receive styles comparable to the user avatar, - but no image is loaded, it isn't wrapped in a link, and tooltips aren't supported. - The svg and avatar size can be configured by props passed to this component. - - Sample configuration: - - <user-avatar-svg - :svg="potentialApproverSvg" - :size="20" - /> - -*/ - -export default { - props: { - svg: { - type: String, - required: true, - }, - size: { - type: Number, - required: false, - default: 20, - }, - }, - computed: { - avatarSizeClass() { - return `s${this.size}`; - }, - }, -}; -</script> - -<template> - <svg :class="avatarSizeClass" :height="size" :width="size" v-html="svg" /> -</template> |