diff options
Diffstat (limited to 'app/assets/javascripts/vue_shared/components')
80 files changed, 1611 insertions, 573 deletions
diff --git a/app/assets/javascripts/vue_shared/components/actions_button.vue b/app/assets/javascripts/vue_shared/components/actions_button.vue new file mode 100644 index 00000000000..f333ab49ead --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/actions_button.vue @@ -0,0 +1,90 @@ +<script> +import { + GlDropdown, + GlDropdownItem, + GlDropdownDivider, + GlLink, + GlTooltipDirective, +} from '@gitlab/ui'; + +export default { + components: { + GlDropdown, + GlDropdownItem, + GlDropdownDivider, + GlLink, + }, + directives: { + GlTooltip: GlTooltipDirective, + }, + props: { + actions: { + type: Array, + required: true, + }, + selectedKey: { + type: String, + required: false, + default: '', + }, + }, + computed: { + hasMultipleActions() { + return this.actions.length > 1; + }, + selectedAction() { + return this.actions.find(x => x.key === this.selectedKey) || this.actions[0]; + }, + }, + methods: { + handleItemClick(action) { + this.$emit('select', action.key); + }, + handleClick(action, evt) { + return action.handle?.(evt); + }, + }, +}; +</script> + +<template> + <gl-dropdown + v-if="hasMultipleActions" + v-gl-tooltip="selectedAction.tooltip" + class="gl-button-deprecated-adapter" + :text="selectedAction.text" + :split-href="selectedAction.href" + split + @click="handleClick(selectedAction, $event)" + > + <template slot="button-content"> + <span class="gl-new-dropdown-button-text" v-bind="selectedAction.attrs"> + {{ selectedAction.text }} + </span> + </template> + <template v-for="(action, index) in actions"> + <gl-dropdown-item + :key="action.key" + class="gl-dropdown-item-deprecated-adapter" + :is-check-item="true" + :is-checked="action.key === selectedAction.key" + :secondary-text="action.secondaryText" + :data-testid="`action_${action.key}`" + @click="handleItemClick(action)" + > + {{ action.text }} + </gl-dropdown-item> + <gl-dropdown-divider v-if="index != actions.length - 1" :key="action.key + '_divider'" /> + </template> + </gl-dropdown> + <gl-link + v-else-if="selectedAction" + v-gl-tooltip="selectedAction.tooltip" + v-bind="selectedAction.attrs" + class="btn" + :href="selectedAction.href" + @click="handleClick(selectedAction, $event)" + > + {{ selectedAction.text }} + </gl-link> +</template> diff --git a/app/assets/javascripts/vue_shared/components/alert_details_table.vue b/app/assets/javascripts/vue_shared/components/alert_details_table.vue new file mode 100644 index 00000000000..c94e784c01e --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/alert_details_table.vue @@ -0,0 +1,70 @@ +<script> +import { GlLoadingIcon, GlTable } from '@gitlab/ui'; +import { s__ } from '~/locale'; +import { + capitalizeFirstCharacter, + convertToSentenceCase, + splitCamelCase, +} from '~/lib/utils/text_utility'; + +const thClass = 'gl-bg-transparent! gl-border-1! gl-border-b-solid! gl-border-gray-200!'; +const tdClass = 'gl-border-gray-100! gl-p-5!'; + +export default { + components: { + GlLoadingIcon, + GlTable, + }, + props: { + alert: { + type: Object, + required: false, + default: null, + }, + loading: { + type: Boolean, + required: true, + }, + }, + fields: [ + { + key: 'fieldName', + label: s__('AlertManagement|Key'), + thClass, + tdClass, + formatter: string => capitalizeFirstCharacter(convertToSentenceCase(splitCamelCase(string))), + }, + { + key: 'value', + thClass: `${thClass} w-60p`, + tdClass, + label: s__('AlertManagement|Value'), + }, + ], + computed: { + items() { + if (!this.alert) { + return []; + } + return Object.entries(this.alert).map(([fieldName, value]) => ({ + fieldName, + value, + })); + }, + }, +}; +</script> +<template> + <gl-table + class="alert-management-details-table" + :busy="loading" + :empty-text="s__('AlertManagement|No alert data to display.')" + :items="items" + :fields="$options.fields" + show-empty + > + <template #table-busy> + <gl-loading-icon size="lg" color="dark" class="gl-mt-5" /> + </template> + </gl-table> +</template> diff --git a/app/assets/javascripts/vue_shared/components/awards_list.vue b/app/assets/javascripts/vue_shared/components/awards_list.vue index c0a42e08dee..e1f54b62223 100644 --- a/app/assets/javascripts/vue_shared/components/awards_list.vue +++ b/app/assets/javascripts/vue_shared/components/awards_list.vue @@ -1,4 +1,5 @@ <script> +/* eslint-disable vue/no-v-html */ import { groupBy } from 'lodash'; import { GlIcon } from '@gitlab/ui'; import tooltip from '~/vue_shared/directives/tooltip'; 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 52ce05f0d99..d0f5570db6b 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,4 +1,5 @@ <script> +import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui'; import MarkdownFieldView from '~/vue_shared/components/markdown/field_view.vue'; import ViewerMixin from './mixins'; import { handleBlobRichViewer } from '~/blob/viewer'; @@ -7,6 +8,9 @@ export default { components: { MarkdownFieldView, }, + directives: { + SafeHtml, + }, mixins: [ViewerMixin], mounted() { handleBlobRichViewer(this.$refs.content, this.type); @@ -14,5 +18,5 @@ export default { }; </script> <template> - <markdown-field-view ref="content" v-html="content" /> + <markdown-field-view ref="content" v-safe-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 55a6267f9ff..bbe72a2b122 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 @@ -1,4 +1,5 @@ <script> +/* eslint-disable vue/no-v-html */ import { GlIcon } from '@gitlab/ui'; import ViewerMixin from './mixins'; import { HIGHLIGHT_CLASS_NAME } from './constants'; diff --git a/app/assets/javascripts/vue_shared/components/changed_file_icon.vue b/app/assets/javascripts/vue_shared/components/changed_file_icon.vue index 7431b7e9ed4..f28e49df56e 100644 --- a/app/assets/javascripts/vue_shared/components/changed_file_icon.vue +++ b/app/assets/javascripts/vue_shared/components/changed_file_icon.vue @@ -1,12 +1,11 @@ <script> -import { GlTooltipDirective } from '@gitlab/ui'; -import Icon from '~/vue_shared/components/icon.vue'; +import { GlTooltipDirective, GlIcon } from '@gitlab/ui'; import getCommitIconMap from '~/ide/commit_icon'; import { __ } from '~/locale'; export default { components: { - Icon, + GlIcon, }, directives: { GlTooltip: GlTooltipDirective, @@ -81,7 +80,7 @@ export default { :class="{ 'ml-auto': isCentered }" class="file-changed-icon d-inline-block" > - <icon v-if="showIcon" :name="changedIcon" :size="size" :class="changedIconClass" /> + <gl-icon v-if="showIcon" :name="changedIcon" :size="size" :class="changedIconClass" /> </span> </template> diff --git a/app/assets/javascripts/vue_shared/components/ci_badge_link.vue b/app/assets/javascripts/vue_shared/components/ci_badge_link.vue index 0e0bb8735b4..d7af3b3298e 100644 --- a/app/assets/javascripts/vue_shared/components/ci_badge_link.vue +++ b/app/assets/javascripts/vue_shared/components/ci_badge_link.vue @@ -39,6 +39,11 @@ export default { required: false, default: true, }, + iconClasses: { + type: String, + required: false, + default: '', + }, }, computed: { cssClass() { @@ -55,7 +60,7 @@ export default { :class="cssClass" :title="!showText ? status.text : ''" > - <ci-icon :status="status" /> + <ci-icon :status="status" :css-classes="iconClasses" /> <template v-if="showText"> {{ status.text }} diff --git a/app/assets/javascripts/vue_shared/components/ci_icon.vue b/app/assets/javascripts/vue_shared/components/ci_icon.vue index 890dbe86c0d..ff665d9cc58 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 './icon.vue'; +import { GlIcon } from '@gitlab/ui'; /** * Renders CI icon based on API response shared between all places where it is used. @@ -28,7 +28,7 @@ const validSizes = [8, 12, 16, 18, 24, 32, 48, 72]; export default { components: { - Icon, + GlIcon, }, props: { status: { @@ -66,5 +66,5 @@ export default { }; </script> <template> - <span :class="cssClass"> <icon :name="icon" :size="size" :class="cssClasses" /> </span> + <span :class="cssClass"> <gl-icon :name="icon" :size="size" :class="cssClasses" /> </span> </template> diff --git a/app/assets/javascripts/vue_shared/components/clone_dropdown.vue b/app/assets/javascripts/vue_shared/components/clone_dropdown.vue index 6f5ea8dcbee..5c6bd5892ae 100644 --- a/app/assets/javascripts/vue_shared/components/clone_dropdown.vue +++ b/app/assets/javascripts/vue_shared/components/clone_dropdown.vue @@ -1,7 +1,7 @@ <script> import { - GlNewDropdown, - GlNewDropdownHeader, + GlDropdown, + GlDropdownSectionHeader, GlFormInputGroup, GlButton, GlTooltipDirective, @@ -11,8 +11,8 @@ import { getHTTPProtocol } from '~/lib/utils/url_utility'; export default { components: { - GlNewDropdown, - GlNewDropdownHeader, + GlDropdown, + GlDropdownSectionHeader, GlFormInputGroup, GlButton, }, @@ -45,10 +45,10 @@ export default { }; </script> <template> - <gl-new-dropdown right :text="$options.labels.defaultLabel" category="primary" variant="info"> + <gl-dropdown right :text="$options.labels.defaultLabel" category="primary" variant="info"> <div class="pb-2 mx-1"> <template v-if="sshLink"> - <gl-new-dropdown-header>{{ $options.labels.ssh }}</gl-new-dropdown-header> + <gl-dropdown-section-header>{{ $options.labels.ssh }}</gl-dropdown-section-header> <div class="mx-3"> <gl-form-input-group :value="sshLink" readonly select-on-click> @@ -67,7 +67,7 @@ export default { </template> <template v-if="httpLink"> - <gl-new-dropdown-header>{{ httpLabel }}</gl-new-dropdown-header> + <gl-dropdown-section-header>{{ httpLabel }}</gl-dropdown-section-header> <div class="mx-3"> <gl-form-input-group :value="httpLink" readonly select-on-click> @@ -85,5 +85,5 @@ export default { </div> </template> </div> - </gl-new-dropdown> + </gl-dropdown> </template> diff --git a/app/assets/javascripts/vue_shared/components/commit.vue b/app/assets/javascripts/vue_shared/components/commit.vue index 23bea6c28b4..c1c8fb3a6e2 100644 --- a/app/assets/javascripts/vue_shared/components/commit.vue +++ b/app/assets/javascripts/vue_shared/components/commit.vue @@ -1,10 +1,9 @@ <script> import { isString, isEmpty } from 'lodash'; -import { GlTooltipDirective, GlLink } from '@gitlab/ui'; +import { GlTooltipDirective, GlLink, GlIcon } 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 './icon.vue'; export default { directives: { @@ -12,14 +11,14 @@ export default { }, components: { UserAvatarLink, - Icon, + GlIcon, GlLink, TooltipOnTruncate, }, props: { /** * Indicates the existence of a tag. - * Used to render the correct icon, if true will render `fa-tag` icon, + * Used to render the correct GlIcon, if true will render `tag` GlIcon, * if false will render a svg sprite fork icon */ tag: { @@ -141,9 +140,9 @@ export default { <div class="branch-commit cgray"> <template v-if="shouldShowRefInfo"> <div class="icon-container"> - <icon v-if="tag" name="tag" /> - <icon v-else-if="mergeRequestRef" name="git-merge" /> - <icon v-else name="branch" /> + <gl-icon v-if="tag" name="tag" /> + <gl-icon v-else-if="mergeRequestRef" name="git-merge" /> + <gl-icon v-else name="branch" /> </div> <gl-link @@ -163,7 +162,7 @@ export default { >{{ commitRef.name }}</gl-link > </template> - <icon name="commit" class="commit-icon js-commit-icon" /> + <gl-icon name="commit" class="commit-icon js-commit-icon" /> <gl-link :href="commitUrl" class="commit-sha mr-0">{{ shortSha }}</gl-link> diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue index 3bf629d4acb..f9d3d76e7f5 100644 --- a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue +++ b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue @@ -1,12 +1,11 @@ <script> -import { GlLink } from '@gitlab/ui'; -import Icon from '../../icon.vue'; +import { GlLink, GlIcon } from '@gitlab/ui'; import { numberToHumanSize } from '../../../../lib/utils/number_utils'; export default { components: { GlLink, - Icon, + GlIcon, }, props: { path: { @@ -52,7 +51,7 @@ export default { :download="fileName" target="_blank" > - <icon :size="16" name="download" class="float-left gl-mr-3" /> + <gl-icon :size="16" name="download" class="float-left gl-mr-3" /> {{ __('Download') }} </gl-link> </div> 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 f9b678e33cd..6bb05e59f6b 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,9 @@ <script> +/* eslint-disable vue/no-v-html */ import $ from 'jquery'; import '~/behaviors/markdown/render_gfm'; -import { GlSkeletonLoading } from '@gitlab/ui'; +import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui'; import { forEach, escape } from 'lodash'; import axios from '~/lib/utils/axios_utils'; import { __ } from '~/locale'; 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 3b6b0a91e97..a7e6438a935 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 @@ -1,11 +1,5 @@ <script> -import { - GlIcon, - GlButton, - GlDeprecatedDropdown, - GlDeprecatedDropdownItem, - GlFormGroup, -} from '@gitlab/ui'; +import { GlIcon, GlButton, GlDropdown, GlDropdownItem, GlFormGroup } from '@gitlab/ui'; import { __, sprintf } from '~/locale'; import { convertToFixedRange, isEqualTimeRanges, findTimeRange } from '~/lib/utils/datetime_range'; @@ -29,8 +23,8 @@ export default { components: { GlIcon, GlButton, - GlDeprecatedDropdown, - GlDeprecatedDropdownItem, + GlDropdown, + GlDropdownItem, GlFormGroup, TooltipOnTruncate, DateTimePickerInput, @@ -212,7 +206,7 @@ export default { placement="top" class="d-inline-block" > - <gl-deprecated-dropdown + <gl-dropdown ref="dropdown" :text="timeWindowText" v-bind="$attrs" @@ -228,15 +222,15 @@ export default { <gl-icon class="gl-dropdown-caret" name="chevron-down" aria-hidden="true" /> </template> - <div class="d-flex justify-content-between gl-p-2-deprecated-no-really-do-not-use-me"> + <div class="d-flex justify-content-between gl-p-2"> <gl-form-group v-if="customEnabled" :label="customLabel" label-for="custom-from-time" - 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" + label-class="gl-pb-2" + class="custom-time-range-form-group col-md-7 gl-pl-2 gl-pr-0 m-0" > - <div class="gl-pt-2-deprecated-no-really-do-not-use-me"> + <div class="gl-pt-3"> <date-time-picker-input id="custom-time-from" v-model="startInput" @@ -264,15 +258,12 @@ export default { </gl-button> </gl-form-group> </gl-form-group> - <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" - > + <gl-form-group label-for="group-id-dropdown" class="col-md-5 gl-px-2 m-0"> <template #label> - <span class="gl-pl-5-deprecated-no-really-do-not-use-me">{{ __('Quick range') }}</span> + <span class="gl-pl-7">{{ __('Quick range') }}</span> </template> - <gl-deprecated-dropdown-item + <gl-dropdown-item v-for="(option, index) in options" :key="index" data-qa-selector="quick_range_item" @@ -286,9 +277,9 @@ export default { :class="{ invisible: !isOptionActive(option) }" /> {{ option.label }} - </gl-deprecated-dropdown-item> + </gl-dropdown-item> </gl-form-group> </div> - </gl-deprecated-dropdown> + </gl-dropdown> </tooltip-on-truncate> </template> diff --git a/app/assets/javascripts/vue_shared/components/dismissible_alert.vue b/app/assets/javascripts/vue_shared/components/dismissible_alert.vue index 986fa14349e..8494f99fd7d 100644 --- a/app/assets/javascripts/vue_shared/components/dismissible_alert.vue +++ b/app/assets/javascripts/vue_shared/components/dismissible_alert.vue @@ -1,4 +1,5 @@ <script> +/* eslint-disable vue/no-v-html */ import { GlAlert } from '@gitlab/ui'; export default { diff --git a/app/assets/javascripts/vue_shared/components/dropdown/dropdown_search_input.vue b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_search_input.vue index 610bce9a705..7157337f8f3 100644 --- a/app/assets/javascripts/vue_shared/components/dropdown/dropdown_search_input.vue +++ b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_search_input.vue @@ -41,12 +41,5 @@ export default { autocomplete="off" /> <i class="fa fa-search dropdown-input-search" aria-hidden="true" data-hidden="true"> </i> - <i - class="fa fa-times dropdown-input-clear js-dropdown-input-clear" - aria-hidden="true" - data-hidden="true" - role="button" - > - </i> </div> </template> diff --git a/app/assets/javascripts/vue_shared/components/droplab_dropdown_button.vue b/app/assets/javascripts/vue_shared/components/droplab_dropdown_button.vue index e1f336f5250..4d85726065b 100644 --- a/app/assets/javascripts/vue_shared/components/droplab_dropdown_button.vue +++ b/app/assets/javascripts/vue_shared/components/droplab_dropdown_button.vue @@ -1,10 +1,9 @@ <script> -import { GlDeprecatedButton } from '@gitlab/ui'; -import Icon from './icon.vue'; +import { GlDeprecatedButton, GlIcon } from '@gitlab/ui'; export default { components: { - Icon, + GlIcon, GlDeprecatedButton, }, props: { @@ -73,7 +72,7 @@ export default { data-display="static" data-toggle="dropdown" > - <icon name="chevron-down" :aria-label="__('toggle dropdown')" /> + <gl-icon name="chevron-down" :aria-label="__('toggle dropdown')" /> </button> <ul :class="dropdownClass" class="dropdown-menu dropdown-open-top"> <template v-for="(action, index) in actions"> diff --git a/app/assets/javascripts/vue_shared/components/file_finder/index.vue b/app/assets/javascripts/vue_shared/components/file_finder/index.vue index d6f591ccca1..012aca8105a 100644 --- a/app/assets/javascripts/vue_shared/components/file_finder/index.vue +++ b/app/assets/javascripts/vue_shared/components/file_finder/index.vue @@ -2,6 +2,7 @@ import fuzzaldrinPlus from 'fuzzaldrin-plus'; import Mousetrap from 'mousetrap'; import VirtualList from 'vue-virtual-scroll-list'; +import { GlIcon } from '@gitlab/ui'; import Item from './item.vue'; import { UP_KEY_CODE, DOWN_KEY_CODE, ENTER_KEY_CODE, ESC_KEY_CODE } from '~/lib/utils/keycodes'; @@ -9,10 +10,11 @@ export const MAX_FILE_FINDER_RESULTS = 40; export const FILE_FINDER_ROW_HEIGHT = 55; export const FILE_FINDER_EMPTY_ROW_HEIGHT = 33; -const originalStopCallback = Mousetrap.stopCallback; +const originalStopCallback = Mousetrap.prototype.stopCallback; export default { components: { + GlIcon, Item, VirtualList, }, @@ -126,7 +128,7 @@ export default { this.focusedIndex = 0; } - Mousetrap.bind(['t', 'command+p', 'ctrl+p'], e => { + Mousetrap.bind(['t', 'mod+p'], e => { if (e.preventDefault) { e.preventDefault(); } @@ -134,7 +136,18 @@ export default { this.toggle(!this.visible); }); - Mousetrap.stopCallback = (e, el, combo) => this.mousetrapStopCallback(e, el, combo); + Mousetrap.prototype.stopCallback = function customStopCallback(e, el, combo) { + if ( + (combo === 't' && el.classList.contains('dropdown-input-field')) || + el.classList.contains('inputarea') + ) { + return true; + } else if (combo === 'mod+p') { + return false; + } + + return originalStopCallback.call(this, e, el, combo); + }; }, methods: { toggle(visible) { @@ -199,18 +212,6 @@ export default { this.cancelMouseOver = false; this.onMouseOver(index); }, - mousetrapStopCallback(e, el, combo) { - if ( - (combo === 't' && el.classList.contains('dropdown-input-field')) || - el.classList.contains('inputarea') - ) { - return true; - } else if (combo === 'command+p' || combo === 'ctrl+p') { - return false; - } - - return originalStopCallback(e, el, combo); - }, }, }; </script> @@ -236,12 +237,13 @@ export default { aria-hidden="true" class="fa fa-search dropdown-input-search" ></i> - <i - :aria-label="__('Clear search input')" + <gl-icon + name="close" + class="dropdown-input-clear" role="button" - class="fa fa-times dropdown-input-clear" + :aria-label="__('Clear search input')" @click="clearSearchInput" - ></i> + /> </div> <div> <virtual-list ref="virtualScrollList" :size="listHeight" :remain="listShowCount" wtag="ul"> 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 79c62cd9938..4c496ba3f9b 100644 --- a/app/assets/javascripts/vue_shared/components/file_finder/item.vue +++ b/app/assets/javascripts/vue_shared/components/file_finder/item.vue @@ -1,6 +1,6 @@ <script> import fuzzaldrinPlus from 'fuzzaldrin-plus'; -import Icon from '~/vue_shared/components/icon.vue'; +import { GlIcon } from '@gitlab/ui'; import FileIcon from '../file_icon.vue'; import ChangedFileIcon from '../changed_file_icon.vue'; @@ -8,7 +8,7 @@ const MAX_PATH_LENGTH = 60; export default { components: { - Icon, + GlIcon, ChangedFileIcon, FileIcon, }, @@ -103,10 +103,10 @@ export default { <span v-if="file.changed || file.tempFile" v-once class="diff-changed-stats"> <span v-if="showDiffStats"> <span class="cgreen bold"> - <icon name="file-addition" class="align-text-top" /> {{ file.addedLines }} + <gl-icon name="file-addition" class="align-text-top" /> {{ file.addedLines }} </span> <span class="cred bold ml-1"> - <icon name="file-deletion" class="align-text-top" /> {{ file.removedLines }} + <gl-icon name="file-deletion" class="align-text-top" /> {{ file.removedLines }} </span> </span> <changed-file-icon v-else :file="file" /> diff --git a/app/assets/javascripts/vue_shared/components/file_row.vue b/app/assets/javascripts/vue_shared/components/file_row.vue index 0952e37e46e..c1c4f437dee 100644 --- a/app/assets/javascripts/vue_shared/components/file_row.vue +++ b/app/assets/javascripts/vue_shared/components/file_row.vue @@ -14,10 +14,20 @@ export default { type: Object, required: true, }, + fileUrl: { + type: String, + required: false, + default: '', + }, level: { type: Number, required: true, }, + fileClasses: { + type: String, + required: false, + default: '', + }, }, computed: { isTree() { @@ -43,6 +53,9 @@ export default { // don't output a title if we don't have the expanded path return this.file?.tree?.length ? this.file.tree[0].parentPath : false; }, + fileRouterUrl() { + return this.fileUrl || `/project${this.file.url}`; + }, }, watch: { 'file.active': function fileActiveWatch(active) { @@ -69,7 +82,7 @@ export default { this.toggleTreeOpen(this.file.path); } - if (this.$router) this.$router.push(`/project${this.file.url}`); + if (this.$router && !this.hasUrlAtCurrentRoute()) this.$router.push(this.fileRouterUrl); if (this.isBlob) this.clickedFile(this.file.path); }, @@ -99,7 +112,7 @@ export default { hasUrlAtCurrentRoute() { if (!this.$router || !this.$router.currentRoute) return true; - return this.$router.currentRoute.path === `/project${escapeFileUrl(this.file.url)}`; + return this.$router.currentRoute.path === escapeFileUrl(this.fileRouterUrl); }, }, }; @@ -123,6 +136,7 @@ export default { :style="levelIndentation" class="file-row-name str-truncated" data-qa-selector="file_name_content" + :class="fileClasses" > <file-icon class="file-row-icon" 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 7b3d1d0afd6..3d8afd162cb 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,8 +1,11 @@ +/* eslint-disable @gitlab/require-i18n-strings */ import { __ } from '~/locale'; -export const ANY_AUTHOR = 'Any'; +const DEFAULT_LABEL_NO_LABEL = { value: 'No label', text: __('No label') }; +export const DEFAULT_LABEL_NONE = { value: 'None', text: __('None') }; +export const DEFAULT_LABEL_ANY = { value: 'Any', text: __('Any') }; -export const NO_LABEL = 'No label'; +export const DEFAULT_LABELS = [DEFAULT_LABEL_NO_LABEL]; export const DEBOUNCE_DELAY = 200; @@ -11,13 +14,11 @@ export const SortDirection = { ascending: 'ascending', }; -export const defaultMilestones = [ - // eslint-disable-next-line @gitlab/require-i18n-strings - { value: 'None', text: __('None') }, - // eslint-disable-next-line @gitlab/require-i18n-strings - { value: 'Any', text: __('Any') }, - // eslint-disable-next-line @gitlab/require-i18n-strings +export const DEFAULT_MILESTONES = [ + DEFAULT_LABEL_NONE, + DEFAULT_LABEL_ANY, { value: 'Upcoming', text: __('Upcoming') }, - // eslint-disable-next-line @gitlab/require-i18n-strings { value: 'Started', text: __('Started') }, ]; + +/* eslint-enable @gitlab/require-i18n-strings */ diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue index ee293d37b66..25478ad6f4f 100644 --- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue +++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue @@ -3,8 +3,8 @@ import { GlFilteredSearch, GlButtonGroup, GlButton, - GlNewDropdown as GlDropdown, - GlNewDropdownItem as GlDropdownItem, + GlDropdown, + GlDropdownItem, GlTooltipDirective, } from '@gitlab/ui'; @@ -15,7 +15,7 @@ import { deprecatedCreateFlash as createFlash } from '~/flash'; import RecentSearchesStore from '~/filtered_search/stores/recent_searches_store'; import RecentSearchesService from '~/filtered_search/services/recent_searches_service'; -import { stripQuotes } from './filtered_search_utils'; +import { stripQuotes, uniqueTokens } from './filtered_search_utils'; import { SortDirection } from './constants'; export default { @@ -120,10 +120,31 @@ export default { ? __('Sort direction: Ascending') : __('Sort direction: Descending'); }, + /** + * This prop fixes a behaviour affecting GlFilteredSearch + * where selecting duplicate token values leads to history + * dropdown also showing that selection. + */ filteredRecentSearches() { - return this.recentSearchesStorageKey - ? this.recentSearches.filter(item => typeof item !== 'string') - : undefined; + if (this.recentSearchesStorageKey) { + const knownItems = []; + return this.recentSearches.reduce((historyItems, item) => { + // Only include non-string history items (discard items from legacy search) + if (typeof item !== 'string') { + const sanitizedItem = uniqueTokens(item); + const itemString = JSON.stringify(sanitizedItem); + // Only include items which aren't already part of history + if (!knownItems.includes(itemString)) { + historyItems.push(sanitizedItem); + // We're storing string for comparision as doing direct object compare + // won't work due to object reference not being the same. + knownItems.push(itemString); + } + } + return historyItems; + }, []); + } + return undefined; }, }, watch: { @@ -245,12 +266,14 @@ export default { this.recentSearchesService.save(resultantSearches); this.recentSearches = []; }, - handleFilterSubmit(filters) { + handleFilterSubmit() { + const filterTokens = uniqueTokens(this.filterValue); + this.filterValue = filterTokens; if (this.recentSearchesStorageKey) { this.recentSearchesPromise .then(() => { - if (filters.length) { - const resultantSearches = this.recentSearchesStore.addRecentSearch(filters); + if (filterTokens.length) { + const resultantSearches = this.recentSearchesStore.addRecentSearch(filterTokens); this.recentSearchesService.save(resultantSearches); this.recentSearches = resultantSearches; } @@ -260,7 +283,7 @@ export default { }); } this.blurSearchInput(); - this.$emit('onFilter', this.removeQuotesEnclosure(filters)); + this.$emit('onFilter', this.removeQuotesEnclosure(filterTokens)); }, }, }; diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_utils.js b/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_utils.js index 85f7f746b49..e7d7b7d9f1b 100644 --- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_utils.js +++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_utils.js @@ -1,4 +1,164 @@ -// eslint-disable-next-line import/prefer-default-export -export const stripQuotes = value => { - return value.includes(' ') ? value.slice(1, -1) : value; +import { isEmpty } from 'lodash'; +import { queryToObject } from '~/lib/utils/url_utility'; + +/** + * Strips enclosing quotations from a string if it has one. + * + * @param {String} value String to strip quotes from + * + * @returns {String} String without any enclosure + */ +export const stripQuotes = value => value.replace(/^('|")(.*)('|")$/, '$2'); + +/** + * This method removes duplicate tokens from tokens array. + * + * @param {Array} tokens Array of tokens as defined by `GlFilteredSearch` + * + * @returns {Array} Unique array of tokens + */ +export const uniqueTokens = tokens => { + const knownTokens = []; + return tokens.reduce((uniques, token) => { + if (typeof token === 'object' && token.type !== 'filtered-search-term') { + const tokenString = `${token.type}${token.value.operator}${token.value.data}`; + if (!knownTokens.includes(tokenString)) { + uniques.push(token); + knownTokens.push(tokenString); + } + } else { + uniques.push(token); + } + return uniques; + }, []); }; + +/** + * Creates a token from a type and a filter. Example returned object + * { type: 'myType', value: { data: 'myData', operator: '= '} } + * @param {String} type the name of the filter + * @param {Object} + * @param {Object.value} filter value to be returned as token data + * @param {Object.operator} filter operator to be retuned as token operator + * @return {Object} + * @return {Object.type} token type + * @return {Object.value} token value + */ +function createToken(type, filter) { + return { type, value: { data: filter.value, operator: filter.operator } }; +} + +/** + * This function takes a filter object and translates it into a token array + * @param {Object} filters + * @param {Object.myFilterName} a single filter value or an array of filters + * @return {Array} tokens an array of tokens created from filter values + */ +export function prepareTokens(filters = {}) { + return Object.keys(filters).reduce((memo, key) => { + const value = filters[key]; + if (!value) { + return memo; + } + if (Array.isArray(value)) { + return [...memo, ...value.map(filterValue => createToken(key, filterValue))]; + } + + return [...memo, createToken(key, value)]; + }, []); +} + +export function processFilters(filters) { + return filters.reduce((acc, token) => { + const { type, value } = token; + const { operator } = value; + const tokenValue = value.data; + + if (!acc[type]) { + acc[type] = []; + } + + acc[type].push({ value: tokenValue, operator }); + return acc; + }, {}); +} + +/** + * This function takes a filter object and maps it into a query object. Example filter: + * { myFilterName: { value: 'foo', operator: '=' } } + * gets translated into: + * { myFilterName: 'foo', 'not[myFilterName]': null } + * @param {Object} filters + * @param {Object.myFilterName} a single filter value or an array of filters + * @return {Object} query object with both filter name and not-name with values + */ +export function filterToQueryObject(filters = {}) { + return Object.keys(filters).reduce((memo, key) => { + const filter = filters[key]; + + let selected; + let unselected; + if (Array.isArray(filter)) { + selected = filter.filter(item => item.operator === '=').map(item => item.value); + unselected = filter.filter(item => item.operator === '!=').map(item => item.value); + } else { + selected = filter?.operator === '=' ? filter.value : null; + unselected = filter?.operator === '!=' ? filter.value : null; + } + + if (isEmpty(selected)) { + selected = null; + } + if (isEmpty(unselected)) { + unselected = null; + } + + return { ...memo, [key]: selected, [`not[${key}]`]: unselected }; + }, {}); +} + +/** + * Extracts filter name from url name, e.g. `not[my_filter]` => `my_filter` + * and returns the operator with it depending on the filter name + * @param {String} filterName from url + * @return {Object} + * @return {Object.filterName} extracted filtern ame + * @return {Object.operator} `=` or `!=` + */ +function extractNameAndOperator(filterName) { + // eslint-disable-next-line @gitlab/require-i18n-strings + if (filterName.startsWith('not[') && filterName.endsWith(']')) { + return { filterName: filterName.slice(4, -1), operator: '!=' }; + } + + return { filterName, operator: '=' }; +} + +/** + * This function takes a URL query string and maps it into a filter object. Example query string: + * '?myFilterName=foo' + * gets translated into: + * { myFilterName: { value: 'foo', operator: '=' } } + * @param {String} query URL quert string, e.g. from `window.location.search` + * @return {Object} filter object with filter names and their values + */ +export function urlQueryToFilter(query = '') { + const filters = queryToObject(query, { gatherArrays: true }); + return Object.keys(filters).reduce((memo, key) => { + const value = filters[key]; + if (!value) { + return memo; + } + const { filterName, operator } = extractNameAndOperator(key); + let previousValues = []; + if (Array.isArray(memo[filterName])) { + previousValues = memo[filterName]; + } + if (Array.isArray(value)) { + const newAdditions = value.filter(Boolean).map(item => ({ value: item, operator })); + return { ...memo, [filterName]: [...previousValues, ...newAdditions] }; + } + + return { ...memo, [filterName]: { value, operator } }; + }, {}); +} diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/author_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/author_token.vue index 969e914ef0c..ee0e00b0f5d 100644 --- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/author_token.vue +++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/author_token.vue @@ -3,7 +3,7 @@ import { GlFilteredSearchToken, GlAvatar, GlFilteredSearchSuggestion, - GlDeprecatedDropdownDivider, + GlDropdownDivider, GlLoadingIcon, } from '@gitlab/ui'; import { debounce } from 'lodash'; @@ -11,15 +11,14 @@ import { debounce } from 'lodash'; import { deprecatedCreateFlash as createFlash } from '~/flash'; import { __ } from '~/locale'; -import { ANY_AUTHOR, DEBOUNCE_DELAY } from '../constants'; +import { DEFAULT_LABEL_ANY, DEBOUNCE_DELAY } from '../constants'; export default { - anyAuthor: ANY_AUTHOR, components: { GlFilteredSearchToken, GlAvatar, GlFilteredSearchSuggestion, - GlDeprecatedDropdownDivider, + GlDropdownDivider, GlLoadingIcon, }, props: { @@ -35,6 +34,7 @@ export default { data() { return { authors: this.config.initialAuthors || [], + defaultAuthors: this.config.defaultAuthors || [DEFAULT_LABEL_ANY], loading: true, }; }, @@ -99,10 +99,14 @@ export default { <span>{{ activeAuthor ? activeAuthor.name : inputValue }}</span> </template> <template #suggestions> - <gl-filtered-search-suggestion :value="$options.anyAuthor"> - {{ __('Any') }} + <gl-filtered-search-suggestion + v-for="author in defaultAuthors" + :key="author.value" + :value="author.value" + > + {{ author.text }} </gl-filtered-search-suggestion> - <gl-deprecated-dropdown-divider /> + <gl-dropdown-divider v-if="defaultAuthors.length" /> <gl-loading-icon v-if="loading" /> <template v-else> <gl-filtered-search-suggestion diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/branch_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/branch_token.vue new file mode 100644 index 00000000000..c18bdfc5c20 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/branch_token.vue @@ -0,0 +1,115 @@ +<script> +import { + GlToken, + GlFilteredSearchToken, + GlFilteredSearchSuggestion, + GlDropdownDivider, + GlLoadingIcon, +} from '@gitlab/ui'; +import { debounce } from 'lodash'; + +import createFlash from '~/flash'; +import { __ } from '~/locale'; + +import { DEBOUNCE_DELAY } from '../constants'; + +export default { + components: { + GlToken, + GlFilteredSearchToken, + GlFilteredSearchSuggestion, + GlDropdownDivider, + GlLoadingIcon, + }, + props: { + config: { + type: Object, + required: true, + }, + value: { + type: Object, + required: true, + }, + }, + data() { + return { + branches: this.config.initialBranches || [], + defaultBranches: this.config.defaultBranches || [], + loading: true, + }; + }, + computed: { + currentValue() { + return this.value.data.toLowerCase(); + }, + activeBranch() { + return this.branches.find(branch => branch.name.toLowerCase() === this.currentValue); + }, + }, + watch: { + active: { + immediate: true, + handler(newValue) { + if (!newValue && !this.branches.length) { + this.fetchBranchBySearchTerm(this.value.data); + } + }, + }, + }, + methods: { + fetchBranchBySearchTerm(searchTerm) { + this.loading = true; + this.config + .fetchBranches(searchTerm) + .then(({ data }) => { + this.branches = data; + }) + .catch(() => createFlash({ message: __('There was a problem fetching branches.') })) + .finally(() => { + this.loading = false; + }); + }, + searchBranches: debounce(function debouncedSearch({ data }) { + this.fetchBranchBySearchTerm(data); + }, DEBOUNCE_DELAY), + }, +}; +</script> + +<template> + <gl-filtered-search-token + :config="config" + v-bind="{ ...$props, ...$attrs }" + v-on="$listeners" + @input="searchBranches" + > + <template #view-token="{ inputValue }"> + <gl-token variant="search-value">{{ + activeBranch ? activeBranch.name : inputValue + }}</gl-token> + </template> + <template #suggestions> + <gl-filtered-search-suggestion + v-for="branch in defaultBranches" + :key="branch.value" + :value="branch.value" + > + {{ branch.text }} + </gl-filtered-search-suggestion> + <gl-dropdown-divider v-if="defaultBranches.length" /> + <gl-loading-icon v-if="loading" /> + <template v-else> + <gl-filtered-search-suggestion + v-for="branch in branches" + :key="branch.id" + :value="branch.name" + > + <div class="gl-display-flex"> + <span class="gl-display-inline-block gl-mr-3 gl-p-3"></span> + <div>{{ branch.name }}</div> + </div> + </gl-filtered-search-suggestion> + </template> + </template> + </gl-filtered-search-token> +</template> 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 726a1c49993..7a9c5c277eb 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 @@ -3,7 +3,7 @@ import { GlToken, GlFilteredSearchToken, GlFilteredSearchSuggestion, - GlNewDropdownDivider as GlDropdownDivider, + GlDropdownDivider, GlLoadingIcon, } from '@gitlab/ui'; import { debounce } from 'lodash'; @@ -14,10 +14,9 @@ import { __ } from '~/locale'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import { stripQuotes } from '../filtered_search_utils'; -import { NO_LABEL, DEBOUNCE_DELAY } from '../constants'; +import { DEFAULT_LABELS, DEBOUNCE_DELAY } from '../constants'; export default { - noLabel: NO_LABEL, components: { GlToken, GlFilteredSearchToken, @@ -38,6 +37,7 @@ export default { data() { return { labels: this.config.initialLabels || [], + defaultLabels: this.config.defaultLabels || DEFAULT_LABELS, loading: true, }; }, @@ -105,10 +105,14 @@ export default { > </template> <template #suggestions> - <gl-filtered-search-suggestion :value="$options.noLabel">{{ - __('No label') - }}</gl-filtered-search-suggestion> - <gl-dropdown-divider /> + <gl-filtered-search-suggestion + v-for="label in defaultLabels" + :key="label.value" + :value="label.value" + > + {{ label.text }} + </gl-filtered-search-suggestion> + <gl-dropdown-divider v-if="defaultLabels.length" /> <gl-loading-icon v-if="loading" /> <template v-else> <gl-filtered-search-suggestion v-for="label in labels" :key="label.id" :value="label.title"> diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue index cf1ac4e718b..89952623d0d 100644 --- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue +++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue @@ -2,7 +2,7 @@ import { GlFilteredSearchToken, GlFilteredSearchSuggestion, - GlNewDropdownDivider as GlDropdownDivider, + GlDropdownDivider, GlLoadingIcon, } from '@gitlab/ui'; import { debounce } from 'lodash'; @@ -11,10 +11,9 @@ import createFlash from '~/flash'; import { __ } from '~/locale'; import { stripQuotes } from '../filtered_search_utils'; -import { defaultMilestones, DEBOUNCE_DELAY } from '../constants'; +import { DEFAULT_MILESTONES, DEBOUNCE_DELAY } from '../constants'; export default { - defaultMilestones, components: { GlFilteredSearchToken, GlFilteredSearchSuggestion, @@ -34,6 +33,7 @@ export default { data() { return { milestones: this.config.initialMilestones || [], + defaultMilestones: this.config.defaultMilestones || DEFAULT_MILESTONES, loading: true, }; }, @@ -89,12 +89,13 @@ export default { </template> <template #suggestions> <gl-filtered-search-suggestion - v-for="milestone in $options.defaultMilestones" + v-for="milestone in defaultMilestones" :key="milestone.value" :value="milestone.value" - >{{ milestone.text }}</gl-filtered-search-suggestion > - <gl-dropdown-divider /> + {{ milestone.text }} + </gl-filtered-search-suggestion> + <gl-dropdown-divider v-if="defaultMilestones.length" /> <gl-loading-icon v-if="loading" /> <template v-else> <gl-filtered-search-suggestion diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_dropdown.vue b/app/assets/javascripts/vue_shared/components/filtered_search_dropdown.vue deleted file mode 100644 index 58afcebb7b3..00000000000 --- a/app/assets/javascripts/vue_shared/components/filtered_search_dropdown.vue +++ /dev/null @@ -1,154 +0,0 @@ -<script> -import $ from 'jquery'; -import { GlDeprecatedButton } from '@gitlab/ui'; -import { __ } from '~/locale'; -import Icon from '~/vue_shared/components/icon.vue'; -/** - * Renders a split dropdown with - * an input that allows to search through the given - * array of options. - * - * When there are no results and `showCreateMode` is true - * it renders a create button with the value typed. - */ -export default { - name: 'FilteredSearchDropdown', - components: { - Icon, - GlDeprecatedButton, - }, - props: { - title: { - type: String, - required: false, - default: '', - }, - buttonType: { - required: false, - validator: value => - ['primary', 'default', 'secondary', 'success', 'info', 'warning', 'danger'].indexOf( - value, - ) !== -1, - default: 'default', - }, - size: { - required: false, - type: String, - default: 'sm', - }, - items: { - type: Array, - required: true, - }, - visibleItems: { - type: Number, - required: false, - default: 5, - }, - filterKey: { - type: String, - required: true, - }, - showCreateMode: { - type: Boolean, - required: false, - default: false, - }, - createButtonText: { - type: String, - required: false, - default: __('Create'), - }, - }, - data() { - return { - filter: '', - }; - }, - computed: { - className() { - return `btn btn-${this.buttonType} btn-${this.size}`; - }, - filteredResults() { - if (this.filter !== '') { - return this.items.filter( - item => - item[this.filterKey] && - item[this.filterKey].toLowerCase().includes(this.filter.toLowerCase()), - ); - } - - return this.items.slice(0, this.visibleItems); - }, - computedCreateButtonText() { - return `${this.createButtonText} ${this.filter}`; - }, - shouldRenderCreateButton() { - return this.showCreateMode && this.filteredResults.length === 0 && this.filter !== ''; - }, - }, - mounted() { - /** - * Resets the filter every time the user closes the dropdown - */ - $(this.$el) - .on('shown.bs.dropdown', () => { - this.$nextTick(() => this.$refs.searchInput.focus()); - }) - .on('hidden.bs.dropdown', () => { - this.filter = ''; - }); - }, -}; -</script> -<template> - <div class="dropdown"> - <div class="btn-group"> - <slot name="mainAction" :class-name="className"> - <button type="button" :class="className">{{ title }}</button> - </slot> - - <button - type="button" - :class="className" - class="dropdown-toggle dropdown-toggle-split" - data-toggle="dropdown" - aria-haspopup="true" - aria-expanded="false" - :aria-label="__('Expand dropdown')" - > - <icon name="angle-down" :size="12" /> - </button> - <div class="dropdown-menu dropdown-menu-right"> - <div class="dropdown-input"> - <input - ref="searchInput" - v-model="filter" - type="search" - :placeholder="__('Filter')" - class="js-filtered-dropdown-input dropdown-input-field" - /> - <icon class="dropdown-input-search" name="search" /> - </div> - - <div class="dropdown-content"> - <ul> - <li v-for="(result, i) in filteredResults" :key="i" class="js-filtered-dropdown-result"> - <slot name="result" :result="result">{{ result[filterKey] }}</slot> - </li> - </ul> - </div> - - <div v-if="shouldRenderCreateButton" class="dropdown-footer"> - <slot name="footer" :filter="filter"> - <gl-deprecated-button - class="js-dropdown-create-button btn-transparent" - @click="$emit('createItem', filter)" - >{{ computedCreateButtonText }}</gl-deprecated-button - > - </slot> - </div> - </div> - </div> - </div> -</template> diff --git a/app/assets/javascripts/vue_shared/components/gl_mentions.vue b/app/assets/javascripts/vue_shared/components/gl_mentions.vue index 00bc46257ed..da4b0aedef5 100644 --- a/app/assets/javascripts/vue_shared/components/gl_mentions.vue +++ b/app/assets/javascripts/vue_shared/components/gl_mentions.vue @@ -9,6 +9,7 @@ const AutoComplete = { Issues: 'issues', Labels: 'labels', Members: 'members', + MergeRequests: 'mergeRequests', }; function doesCurrentLineStartWith(searchString, fullText, selectionStart) { @@ -99,6 +100,14 @@ const autoCompleteMap = { ${icon}`; }, }, + [AutoComplete.MergeRequests]: { + filterValues() { + return this[AutoComplete.MergeRequests]; + }, + menuItemTemplate({ original }) { + return `<small>${original.reference || original.iid}</small> ${escape(original.title)}`; + }, + }, }; export default { @@ -139,6 +148,13 @@ export default { : `~${original.title}`, values: this.getValues(AutoComplete.Labels), }, + { + trigger: '!', + lookup: value => value.iid + value.title, + menuItemTemplate: autoCompleteMap[AutoComplete.MergeRequests].menuItemTemplate, + selectTemplate: ({ original }) => original.reference || `!${original.iid}`, + values: this.getValues(AutoComplete.MergeRequests), + }, ], }); diff --git a/app/assets/javascripts/vue_shared/components/header_ci_component.vue b/app/assets/javascripts/vue_shared/components/header_ci_component.vue index 2625fcc9d09..6ff6f10f786 100644 --- a/app/assets/javascripts/vue_shared/components/header_ci_component.vue +++ b/app/assets/javascripts/vue_shared/components/header_ci_component.vue @@ -1,4 +1,5 @@ <script> +/* eslint-disable vue/no-v-html */ import { GlTooltipDirective, GlLink, GlDeprecatedButton } from '@gitlab/ui'; import { __, sprintf } from '~/locale'; import CiIconBadge from './ci_badge_link.vue'; diff --git a/app/assets/javascripts/vue_shared/components/help_popover.vue b/app/assets/javascripts/vue_shared/components/help_popover.vue index a57fa09f753..7154360611f 100644 --- a/app/assets/javascripts/vue_shared/components/help_popover.vue +++ b/app/assets/javascripts/vue_shared/components/help_popover.vue @@ -1,6 +1,6 @@ <script> import $ from 'jquery'; -import Icon from '~/vue_shared/components/icon.vue'; +import { GlIcon } from '@gitlab/ui'; import { inserted } from '~/feature_highlight/feature_highlight_helper'; import { mouseenter, debouncedMouseleave, togglePopover } from '~/shared/popover'; @@ -11,7 +11,7 @@ import { mouseenter, debouncedMouseleave, togglePopover } from '~/shared/popover export default { name: 'HelpPopover', components: { - Icon, + GlIcon, }, props: { options: { @@ -44,6 +44,6 @@ export default { </script> <template> <button type="button" class="btn btn-blank btn-transparent btn-help" tabindex="0"> - <icon name="question" /> + <gl-icon name="question" /> </button> </template> diff --git a/app/assets/javascripts/vue_shared/components/icon.vue b/app/assets/javascripts/vue_shared/components/icon.vue deleted file mode 100644 index 68eeadf0f25..00000000000 --- a/app/assets/javascripts/vue_shared/components/icon.vue +++ /dev/null @@ -1,72 +0,0 @@ -<script> -import iconsPath from '@gitlab/svgs/dist/icons.svg'; - -// only allow classes in images.scss e.g. s12 -const validSizes = [8, 10, 12, 14, 16, 18, 24, 32, 48, 72]; -let iconValidator = () => true; - -/* - During development/tests we want to validate that we are just using icons that are actually defined -*/ -if (process.env.NODE_ENV !== 'production') { - // eslint-disable-next-line global-require - const data = require('@gitlab/svgs/dist/icons.json'); - const { icons } = data; - iconValidator = value => { - if (icons.includes(value)) { - return true; - } - // eslint-disable-next-line no-console - console.warn(`Icon '${value}' is not a known icon of @gitlab/gitlab-svg`); - return false; - }; -} - -/** This is a re-usable vue component for rendering a svg sprite icon - * @example - * <icon - * name="retry" - * :size="32" - * class="top" - * /> - */ -export default { - props: { - name: { - type: String, - required: true, - validator: iconValidator, - }, - - size: { - type: Number, - required: false, - default: 16, - validator: value => validSizes.includes(value), - }, - }, - - computed: { - spriteHref() { - return `${iconsPath}#${this.name}`; - }, - iconTestClass() { - return `ic-${this.name}`; - }, - iconSizeClass() { - return this.size ? `s${this.size}` : ''; - }, - }, -}; -</script> - -<template> - <svg - :key="spriteHref" - :class="[iconSizeClass, iconTestClass]" - aria-hidden="true" - v-on="$listeners" - > - <use v-bind="{ 'xlink:href': spriteHref }" /> - </svg> -</template> diff --git a/app/assets/javascripts/vue_shared/components/issue/issue_milestone.vue b/app/assets/javascripts/vue_shared/components/issue/issue_milestone.vue index cfbc5b0df3c..c745ea61f8b 100644 --- a/app/assets/javascripts/vue_shared/components/issue/issue_milestone.vue +++ b/app/assets/javascripts/vue_shared/components/issue/issue_milestone.vue @@ -1,13 +1,12 @@ <script> -import { GlTooltip } from '@gitlab/ui'; +import { GlTooltip, GlIcon } from '@gitlab/ui'; import { __, sprintf } from '~/locale'; import timeagoMixin from '~/vue_shared/mixins/timeago'; import { timeFor, parsePikadayDate, dateInWords } from '~/lib/utils/datetime_utility'; -import Icon from '~/vue_shared/components/icon.vue'; export default { components: { - Icon, + GlIcon, GlTooltip, }, mixins: [timeagoMixin], @@ -73,7 +72,7 @@ export default { </script> <template> <div ref="milestoneDetails" class="issue-milestone-details"> - <icon :size="16" class="inline icon" name="clock" /> + <gl-icon :size="16" class="gl-mr-2" name="clock" /> <span class="milestone-title d-inline-block">{{ milestone.title }}</span> <gl-tooltip :target="() => $refs.milestoneDetails" placement="bottom" class="js-item-milestone"> <span class="bold">{{ __('Milestone') }}</span> <br /> 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 1662e7923b7..2ff4033a07e 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,6 +1,7 @@ <script> +/* eslint-disable vue/no-v-html */ import '~/commons/bootstrap'; -import { GlIcon, GlTooltip, GlTooltipDirective } from '@gitlab/ui'; +import { GlIcon, GlTooltip, GlTooltipDirective, GlButton } from '@gitlab/ui'; import { sprintf } from '~/locale'; import IssueMilestone from './issue_milestone.vue'; import IssueAssignees from './issue_assignees.vue'; @@ -18,6 +19,7 @@ export default { GlTooltip, IssueWeight: () => import('ee_component/boards/components/issue_card_weight.vue'), IssueDueDate, + GlButton, }, directives: { GlTooltip: GlTooltipDirective, @@ -29,6 +31,16 @@ export default { required: false, default: false, }, + isLocked: { + type: Boolean, + required: false, + default: false, + }, + lockedMessage: { + type: String, + required: false, + default: '', + }, }, computed: { stateTitle() { @@ -156,19 +168,27 @@ export default { </div> </div> - <button - v-if="canRemove" + <span + v-if="isLocked" + ref="lockIcon" + v-gl-tooltip + class="gl-px-3 gl-display-inline-block gl-cursor-not-allowed" + :title="lockedMessage" + > + <gl-icon name="lock" /> + </span> + <gl-button + v-else-if="canRemove" ref="removeButton" v-gl-tooltip + icon="close" + category="tertiary" :disabled="removeDisabled" - type="button" - class="btn btn-default btn-svg btn-item-remove js-issue-item-remove-button" + class="js-issue-item-remove-button gl-ml-3" data-qa-selector="remove_related_issue_button" :title="__('Remove')" :aria-label="__('Remove')" @click="onRemoveRequest" - > - <icon :size="16" class="btn-item-remove-icon" name="close" /> - </button> + /> </div> </template> diff --git a/app/assets/javascripts/vue_shared/components/lib/utils/diff_utils.js b/app/assets/javascripts/vue_shared/components/lib/utils/diff_utils.js index 188ab1769a4..221c4f5b8a8 100644 --- a/app/assets/javascripts/vue_shared/components/lib/utils/diff_utils.js +++ b/app/assets/javascripts/vue_shared/components/lib/utils/diff_utils.js @@ -1,5 +1,3 @@ -/* eslint-disable import/prefer-default-export */ - function trimFirstCharOfLineContent(text) { if (!text) { return text; diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue index 6df0119c3db..a48c279d0e3 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/field.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue @@ -1,14 +1,15 @@ <script> +/* eslint-disable vue/no-v-html */ import $ from 'jquery'; import '~/behaviors/markdown/render_gfm'; import { unescape } from 'lodash'; +import { GlIcon } from '@gitlab/ui'; import { __, sprintf } from '~/locale'; import { stripHtml } from '~/lib/utils/text_utility'; import { deprecatedCreateFlash as Flash } from '~/flash'; import GLForm from '~/gl_form'; import MarkdownHeader from './header.vue'; import MarkdownToolbar from './toolbar.vue'; -import Icon from '../icon.vue'; import GlMentions from '~/vue_shared/components/gl_mentions.vue'; import Suggestions from '~/vue_shared/components/markdown/suggestions.vue'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; @@ -19,7 +20,7 @@ export default { GlMentions, MarkdownHeader, MarkdownToolbar, - Icon, + GlIcon, Suggestions, }, mixins: [glFeatureFlagsMixin()], @@ -168,11 +169,12 @@ export default { emojis: this.enableAutocomplete, members: this.enableAutocomplete && !this.glFeatures.tributeAutocomplete, issues: this.enableAutocomplete && !this.glFeatures.tributeAutocomplete, - mergeRequests: this.enableAutocomplete, + mergeRequests: this.enableAutocomplete && !this.glFeatures.tributeAutocomplete, epics: this.enableAutocomplete, milestones: this.enableAutocomplete, labels: this.enableAutocomplete && !this.glFeatures.tributeAutocomplete, snippets: this.enableAutocomplete, + vulnerabilities: this.enableAutocomplete, }); }, beforeDestroy() { @@ -254,7 +256,7 @@ export default { href="#" :aria-label="__('Leave zen mode')" > - <icon :size="16" name="screen-normal" /> + <gl-icon :size="16" name="minimize" /> </a> <markdown-toolbar :markdown-docs-path="markdownDocsPath" diff --git a/app/assets/javascripts/vue_shared/components/markdown/header.vue b/app/assets/javascripts/vue_shared/components/markdown/header.vue index 7e6edcfbd25..d0a0560846a 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/header.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/header.vue @@ -1,15 +1,15 @@ <script> import $ from 'jquery'; -import { GlPopover, GlButton, GlTooltipDirective } from '@gitlab/ui'; +import { GlPopover, GlButton, GlTooltipDirective, GlIcon } from '@gitlab/ui'; +import { s__ } from '~/locale'; import { getSelectedFragment } from '~/lib/utils/common_utils'; import { CopyAsGFM } from '../../../behaviors/markdown/copy_as_gfm'; import ToolbarButton from './toolbar_button.vue'; -import Icon from '../icon.vue'; export default { components: { ToolbarButton, - Icon, + GlIcon, GlPopover, GlButton, }, @@ -55,6 +55,15 @@ export default { mdSuggestion() { return ['```suggestion:-0+0', `{text}`, '```'].join('\n'); }, + isMac() { + // Accessing properties using ?. to allow tests to use + // this component without setting up window.gl.client. + // In production, window.gl.client should always be present. + return Boolean(window.gl?.client?.isMac); + }, + modifierKey() { + return this.isMac ? '⌘' : s__('KeyboardKey|Ctrl+'); + }, }, mounted() { $(document).on('markdown-preview:show.vue', this.previewMarkdownTab); @@ -129,8 +138,22 @@ export default { </li> <li :class="{ active: !previewMarkdown }" class="md-header-toolbar"> <div class="d-inline-block"> - <toolbar-button tag="**" :button-title="__('Add bold text')" icon="bold" /> - <toolbar-button tag="*" :button-title="__('Add italic text')" icon="italic" /> + <toolbar-button + tag="**" + :button-title=" + sprintf(s__('MarkdownEditor|Add bold text (%{modifierKey}B)'), { modifierKey }) + " + shortcuts="mod+b" + icon="bold" + /> + <toolbar-button + tag="_" + :button-title=" + sprintf(s__('MarkdownEditor|Add italic text (%{modifierKey}I)'), { modifierKey }) + " + shortcuts="mod+i" + icon="italic" + /> <toolbar-button :prepend="true" :tag="tag" @@ -181,7 +204,10 @@ export default { <toolbar-button tag="[{text}](url)" tag-select="url" - :button-title="__('Add a link')" + :button-title=" + sprintf(s__('MarkdownEditor|Add a link (%{modifierKey}K)'), { modifierKey }) + " + shortcuts="mod+k" icon="link" /> </div> @@ -221,7 +247,7 @@ export default { :title="__('Go full screen')" type="button" > - <icon name="screen-full" /> + <gl-icon name="maximize" /> </button> </div> </li> diff --git a/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue b/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue index 4de80e9b4c2..1fc54d2f52e 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue @@ -1,11 +1,10 @@ <script> -import { GlDeprecatedButton, GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui'; -import Icon from '~/vue_shared/components/icon.vue'; +import { GlButton, GlLoadingIcon, GlTooltipDirective, GlIcon } from '@gitlab/ui'; import { __ } from '~/locale'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; export default { - components: { Icon, GlDeprecatedButton, GlLoadingIcon }, + components: { GlIcon, GlButton, GlLoadingIcon }, directives: { 'gl-tooltip': GlTooltipDirective }, mixins: [glFeatureFlagsMixin()], props: { @@ -97,7 +96,7 @@ export default { <div class="qa-suggestion-diff-header js-suggestion-diff-header font-weight-bold"> {{ __('Suggested change') }} <a v-if="helpPagePath" :href="helpPagePath" :aria-label="__('Help')" class="js-help-btn"> - <icon name="question-o" css-classes="link-highlight" /> + <gl-icon name="question-o" css-classes="link-highlight" /> </a> </div> <div v-if="isApplied" class="badge badge-success">{{ __('Applied') }}</div> @@ -106,14 +105,14 @@ export default { <span>{{ applyingSuggestionsMessage }}</span> </div> <div v-else-if="canApply && canBeBatched && isBatched" class="d-flex align-items-center"> - <gl-deprecated-button + <gl-button class="btn-inverted js-remove-from-batch-btn btn-grouped" :disabled="isApplying" @click="removeSuggestionFromBatch" > {{ __('Remove from batch') }} - </gl-deprecated-button> - <gl-deprecated-button + </gl-button> + <gl-button v-gl-tooltip.viewport="__('This also resolves all related threads')" class="btn-inverted js-apply-batch-btn btn-grouped" :disabled="isApplying" @@ -124,26 +123,26 @@ export default { <span class="badge badge-pill badge-pill-success"> {{ batchSuggestionsCount }} </span> - </gl-deprecated-button> + </gl-button> </div> <div v-else class="d-flex align-items-center"> - <gl-deprecated-button + <gl-button v-if="canBeBatched && !isDisableButton" class="btn-inverted js-add-to-batch-btn btn-grouped" :disabled="isDisableButton" @click="addSuggestionToBatch" > {{ __('Add suggestion to batch') }} - </gl-deprecated-button> + </gl-button> <span v-gl-tooltip.viewport="tooltipMessage" tabindex="0"> - <gl-deprecated-button + <gl-button class="btn-inverted js-apply-btn btn-grouped" :disabled="isDisableButton" variant="success" @click="applySuggestion" > {{ __('Apply suggestion') }} - </gl-deprecated-button> + </gl-button> </span> </div> </div> diff --git a/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_row.vue b/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_row.vue index 112bd03b49b..9059f0d2a8b 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_row.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_row.vue @@ -1,4 +1,5 @@ <script> +/* eslint-disable vue/no-v-html */ export default { name: 'SuggestionDiffRow', props: { @@ -26,9 +27,14 @@ export default { <td class="diff-line-num new_line border-top-0 border-bottom-0" :class="lineType"> {{ line.new_line }} </td> - <td class="line_content" :class="[{ 'd-table-cell': displayAsCell }, lineType]"> - <span v-if="line.rich_text" v-html="line.rich_text"></span> - <span v-else-if="line.text">{{ line.text }}</span> + <td + class="line_content" + :class="[{ 'd-table-cell': displayAsCell }, lineType]" + data-testid="suggestion-diff-content" + > + <span v-if="line.rich_text" class="line" v-html="line.rich_text"></span> + <span v-else-if="line.text" class="line">{{ line.text }}</span> + <span v-else class="line"></span> </td> </tr> </template> diff --git a/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue b/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue index 1216484b35f..083f581af05 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue @@ -1,10 +1,14 @@ <script> import Vue from 'vue'; +import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui'; import { __ } from '~/locale'; import SuggestionDiff from './suggestion_diff.vue'; import { deprecatedCreateFlash as Flash } from '~/flash'; export default { + directives: { + SafeHtml, + }, props: { lineType: { type: String, @@ -115,6 +119,6 @@ export default { <template> <div> <div class="flash-container js-suggestions-flash"></div> - <div v-show="isRendered" ref="container" class="md" v-html="noteHtml"></div> + <div v-show="isRendered" ref="container" v-safe-html="noteHtml" class="md"></div> </div> </template> diff --git a/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue b/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue index f37dd9e171c..6c35741e7e5 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue @@ -1,10 +1,9 @@ <script> -import { GlTooltipDirective } from '@gitlab/ui'; -import Icon from '../icon.vue'; +import { GlTooltipDirective, GlIcon } from '@gitlab/ui'; export default { components: { - Icon, + GlIcon, }, directives: { GlTooltip: GlTooltipDirective, @@ -47,6 +46,26 @@ export default { required: false, default: 0, }, + + /** + * A string (or an array of strings) of + * [mousetrap](https://craig.is/killing/mice) keyboard shortcuts + * that should be attached to this button. For example: + * "command+k" + * ...or... + * ["command+k", "ctrl+k"] + */ + shortcuts: { + type: [String, Array], + required: false, + default: () => [], + }, + }, + computed: { + shortcutsString() { + const shortcutArray = Array.isArray(this.shortcuts) ? this.shortcuts : [this.shortcuts]; + return JSON.stringify(shortcutArray); + }, }, }; </script> @@ -60,6 +79,7 @@ export default { :data-md-block="tagBlock" :data-md-tag-content="tagContent" :data-md-prepend="prepend" + :data-md-shortcuts="shortcutsString" :title="buttonTitle" :aria-label="buttonTitle" type="button" @@ -67,6 +87,6 @@ export default { data-container="body" @click="() => $emit('click')" > - <icon :name="icon" /> + <gl-icon :name="icon" /> </button> </template> diff --git a/app/assets/javascripts/vue_shared/components/modal_copy_button.vue b/app/assets/javascripts/vue_shared/components/modal_copy_button.vue index 69ba5cb97e2..35ba7c665d5 100644 --- a/app/assets/javascripts/vue_shared/components/modal_copy_button.vue +++ b/app/assets/javascripts/vue_shared/components/modal_copy_button.vue @@ -1,14 +1,13 @@ <script> import $ from 'jquery'; -import { GlDeprecatedButton, GlTooltipDirective } from '@gitlab/ui'; +import { GlDeprecatedButton, GlTooltipDirective, GlIcon } from '@gitlab/ui'; import Clipboard from 'clipboard'; import { __ } from '~/locale'; -import Icon from '~/vue_shared/components/icon.vue'; export default { components: { GlDeprecatedButton, - Icon, + GlIcon, }, directives: { @@ -121,7 +120,7 @@ export default { :title="title" > <slot> - <icon name="copy-to-clipboard" /> + <gl-icon name="copy-to-clipboard" /> </slot> </gl-deprecated-button> </template> 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 f986b105f20..c12012d8419 100644 --- a/app/assets/javascripts/vue_shared/components/notes/noteable_warning.vue +++ b/app/assets/javascripts/vue_shared/components/notes/noteable_warning.vue @@ -1,8 +1,8 @@ <script> -import { GlLink } from '@gitlab/ui'; +/* eslint-disable vue/no-v-html */ +import { GlLink, GlIcon } from '@gitlab/ui'; import { escape } from 'lodash'; import { __, sprintf } from '~/locale'; -import icon from '../icon.vue'; function buildDocsLinkStart(path) { return `<a href="${escape(path)}" target="_blank" rel="noopener noreferrer">`; @@ -16,7 +16,7 @@ const NoteableTypeText = { export default { components: { - icon, + GlIcon, GlLink, }, props: { @@ -89,7 +89,7 @@ export default { </script> <template> <div class="issuable-note-warning"> - <icon v-if="!isLockedAndConfidential" :name="warningIcon" :size="16" class="icon inline" /> + <gl-icon v-if="!isLockedAndConfidential" :name="warningIcon" :size="16" class="icon inline" /> <span v-if="isLockedAndConfidential" ref="lockedAndConfidential"> <span v-html="confidentialAndLockedDiscussionText"></span> 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 e75ac8c54bc..53dbae39608 100644 --- a/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue +++ b/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue @@ -1,5 +1,5 @@ <script> -import { GlSkeletonLoading } from '@gitlab/ui'; +import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui'; import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue'; export default { 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 fe57d4f29ca..f30676e8ef3 100644 --- a/app/assets/javascripts/vue_shared/components/notes/system_note.vue +++ b/app/assets/javascripts/vue_shared/components/notes/system_note.vue @@ -1,4 +1,6 @@ <script> +/* eslint-disable vue/no-v-html */ + /** * Common component to render a system note, icon and user information. * @@ -18,10 +20,15 @@ */ import $ from 'jquery'; import { mapGetters, mapActions, mapState } from 'vuex'; -import { GlDeprecatedButton, GlSkeletonLoading, GlTooltipDirective } from '@gitlab/ui'; +import { + GlButton, + GlDeprecatedSkeletonLoading as GlSkeletonLoading, + GlTooltipDirective, + GlIcon, + GlSafeHtmlDirective as SafeHtml, +} from '@gitlab/ui'; import descriptionVersionHistoryMixin from 'ee_else_ce/notes/mixins/description_version_history'; import noteHeader from '~/notes/components/note_header.vue'; -import Icon from '~/vue_shared/components/icon.vue'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import TimelineEntryItem from './timeline_entry_item.vue'; import { spriteIcon } from '../../../lib/utils/common_utils'; @@ -32,14 +39,15 @@ const MAX_VISIBLE_COMMIT_LIST_COUNT = 3; export default { name: 'SystemNote', components: { - Icon, + GlIcon, noteHeader, TimelineEntryItem, - GlDeprecatedButton, + GlButton, GlSkeletonLoading, }, directives: { GlTooltip: GlTooltipDirective, + SafeHtml, }, mixins: [descriptionVersionHistoryMixin, glFeatureFlagsMixin()], props: { @@ -104,25 +112,28 @@ 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-html="actionTextHtml"></span> + <span v-safe-html="actionTextHtml"></span> <template v-if="canSeeDescriptionVersion" slot="extra-controls"> · - <button type="button" class="btn-blank btn-link" @click="toggleDescriptionVersion"> - {{ __('Compare with previous version') }} - <icon :name="descriptionVersionToggleIcon" :size="12" class="append-left-5" /> - </button> + <gl-button + variant="link" + :icon="descriptionVersionToggleIcon" + data-testid="compare-btn" + @click="toggleDescriptionVersion" + >{{ __('Compare with previous version') }}</gl-button + > </template> </note-header> </div> <div class="note-body"> <div + v-safe-html="note.note_html" :class="{ 'system-note-commit-list': hasMoreCommits, 'hide-shade': expanded }" class="note-text md" - v-html="note.note_html" ></div> <div v-if="hasMoreCommits" class="flex-list"> <div class="system-note-commit-list-toggler flex-row" @click="expanded = !expanded"> - <icon :name="toggleIcon" :size="8" class="gl-mr-2" /> + <gl-icon :name="toggleIcon" :size="8" class="gl-mr-2" /> <span>{{ __('Toggle commit list') }}</span> </div> </div> @@ -130,17 +141,18 @@ export default { <pre v-if="isLoadingDescriptionVersion" class="loading-state"> <gl-skeleton-loading /> </pre> - <pre v-else class="wrapper mt-2" v-html="descriptionVersion"></pre> - <gl-deprecated-button + <pre v-else v-safe-html="descriptionVersion" class="wrapper mt-2"></pre> + <gl-button v-if="displayDeleteButton" - ref="deleteDescriptionVersionButton" v-gl-tooltip :title="__('Remove description history')" - class="btn-transparent delete-description-history" + variant="default" + category="tertiary" + icon="remove" + class="delete-description-history" + data-testid="delete-description-version-button" @click="deleteDescriptionVersion" - > - <icon name="remove" /> - </gl-deprecated-button> + /> </div> </div> </div> diff --git a/app/assets/javascripts/vue_shared/components/project_selector/project_list_item.vue b/app/assets/javascripts/vue_shared/components/project_selector/project_list_item.vue index e053a9ddaa6..154671fe9fa 100644 --- a/app/assets/javascripts/vue_shared/components/project_selector/project_list_item.vue +++ b/app/assets/javascripts/vue_shared/components/project_selector/project_list_item.vue @@ -1,14 +1,14 @@ <script> -import { GlDeprecatedButton } from '@gitlab/ui'; +/* eslint-disable vue/no-v-html */ +import { GlButton, GlIcon } from '@gitlab/ui'; import { isString } from 'lodash'; -import Icon from '~/vue_shared/components/icon.vue'; import ProjectAvatar from '~/vue_shared/components/project_avatar/default.vue'; import highlight from '~/lib/utils/highlight'; import { truncateNamespace } from '~/lib/utils/text_utility'; export default { name: 'ProjectListItem', - components: { Icon, ProjectAvatar, GlDeprecatedButton }, + components: { GlIcon, ProjectAvatar, GlButton }, props: { project: { type: Object, @@ -40,17 +40,16 @@ export default { }; </script> <template> - <gl-deprecated-button - class="d-flex align-items-center btn pt-1 pb-1 border-0 project-list-item" + <gl-button + category="tertiary" + class="gl-display-flex gl-align-items-center gl-justify-content-start! gl-mb-2 gl-w-full" @click="onClick" > - <icon - class="gl-ml-3 gl-mr-3 flex-shrink-0 position-top-0 js-selected-icon" - :class="{ 'js-selected visible': selected, 'js-unselected invisible': !selected }" - name="mobile-issue-close" - /> - <project-avatar class="flex-shrink-0 js-project-avatar" :project="project" :size="32" /> - <div class="d-flex flex-wrap project-namespace-name-container"> + <div + class="gl-display-flex gl-align-items-center gl-flex-wrap project-namespace-name-container" + > + <gl-icon v-if="selected" class="js-selected-icon" name="mobile-issue-close" /> + <project-avatar class="gl-flex-shrink-0 js-project-avatar" :project="project" :size="32" /> <div v-if="truncatedNamespace" :title="projectNameWithNamespace" @@ -65,5 +64,5 @@ export default { v-html="highlightedProjectName" ></div> </div> - </gl-deprecated-button> + </gl-button> </template> 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 0b91588a006..4e2029cd74f 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 @@ -100,7 +100,7 @@ export default { @bottomReached="bottomReached" > <template v-if="!showLoadingIndicator" #items> - <div class="d-flex flex-column"> + <div class="gl-display-flex gl-flex-direction-column gl-p-3"> <project-list-item v-for="project in projectSearchResults" :key="project.id" diff --git a/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue b/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue index 25701df33f3..fc1f3675a3d 100644 --- a/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue +++ b/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue @@ -1,4 +1,5 @@ <script> +/* eslint-disable vue/no-v-html */ import DeprecatedModal from './deprecated_modal.vue'; import { eventHub } from './recaptcha_eventhub'; diff --git a/app/assets/javascripts/vue_shared/components/registry/code_instruction.vue b/app/assets/javascripts/vue_shared/components/registry/code_instruction.vue new file mode 100644 index 00000000000..08ee23d25bf --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/registry/code_instruction.vue @@ -0,0 +1,82 @@ +<script> +import { uniqueId } from 'lodash'; +import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; +import Tracking from '~/tracking'; + +export default { + name: 'CodeInstruction', + components: { + ClipboardButton, + }, + mixins: [Tracking.mixin()], + props: { + label: { + type: String, + required: false, + default: '', + }, + instruction: { + type: String, + required: true, + }, + copyText: { + type: String, + required: true, + }, + multiline: { + type: Boolean, + required: false, + default: false, + }, + trackingAction: { + type: String, + required: false, + default: '', + }, + trackingLabel: { + type: String, + required: false, + default: '', + }, + }, + created() { + this.uniqueId = uniqueId(); + }, + methods: { + trackCopy() { + if (this.trackingAction) { + this.track(this.trackingAction, { label: this.trackingLabel }); + } + }, + generateFormId(name) { + return `${name}_${this.uniqueId}`; + }, + }, +}; +</script> + +<template> + <div v-if="!multiline" class="gl-mb-3"> + <label v-if="label" :for="generateFormId('instruction-input')">{{ label }}</label> + <div class="input-group gl-mb-3"> + <input + :id="generateFormId('instruction-input')" + :value="instruction" + type="text" + class="form-control gl-font-monospace" + data-testid="instruction-input" + readonly + @copy="trackCopy" + /> + <span class="input-group-append" data-testid="instruction-button" @click="trackCopy"> + <clipboard-button :text="instruction" :title="copyText" class="input-group-text" /> + </span> + </div> + </div> + + <div v-else> + <pre class="gl-font-monospace" data-testid="multiline-instruction" @copy="trackCopy">{{ + instruction + }}</pre> + </div> +</template> diff --git a/app/assets/javascripts/vue_shared/components/registry/details_row.vue b/app/assets/javascripts/vue_shared/components/registry/details_row.vue new file mode 100644 index 00000000000..2e245fadead --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/registry/details_row.vue @@ -0,0 +1,42 @@ +<script> +import { GlIcon } from '@gitlab/ui'; + +export default { + components: { + GlIcon, + }, + props: { + icon: { + type: String, + required: true, + }, + padding: { + type: String, + default: 'gl-py-2', + required: false, + }, + dashed: { + type: Boolean, + default: false, + required: false, + }, + }, + computed: { + borderClass() { + return this.dashed ? 'gl-border-b-solid gl-border-gray-100 gl-border-b-1' : ''; + }, + }, +}; +</script> + +<template> + <div + class="gl-display-flex gl-align-items-center gl-font-monospace gl-font-sm gl-word-break-all" + :class="[padding, borderClass]" + > + <gl-icon :name="icon" class="gl-mr-4" /> + <span> + <slot></slot> + </span> + </div> +</template> diff --git a/app/assets/javascripts/vue_shared/components/registry/history_item.vue b/app/assets/javascripts/vue_shared/components/registry/history_item.vue new file mode 100644 index 00000000000..a60b630b207 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/registry/history_item.vue @@ -0,0 +1,36 @@ +<script> +import { GlIcon } from '@gitlab/ui'; +import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue'; + +export default { + name: 'HistoryItem', + components: { + GlIcon, + TimelineEntryItem, + }, + props: { + icon: { + type: String, + required: true, + }, + }, +}; +</script> + +<template> + <timeline-entry-item class="system-note note-wrapper gl-mb-6!"> + <div class="timeline-icon"> + <gl-icon :name="icon" /> + </div> + <div class="timeline-content"> + <div class="note-header"> + <span> + <slot></slot> + </span> + </div> + <div class="note-body"> + <slot name="body"></slot> + </div> + </div> + </timeline-entry-item> +</template> diff --git a/app/assets/javascripts/vue_shared/components/registry/list_item.vue b/app/assets/javascripts/vue_shared/components/registry/list_item.vue new file mode 100644 index 00000000000..50a19dc2156 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/registry/list_item.vue @@ -0,0 +1,135 @@ +<script> +import { GlButton } from '@gitlab/ui'; + +export default { + name: 'ListItem', + components: { GlButton }, + props: { + first: { + type: Boolean, + default: false, + required: false, + }, + disabled: { + type: Boolean, + default: false, + required: false, + }, + selected: { + type: Boolean, + default: false, + required: false, + }, + }, + data() { + return { + isDetailsShown: false, + detailsSlots: [], + }; + }, + computed: { + optionalClasses() { + return { + 'gl-border-t-transparent': !this.first && !this.selected, + 'gl-border-t-gray-100': this.first && !this.selected, + 'disabled-content': this.disabled, + 'gl-border-b-gray-100': !this.selected, + 'gl-bg-blue-50 gl-border-blue-200': this.selected, + }; + }, + }, + mounted() { + this.detailsSlots = Object.keys(this.$slots).filter(k => k.startsWith('details_')); + }, + methods: { + toggleDetails() { + this.isDetailsShown = !this.isDetailsShown; + }, + }, +}; +</script> + +<template> + <div + class="gl-display-flex gl-flex-direction-column gl-border-b-solid gl-border-t-solid gl-border-t-1 gl-border-b-1" + :class="optionalClasses" + > + <div class="gl-display-flex gl-align-items-center gl-py-5"> + <div + v-if="$slots['left-action']" + class="gl-w-7 gl-display-none gl-display-sm-flex gl-justify-content-start gl-pl-2" + > + <slot name="left-action"></slot> + </div> + <div + class="gl-display-flex gl-xs-flex-direction-column gl-justify-content-space-between gl-align-items-stretch gl-flex-fill-1" + > + <div + class="gl-display-flex gl-flex-direction-column gl-justify-content-space-between gl-xs-mb-3 gl-min-w-0 gl-flex-grow-1" + > + <div + v-if="$slots['left-primary']" + 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"></slot> + <gl-button + v-if="detailsSlots.length > 0" + :selected="isDetailsShown" + icon="ellipsis_h" + size="small" + class="gl-ml-2 gl-display-none gl-display-sm-block" + @click="toggleDetails" + /> + </div> + <div + v-if="$slots['left-secondary']" + class="gl-display-flex gl-align-items-center gl-text-gray-500 gl-mt-1 gl-min-h-6 gl-min-w-0 gl-flex-fill-1" + > + <slot name="left-secondary"></slot> + </div> + </div> + <div + class="gl-display-flex gl-flex-direction-column gl-sm-align-items-flex-end gl-justify-content-space-between gl-text-gray-500 gl-flex-shrink-0" + > + <div + v-if="$slots['right-primary']" + class="gl-display-flex gl-align-items-center gl-sm-text-body gl-sm-font-weight-bold gl-min-h-6" + > + <slot name="right-primary"></slot> + </div> + <div + v-if="$slots['right-secondary']" + class="gl-display-flex gl-align-items-center gl-mt-1 gl-min-h-6" + > + <slot name="right-secondary"></slot> + </div> + </div> + </div> + <div + v-if="$slots['right-action']" + class="gl-w-9 gl-display-none gl-display-sm-flex gl-justify-content-end gl-pr-1" + > + <slot name="right-action"></slot> + </div> + </div> + <div class="gl-display-flex"> + <div class="gl-w-7"></div> + <div + v-if="isDetailsShown" + class="gl-display-flex gl-flex-direction-column gl-flex-fill-1 gl-bg-gray-10 gl-rounded-base gl-inset-border-1-gray-100 gl-mb-3" + > + <div + v-for="(row, detailIndex) in detailsSlots" + :key="detailIndex" + class="gl-px-5 gl-py-2" + :class="{ + 'gl-border-gray-100 gl-border-t-solid gl-border-t-1': detailIndex !== 0, + }" + > + <slot :name="row"></slot> + </div> + </div> + <div class="gl-w-9"></div> + </div> + </div> +</template> diff --git a/app/assets/javascripts/vue_shared/components/registry/metadata_item.vue b/app/assets/javascripts/vue_shared/components/registry/metadata_item.vue new file mode 100644 index 00000000000..8ef623b68eb --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/registry/metadata_item.vue @@ -0,0 +1,63 @@ +<script> +import { GlIcon, GlLink } from '@gitlab/ui'; +import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue'; + +export default { + name: 'MetadataItem', + components: { + GlIcon, + GlLink, + TooltipOnTruncate, + }, + props: { + icon: { + type: String, + required: false, + default: null, + }, + text: { + type: String, + required: true, + }, + link: { + type: String, + required: false, + default: '', + }, + size: { + type: String, + required: false, + default: 's', + validator(value) { + return !value || ['xs', 's', 'm', 'l', 'xl'].includes(value); + }, + }, + }, + computed: { + sizeClass() { + return `mw-${this.size}`; + }, + }, +}; +</script> + +<template> + <div class="gl-display-inline-flex gl-align-items-center"> + <gl-icon v-if="icon" :name="icon" class="gl-text-gray-500 gl-mr-3" /> + <tooltip-on-truncate v-if="link" :title="text" class="gl-text-truncate" :class="sizeClass"> + <gl-link :href="link" class="gl-font-weight-bold"> + {{ text }} + </gl-link> + </tooltip-on-truncate> + <div + v-else + data-testid="metadata-item-text" + class="gl-font-weight-bold gl-display-inline-flex" + :class="sizeClass" + > + <tooltip-on-truncate :title="text" class="gl-text-truncate"> + {{ text }} + </tooltip-on-truncate> + </div> + </div> +</template> diff --git a/app/assets/javascripts/vue_shared/components/registry/title_area.vue b/app/assets/javascripts/vue_shared/components/registry/title_area.vue new file mode 100644 index 00000000000..cc33b8f85cd --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/registry/title_area.vue @@ -0,0 +1,66 @@ +<script> +import { GlAvatar } from '@gitlab/ui'; + +export default { + name: 'TitleArea', + components: { + GlAvatar, + }, + props: { + avatar: { + type: String, + default: null, + required: false, + }, + title: { + type: String, + default: null, + required: false, + }, + }, + data() { + return { + metadataSlots: [], + }; + }, + mounted() { + this.metadataSlots = Object.keys(this.$slots).filter(k => k.startsWith('metadata_')); + }, +}; +</script> + +<template> + <div class="gl-display-flex gl-justify-content-space-between gl-py-3"> + <div class="gl-flex-direction-column"> + <div class="gl-display-flex"> + <gl-avatar v-if="avatar" :src="avatar" shape="rect" class="gl-align-self-center gl-mr-4" /> + + <div class="gl-display-flex gl-flex-direction-column"> + <h1 class="gl-font-size-h1 gl-mt-3 gl-mb-2" data-testid="title"> + <slot name="title">{{ title }}</slot> + </h1> + + <div + v-if="$slots['sub-header']" + class="gl-display-flex gl-align-items-center gl-text-gray-500 gl-mt-1" + > + <slot name="sub-header"></slot> + </div> + </div> + </div> + + <div class="gl-display-flex gl-flex-wrap gl-align-items-center gl-mt-3"> + <div + v-for="(row, metadataIndex) in metadataSlots" + :key="metadataIndex" + class="gl-display-flex gl-align-items-center gl-mr-5" + > + <slot :name="row"></slot> + </div> + </div> + </div> + <div v-if="$slots['right-actions']" class="gl-mt-3"> + <slot name="right-actions"></slot> + </div> + </div> +</template> diff --git a/app/assets/javascripts/vue_shared/components/resizable_chart/constants.js b/app/assets/javascripts/vue_shared/components/resizable_chart/constants.js index edc5ffb7b77..68d86777995 100644 --- a/app/assets/javascripts/vue_shared/components/resizable_chart/constants.js +++ b/app/assets/javascripts/vue_shared/components/resizable_chart/constants.js @@ -1,6 +1,6 @@ export const DEFAULT_RX = 0.4; -export const DEFAULT_BAR_WIDTH = 6; -export const DEFAULT_LABEL_WIDTH = 4; -export const DEFAULT_LABEL_HEIGHT = 5; +export const DEFAULT_BAR_WIDTH = 4; +export const DEFAULT_LABEL_WIDTH = 3; +export const DEFAULT_LABEL_HEIGHT = 3; export const BAR_HEIGHTS = [5, 7, 9, 14, 21, 35, 50, 80]; export const GRID_YS = [30, 60, 90]; diff --git a/app/assets/javascripts/vue_shared/components/resizable_chart/skeleton_loader.vue b/app/assets/javascripts/vue_shared/components/resizable_chart/skeleton_loader.vue index 306fa61780f..a9f35a73db0 100644 --- a/app/assets/javascripts/vue_shared/components/resizable_chart/skeleton_loader.vue +++ b/app/assets/javascripts/vue_shared/components/resizable_chart/skeleton_loader.vue @@ -61,35 +61,37 @@ export default { }; </script> <template> - <gl-skeleton-loader :unique-key="uniqueKey"> - <rect - v-for="(y, index) in $options.GRID_YS" - :key="`grid-${index}`" - data-testid="skeleton-chart-grid" - x="0" - :y="`${y}%`" - width="100%" - height="1px" - /> - <rect - v-for="(height, index) in $options.BAR_HEIGHTS" - :key="`bar-${index}`" - data-testid="skeleton-chart-bar" - :x="`${getBarXPosition(index)}%`" - :y="`${90 - height}%`" - :width="`${barWidth}%`" - :height="`${height}%`" - :rx="`${rx}%`" - /> - <rect - v-for="(height, index) in $options.BAR_HEIGHTS" - :key="`label-${index}`" - data-testid="skeleton-chart-label" - :x="`${labelCentering + getBarXPosition(index)}%`" - :y="`${100 - labelHeight}%`" - :width="`${labelWidth}%`" - :height="`${labelHeight}%`" - :rx="`${rx}%`" - /> - </gl-skeleton-loader> + <div class="gl-px-8"> + <gl-skeleton-loader :unique-key="uniqueKey" class="gl-p-8"> + <rect + v-for="(y, index) in $options.GRID_YS" + :key="`grid-${index}`" + data-testid="skeleton-chart-grid" + x="0" + :y="`${y}%`" + width="100%" + height="1px" + /> + <rect + v-for="(height, index) in $options.BAR_HEIGHTS" + :key="`bar-${index}`" + data-testid="skeleton-chart-bar" + :x="`${getBarXPosition(index)}%`" + :y="`${90 - height}%`" + :width="`${barWidth}%`" + :height="`${height}%`" + :rx="`${rx}%`" + /> + <rect + v-for="(height, index) in $options.BAR_HEIGHTS" + :key="`label-${index}`" + data-testid="skeleton-chart-label" + :x="`${labelCentering + getBarXPosition(index)}%`" + :y="`${100 - labelHeight}%`" + :width="`${labelWidth}%`" + :height="`${labelHeight}%`" + :rx="`${rx}%`" + /> + </gl-skeleton-loader> + </div> </template> diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_custom_renderer.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_custom_renderer.js index a9c5d442f62..108c60c3edb 100644 --- a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_custom_renderer.js +++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_custom_renderer.js @@ -1,17 +1,19 @@ import { union, mapValues } from 'lodash'; import renderBlockHtml from './renderers/render_html_block'; -import renderKramdownList from './renderers/render_kramdown_list'; -import renderKramdownText from './renderers/render_kramdown_text'; +import renderHeading from './renderers/render_heading'; import renderIdentifierInstanceText from './renderers/render_identifier_instance_text'; import renderIdentifierParagraph from './renderers/render_identifier_paragraph'; import renderFontAwesomeHtmlInline from './renderers/render_font_awesome_html_inline'; import renderSoftbreak from './renderers/render_softbreak'; +import renderAttributeDefinition from './renderers/render_attribute_definition'; +import renderListItem from './renderers/render_list_item'; const htmlInlineRenderers = [renderFontAwesomeHtmlInline]; const htmlBlockRenderers = [renderBlockHtml]; -const listRenderers = [renderKramdownList]; -const paragraphRenderers = [renderIdentifierParagraph]; -const textRenderers = [renderKramdownText, renderIdentifierInstanceText]; +const headingRenderers = [renderHeading]; +const paragraphRenderers = [renderIdentifierParagraph, renderBlockHtml]; +const textRenderers = [renderIdentifierInstanceText, renderAttributeDefinition]; +const listItemRenderers = [renderListItem]; const softbreakRenderers = [renderSoftbreak]; const executeRenderer = (renderers, node, context) => { @@ -25,7 +27,8 @@ const buildCustomHTMLRenderer = customRenderers => { ...customRenderers, htmlBlock: union(htmlBlockRenderers, customRenderers?.htmlBlock), htmlInline: union(htmlInlineRenderers, customRenderers?.htmlInline), - list: union(listRenderers, customRenderers?.list), + heading: union(headingRenderers, customRenderers?.heading), + item: union(listItemRenderers, customRenderers?.listItem), paragraph: union(paragraphRenderers, customRenderers?.paragraph), text: union(textRenderers, customRenderers?.text), softbreak: union(softbreakRenderers, customRenderers?.softbreak), diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer.js index 868ede9426e..2bce691e793 100644 --- a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer.js +++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer.js @@ -28,6 +28,8 @@ const buildHTMLToMarkdownRender = (baseRenderer, formattingPreferences = {}) => const orderedListItemNode = 'OL LI'; const emphasisNode = 'EM, I'; const strongNode = 'STRONG, B'; + const headingNode = 'H1, H2, H3, H4, H5, H6'; + const preCodeNode = 'PRE CODE'; return { TEXT_NODE(node) { @@ -63,8 +65,10 @@ const buildHTMLToMarkdownRender = (baseRenderer, formattingPreferences = {}) => }, [unorderedListItemNode](node, subContent) { const baseResult = baseRenderer.convert(node, subContent); + const formatted = baseResult.replace(/^(\s*)([*|-])/, `$1${unorderedListBulletChar}`); + const { attributeDefinition } = node.dataset; - return baseResult.replace(/^(\s*)([*|-])/, `$1${unorderedListBulletChar}`); + return attributeDefinition ? `${formatted.trimRight()}\n${attributeDefinition}\n` : formatted; }, [orderedListItemNode](node, subContent) { const baseResult = baseRenderer.convert(node, subContent); @@ -82,6 +86,19 @@ const buildHTMLToMarkdownRender = (baseRenderer, formattingPreferences = {}) => return result.replace(/^[*_]{2}/, strongSyntax).replace(/[*_]{2}$/, strongSyntax); }, + [headingNode](node, subContent) { + const result = baseRenderer.convert(node, subContent); + const { attributeDefinition } = node.dataset; + + return attributeDefinition ? `${result.trimRight()}\n${attributeDefinition}\n\n` : result; + }, + [preCodeNode](node, subContent) { + const isReferenceDefinition = Boolean(node.dataset.sseReferenceDefinition); + + return isReferenceDefinition + ? `\n\n${node.innerText}\n\n` + : baseRenderer.convert(node, subContent); + }, }; }; diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_attribute_definition.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_attribute_definition.js new file mode 100644 index 00000000000..bd419447a48 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_attribute_definition.js @@ -0,0 +1,7 @@ +import { isAttributeDefinition } from './render_utils'; + +const canRender = ({ literal }) => isAttributeDefinition(literal); + +const render = () => ({ type: 'html', content: '<!-- sse-attribute-definition -->' }); + +export default { canRender, render }; diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_heading.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_heading.js new file mode 100644 index 00000000000..71026fd0d65 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_heading.js @@ -0,0 +1,6 @@ +import { + renderWithAttributeDefinitions as render, + willAlwaysRender as canRender, +} from './render_utils'; + +export default { render, canRender }; diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_identifier_paragraph.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_identifier_paragraph.js index 4ec45ecd3a7..3f9c6291d1b 100644 --- a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_identifier_paragraph.js +++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_identifier_paragraph.js @@ -1,5 +1,3 @@ -import { renderUneditableBranch as render } from './render_utils'; - const identifierRegex = /(^\[.+\]: .+)/; const isIdentifier = text => { @@ -10,4 +8,33 @@ const canRender = (node, context) => { return isIdentifier(context.getChildrenText(node)); }; +const getReferenceDefinitions = (node, definitions = '') => { + if (!node) { + return definitions; + } + + const definition = node.type === 'text' ? node.literal : '\n'; + + return getReferenceDefinitions(node.next, `${definitions}${definition}`); +}; + +const render = (node, { skipChildren }) => { + const content = getReferenceDefinitions(node.firstChild); + + skipChildren(); + + return [ + { + type: 'openTag', + tagName: 'pre', + classNames: ['code-block', 'language-markdown'], + attributes: { 'data-sse-reference-definition': true }, + }, + { type: 'openTag', tagName: 'code' }, + { type: 'text', content }, + { type: 'closeTag', tagName: 'code' }, + { type: 'closeTag', tagName: 'pre' }, + ]; +}; + export default { canRender, render }; diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_list.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_list.js deleted file mode 100644 index 949ca0e5c2a..00000000000 --- a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_list.js +++ /dev/null @@ -1,24 +0,0 @@ -import { renderUneditableBranch as render } from './render_utils'; - -const isKramdownTOC = ({ type, literal }) => type === 'text' && literal === 'TOC'; - -const canRender = node => { - let targetNode = node; - while (targetNode !== null) { - const { firstChild } = targetNode; - const isLeaf = firstChild === null; - if (isLeaf) { - if (isKramdownTOC(targetNode)) { - return true; - } - - break; - } - - targetNode = targetNode.firstChild; - } - - return false; -}; - -export default { canRender, render }; diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_text.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_text.js deleted file mode 100644 index 0551894918c..00000000000 --- a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_text.js +++ /dev/null @@ -1,9 +0,0 @@ -import { renderUneditableLeaf as render } from './render_utils'; - -const kramdownRegex = /(^{:.+}$)/; - -const canRender = ({ literal }) => { - return kramdownRegex.test(literal); -}; - -export default { canRender, render }; diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_list_item.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_list_item.js new file mode 100644 index 00000000000..71026fd0d65 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_list_item.js @@ -0,0 +1,6 @@ +import { + renderWithAttributeDefinitions as render, + willAlwaysRender as canRender, +} from './render_utils'; + +export default { render, canRender }; diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_utils.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_utils.js index cec6491557b..4cba2c70486 100644 --- a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_utils.js +++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_utils.js @@ -8,3 +8,31 @@ export const renderUneditableLeaf = (_, { origin }) => buildUneditableBlockToken export const renderUneditableBranch = (_, { entering, origin }) => entering ? buildUneditableOpenTokens(origin()) : buildUneditableCloseToken(); + +const attributeDefinitionRegexp = /(^{:.+}$)/; + +export const isAttributeDefinition = text => attributeDefinitionRegexp.test(text); + +const findAttributeDefinition = node => { + const literal = + node?.next?.firstChild?.literal || node?.firstChild?.firstChild?.next?.next?.literal; // for headings // for list items; + + return isAttributeDefinition(literal) ? literal : null; +}; + +export const renderWithAttributeDefinitions = (node, { origin }) => { + const attributes = findAttributeDefinition(node); + const token = origin(); + + if (token.type === 'openTag' && attributes) { + Object.assign(token, { + attributes: { + 'data-attribute-definition': attributes, + }, + }); + } + + return token; +}; + +export const willAlwaysRender = () => true; diff --git a/app/assets/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon.vue b/app/assets/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon.vue index cc24fedceed..0ed5a050fe4 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon.vue @@ -1,4 +1,5 @@ <script> +import { GlIcon } from '@gitlab/ui'; import tooltip from '~/vue_shared/directives/tooltip'; export default { @@ -6,6 +7,9 @@ export default { directives: { tooltip, }, + components: { + GlIcon, + }, props: { containerClass: { type: String, @@ -47,7 +51,7 @@ export default { data-boundary="viewport" @click="click" > - <i v-if="showIcon" class="fa fa-calendar" aria-hidden="true"> </i> + <gl-icon v-if="showIcon" name="calendar" /> <slot> <span> {{ text }} </span> </slot> 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 5eef439aa90..1ef3d5627ae 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 @@ -14,7 +14,10 @@ import DropdownSearchInput from './dropdown_search_input.vue'; import DropdownFooter from './dropdown_footer.vue'; import DropdownCreateLabel from './dropdown_create_label.vue'; +import { DropdownVariant } from '../labels_select_vue/constants'; + export default { + DropdownVariant, components: { DropdownTitle, DropdownValue, @@ -80,6 +83,11 @@ export default { required: false, default: false, }, + variant: { + type: String, + required: false, + default: DropdownVariant.Sidebar, + }, }, computed: { hiddenInputName() { @@ -123,7 +131,7 @@ export default { <template> <div class="block labels js-labels-block"> <dropdown-value-collapsed - v-if="showCreate" + v-if="showCreate && variant === $options.DropdownVariant.Sidebar" :labels="context.labels" @onValueClick="handleCollapsedValueClick" /> @@ -150,18 +158,21 @@ export default { :labels-path="labelsPath" :namespace="namespace" :labels="context.labels" - :show-extra-options="!showCreate" + :show-extra-options="!showCreate || variant !== $options.DropdownVariant.Sidebar" :enable-scoped-labels="enableScopedLabels" /> <div - class="dropdown-menu dropdown-select dropdown-menu-paging -dropdown-menu-labels dropdown-menu-selectable" + class="dropdown-menu dropdown-select dropdown-menu-paging dropdown-menu-labels dropdown-menu-selectable" > <div class="dropdown-page-one"> - <dropdown-header v-if="showCreate" /> + <dropdown-header v-if="showCreate && variant === $options.DropdownVariant.Sidebar" /> <dropdown-search-input /> <div class="dropdown-content" data-qa-selector="labels_dropdown_content"></div> - <div class="dropdown-loading"><gl-loading-icon /></div> + <div class="dropdown-loading"> + <gl-loading-icon + class="gl-display-flex gl-justify-content-center gl-align-items-center gl-h-full" + /> + </div> <dropdown-footer v-if="showCreate" :labels-web-url="labelsWebUrl" diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_create_label.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_create_label.vue index 74c5e063c3d..434aabc3df9 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_create_label.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_create_label.vue @@ -1,7 +1,14 @@ <script> +import { GlButton, GlTooltipDirective } from '@gitlab/ui'; import { __ } from '~/locale'; export default { + components: { + GlButton, + }, + directives: { + GlTooltip: GlTooltipDirective, + }, props: { headerTitle: { type: String, @@ -10,29 +17,35 @@ export default { }, }, created() { - this.suggestedColors = gon.suggested_label_colors; + const rawLabelsColors = gon.suggested_label_colors; + this.suggestedColors = Object.keys(rawLabelsColors).map(colorCode => ({ + colorCode, + title: rawLabelsColors[colorCode], + })); }, }; </script> <template> <div class="dropdown-page-two dropdown-new-label"> - <div class="dropdown-title"> - <button + <div + class="dropdown-title gl-display-flex gl-justify-content-space-between gl-align-items-center" + > + <gl-button :aria-label="__('Go back')" - type="button" - class="dropdown-title-button dropdown-menu-back" - > - <i aria-hidden="true" class="fa fa-arrow-left" data-hidden="true"> </i> - </button> + category="tertiary" + class="dropdown-menu-back" + icon="arrow-left" + size="small" + /> {{ headerTitle }} - <button + <gl-button :aria-label="__('Close')" - type="button" - class="dropdown-title-button dropdown-menu-close" - > - <i aria-hidden="true" class="fa fa-times dropdown-menu-close-icon" data-hidden="true"> </i> - </button> + category="tertiary" + class="dropdown-menu-close" + icon="close" + size="small" + /> </div> <div class="dropdown-content"> <div class="dropdown-labels-error js-label-error"></div> @@ -46,10 +59,12 @@ export default { <a v-for="(color, index) in suggestedColors" :key="index" - :data-color="color" + v-gl-tooltip + :data-color="color.colorCode" :style="{ - backgroundColor: color, + backgroundColor: color.colorCode, }" + :title="color.title" href="#" > @@ -65,12 +80,12 @@ export default { /> </div> <div class="clearfix"> - <button type="button" class="btn btn-primary float-left js-new-label-btn disabled"> + <gl-button category="secondary" class="float-left js-new-label-btn disabled"> {{ __('Create') }} - </button> - <button type="button" class="btn btn-default float-right js-cancel-label-btn"> + </gl-button> + <gl-button category="secondary" class="float-right js-cancel-label-btn"> {{ __('Cancel') }} - </button> + </gl-button> </div> </div> </div> diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_header.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_header.vue index eb837be165b..7b2802650a2 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_header.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_header.vue @@ -1,16 +1,22 @@ <script> -export default {}; +import { GlIcon } from '@gitlab/ui'; + +export default { + components: { + GlIcon, + }, +}; </script> <template> - <div class="dropdown-title"> - <span>{{ __('Assign labels') }}</span> + <div class="dropdown-title gl-display-flex gl-justify-content-center"> + <span class="gl-ml-auto">{{ __('Assign labels') }}</span> <button :aria-label="__('Close')" type="button" - class="dropdown-title-button dropdown-menu-close" + class="dropdown-title-button dropdown-menu-close gl-ml-auto" > - <i aria-hidden="true" class="fa fa-times dropdown-menu-close-icon" data-hidden="true"> </i> + <gl-icon name="close" aria-hidden="true" class="dropdown-menu-close-icon" /> </button> </div> </template> diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue index 05446903286..c2ebf78d541 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue @@ -1,4 +1,5 @@ <script> +import { GlIcon } from '@gitlab/ui'; import { s__, sprintf } from '~/locale'; import tooltip from '~/vue_shared/directives/tooltip'; @@ -6,6 +7,9 @@ export default { directives: { tooltip, }, + components: { + GlIcon, + }, props: { labels: { type: Array, @@ -49,7 +53,7 @@ export default { data-boundary="viewport" @click="handleClick" > - <i aria-hidden="true" data-hidden="true" class="fa fa-tags"> </i> + <gl-icon name="labels" /> <span>{{ labels.length }}</span> </div> </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 248e9929833..34f5517ef99 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 @@ -166,7 +166,11 @@ export default { !state.showDropdownButton && !state.showDropdownContents ) { - this.handleDropdownClose(state.labels.filter(label => label.touched)); + let filterFn = label => label.touched; + if (this.isDropdownVariantEmbedded) { + filterFn = label => label.set; + } + this.handleDropdownClose(state.labels.filter(filterFn)); } }, /** @@ -186,7 +190,7 @@ export default { ].some( className => target?.classList.contains(className) || - target?.parentElement.classList.contains(className), + target?.parentElement?.classList.contains(className), ); const hadExceptionParent = ['.js-btn-back', '.js-labels-list'].some( @@ -248,10 +252,10 @@ export default { :allow-label-edit="allowLabelEdit" :labels-select-in-progress="labelsSelectInProgress" /> - <dropdown-value v-show="!showDropdownButton"> + <dropdown-value> <slot></slot> </dropdown-value> - <dropdown-button v-show="dropdownButtonVisible" /> + <dropdown-button v-show="dropdownButtonVisible" class="gl-mt-2" /> <dropdown-contents v-if="dropdownButtonVisible && showDropdownContents" ref="dropdownContents" diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/actions.js b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/actions.js index e624bd1eaee..2d236566b3d 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/actions.js +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/actions.js @@ -54,5 +54,8 @@ export const createLabel = ({ state, dispatch }, label) => { }); }; +export const replaceSelectedLabels = ({ commit }, selectedLabels) => + commit(types.REPLACE_SELECTED_LABELS, selectedLabels); + export const updateSelectedLabels = ({ commit }, labels) => commit(types.UPDATE_SELECTED_LABELS, { labels }); diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutation_types.js b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutation_types.js index 2e044dc3b3c..af92665d4eb 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutation_types.js +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutation_types.js @@ -15,6 +15,7 @@ export const RECEIVE_CREATE_LABEL_FAILURE = 'RECEIVE_CREATE_LABEL_FAILURE'; export const TOGGLE_DROPDOWN_BUTTON = 'TOGGLE_DROPDOWN_VISIBILITY'; export const TOGGLE_DROPDOWN_CONTENTS = 'TOGGLE_DROPDOWN_CONTENTS'; +export const REPLACE_SELECTED_LABELS = 'REPLACE_SELECTED_LABELS'; export const UPDATE_SELECTED_LABELS = 'UPDATE_SELECTED_LABELS'; export const TOGGLE_DROPDOWN_CONTENTS_CREATE_VIEW = 'TOGGLE_DROPDOWN_CONTENTS_CREATE_VIEW'; 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 54f8c78b4e1..7edd290a819 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 @@ -57,6 +57,10 @@ export default { state.labelCreateInProgress = false; }, + [types.REPLACE_SELECTED_LABELS](state, selectedLabels = []) { + state.selectedLabels = selectedLabels; + }, + [types.UPDATE_SELECTED_LABELS](state, { labels }) { // Find the label to update from all the labels // and change `set` prop value to represent their current state. diff --git a/app/assets/javascripts/vue_shared/components/timezone_dropdown.vue b/app/assets/javascripts/vue_shared/components/timezone_dropdown.vue index 148bd501a8e..135b9842cbf 100644 --- a/app/assets/javascripts/vue_shared/components/timezone_dropdown.vue +++ b/app/assets/javascripts/vue_shared/components/timezone_dropdown.vue @@ -1,12 +1,12 @@ <script> -import { GlNewDropdown, GlDeprecatedDropdownItem, GlSearchBoxByType, GlIcon } from '@gitlab/ui'; +import { GlDropdown, GlDeprecatedDropdownItem, GlSearchBoxByType, GlIcon } from '@gitlab/ui'; import { __ } from '~/locale'; import autofocusonshow from '~/vue_shared/directives/autofocusonshow'; export default { name: 'TimezoneDropdown', components: { - GlNewDropdown, + GlDropdown, GlDeprecatedDropdownItem, GlSearchBoxByType, GlIcon, @@ -74,7 +74,7 @@ export default { }; </script> <template> - <gl-new-dropdown :text="value" block lazy menu-class="gl-w-full!"> + <gl-dropdown :text="value" block lazy menu-class="gl-w-full!"> <template #button-content> <span class="gl-flex-grow-1" :class="{ 'gl-text-gray-300': !value }"> {{ selectedTimezoneLabel }} @@ -98,5 +98,5 @@ export default { <gl-deprecated-dropdown-item v-if="!filteredResults.length" data-testid="noMatchingResults"> {{ $options.tranlations.noResultsText }} </gl-deprecated-dropdown-item> - </gl-new-dropdown> + </gl-dropdown> </template> diff --git a/app/assets/javascripts/vue_shared/components/todo_button.vue b/app/assets/javascripts/vue_shared/components/todo_button.vue new file mode 100644 index 00000000000..debf19ccca6 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/todo_button.vue @@ -0,0 +1,28 @@ +<script> +import { GlButton } from '@gitlab/ui'; +import { __ } from '~/locale'; + +export default { + components: { + GlButton, + }, + props: { + isTodo: { + type: Boolean, + required: false, + default: true, + }, + }, + computed: { + buttonLabel() { + return this.isTodo ? __('Mark as done') : __('Add a To-Do'); + }, + }, +}; +</script> + +<template> + <gl-button v-bind="$attrs" :aria-label="buttonLabel" @click="$emit('click', $event)"> + {{ buttonLabel }} + </gl-button> +</template> diff --git a/app/assets/javascripts/vue_shared/components/toggle_button.vue b/app/assets/javascripts/vue_shared/components/toggle_button.vue index 540edc9f61c..29d4516bece 100644 --- a/app/assets/javascripts/vue_shared/components/toggle_button.vue +++ b/app/assets/javascripts/vue_shared/components/toggle_button.vue @@ -73,7 +73,7 @@ export default { 'is-disabled': disabledInput, 'is-loading': isLoading, }" - @click="toggleFeature" + @click.prevent="toggleFeature" > <gl-loading-icon class="loading-icon" /> <span class="toggle-icon"> diff --git a/app/assets/javascripts/vue_shared/components/tooltip_on_truncate.vue b/app/assets/javascripts/vue_shared/components/tooltip_on_truncate.vue index 4ea3d162da2..579ad53e6db 100644 --- a/app/assets/javascripts/vue_shared/components/tooltip_on_truncate.vue +++ b/app/assets/javascripts/vue_shared/components/tooltip_on_truncate.vue @@ -61,9 +61,9 @@ export default { v-tooltip :title="title" :data-placement="placement" - class="js-show-tooltip" + class="js-show-tooltip gl-min-w-0" > <slot></slot> </span> - <span v-else> <slot></slot> </span> + <span v-else class="gl-min-w-0"> <slot></slot> </span> </template> diff --git a/app/assets/javascripts/vue_shared/components/url_sync.vue b/app/assets/javascripts/vue_shared/components/url_sync.vue index 389d42f0829..2844d9e9e94 100644 --- a/app/assets/javascripts/vue_shared/components/url_sync.vue +++ b/app/assets/javascripts/vue_shared/components/url_sync.vue @@ -1,6 +1,6 @@ <script> import { historyPushState } from '~/lib/utils/common_utils'; -import { setUrlParams } from '~/lib/utils/url_utility'; +import { mergeUrlParams } from '~/lib/utils/url_utility'; export default { props: { @@ -14,7 +14,7 @@ export default { immediate: true, deep: true, handler(newQuery) { - historyPushState(setUrlParams(newQuery, window.location.href, true)); + historyPushState(mergeUrlParams(newQuery, window.location.href, { spreadArrays: true })); }, }, }, 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 699e466e848..6aaff000845 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 @@ -1,6 +1,6 @@ <script> -import { GlPopover, GlSkeletonLoading } from '@gitlab/ui'; -import Icon from '~/vue_shared/components/icon.vue'; +/* eslint-disable vue/no-v-html */ +import { GlPopover, GlDeprecatedSkeletonLoading as GlSkeletonLoading, GlIcon } from '@gitlab/ui'; import UserAvatarImage from '../user_avatar/user_avatar_image.vue'; import { glEmojiTag } from '../../../emoji'; @@ -10,7 +10,7 @@ export default { name: 'UserPopover', maxSkeletonLines: MAX_SKELETON_LINES, components: { - Icon, + GlIcon, GlPopover, GlSkeletonLoading, UserAvatarImage, @@ -74,16 +74,16 @@ export default { </div> <div class="gl-text-gray-500"> <div v-if="user.bio" class="gl-display-flex gl-mb-2"> - <icon name="profile" class="gl-text-gray-400 gl-flex-shrink-0" /> + <gl-icon name="profile" class="gl-text-gray-400 gl-flex-shrink-0" /> <span ref="bio" class="gl-ml-2" v-html="user.bioHtml"></span> </div> <div v-if="user.workInformation" class="gl-display-flex gl-mb-2"> - <icon name="work" class="gl-text-gray-400 gl-flex-shrink-0" /> + <gl-icon name="work" class="gl-text-gray-400 gl-flex-shrink-0" /> <span ref="workInformation" class="gl-ml-2">{{ user.workInformation }}</span> </div> </div> <div v-if="user.location" class="js-location gl-text-gray-500 gl-display-flex"> - <icon name="location" class="gl-text-gray-400 flex-shrink-0" /> + <gl-icon name="location" class="gl-text-gray-400 flex-shrink-0" /> <span class="gl-ml-2">{{ user.location }}</span> </div> <div v-if="statusHtml" class="js-user-status gl-mt-3"> diff --git a/app/assets/javascripts/vue_shared/components/web_ide_link.vue b/app/assets/javascripts/vue_shared/components/web_ide_link.vue new file mode 100644 index 00000000000..8307c6d3b55 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/web_ide_link.vue @@ -0,0 +1,118 @@ +<script> +import $ from 'jquery'; +import { __ } from '~/locale'; +import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue'; +import ActionsButton from '~/vue_shared/components/actions_button.vue'; + +const KEY_WEB_IDE = 'webide'; +const KEY_GITPOD = 'gitpod'; + +export default { + components: { + ActionsButton, + LocalStorageSync, + }, + props: { + webIdeUrl: { + type: String, + required: true, + }, + needsToFork: { + type: Boolean, + required: false, + default: false, + }, + showWebIdeButton: { + type: Boolean, + required: false, + default: true, + }, + showGitpodButton: { + type: Boolean, + required: false, + default: false, + }, + gitpodUrl: { + type: String, + required: false, + default: '', + }, + gitpodEnabled: { + type: Boolean, + required: false, + default: false, + }, + }, + data() { + return { + selection: KEY_WEB_IDE, + }; + }, + computed: { + actions() { + return [this.webIdeAction, this.gitpodAction].filter(x => x); + }, + webIdeAction() { + if (!this.showWebIdeButton) { + return null; + } + + const handleOptions = this.needsToFork + ? { href: '#modal-confirm-fork', handle: () => this.showModal('#modal-confirm-fork') } + : { href: this.webIdeUrl }; + + return { + key: KEY_WEB_IDE, + text: __('Web IDE'), + secondaryText: __('Quickly and easily edit multiple files in your project.'), + tooltip: '', + attrs: { + 'data-qa-selector': 'web_ide_button', + }, + ...handleOptions, + }; + }, + gitpodAction() { + if (!this.showGitpodButton) { + return null; + } + + const handleOptions = this.gitpodEnabled + ? { href: this.gitpodUrl } + : { href: '#modal-enable-gitpod', handle: () => this.showModal('#modal-enable-gitpod') }; + + const secondaryText = __('Launch a ready-to-code development environment for your project.'); + + return { + key: KEY_GITPOD, + text: __('Gitpod'), + secondaryText, + tooltip: secondaryText, + attrs: { + 'data-qa-selector': 'gitpod_button', + }, + ...handleOptions, + }; + }, + }, + methods: { + select(key) { + this.selection = key; + }, + showModal(id) { + $(id).modal('show'); + }, + }, +}; +</script> + +<template> + <div> + <actions-button :actions="actions" :selected-key="selection" @select="select" /> + <local-storage-sync + storage-key="gl-web-ide-button-selected" + :value="selection" + @input="select" + /> + </div> +</template> |