diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-06-20 14:10:13 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-06-20 14:10:13 +0300 |
commit | 0ea3fcec397b69815975647f5e2aa5fe944a8486 (patch) | |
tree | 7979381b89d26011bcf9bdc989a40fcc2f1ed4ff /app/assets/javascripts/vue_shared/components | |
parent | 72123183a20411a36d607d70b12d57c484394c8e (diff) |
Add latest changes from gitlab-org/gitlab@15-1-stable-eev15.1.0-rc42
Diffstat (limited to 'app/assets/javascripts/vue_shared/components')
51 files changed, 958 insertions, 218 deletions
diff --git a/app/assets/javascripts/vue_shared/components/ci_icon.vue b/app/assets/javascripts/vue_shared/components/ci_icon.vue index 9bccc49e894..8bffc2479a1 100644 --- a/app/assets/javascripts/vue_shared/components/ci_icon.vue +++ b/app/assets/javascripts/vue_shared/components/ci_icon.vue @@ -45,7 +45,12 @@ export default { return validSizes.includes(value); }, }, - borderless: { + isActive: { + type: Boolean, + required: false, + default: false, + }, + isBorderless: { type: Boolean, required: false, default: false, @@ -67,15 +72,19 @@ export default { return `ci-status-icon ci-status-icon-${status} js-ci-status-icon-${status} gl-rounded-full gl-justify-content-center`; }, icon() { - return this.borderless ? `${this.status.icon}_borderless` : this.status.icon; + return this.isBorderless ? `${this.status.icon}_borderless` : this.status.icon; }, }, }; </script> <template> <span - :class="[wrapperStyleClasses, { interactive: isInteractive }]" + :class="[ + wrapperStyleClasses, + { interactive: isInteractive, active: isActive, borderless: isBorderless }, + ]" :style="{ height: `${size}px`, width: `${size}px` }" + data-testid="ci-icon-wrapper" > <gl-icon :name="icon" :size="size" :class="cssClasses" :aria-label="status.icon" /> </span> diff --git a/app/assets/javascripts/vue_shared/components/clone_dropdown.vue b/app/assets/javascripts/vue_shared/components/clone_dropdown.vue index f14e1992901..dd6923d9fcd 100644 --- a/app/assets/javascripts/vue_shared/components/clone_dropdown.vue +++ b/app/assets/javascripts/vue_shared/components/clone_dropdown.vue @@ -45,7 +45,7 @@ export default { }; </script> <template> - <gl-dropdown right :text="$options.labels.defaultLabel" category="primary" variant="info"> + <gl-dropdown right :text="$options.labels.defaultLabel" category="primary" variant="confirm"> <div class="pb-2 mx-1"> <template v-if="sshLink"> <gl-dropdown-section-header>{{ $options.labels.ssh }}</gl-dropdown-section-header> diff --git a/app/assets/javascripts/vue_shared/components/color_select_dropdown/color_item.vue b/app/assets/javascripts/vue_shared/components/color_select_dropdown/color_item.vue new file mode 100644 index 00000000000..92817d5fa70 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/color_select_dropdown/color_item.vue @@ -0,0 +1,25 @@ +<script> +export default { + props: { + color: { + type: String, + required: true, + }, + title: { + type: String, + required: true, + }, + }, +}; +</script> + +<template> + <div> + <span + class="dropdown-label-box gl-flex-shrink-0 gl-top-1 gl-mr-0" + data-testid="color-item" + :style="{ backgroundColor: color }" + ></span> + <span class="hide-collapsed">{{ title }}</span> + </div> +</template> diff --git a/app/assets/javascripts/vue_shared/components/color_select_dropdown/color_select_root.vue b/app/assets/javascripts/vue_shared/components/color_select_dropdown/color_select_root.vue new file mode 100644 index 00000000000..6b79883d76b --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/color_select_dropdown/color_select_root.vue @@ -0,0 +1,214 @@ +<script> +import createFlash from '~/flash'; +import { s__ } from '~/locale'; +import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue'; +import { DEFAULT_COLOR, COLOR_WIDGET_COLOR, DROPDOWN_VARIANT, ISSUABLE_COLORS } from './constants'; +import DropdownContents from './dropdown_contents.vue'; +import DropdownValue from './dropdown_value.vue'; +import { isDropdownVariantSidebar, isDropdownVariantEmbedded } from './utils'; +import epicColorQuery from './graphql/epic_color.query.graphql'; +import updateEpicColorMutation from './graphql/epic_update_color.mutation.graphql'; + +export default { + i18n: { + assignColor: s__('ColorWidget|Assign epic color'), + dropdownButtonText: COLOR_WIDGET_COLOR, + fetchingError: s__('ColorWidget|Error fetching epic color.'), + updatingError: s__('ColorWidget|An error occurred while updating color.'), + widgetTitle: COLOR_WIDGET_COLOR, + }, + components: { + DropdownValue, + DropdownContents, + SidebarEditableItem, + }, + props: { + allowEdit: { + type: Boolean, + required: false, + default: false, + }, + iid: { + type: String, + required: false, + default: '', + }, + fullPath: { + type: String, + required: true, + }, + variant: { + type: String, + required: false, + default: DROPDOWN_VARIANT.Sidebar, + }, + dropdownButtonText: { + type: String, + required: false, + default: COLOR_WIDGET_COLOR, + }, + dropdownTitle: { + type: String, + required: false, + default: s__('ColorWidget|Assign epic color'), + }, + }, + data() { + return { + issuableColor: { + color: '', + title: '', + }, + colorUpdateInProgress: false, + oldIid: null, + sidebarExpandedOnClick: false, + }; + }, + apollo: { + issuableColor: { + query: epicColorQuery, + skip() { + return !isDropdownVariantSidebar(this.variant); + }, + variables() { + return { + iid: this.iid, + fullPath: this.fullPath, + }; + }, + update(data) { + const issuableColor = data.workspace?.issuable?.color; + + if (issuableColor) { + return ISSUABLE_COLORS.find((color) => color.color === issuableColor) ?? DEFAULT_COLOR; + } + + return DEFAULT_COLOR; + }, + error() { + createFlash({ + message: this.$options.i18n.fetchingError, + captureError: true, + }); + }, + }, + }, + computed: { + isLoading() { + return this.colorUpdateInProgress || this.$apollo.queries.issuableColor.loading; + }, + }, + watch: { + iid(_, oldVal) { + this.oldIid = oldVal; + }, + }, + methods: { + handleDropdownClose(color) { + if (this.iid !== '') { + this.updateSelectedColor(this.getUpdateVariables(color)); + } else { + this.$emit('updateSelectedColor', color); + } + + this.collapseEditableItem(); + }, + collapseEditableItem() { + this.$refs.editable?.collapse(); + if (this.sidebarExpandedOnClick) { + this.sidebarExpandedOnClick = false; + this.$emit('toggleCollapse'); + } + }, + getUpdateVariables(color) { + const currentIid = this.oldIid || this.iid; + + return { + iid: currentIid, + groupPath: this.fullPath, + color: color.color, + }; + }, + updateSelectedColor(inputVariables) { + this.colorUpdateInProgress = true; + + this.$apollo + .mutate({ + mutation: updateEpicColorMutation, + variables: { input: inputVariables }, + }) + .then(({ data }) => { + if (data.updateIssuableColor?.errors?.length) { + throw new Error(); + } + + this.$emit('updateSelectedColor', { + id: data.updateIssuableColor?.issuable?.id, + color: data.updateIssuableColor?.issuable?.color, + }); + }) + .catch((error) => + createFlash({ + message: this.$options.i18n.updatingError, + captureError: true, + error, + }), + ) + .finally(() => { + this.colorUpdateInProgress = false; + }); + }, + isDropdownVariantSidebar, + isDropdownVariantEmbedded, + }, +}; +</script> + +<template> + <div + class="labels-select-wrapper gl-relative" + :class="{ + 'is-embedded': isDropdownVariantEmbedded(variant), + }" + > + <template v-if="isDropdownVariantSidebar(variant)"> + <sidebar-editable-item + ref="editable" + :title="$options.i18n.widgetTitle" + :loading="isLoading" + :can-edit="allowEdit" + @open="oldIid = null" + > + <template #collapsed> + <dropdown-value :selected-color="issuableColor"> + <slot></slot> + </dropdown-value> + </template> + <template #default="{ edit }"> + <dropdown-value :selected-color="issuableColor" class="gl-mb-2"> + <slot></slot> + </dropdown-value> + <dropdown-contents + ref="dropdownContents" + :dropdown-button-text="dropdownButtonText" + :dropdown-title="dropdownTitle" + :selected-color="issuableColor" + :variant="variant" + :is-visible="edit" + @setColor="handleDropdownClose" + @closeDropdown="collapseEditableItem" + /> + </template> + </sidebar-editable-item> + </template> + <dropdown-contents + v-else + ref="dropdownContents" + :dropdown-button-text="dropdownButtonText" + :dropdown-title="dropdownTitle" + :selected-color="issuableColor" + :variant="variant" + @setColor="handleDropdownClose" + /> + </div> +</template> diff --git a/app/assets/javascripts/vue_shared/components/color_select_dropdown/constants.js b/app/assets/javascripts/vue_shared/components/color_select_dropdown/constants.js new file mode 100644 index 00000000000..c70785abd1e --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/color_select_dropdown/constants.js @@ -0,0 +1,30 @@ +import { __, s__ } from '~/locale'; + +export const COLOR_WIDGET_COLOR = s__('ColorWidget|Color'); + +export const DROPDOWN_VARIANT = { + Sidebar: 'sidebar', + Embedded: 'embedded', +}; + +export const DEFAULT_COLOR = { title: __('SuggestedColors|Blue'), color: '#1068bf' }; + +export const ISSUABLE_COLORS = [ + DEFAULT_COLOR, + { + title: s__('SuggestedColors|Green'), + color: '#217645', + }, + { + title: s__('SuggestedColors|Red'), + color: '#c91c00', + }, + { + title: s__('SuggestedColors|Orange'), + color: '#9e5400', + }, + { + title: s__('SuggestedColors|Purple'), + color: '#694cc0', + }, +]; diff --git a/app/assets/javascripts/vue_shared/components/color_select_dropdown/dropdown_contents.vue b/app/assets/javascripts/vue_shared/components/color_select_dropdown/dropdown_contents.vue new file mode 100644 index 00000000000..4eb1d3d08ca --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/color_select_dropdown/dropdown_contents.vue @@ -0,0 +1,109 @@ +<script> +import { GlDropdown } from '@gitlab/ui'; +import DropdownContentsColorView from './dropdown_contents_color_view.vue'; +import DropdownHeader from './dropdown_header.vue'; +import { isDropdownVariantSidebar } from './utils'; + +export default { + components: { + DropdownContentsColorView, + DropdownHeader, + GlDropdown, + }, + props: { + dropdownTitle: { + type: String, + required: true, + }, + selectedColor: { + type: Object, + required: true, + }, + dropdownButtonText: { + type: String, + required: true, + }, + variant: { + type: String, + required: true, + }, + isVisible: { + type: Boolean, + required: false, + default: false, + }, + }, + data() { + return { + showDropdownContentsCreateView: false, + localSelectedColor: this.selectedColor, + isDirty: false, + }; + }, + computed: { + buttonText() { + if (!this.localSelectedColor?.title) { + return this.dropdownButtonText; + } + + return this.localSelectedColor.title; + }, + }, + watch: { + localSelectedColor: { + handler() { + this.isDirty = true; + }, + deep: true, + }, + isVisible(newVal) { + if (newVal) { + this.$refs.dropdown.show(); + this.isDirty = false; + this.localSelectedColor = this.selectedColor; + } else { + this.$refs.dropdown.hide(); + this.setColor(); + } + }, + selectedColor(newVal) { + if (!this.isDirty) { + this.localSelectedColor = newVal; + } + }, + }, + methods: { + setColor() { + if (!this.isDirty) { + return; + } + this.$emit('setColor', this.localSelectedColor); + }, + handleDropdownHide() { + this.$emit('closeDropdown'); + if (!isDropdownVariantSidebar(this.variant)) { + this.setColor(); + } + this.$refs.dropdown.hide(); + }, + }, +}; +</script> + +<template> + <gl-dropdown ref="dropdown" :text="buttonText" class="gl-w-full" @hide="handleDropdownHide"> + <template #header> + <dropdown-header + ref="header" + :dropdown-title="dropdownTitle" + @closeDropdown="handleDropdownHide" + /> + </template> + <template #default> + <dropdown-contents-color-view + v-model="localSelectedColor" + @closeDropdown="handleDropdownHide" + /> + </template> + </gl-dropdown> +</template> diff --git a/app/assets/javascripts/vue_shared/components/color_select_dropdown/dropdown_contents_color_view.vue b/app/assets/javascripts/vue_shared/components/color_select_dropdown/dropdown_contents_color_view.vue new file mode 100644 index 00000000000..62f4cf59c14 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/color_select_dropdown/dropdown_contents_color_view.vue @@ -0,0 +1,53 @@ +<script> +import { GlDropdownForm, GlDropdownItem } from '@gitlab/ui'; +import ColorItem from './color_item.vue'; +import { ISSUABLE_COLORS } from './constants'; + +export default { + components: { + GlDropdownForm, + GlDropdownItem, + ColorItem, + }, + model: { + prop: 'selectedColor', + }, + props: { + selectedColor: { + type: Object, + required: true, + }, + }, + data() { + return { + colors: ISSUABLE_COLORS, + }; + }, + methods: { + isColorSelected(color) { + return this.selectedColor.color === color.color; + }, + handleColorClick(color) { + this.$emit('input', color); + this.$emit('closeDropdown', this.selectedColor); + }, + }, +}; +</script> + +<template> + <gl-dropdown-form> + <div> + <gl-dropdown-item + v-for="color in colors" + :key="color.color" + :is-checked="isColorSelected(color)" + :is-check-centered="true" + :is-check-item="true" + @click.native.capture.stop="handleColorClick(color)" + > + <color-item :color="color.color" :title="color.title" /> + </gl-dropdown-item> + </div> + </gl-dropdown-form> +</template> diff --git a/app/assets/javascripts/vue_shared/components/color_select_dropdown/dropdown_header.vue b/app/assets/javascripts/vue_shared/components/color_select_dropdown/dropdown_header.vue new file mode 100644 index 00000000000..a32b1570f5f --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/color_select_dropdown/dropdown_header.vue @@ -0,0 +1,31 @@ +<script> +import { GlButton } from '@gitlab/ui'; + +export default { + components: { + GlButton, + }, + props: { + dropdownTitle: { + type: String, + required: true, + }, + }, +}; +</script> + +<template> + <div> + <div class="dropdown-title gl-display-flex gl-align-items-center gl-pt-0 gl-pb-3!"> + <span class="gl-flex-grow-1">{{ dropdownTitle }}</span> + <gl-button + :aria-label="__('Close')" + variant="link" + size="small" + class="dropdown-header-button gl-p-0!" + icon="close" + @click="$emit('closeDropdown')" + /> + </div> + </div> +</template> diff --git a/app/assets/javascripts/vue_shared/components/color_select_dropdown/dropdown_value.vue b/app/assets/javascripts/vue_shared/components/color_select_dropdown/dropdown_value.vue new file mode 100644 index 00000000000..4cba66eefd2 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/color_select_dropdown/dropdown_value.vue @@ -0,0 +1,43 @@ +<script> +import { GlIcon, GlTooltipDirective } from '@gitlab/ui'; +import { COLOR_WIDGET_COLOR } from './constants'; +import ColorItem from './color_item.vue'; + +export default { + i18n: { + dropdownTitle: COLOR_WIDGET_COLOR, + }, + directives: { + GlTooltip: GlTooltipDirective, + }, + components: { + GlIcon, + ColorItem, + }, + props: { + selectedColor: { + type: Object, + required: true, + }, + }, +}; +</script> + +<template> + <div class="value js-value"> + <div + v-gl-tooltip.left.viewport + :title="$options.i18n.dropdownTitle" + class="sidebar-collapsed-icon" + > + <gl-icon name="appearance" /> + <color-item + :color="selectedColor.color" + :title="selectedColor.title" + class="gl-font-base gl-line-height-24" + /> + </div> + + <color-item class="hide-collapsed" :color="selectedColor.color" :title="selectedColor.title" /> + </div> +</template> diff --git a/app/assets/javascripts/vue_shared/components/color_select_dropdown/graphql/epic_color.query.graphql b/app/assets/javascripts/vue_shared/components/color_select_dropdown/graphql/epic_color.query.graphql new file mode 100644 index 00000000000..959e0f8c1a5 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/color_select_dropdown/graphql/epic_color.query.graphql @@ -0,0 +1,9 @@ +query epicColor($fullPath: ID!, $iid: ID) { + workspace: group(fullPath: $fullPath) { + id + issuable: epic(iid: $iid) { + id + color + } + } +} diff --git a/app/assets/javascripts/vue_shared/components/color_select_dropdown/graphql/epic_update_color.mutation.graphql b/app/assets/javascripts/vue_shared/components/color_select_dropdown/graphql/epic_update_color.mutation.graphql new file mode 100644 index 00000000000..2975b42253f --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/color_select_dropdown/graphql/epic_update_color.mutation.graphql @@ -0,0 +1,9 @@ +mutation updateEpicColor($input: UpdateEpicInput!) { + updateIssuableColor: updateEpic(input: $input) { + issuable: epic { + id + color + } + errors + } +} diff --git a/app/assets/javascripts/vue_shared/components/color_select_dropdown/utils.js b/app/assets/javascripts/vue_shared/components/color_select_dropdown/utils.js new file mode 100644 index 00000000000..46196e793b3 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/color_select_dropdown/utils.js @@ -0,0 +1,15 @@ +import { DROPDOWN_VARIANT } from './constants'; + +/** + * Returns boolean representing whether dropdown variant + * is `sidebar` + * @param {string} variant + */ +export const isDropdownVariantSidebar = (variant) => variant === DROPDOWN_VARIANT.Sidebar; + +/** + * Returns boolean representing whether dropdown variant + * is `embedded` + * @param {string} variant + */ +export const isDropdownVariantEmbedded = (variant) => variant === DROPDOWN_VARIANT.Embedded; 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 9cf8638f3cb..3ecfac10f9c 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,5 @@ <script> -import { - GlDeprecatedSkeletonLoading as GlSkeletonLoading, - GlSafeHtmlDirective as SafeHtml, -} from '@gitlab/ui'; +import { GlSkeletonLoader, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui'; import $ from 'jquery'; import '~/behaviors/markdown/render_gfm'; import { forEach, escape } from 'lodash'; @@ -14,7 +11,7 @@ let axiosSource; export default { components: { - GlSkeletonLoading, + GlSkeletonLoader, }, directives: { SafeHtml, @@ -115,7 +112,7 @@ export default { <template> <div ref="markdownPreview" class="md-previewer" data-testid="md-previewer"> - <gl-skeleton-loading v-if="isLoading" /> + <gl-skeleton-loader v-if="isLoading" /> <div v-else v-safe-html:[$options.safeHtmlConfig]="previewContent" diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js b/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js index d7a84798e47..5d7f4ae2a01 100644 --- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js +++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js @@ -1,4 +1,4 @@ -import { __ } from '~/locale'; +import { __, s__ } from '~/locale'; export const DEBOUNCE_DELAY = 500; export const MAX_RECENT_TOKENS_SIZE = 3; @@ -46,11 +46,13 @@ export const SortDirection = { export const FILTERED_SEARCH_LABELS = 'labels'; export const FILTERED_SEARCH_TERM = 'filtered-search-term'; -export const TOKEN_TITLE_AUTHOR = __('Author'); export const TOKEN_TITLE_ASSIGNEE = __('Assignee'); -export const TOKEN_TITLE_MILESTONE = __('Milestone'); +export const TOKEN_TITLE_AUTHOR = __('Author'); +export const TOKEN_TITLE_CONFIDENTIAL = __('Confidential'); +export const TOKEN_TITLE_CONTACT = s__('Crm|Contact'); export const TOKEN_TITLE_LABEL = __('Label'); -export const TOKEN_TITLE_TYPE = __('Type'); -export const TOKEN_TITLE_RELEASE = __('Release'); +export const TOKEN_TITLE_MILESTONE = __('Milestone'); export const TOKEN_TITLE_MY_REACTION = __('My-Reaction'); -export const TOKEN_TITLE_CONFIDENTIAL = __('Confidential'); +export const TOKEN_TITLE_ORGANIZATION = s__('Crm|Organization'); +export const TOKEN_TITLE_RELEASE = __('Release'); +export const TOKEN_TITLE_TYPE = __('Type'); diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue index c3a0a97a7ba..6a4ff07c999 100644 --- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue +++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue @@ -168,7 +168,7 @@ export default { if (data || operator) { this.searchKey = data; - if (!this.suggestionsLoading && !this.activeTokenValue) { + if (!this.activeTokenValue) { let search = this.searchTerm ? this.searchTerm : data; if (search.startsWith('"') && search.endsWith('"')) { diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue index 6f24955814c..178c57a5666 100644 --- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue +++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue @@ -43,9 +43,7 @@ export default { }, methods: { getActiveLabel(labels, data) { - return labels.find( - (label) => this.getLabelName(label).toLowerCase() === stripQuotes(data).toLowerCase(), - ); + return labels.find((label) => this.getLabelName(label) === stripQuotes(data)); }, /** * There's an inconsistency between private and public API @@ -128,7 +126,7 @@ export default { <div class="gl-display-flex gl-align-items-center"> <span :style="{ backgroundColor: label.color }" - class="gl-display-inline-block mr-2 p-2" + class="gl-display-inline-block gl-mr-3 gl-p-3" ></span> <div>{{ getLabelName(label) }}</div> </div> diff --git a/app/assets/javascripts/vue_shared/components/form/input_copy_toggle_visibility.vue b/app/assets/javascripts/vue_shared/components/form/input_copy_toggle_visibility.vue index 69548f0e7a8..15d858b99b9 100644 --- a/app/assets/javascripts/vue_shared/components/form/input_copy_toggle_visibility.vue +++ b/app/assets/javascripts/vue_shared/components/form/input_copy_toggle_visibility.vue @@ -1,5 +1,11 @@ <script> -import { GlFormInputGroup, GlFormGroup, GlButton, GlTooltipDirective } from '@gitlab/ui'; +import { + GlFormInputGroup, + GlFormInput, + GlFormGroup, + GlButton, + GlTooltipDirective, +} from '@gitlab/ui'; import { __ } from '~/locale'; import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; @@ -12,6 +18,7 @@ export default { }, components: { GlFormInputGroup, + GlFormInput, GlFormGroup, GlButton, ClipboardButton, @@ -80,10 +87,15 @@ export default { this.$emit('visibility-change', this.valueIsVisible); }, + handleClick() { + this.$refs.input.$el.select(); + }, handleCopyButtonClick() { this.$emit('copy'); }, handleFormInputCopy(event) { + this.handleCopyButtonClick(); + if (this.computedValueIsVisible) { return; } @@ -96,14 +108,21 @@ export default { </script> <template> <gl-form-group v-bind="$attrs"> - <gl-form-input-group - :value="displayedValue" - input-class="gl-font-monospace! gl-cursor-default!" - select-on-click - readonly - v-bind="formInputGroupProps" - @copy="handleFormInputCopy" - > + <gl-form-input-group> + <gl-form-input + ref="input" + readonly + class="gl-font-monospace! gl-cursor-default!" + v-bind="formInputGroupProps" + :value="displayedValue" + @copy="handleFormInputCopy" + @click="handleClick" + /> + + <!-- + This v-if is necessary to avoid an issue with border radius. + See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/88059#note_969812649 + --> <template v-if="showToggleVisibilityButton || showCopyButton" #append> <gl-button v-if="showToggleVisibilityButton" diff --git a/app/assets/javascripts/vue_shared/components/markdown/header.vue b/app/assets/javascripts/vue_shared/components/markdown/header.vue index ba2b5eaa4f9..4fdf7f45643 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/header.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/header.vue @@ -266,7 +266,7 @@ export default { }} </p> <gl-button - variant="info" + variant="confirm" category="primary" size="small" @click="handleSuggestDismissed" diff --git a/app/assets/javascripts/vue_shared/components/notes/noteable_warning.vue b/app/assets/javascripts/vue_shared/components/notes/noteable_warning.vue index 7a7074da084..78a7fed6293 100644 --- a/app/assets/javascripts/vue_shared/components/notes/noteable_warning.vue +++ b/app/assets/javascripts/vue_shared/components/notes/noteable_warning.vue @@ -98,13 +98,15 @@ export default { <span v-else-if="isConfidential" ref="confidential"> {{ confidentialContextText }} {{ __('People without permission will never get a notification.') }} - <gl-link :href="confidentialNoteableDocsPath" target="_blank">{{ __('Learn more') }}</gl-link> + <gl-link :href="confidentialNoteableDocsPath" target="_blank">{{ + __('Learn more.') + }}</gl-link> </span> <span v-else-if="isLocked" ref="locked"> {{ lockedContextText }} {{ __('Only project members can comment.') }} - <gl-link :href="lockedNoteableDocsPath" target="_blank">{{ __('Learn more') }}</gl-link> + <gl-link :href="lockedNoteableDocsPath" target="_blank">{{ __('Learn more.') }}</gl-link> </span> </div> </template> diff --git a/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue b/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue index 3aca068c074..2206ae98c73 100644 --- a/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue +++ b/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue @@ -1,11 +1,11 @@ <script> -import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui'; +import { GlSkeletonLoader } from '@gitlab/ui'; import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue'; export default { name: 'SkeletonNote', components: { - GlSkeletonLoading, + GlSkeletonLoader, TimelineEntryItem, }, }; @@ -16,7 +16,7 @@ export default { <div class="timeline-icon"></div> <div class="timeline-content"> <div class="note-header"></div> - <div class="note-body"><gl-skeleton-loading /></div> + <div class="note-body"><gl-skeleton-loader /></div> </div> </timeline-entry-item> </template> diff --git a/app/assets/javascripts/vue_shared/components/notes/system_note.vue b/app/assets/javascripts/vue_shared/components/notes/system_note.vue index dd7a851b1be..3593ea16968 100644 --- a/app/assets/javascripts/vue_shared/components/notes/system_note.vue +++ b/app/assets/javascripts/vue_shared/components/notes/system_note.vue @@ -18,7 +18,7 @@ */ import { GlButton, - GlDeprecatedSkeletonLoading as GlSkeletonLoading, + GlSkeletonLoader, GlTooltipDirective, GlIcon, GlSafeHtmlDirective as SafeHtml, @@ -26,9 +26,9 @@ import { import $ from 'jquery'; import { mapGetters, mapActions, mapState } from 'vuex'; import descriptionVersionHistoryMixin from 'ee_else_ce/notes/mixins/description_version_history'; +import '~/behaviors/markdown/render_gfm'; import axios from '~/lib/utils/axios_utils'; import { __ } from '~/locale'; -import initMRPopovers from '~/mr_popover/'; import noteHeader from '~/notes/components/note_header.vue'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import { spriteIcon } from '~/lib/utils/common_utils'; @@ -46,7 +46,7 @@ export default { noteHeader, TimelineEntryItem, GlButton, - GlSkeletonLoading, + GlSkeletonLoader, }, directives: { GlTooltip: GlTooltipDirective, @@ -94,7 +94,7 @@ export default { }, }, mounted() { - initMRPopovers(this.$el.querySelectorAll('.gfm-merge_request')); + $(this.$refs['gfm-content']).renderGFM(); }, methods: { ...mapActions(['fetchDescriptionVersion', 'softDeleteDescriptionVersion']), @@ -130,7 +130,7 @@ export default { <div class="timeline-content"> <div class="note-header"> <note-header :author="note.author" :created-at="note.created_at" :note-id="note.id"> - <span v-safe-html="actionTextHtml"></span> + <span ref="gfm-content" v-safe-html="actionTextHtml"></span> <template v-if="canSeeDescriptionVersion || note.outdated_line_change_path" #extra-controls @@ -172,7 +172,7 @@ export default { </div> <div v-if="shouldShowDescriptionVersion" class="description-version pt-2"> <pre v-if="isLoadingDescriptionVersion" class="loading-state"> - <gl-skeleton-loading /> + <gl-skeleton-loader /> </pre> <pre v-else v-safe-html="descriptionVersion" class="wrapper mt-2"></pre> <gl-button @@ -218,7 +218,9 @@ export default { </tr> </table> </div> - <gl-skeleton-loading v-else-if="showLines" class="gl-mt-4" /> + <div v-else-if="showLines" class="mt-4"> + <gl-skeleton-loader /> + </div> </div> </div> </timeline-entry-item> diff --git a/app/assets/javascripts/vue_shared/components/project_selector/project_selector.vue b/app/assets/javascripts/vue_shared/components/project_selector/project_selector.vue index f21092af501..67ad7769c7c 100644 --- a/app/assets/javascripts/vue_shared/components/project_selector/project_selector.vue +++ b/app/assets/javascripts/vue_shared/components/project_selector/project_selector.vue @@ -130,16 +130,19 @@ export default { <span data-testid="legend-text">{{ legendText }}</span> </template> </gl-infinite-scroll> - <div v-if="showNoResultsMessage" class="text-muted ml-2 js-no-results-message"> + <div v-if="showNoResultsMessage" class="gl-text-gray-600 gl-ml-3 js-no-results-message"> {{ __('Sorry, no projects matched your search') }} </div> <div v-if="showMinimumSearchQueryMessage" - class="text-muted ml-2 js-minimum-search-query-message" + class="gl-text-gray-600 gl-ml-3 js-minimum-search-query-message" > {{ __('Enter at least three characters to search') }} </div> - <div v-if="showSearchErrorMessage" class="text-danger ml-2 js-search-error-message"> + <div + v-if="showSearchErrorMessage" + class="gl-text-red-500 gl-font-weight-bold gl-ml-3 js-search-error-message" + > {{ __('Something went wrong, unable to search projects') }} </div> </div> diff --git a/app/assets/javascripts/vue_shared/components/registry/registry_search.vue b/app/assets/javascripts/vue_shared/components/registry/registry_search.vue index da68fe961a6..1948a6778f4 100644 --- a/app/assets/javascripts/vue_shared/components/registry/registry_search.vue +++ b/app/assets/javascripts/vue_shared/components/registry/registry_search.vue @@ -12,7 +12,7 @@ export default { GlFilteredSearch, }, props: { - filter: { + filters: { type: Array, required: true, }, @@ -33,7 +33,7 @@ export default { computed: { internalFilter: { get() { - return this.filter; + return this.filters; }, set(value) { this.$emit('filter:changed', value); @@ -71,7 +71,7 @@ export default { const sort = this.isSortAscending ? DESCENDING_ORDER : ASCENDING_ORDER; const newQueryString = this.generateQueryData({ sorting: { ...this.sorting, sort }, - filter: this.filter, + filter: this.filters, }); this.$emit('sorting:changed', { sort }); this.$emit('query:changed', newQueryString); @@ -79,7 +79,7 @@ export default { onSortItemClick(item) { const newQueryString = this.generateQueryData({ sorting: { ...this.sorting, orderBy: item }, - filter: this.filter, + filter: this.filters, }); this.$emit('sorting:changed', { orderBy: item }); this.$emit('query:changed', newQueryString); @@ -87,7 +87,7 @@ export default { submitSearch() { const newQueryString = this.generateQueryData({ sorting: this.sorting, - filter: this.filter, + filter: this.filters, }); this.$emit('filter:submit'); this.$emit('query:changed', newQueryString); diff --git a/app/assets/javascripts/vue_shared/components/runner_instructions/constants.js b/app/assets/javascripts/vue_shared/components/runner_instructions/constants.js index 34845e3d9e4..c97e191b630 100644 --- a/app/assets/javascripts/vue_shared/components/runner_instructions/constants.js +++ b/app/assets/javascripts/vue_shared/components/runner_instructions/constants.js @@ -1,7 +1,5 @@ import { s__ } from '~/locale'; -export const PLATFORMS_WITHOUT_ARCHITECTURES = ['docker', 'kubernetes']; - export const REGISTRATION_TOKEN_PLACEHOLDER = '$REGISTRATION_TOKEN'; export const INSTRUCTIONS_PLATFORMS_WITHOUT_ARCHITECTURES = { diff --git a/app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions.vue b/app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions.vue index 5d144c0d699..06852f511bf 100644 --- a/app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions.vue +++ b/app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions.vue @@ -9,35 +9,19 @@ export default { RunnerInstructionsModal, }, directives: { - GlModalDirective, + GlModal: GlModalDirective, }, modalId: 'runner-instructions-modal', i18n: { buttonText: s__('Runners|Show runner installation instructions'), }, - data() { - return { - opened: false, - }; - }, - methods: { - onClick() { - // lazily mount modal to prevent premature instructions requests - this.opened = true; - }, - }, }; </script> <template> <div> - <gl-button - v-gl-modal-directive="$options.modalId" - class="gl-mt-4" - data-testid="show-modal-button" - @click="onClick" - > + <gl-button v-gl-modal="$options.modalId" class="gl-mt-4" data-testid="show-modal-button"> {{ $options.i18n.buttonText }} </gl-button> - <runner-instructions-modal v-if="opened" :modal-id="$options.modalId" /> + <runner-instructions-modal :modal-id="$options.modalId" /> </div> </template> diff --git a/app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions_modal.vue b/app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions_modal.vue index 9eaaf7d1c18..bfaf3b92c34 100644 --- a/app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions_modal.vue +++ b/app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions_modal.vue @@ -17,7 +17,6 @@ import { __, s__ } from '~/locale'; import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue'; import { INSTRUCTIONS_PLATFORMS_WITHOUT_ARCHITECTURES, - PLATFORMS_WITHOUT_ARCHITECTURES, REGISTRATION_TOKEN_PLACEHOLDER, } from './constants'; import getRunnerPlatformsQuery from './graphql/queries/get_runner_platforms.query.graphql'; @@ -59,21 +58,25 @@ export default { apollo: { platforms: { query: getRunnerPlatformsQuery, + skip() { + // Only load instructions once the modal is shown + return !this.shown; + }, update(data) { - return data?.runnerPlatforms?.nodes.map(({ name, humanReadableName, architectures }) => { - return { - name, - humanReadableName, - architectures: architectures?.nodes || [], - }; - }); + return ( + data?.runnerPlatforms?.nodes.map(({ name, humanReadableName, architectures }) => { + return { + name, + humanReadableName, + architectures: architectures?.nodes || [], + }; + }) ?? [] + ); }, result() { - if (this.platforms.length) { - // If it is set and available, select the defaultSelectedPlatform. - // Otherwise, select the first available platform - this.selectPlatform(this.defaultPlatform() || this.platforms[0]); - } + // If it is set and available, select the defaultSelectedPlatform. + // Otherwise, select the first available platform + this.selectPlatform(this.defaultPlatformName || this.platforms?.[0].name); }, error() { this.toggleAlert(true); @@ -82,12 +85,12 @@ export default { instructions: { query: getRunnerSetupInstructionsQuery, skip() { - return !this.selectedPlatform; + return !this.shown || !this.selectedPlatform; }, variables() { return { - platform: this.selectedPlatformName, - architecture: this.selectedArchitectureName || '', + platform: this.selectedPlatform, + architecture: this.selectedArchitecture || '', }; }, update(data) { @@ -100,6 +103,7 @@ export default { }, data() { return { + shown: false, platforms: [], selectedPlatform: null, selectedArchitecture: null, @@ -109,55 +113,63 @@ export default { }; }, computed: { - platformsEmpty() { - return isEmpty(this.platforms); - }, instructionsEmpty() { return isEmpty(this.instructions); }, - selectedPlatformName() { - return this.selectedPlatform?.name; - }, - selectedArchitectureName() { - return this.selectedArchitecture?.name; + architectures() { + return this.platforms.find(({ name }) => name === this.selectedPlatform)?.architectures || []; }, - hasArchitecureList() { - return !PLATFORMS_WITHOUT_ARCHITECTURES.includes(this.selectedPlatformName); + binaryUrl() { + return this.architectures.find(({ name }) => name === this.selectedArchitecture) + ?.downloadLocation; }, instructionsWithoutArchitecture() { - return INSTRUCTIONS_PLATFORMS_WITHOUT_ARCHITECTURES[this.selectedPlatformName]?.instructions; + return INSTRUCTIONS_PLATFORMS_WITHOUT_ARCHITECTURES[this.selectedPlatform]?.instructions; }, runnerInstallationLink() { - return INSTRUCTIONS_PLATFORMS_WITHOUT_ARCHITECTURES[this.selectedPlatformName]?.link; + return INSTRUCTIONS_PLATFORMS_WITHOUT_ARCHITECTURES[this.selectedPlatform]?.link; }, registerInstructionsWithToken() { const { registerInstructions } = this.instructions || {}; if (this.registrationToken) { - return registerInstructions.replace(REGISTRATION_TOKEN_PLACEHOLDER, this.registrationToken); + return registerInstructions?.replace( + REGISTRATION_TOKEN_PLACEHOLDER, + this.registrationToken, + ); } - return registerInstructions; }, }, + updated() { + // Refocus on dom changes, after loading data + this.refocusSelectedPlatformButton(); + }, methods: { show() { this.$refs.modal.show(); }, - focusSelected() { - // By default the first platform always gets the focus, but when the `defaultPlatformName` - // property is present, any other platform might actually be selected. - this.$refs[this.selectedPlatformName]?.[0].$el.focus(); + onShown() { + this.shown = true; + this.refocusSelectedPlatformButton(); }, - defaultPlatform() { - return this.platforms.find((platform) => platform.name === this.defaultPlatformName); + refocusSelectedPlatformButton() { + // On modal opening, the first focusable element is auto-focused by bootstrap-vue + // This can be confusing for users, because the wrong platform button can + // get focused when setting a `defaultPlatformName`. + // This method refocuses the expected button. + // See more about this auto-focus: https://bootstrap-vue.org/docs/components/modal#auto-focus-on-open + this.$refs[this.selectedPlatform]?.[0].$el.focus(); }, - selectPlatform(platform) { - this.selectedPlatform = platform; + selectPlatform(platformName) { + this.selectedPlatform = platformName; - if (!platform.architectures?.some(({ name }) => name === this.selectedArchitectureName)) { - // Select first architecture when current value is not available - this.selectArchitecture(platform.architectures[0]); + // Update architecture when platform changes + const arch = this.architectures.find(({ name }) => name === this.selectedArchitecture); + if (arch) { + this.selectArchitecture(arch.name); + } else { + this.selectArchitecture(this.architectures[0]?.name); } }, selectArchitecture(architecture) { @@ -175,6 +187,7 @@ export default { }, }, i18n: { + environment: __('Environment'), installARunner: s__('Runners|Install a runner'), architecture: s__('Runners|Architecture'), downloadInstallBinary: s__('Runners|Download and install binary'), @@ -182,6 +195,7 @@ export default { registerRunnerCommand: s__('Runners|Command to register runner'), fetchError: s__('Runners|An error has occurred fetching instructions'), copyInstructions: s__('Runners|Copy instructions'), + viewInstallationInstructions: s__('Runners|View installation instructions'), }, closeButton: { text: __('Close'), @@ -197,17 +211,17 @@ export default { :action-secondary="$options.closeButton" v-bind="$attrs" v-on="$listeners" - @shown="focusSelected" + @shown="onShown" > <gl-alert v-if="showAlert" variant="danger" @dismiss="toggleAlert(false)"> {{ $options.i18n.fetchError }} </gl-alert> - <gl-skeleton-loader v-if="platformsEmpty && $apollo.loading" /> + <gl-skeleton-loader v-if="!platforms.length && $apollo.loading" /> - <template v-if="!platformsEmpty"> + <template v-if="platforms.length"> <h5> - {{ __('Environment') }} + {{ $options.i18n.environment }} </h5> <div v-gl-resize-observer="onPlatformsButtonResize"> <gl-button-group @@ -220,29 +234,29 @@ export default { v-for="platform in platforms" :key="platform.name" :ref="platform.name" - :selected="selectedPlatform && selectedPlatform.name === platform.name" - @click="selectPlatform(platform)" + :selected="selectedPlatform === platform.name" + @click="selectPlatform(platform.name)" > {{ platform.humanReadableName }} </gl-button> </gl-button-group> </div> </template> - <template v-if="hasArchitecureList"> + <template v-if="architectures.length"> <template v-if="selectedPlatform"> <h5> {{ $options.i18n.architecture }} <gl-loading-icon v-if="$apollo.loading" size="sm" inline /> </h5> - <gl-dropdown class="gl-mb-3" :text="selectedArchitectureName"> + <gl-dropdown class="gl-mb-3" :text="selectedArchitecture"> <gl-dropdown-item - v-for="architecture in selectedPlatform.architectures" + v-for="architecture in architectures" :key="architecture.name" :is-check-item="true" - :is-checked="selectedArchitectureName === architecture.name" + :is-checked="selectedArchitecture === architecture.name" data-testid="architecture-dropdown-item" - @click="selectArchitecture(architecture)" + @click="selectArchitecture(architecture.name)" > {{ architecture.name }} </gl-dropdown-item> @@ -250,8 +264,9 @@ export default { <div class="gl-sm-display-flex gl-align-items-center gl-mb-3"> <h5>{{ $options.i18n.downloadInstallBinary }}</h5> <gl-button + v-if="binaryUrl" class="gl-ml-auto" - :href="selectedArchitecture.downloadLocation" + :href="binaryUrl" download icon="download" data-testid="binary-download-button" @@ -298,7 +313,7 @@ export default { <p>{{ instructionsWithoutArchitecture }}</p> <gl-button :href="runnerInstallationLink"> <gl-icon name="external-link" /> - {{ s__('Runners|View installation instructions') }} + {{ $options.i18n.viewInstallationInstructions }} </gl-button> </div> </template> 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 60111210f5d..9388ef4ba45 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 @@ -2,6 +2,9 @@ import { GlButton, GlIcon } from '@gitlab/ui'; import { mapActions, mapGetters } from 'vuex'; +// @deprecated This component should only be used when there is no GraphQL API. +// In most cases you should use +// `app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget` instead. export default { components: { GlButton, diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents.vue index 399db978b60..1064cbc26e3 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents.vue @@ -4,6 +4,9 @@ import { mapGetters, mapState } from 'vuex'; import DropdownContentsCreateView from './dropdown_contents_create_view.vue'; import DropdownContentsLabelsView from './dropdown_contents_labels_view.vue'; +// @deprecated This component should only be used when there is no GraphQL API. +// In most cases you should use +// `app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue` instead. export default { components: { DropdownContentsLabelsView, 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 2cccb8325f4..3ff3755de46 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 @@ -2,6 +2,9 @@ import { GlTooltipDirective, GlButton, GlFormInput, GlLink, GlLoadingIcon } from '@gitlab/ui'; import { mapState, mapActions } from 'vuex'; +// @deprecated This component should only be used when there is no GraphQL API. +// In most cases you should use +// `app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue` instead. export default { components: { GlButton, @@ -51,10 +54,10 @@ 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"> + <div class="dropdown-title d-flex align-items-center pt-0 pb-2 gl-mb-0"> <gl-button :aria-label="__('Go back')" - variant="link" + category="tertiary" size="small" class="js-btn-back dropdown-header-button p-0" icon="arrow-left" @@ -63,7 +66,7 @@ export default { <span class="flex-grow-1">{{ labelsCreateTitle }}</span> <gl-button :aria-label="__('Close')" - variant="link" + category="tertiary" size="small" class="dropdown-header-button p-0" icon="close" @@ -95,7 +98,7 @@ export default { ></span> <gl-form-input v-model.trim="selectedColor" - class="gl-rounded-top-left-none gl-rounded-bottom-left-none" + class="gl-rounded-top-left-none gl-rounded-bottom-left-none gl-mb-2" :placeholder="__('Use custom color #FF0000')" /> </div> 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 134575b7a27..e235bfde394 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 @@ -13,6 +13,9 @@ import { UP_KEY_CODE, DOWN_KEY_CODE, ENTER_KEY_CODE, ESC_KEY_CODE } from '~/lib/ import LabelItem from './label_item.vue'; +// @deprecated This component should only be used when there is no GraphQL API. +// In most cases you should use +// `app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue` instead. export default { components: { GlIntersectionObserver, @@ -166,7 +169,7 @@ export default { <span class="flex-grow-1">{{ labelsListTitle }}</span> <gl-button :aria-label="__('Close')" - variant="link" + category="tertiary" size="small" class="dropdown-header-button gl-p-0!" icon="close" @@ -193,6 +196,7 @@ export default { :key="label.id" :label="label" :is-label-set="label.set" + :is-label-indeterminate="label.indeterminate" :highlight="index === currentHighlightItem" @clickLabel="handleLabelClick(label)" /> diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_title.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_title.vue index e91a0489ef1..e4325492334 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_title.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_title.vue @@ -2,6 +2,9 @@ import { GlButton, GlLoadingIcon } from '@gitlab/ui'; import { mapState, mapActions } from 'vuex'; +// @deprecated This component should only be used when there is no GraphQL API. +// In most cases you should use +// `app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_header.vue` instead. export default { components: { GlButton, @@ -23,7 +26,9 @@ export default { </script> <template> - <div class="hide-collapsed gl-line-height-20 gl-mb-2 gl-text-gray-900 gl-font-weight-bold"> + <div + class="hide-collapsed gl-line-height-20 gl-mb-2 gl-text-gray-900 gl-font-weight-bold gl-mb-0" + > {{ __('Labels') }} <template v-if="allowLabelEdit"> <gl-loading-icon v-show="labelsSelectInProgress" size="sm" inline /> 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 35ac9ef8565..e59d150dd43 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 @@ -5,6 +5,9 @@ import { mapState } from 'vuex'; import { isScopedLabel } from '~/lib/utils/common_utils'; +// @deprecated This component should only be used when there is no GraphQL API. +// In most cases you should use +// `app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_value.vue` instead. export default { components: { GlLabel, diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_value_collapsed.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_value_collapsed.vue index 8a26c4a6618..5966c78aa51 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_value_collapsed.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_value_collapsed.vue @@ -2,6 +2,9 @@ import { GlIcon, GlTooltipDirective } from '@gitlab/ui'; import { s__, sprintf } from '~/locale'; +// @deprecated This component should only be used when there is no GraphQL API. +// In most cases you should use +// `app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget` instead. export default { directives: { GlTooltip: GlTooltipDirective, 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 index dd40add6376..154e3013acd 100644 --- 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 @@ -1,6 +1,10 @@ <script> import { GlLink, GlIcon } from '@gitlab/ui'; +import { __ } from '~/locale'; +// @deprecated This component should only be used when there is no GraphQL API. +// In most cases you should use +// `app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/label_item.vue` instead. export default { functional: true, props: { @@ -12,6 +16,11 @@ export default { type: Boolean, required: true, }, + isLabelIndeterminate: { + type: Boolean, + required: false, + default: false, + }, highlight: { type: Boolean, required: false, @@ -19,7 +28,7 @@ export default { }, }, render(h, { props, listeners }) { - const { label, highlight, isLabelSet } = props; + const { label, highlight, isLabelSet, isLabelIndeterminate } = props; const labelColorBox = h('span', { class: 'dropdown-label-box gl-flex-shrink-0 gl-top-0 gl-mr-3', @@ -33,18 +42,36 @@ export default { const checkedIcon = h(GlIcon, { class: { - 'gl-mr-3 gl-flex-shrink-0': true, + 'gl-mr-3 gl-flex-shrink-0 has-tooltip': true, hidden: !isLabelSet, }, + attrs: { + title: __('Selected for all items.'), + 'data-testid': 'checked-icon', + }, props: { name: 'mobile-issue-close', }, }); + const indeterminateIcon = h(GlIcon, { + class: { + 'gl-mr-3 gl-flex-shrink-0 has-tooltip': true, + hidden: !isLabelIndeterminate, + }, + attrs: { + title: __('Selected for some items.'), + 'data-testid': 'indeterminate-icon', + }, + props: { + name: 'dash', + }, + }); + const noIcon = h('span', { class: { 'gl-mr-5 gl-pr-3': true, - hidden: isLabelSet, + hidden: isLabelSet || isLabelIndeterminate, }, attrs: { 'data-testid': 'no-icon', @@ -63,7 +90,7 @@ export default { }, }, }, - [noIcon, checkedIcon, labelColorBox, labelTitle], + [noIcon, checkedIcon, indeterminateIcon, labelColorBox, labelTitle], ); return h( 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 7e259cb8b96..b61996cdcdb 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 @@ -15,6 +15,9 @@ import labelsSelectModule from './store'; Vue.use(Vuex); +// @deprecated This component should only be used when there is no GraphQL API. +// In most cases you should use +// `app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue` instead. export default { store: new Vuex.Store(labelsSelectModule()), components: { @@ -198,11 +201,12 @@ export default { !state.showDropdownButton && !state.showDropdownContents ) { - let filterFn = (label) => label.touched; - if (this.isDropdownVariantEmbedded) { - filterFn = (label) => label.set; - } - this.handleDropdownClose(state.labels.filter(filterFn)); + const filterTouchedLabelsFn = (label) => label.touched; + const filterSetLabelsFn = (label) => label.set; + const labels = this.isDropdownVariantEmbedded + ? state.labels.filter(filterSetLabelsFn) + : state.labels.filter(filterTouchedLabelsFn); + this.handleDropdownClose(labels, state.labels.filter(filterTouchedLabelsFn)); } }, /** @@ -265,11 +269,11 @@ export default { isInDropdownContents ); }, - handleDropdownClose(labels) { - // Only emit label updates if there are any labels to update - // on UI. + handleDropdownClose(labels, touchedLabels) { + // Only emit label updates if there are any + // labels to update on UI. if (labels.length) this.$emit('updateSelectedLabels', labels); - this.$emit('onDropdownClose'); + this.$emit('onDropdownClose', touchedLabels); }, handleCollapsedValueClick() { this.$emit('toggleCollapse'); 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 d14f96720b7..ef3eedd9bb2 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 @@ -8,9 +8,10 @@ import { DropdownVariant } from '../constants'; * @param {object} state */ export const dropdownButtonText = (state, getters) => { - const selectedLabels = getters.isDropdownVariantSidebar - ? state.labels.filter((label) => label.set) - : state.selectedLabels; + const selectedLabels = + getters.isDropdownVariantSidebar || getters.isDropdownVariantEmbedded + ? state.labels.filter((label) => label.set || label.indeterminate) + : state.selectedLabels; if (!selectedLabels.length) { return state.dropdownButtonText || __('Label'); 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 9e64f03fe84..43b23994cdf 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 @@ -2,8 +2,39 @@ import { isScopedLabel, scopedLabelKey } from '~/lib/utils/common_utils'; import { DropdownVariant } from '../constants'; import * as types from './mutation_types'; +const transformLabels = (labels, selectedLabels) => + labels.map((label) => { + const selectedLabel = selectedLabels.find(({ id }) => id === label.id); + + return { + ...label, + set: Boolean(selectedLabel?.set), + indeterminate: Boolean(selectedLabel?.indeterminate), + }; + }); + export default { [types.SET_INITIAL_STATE](state, props) { + // We need to ensure that selectedLabels have + // `set` & `indeterminate` properties defined. + if (props.selectedLabels?.length) { + props.selectedLabels.forEach((label) => { + /* eslint-disable no-param-reassign */ + if (label.set === undefined && label.indeterminate === undefined) { + label.set = true; + label.indeterminate = false; + } else if (label.set === undefined && label.indeterminate !== undefined) { + label.set = false; + } else if (label.set !== undefined && label.indeterminate === undefined) { + label.indeterminate = false; + } else { + label.set = false; + label.indeterminate = false; + } + /* eslint-enable no-param-reassign */ + }); + } + Object.assign(state, { ...props }); }, @@ -36,10 +67,7 @@ export default { // selectedLabels array. state.labelsFetchInProgress = false; state.labelsFetched = true; - state.labels = labels.map((label) => ({ - ...label, - set: state.selectedLabels.some((selectedLabel) => selectedLabel.id === label.id), - })); + state.labels = transformLabels(labels, state.selectedLabels); }, [types.RECEIVE_SET_LABELS_FAILURE](state) { state.labelsFetchInProgress = false; @@ -62,7 +90,8 @@ export default { const candidateLabel = state.labels.find((label) => labelId === label.id); if (candidateLabel) { candidateLabel.touched = true; - candidateLabel.set = !candidateLabel.set; + candidateLabel.set = candidateLabel.indeterminate ? true : !candidateLabel.set; + candidateLabel.indeterminate = false; } if (isScopedLabel(candidateLabel)) { @@ -80,9 +109,6 @@ export default { }, [types.UPDATE_LABELS_SET_STATE](state) { - state.labels = state.labels.map((label) => ({ - ...label, - set: state.selectedLabels.some((selectedLabel) => selectedLabel.id === label.id), - })); + state.labels = transformLabels(state.labels, state.selectedLabels); }, }; diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue index 0fa64a29b3a..5471cda0cc5 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue @@ -169,6 +169,9 @@ export default { setFocus() { this.$refs.header.focusInput(); }, + hideDropdown() { + this.$refs.dropdown.hide(); + }, showDropdown() { this.$refs.dropdown.show(); }, @@ -205,7 +208,7 @@ export default { :show-dropdown-contents-create-view="showDropdownContentsCreateView" :is-standalone="isStandalone" @toggleDropdownContentsCreateView="toggleDropdownContent" - @closeDropdown="$emit('closeDropdown')" + @closeDropdown="hideDropdown" @input="debouncedSearchKeyUpdate" @searchEnter="selectFirstItem" /> diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue index 090bf9493bf..5f344ae4214 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue @@ -140,18 +140,19 @@ export default { <template> <div class="labels-select-contents-create js-labels-create"> <div class="dropdown-input"> - <gl-alert v-if="error" variant="danger" :dismissible="false" class="gl-mb-3"> + <gl-alert v-if="error" variant="danger" :dismissible="false" class="gl-mt-3"> {{ error }} </gl-alert> <gl-form-input v-model.trim="labelTitle" + class="gl-mt-3" :placeholder="__('Name new label')" :autofocus="true" data-testid="label-title-input" /> </div> <div class="dropdown-content gl-px-3"> - <div class="suggest-colors suggest-colors-dropdown gl-mt-0! gl-mb-3!"> + <div class="suggest-colors suggest-colors-dropdown gl-mt-0! gl-mb-3! gl-mb-0"> <gl-link v-for="(color, index) in suggestedColors" :key="index" @@ -169,7 +170,7 @@ export default { ></span> <gl-form-input v-model.trim="selectedColor" - class="gl-rounded-top-left-none gl-rounded-bottom-left-none" + class="gl-rounded-top-left-none gl-rounded-bottom-left-none gl-mb-2" :placeholder="__('Use custom color #FF0000')" data-testid="selected-color-text" /> diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_header.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_header.vue index faad69732dd..aaddab43e2a 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_header.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_header.vue @@ -51,7 +51,7 @@ export default { <div data-testid="dropdown-header"> <div v-if="!isStandalone" - class="dropdown-title gl-display-flex gl-align-items-center gl-pt-0 gl-pb-3!" + class="dropdown-title gl-display-flex gl-align-items-center gl-pt-0 gl-pb-3! gl-mb-0" data-testid="dropdown-header-title" > <gl-button @@ -72,6 +72,7 @@ export default { class="dropdown-header-button gl-p-0!" icon="close" data-testid="close-button" + data-qa-selector="close_labels_dropdown_button" @click="$emit('closeDropdown')" /> </div> diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_value.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_value.vue index 57ee816c4c7..57e3ee4aaa5 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_value.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_value.vue @@ -92,7 +92,9 @@ export default { @click="handleCollapsedClick" > <gl-icon name="labels" /> - <span class="gl-font-base gl-line-height-24">{{ selectedLabels.length }}</span> + <span class="collapse-truncated-title gl-pt-2 gl-px-3 gl-font-sm">{{ + selectedLabels.length + }}</span> </div> <span v-if="!selectedLabels.length" 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 c30ca5369ee..7b62f0cdb7d 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 @@ -72,7 +72,7 @@ export default { </div> <pre - class="gl-p-0! gl-w-full gl-overflow-visible! gl-ml-11! gl-border-none! code highlight" + class="gl-p-0! gl-w-full gl-overflow-visible! gl-ml-11! gl-border-none! code highlight gl-line-height-normal" :class="firstLineClass" ><code><span :id="`LC${number}`" v-safe-html="formattedContent" :lang="language" class="line" data-testid="content"></span></code></pre> </div> 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 bed6dd4d5c6..0d78530d878 100644 --- a/app/assets/javascripts/vue_shared/components/source_viewer/constants.js +++ b/app/assets/javascripts/vue_shared/components/source_viewer/constants.js @@ -134,3 +134,7 @@ export const BIDI_CHARS_CLASS_LIST = 'unicode-bidi has-tooltip'; export const BIDI_CHAR_TOOLTIP = __( 'Potentially unwanted character detected: Unicode BiDi Control', ); + +export const HLJS_COMMENT_SELECTOR = 'hljs-comment'; + +export const HLJS_ON_AFTER_HIGHLIGHT = 'after:highlight'; diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/plugins/index.js b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/index.js new file mode 100644 index 00000000000..c9f7e5508be --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/index.js @@ -0,0 +1,13 @@ +import { HLJS_ON_AFTER_HIGHLIGHT } from '../constants'; +import wrapComments from './wrap_comments'; + +/** + * Registers our plugins for Highlight.js + * + * Plugin API: https://github.com/highlightjs/highlight.js/blob/main/docs/plugin-api.rst + * + * @param {Object} hljs - the Highlight.js instance. + */ +export const registerPlugins = (hljs) => { + hljs.addPlugin({ [HLJS_ON_AFTER_HIGHLIGHT]: wrapComments }); +}; diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/plugins/wrap_comments.js b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/wrap_comments.js new file mode 100644 index 00000000000..5be92af5b55 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/wrap_comments.js @@ -0,0 +1,39 @@ +import { HLJS_COMMENT_SELECTOR } from '../constants'; + +const createWrapper = (content) => { + const span = document.createElement('span'); + span.className = HLJS_COMMENT_SELECTOR; + span.innerHTML = content; + return span.outerHTML; +}; + +/** + * Highlight.js plugin for wrapping multi-line comments in the `hljs-comment` class. + * This ensures that multi-line comments are rendered correctly in the GitLab UI. + * + * Plugin API: https://github.com/highlightjs/highlight.js/blob/main/docs/plugin-api.rst + * + * @param {Object} Result - an object that represents the highlighted result from Highlight.js + */ +export default (result) => { + if (!result.value.includes(HLJS_COMMENT_SELECTOR)) return; + + let wrapComment = false; + + // eslint-disable-next-line no-param-reassign + result.value = result.value // Highlight.js expects the result param to be mutated for plugins to work + .split('\n') + .map((lineContent) => { + const includesClosingTag = lineContent.includes('</span>'); + if (lineContent.includes(HLJS_COMMENT_SELECTOR) && !includesClosingTag) { + wrapComment = true; + return lineContent; + } + const line = wrapComment ? createWrapper(lineContent) : lineContent; + if (includesClosingTag) { + wrapComment = false; + } + return line; + }) + .join('\n'); +}; 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 ed87a202b15..f819a9e5be2 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 @@ -5,6 +5,7 @@ import eventHub from '~/notes/event_hub'; import languageLoader from '~/content_editor/services/highlight_js_language_loader'; import { ROUGE_TO_HLJS_LANGUAGE_MAP, LINES_PER_CHUNK } from './constants'; import Chunk from './components/chunk.vue'; +import { registerPlugins } from './plugins/index'; /* * This component is optimized to handle source code with many lines of code by splitting source code into chunks of 70 lines of code, @@ -111,6 +112,7 @@ export default { let detectedLanguage = language; let highlightedContent; if (this.hljs) { + registerPlugins(this.hljs); if (!detectedLanguage) { const hljsHighlightAuto = this.hljs.highlightAuto(content); highlightedContent = hljsHighlightAuto.value; diff --git a/app/assets/javascripts/vue_shared/components/upload_dropzone/upload_dropzone.vue b/app/assets/javascripts/vue_shared/components/upload_dropzone/upload_dropzone.vue index f62bf686f85..424cab20c7e 100644 --- a/app/assets/javascripts/vue_shared/components/upload_dropzone/upload_dropzone.vue +++ b/app/assets/javascripts/vue_shared/components/upload_dropzone/upload_dropzone.vue @@ -149,7 +149,7 @@ export default { > <slot> <button - class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-3" + class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-4" type="button" @click="openFileUpload" > @@ -192,7 +192,7 @@ export default { <transition name="upload-dropzone-fade"> <div v-show="dragging && !enableDragBehavior" - class="card upload-dropzone-border upload-dropzone-overlay gl-w-full gl-h-full gl-absolute gl-display-flex gl-align-items-center gl-justify-content-center gl-p-3 gl-bg-white" + class="card upload-dropzone-border upload-dropzone-overlay gl-w-full gl-h-full gl-absolute gl-display-flex gl-align-items-center gl-justify-content-center gl-p-4" > <div v-show="!isDragDataValid" class="mw-50 gl-text-center"> <slot name="invalid-drag-data-slot"> diff --git a/app/assets/javascripts/vue_shared/components/usage_quotas/usage_banner.vue b/app/assets/javascripts/vue_shared/components/usage_quotas/usage_banner.vue index bc5e0cf10dd..20a666509a4 100644 --- a/app/assets/javascripts/vue_shared/components/usage_quotas/usage_banner.vue +++ b/app/assets/javascripts/vue_shared/components/usage_quotas/usage_banner.vue @@ -34,7 +34,7 @@ export default { <div class="gl-display-flex gl-flex-direction-column gl-xs-mb-3 gl-min-w-0 gl-flex-grow-1"> <div v-if="$slots['left-primary-text']" - class="gl-display-flex gl-align-items-center gl-text-body gl-font-weight-bold gl-min-h-6 gl-min-w-0 gl-mb-4" + class="gl-display-flex gl-align-items-center gl-text-body gl-font-weight-bold gl-min-h-6 gl-min-w-0" > <slot name="left-primary-text"></slot> </div> diff --git a/app/assets/javascripts/vue_shared/components/user_popover/constants.js b/app/assets/javascripts/vue_shared/components/user_popover/constants.js new file mode 100644 index 00000000000..1d49aefd297 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/user_popover/constants.js @@ -0,0 +1 @@ +export const USER_POPOVER_DELAY = 200; 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 ec7a7cd72ae..768cd005727 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,12 +14,14 @@ import { glEmojiTag } from '~/emoji'; import createFlash from '~/flash'; import { followUser, unfollowUser } from '~/rest_api'; import UserAvatarImage from '../user_avatar/user_avatar_image.vue'; +import { USER_POPOVER_DELAY } from './constants'; const MAX_SKELETON_LINES = 4; export default { name: 'UserPopover', maxSkeletonLines: MAX_SKELETON_LINES, + USER_POPOVER_DELAY, components: { GlIcon, GlLink, @@ -48,6 +50,11 @@ export default { required: false, default: 'top', }, + show: { + type: Boolean, + required: false, + default: false, + }, }, data() { return { @@ -133,25 +140,21 @@ export default { </script> <template> - <!-- 200ms delay so not every mouseover triggers Popover --> - <gl-popover :target="target" :delay="200" :placement="placement" boundary="viewport"> - <div class="gl-p-3 gl-line-height-normal gl-display-flex" data-testid="user-popover"> - <div - class="gl-p-2 flex-shrink-1 gl-display-flex gl-flex-direction-column align-items-center gl-w-70p" - > + <!-- Delayed so not every mouseover triggers Popover --> + <gl-popover + :css-classes="['gl-max-w-48']" + :show="show" + :target="target" + :delay="$options.USER_POPOVER_DELAY" + :placement="placement" + boundary="viewport" + triggers="hover focus manual" + > + <div class="gl-py-3 gl-line-height-normal gl-display-flex" data-testid="user-popover"> + <div class="gl-mr-4 gl-flex-shrink-0"> <user-avatar-image :img-src="user.avatarUrl" :size="64" css-classes="gl-m-0!" /> - <div v-if="shouldRenderToggleFollowButton" class="gl-mt-3"> - <gl-button - :variant="toggleFollowButtonVariant" - :loading="toggleFollowLoading" - size="small" - data-testid="toggle-follow-button" - @click="toggleFollow" - >{{ toggleFollowButtonText }}</gl-button - > - </div> </div> - <div class="gl-w-full gl-min-w-0 gl-word-break-word"> + <div class="gl-w-full gl-word-break-word gl-display-flex gl-align-items-center"> <template v-if="userIsLoading"> <gl-skeleton-loader :lines="$options.maxSkeletonLines" @@ -161,7 +164,7 @@ export default { /> </template> <template v-else> - <div class="gl-mb-3"> + <div> <h5 class="gl-m-0"> <user-name-with-status :name="user.name" @@ -170,42 +173,64 @@ export default { /> </h5> <span class="gl-text-gray-500">@{{ user.username }}</span> - </div> - <div class="gl-text-gray-500"> - <div v-if="user.bio" class="gl-display-flex gl-mb-2"> - <gl-icon name="profile" class="gl-flex-shrink-0" /> - <span ref="bio" class="gl-ml-2">{{ user.bio }}</span> + <div v-if="shouldRenderToggleFollowButton" class="gl-mt-3"> + <gl-button + :variant="toggleFollowButtonVariant" + :loading="toggleFollowLoading" + size="small" + data-testid="toggle-follow-button" + @click="toggleFollow" + >{{ toggleFollowButtonText }}</gl-button + > </div> - <div v-if="user.workInformation" class="gl-display-flex gl-mb-2"> - <gl-icon name="work" class="gl-flex-shrink-0" /> - <span ref="workInformation" class="gl-ml-2">{{ user.workInformation }}</span> - </div> - <div v-if="user.location" class="gl-display-flex gl-mb-2"> - <gl-icon name="location" class="gl-flex-shrink-0" /> - <span class="gl-ml-2">{{ user.location }}</span> - </div> - <div - v-if="user.localTime && !user.bot" - class="gl-display-flex gl-mb-2" - data-testid="user-popover-local-time" - > - <gl-icon name="clock" class="gl-flex-shrink-0" /> - <span class="gl-ml-2">{{ user.localTime }}</span> - </div> - </div> - <div v-if="statusHtml" class="gl-mb-2" data-testid="user-popover-status"> - <span v-safe-html:[$options.safeHtmlConfig]="statusHtml"></span> - </div> - <div v-if="user.bot && user.websiteUrl" class="gl-text-blue-500"> - <gl-icon name="question" /> - <gl-link data-testid="user-popover-bot-docs-link" :href="user.websiteUrl"> - <gl-sprintf :message="__('Learn more about %{username}')"> - <template #username>{{ user.name }}</template> - </gl-sprintf> - </gl-link> </div> </template> </div> </div> + <div class="gl-mt-2 gl-w-full gl-word-break-word"> + <template v-if="userIsLoading"> + <gl-skeleton-loader + :lines="$options.maxSkeletonLines" + preserve-aspect-ratio="none" + equal-width-lines + :height="24" + /> + </template> + <template v-else> + <div class="gl-text-gray-500"> + <div v-if="user.bio" class="gl-display-flex gl-mb-2"> + <gl-icon name="profile" class="gl-flex-shrink-0" /> + <span ref="bio" class="gl-ml-2">{{ user.bio }}</span> + </div> + <div v-if="user.workInformation" class="gl-display-flex gl-mb-2"> + <gl-icon name="work" class="gl-flex-shrink-0" /> + <span ref="workInformation" class="gl-ml-2">{{ user.workInformation }}</span> + </div> + <div v-if="user.location" class="gl-display-flex gl-mb-2"> + <gl-icon name="location" class="gl-flex-shrink-0" /> + <span class="gl-ml-2">{{ user.location }}</span> + </div> + <div + v-if="user.localTime && !user.bot" + class="gl-display-flex gl-mb-2" + data-testid="user-popover-local-time" + > + <gl-icon name="clock" class="gl-flex-shrink-0" /> + <span class="gl-ml-2">{{ user.localTime }}</span> + </div> + </div> + <div v-if="statusHtml" class="gl-mb-2" data-testid="user-popover-status"> + <span v-safe-html:[$options.safeHtmlConfig]="statusHtml"></span> + </div> + <div v-if="user.bot && user.websiteUrl" class="gl-text-blue-500"> + <gl-icon name="question" /> + <gl-link data-testid="user-popover-bot-docs-link" :href="user.websiteUrl"> + <gl-sprintf :message="__('Learn more about %{username}')"> + <template #username>{{ user.name }}</template> + </gl-sprintf> + </gl-link> + </div> + </template> + </div> </gl-popover> </template> 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 15f84e48179..cac0d5a45c9 100644 --- a/app/assets/javascripts/vue_shared/components/web_ide_link.vue +++ b/app/assets/javascripts/vue_shared/components/web_ide_link.vue @@ -307,7 +307,7 @@ export default { <actions-button :actions="actions" :selected-key="selection" - :variant="isBlob ? 'info' : 'default'" + :variant="isBlob ? 'confirm' : 'default'" :category="isBlob ? 'primary' : 'secondary'" @select="select" /> |