diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-02-18 09:45:46 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-02-18 09:45:46 +0000 |
commit | a7b3560714b4d9cc4ab32dffcd1f74a284b93580 (patch) | |
tree | 7452bd5c3545c2fa67a28aa013835fb4fa071baf /app/assets/javascripts/vue_shared | |
parent | ee9173579ae56a3dbfe5afe9f9410c65bb327ca7 (diff) | |
download | gitlab-ce-a7b3560714b4d9cc4ab32dffcd1f74a284b93580.tar.gz |
Add latest changes from gitlab-org/gitlab@14-8-stable-eev14.8.0-rc42
Diffstat (limited to 'app/assets/javascripts/vue_shared')
35 files changed, 635 insertions, 856 deletions
diff --git a/app/assets/javascripts/vue_shared/alert_details/index.js b/app/assets/javascripts/vue_shared/alert_details/index.js index 9f1da9ae173..d0155c18b9c 100644 --- a/app/assets/javascripts/vue_shared/alert_details/index.js +++ b/app/assets/javascripts/vue_shared/alert_details/index.js @@ -1,4 +1,4 @@ -import { defaultDataIdFromObject } from 'apollo-cache-inmemory'; +import { defaultDataIdFromObject } from '@apollo/client/core'; import produce from 'immer'; import Vue from 'vue'; import VueApollo from 'vue-apollo'; @@ -70,6 +70,7 @@ export default (selector) => { // eslint-disable-next-line no-new new Vue({ el: selector, + name: 'AlertDetailsRoot', components: { AlertDetails, }, diff --git a/app/assets/javascripts/vue_shared/components/awards_list.vue b/app/assets/javascripts/vue_shared/components/awards_list.vue index 82a28d4cb5f..b6010d4b70c 100644 --- a/app/assets/javascripts/vue_shared/components/awards_list.vue +++ b/app/assets/javascripts/vue_shared/components/awards_list.vue @@ -75,6 +75,9 @@ export default { return this.noteAuthorId === this.currentUserId; }, }, + mounted() { + this.virtualScrollerItem = this.$el.closest('.vue-recycle-scroller__item-view'); + }, methods: { getAwardClassBindings(awardList) { return { @@ -162,6 +165,10 @@ export default { }, setIsMenuOpen(menuOpen) { this.isMenuOpen = menuOpen; + + if (this.virtualScrollerItem) { + this.virtualScrollerItem.style.zIndex = this.isMenuOpen ? 1 : null; + } }, }, safeHtmlConfig: { ADD_TAGS: ['gl-emoji'] }, 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 2c74d56f617..3aaa7d915ea 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,6 +1,7 @@ <script> import { GlIcon, GlSafeHtmlDirective } from '@gitlab/ui'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; +import LineHighlighter from '~/blob/line_highlighter'; import { HIGHLIGHT_CLASS_NAME } from './constants'; import ViewerMixin from './mixins'; @@ -20,13 +21,22 @@ export default { }; }, computed: { + refactorBlobViewerEnabled() { + return this.glFeatures.refactorBlobViewer; + }, + lineNumbers() { return this.content.split('\n').length; }, }, mounted() { - const { hash } = window.location; - if (hash) this.scrollToLine(hash, true); + if (this.refactorBlobViewerEnabled) { + // This line will be removed once we start using highlight.js on the frontend (https://gitlab.com/groups/gitlab-org/-/epics/7146) + new LineHighlighter(); // eslint-disable-line no-new + } else { + const { hash } = window.location; + if (hash) this.scrollToLine(hash, true); + } }, methods: { scrollToLine(hash, scroll = false) { @@ -51,7 +61,7 @@ export default { <template> <div> <div class="file-content code js-syntax-highlight" :class="$options.userColorScheme"> - <div v-if="!hideLineNumbers" class="line-numbers"> + <div v-if="!hideLineNumbers" class="line-numbers gl-pt-0!"> <a v-for="line in lineNumbers" :id="`L${line}`" @@ -67,7 +77,7 @@ export default { </div> <div class="blob-content"> <pre - class="code highlight" + class="code highlight gl-p-0! gl-display-flex" ><code v-safe-html="content" :data-blob-hash="blobHash"></code></pre> </div> </div> diff --git a/app/assets/javascripts/vue_shared/components/ci_cd_analytics/ci_cd_analytics_charts.vue b/app/assets/javascripts/vue_shared/components/ci_cd_analytics/ci_cd_analytics_charts.vue index 0575d7f6404..8b76af05ffe 100644 --- a/app/assets/javascripts/vue_shared/components/ci_cd_analytics/ci_cd_analytics_charts.vue +++ b/app/assets/javascripts/vue_shared/components/ci_cd_analytics/ci_cd_analytics_charts.vue @@ -45,7 +45,8 @@ export default { :chart-data="chart.data" :area-chart-options="chartOptions" > - {{ dateRange }} + <p>{{ dateRange }}</p> + <slot name="metrics" :selected-chart="selectedChart"></slot> <template #tooltip-title> <slot name="tooltip-title"></slot> </template> diff --git a/app/assets/javascripts/vue_shared/components/confirm_fork_modal.vue b/app/assets/javascripts/vue_shared/components/confirm_fork_modal.vue new file mode 100644 index 00000000000..64e3b5d0bae --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/confirm_fork_modal.vue @@ -0,0 +1,68 @@ +<script> +import { GlModal } from '@gitlab/ui'; +import { __ } from '~/locale'; + +export const i18n = { + btnText: __('Fork project'), + title: __('Fork project?'), + message: __( + 'You can’t edit files directly in this project. Fork this project and submit a merge request with your changes.', + ), +}; + +export default { + name: 'ConfirmForkModal', + components: { + GlModal, + }, + model: { + prop: 'visible', + event: 'change', + }, + props: { + visible: { + type: Boolean, + required: false, + default: false, + }, + modalId: { + type: String, + required: true, + }, + forkPath: { + type: String, + required: true, + }, + }, + computed: { + btnActions() { + return { + cancel: { text: __('Cancel') }, + primary: { + text: this.$options.i18n.btnText, + attributes: { + href: this.forkPath, + variant: 'confirm', + 'data-qa-selector': 'fork_project_button', + 'data-method': 'post', + }, + }, + }; + }, + }, + i18n, +}; +</script> +<template> + <gl-modal + :visible="visible" + data-qa-selector="confirm_fork_modal" + :modal-id="modalId" + :title="$options.i18n.title" + :action-primary="btnActions.primary" + :action-cancel="btnActions.cancel" + @change="$emit('change', $event)" + > + <p>{{ $options.i18n.message }}</p> + </gl-modal> +</template> diff --git a/app/assets/javascripts/vue_shared/components/design_management/design_note_pin.vue b/app/assets/javascripts/vue_shared/components/design_management/design_note_pin.vue index cb038a8c4e1..c411496fad1 100644 --- a/app/assets/javascripts/vue_shared/components/design_management/design_note_pin.vue +++ b/app/assets/javascripts/vue_shared/components/design_management/design_note_pin.vue @@ -28,12 +28,37 @@ export default { required: false, default: false, }, + isOnImage: { + type: Boolean, + required: false, + default: false, + }, + isDraft: { + type: Boolean, + required: false, + default: false, + }, + size: { + type: String, + required: false, + default: 'md', + validator: (value) => ['sm', 'md'].includes(value), + }, + ariaLabel: { + type: String, + required: false, + default: null, + }, }, computed: { isNewNote() { return this.label === null; }, pinLabel() { + if (this.ariaLabel) { + return this.ariaLabel; + } + return this.isNewNote ? __('Comment form position') : sprintf(__("Comment '%{label}' position"), { label: this.label }); @@ -51,7 +76,10 @@ export default { 'js-image-badge design-note-pin': !isNewNote, resolved: isResolved, inactive: isInactive, + draft: isDraft, + 'on-image': isOnImage, 'gl-absolute': position, + small: size === 'sm', }" class="gl-display-flex gl-align-items-center gl-justify-content-center gl-font-sm" type="button" 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 810d9f782b9..3d48c74b40b 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 @@ -23,9 +23,19 @@ export const DEFAULT_LABEL_NONE = { value: FILTER_NONE, text: __('None'), title: export const DEFAULT_LABEL_ANY = { value: FILTER_ANY, text: __('Any'), title: __('Any') }; export const DEFAULT_NONE_ANY = [DEFAULT_LABEL_NONE, DEFAULT_LABEL_ANY]; +export const DEFAULT_MILESTONE_UPCOMING = { + value: FILTER_UPCOMING, + text: __('Upcoming'), + title: __('Upcoming'), +}; +export const DEFAULT_MILESTONE_STARTED = { + value: FILTER_STARTED, + text: __('Started'), + title: __('Started'), +}; export const DEFAULT_MILESTONES = DEFAULT_NONE_ANY.concat([ - { value: FILTER_UPCOMING, text: __('Upcoming'), title: __('Upcoming') }, - { value: FILTER_STARTED, text: __('Started'), title: __('Started') }, + DEFAULT_MILESTONE_UPCOMING, + DEFAULT_MILESTONE_STARTED, ]); export const SortDirection = { diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue index bbc1888bc0b..157068b2c0f 100644 --- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue +++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue @@ -163,19 +163,22 @@ export default { }, }, methods: { - handleInput: debounce(function debouncedSearch({ data }) { - this.searchKey = data; + handleInput: debounce(function debouncedSearch({ data, operator }) { + // Prevent fetching suggestions when data or operator is not present + if (data || operator) { + this.searchKey = data; - if (!this.suggestionsLoading && !this.activeTokenValue) { - let search = this.searchTerm ? this.searchTerm : data; + if (!this.suggestionsLoading && !this.activeTokenValue) { + let search = this.searchTerm ? this.searchTerm : data; - if (search.startsWith('"') && search.endsWith('"')) { - search = stripQuotes(search); - } else if (search.startsWith('"')) { - search = search.slice(1, search.length); - } + if (search.startsWith('"') && search.endsWith('"')) { + search = stripQuotes(search); + } else if (search.startsWith('"')) { + search = search.slice(1, search.length); + } - this.$emit('fetch-suggestions', search); + this.$emit('fetch-suggestions', search); + } } }, DEBOUNCE_DELAY), handleTokenValueSelected(selectedValue) { 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 0d3394788fa..11c081ab4f8 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 @@ -57,7 +57,12 @@ export default { .fetchMilestones(searchTerm) .then((response) => { const data = Array.isArray(response) ? response : response.data; - this.milestones = data.slice().sort(sortMilestonesByDueDate); + + if (this.config.shouldSkipSort) { + this.milestones = data; + } else { + this.milestones = data.slice().sort(sortMilestonesByDueDate); + } }) .catch(() => { createFlash({ message: __('There was a problem fetching milestones.') }); diff --git a/app/assets/javascripts/vue_shared/components/gfm_autocomplete/gfm_autocomplete.vue b/app/assets/javascripts/vue_shared/components/gfm_autocomplete/gfm_autocomplete.vue deleted file mode 100644 index 9ab91e567e6..00000000000 --- a/app/assets/javascripts/vue_shared/components/gfm_autocomplete/gfm_autocomplete.vue +++ /dev/null @@ -1,106 +0,0 @@ -<script> -import Tribute from '@gitlab/tributejs'; -import { - GfmAutocompleteType, - tributeConfig, -} from 'ee_else_ce/vue_shared/components/gfm_autocomplete/utils'; -import * as Emoji from '~/emoji'; -import createFlash from '~/flash'; -import axios from '~/lib/utils/axios_utils'; -import { __ } from '~/locale'; -import SidebarMediator from '~/sidebar/sidebar_mediator'; - -export default { - errorMessage: __( - 'An error occurred while getting autocomplete data. Please refresh the page and try again.', - ), - props: { - autocompleteTypes: { - type: Array, - required: false, - default: () => Object.values(GfmAutocompleteType), - }, - dataSources: { - type: Object, - required: false, - default: () => gl.GfmAutoComplete?.dataSources || {}, - }, - }, - computed: { - config() { - return this.autocompleteTypes.map((type) => ({ - ...tributeConfig[type].config, - loadingItemTemplate: `<span class="gl-spinner gl-vertical-align-text-bottom gl-ml-3 gl-mr-2"></span>${__( - 'Loading', - )}`, - requireLeadingSpace: true, - values: this.getValues(type), - })); - }, - }, - mounted() { - this.cache = {}; - this.tribute = new Tribute({ collection: this.config }); - - const input = this.$slots.default?.[0]?.elm; - this.tribute.attach(input); - }, - beforeDestroy() { - const input = this.$slots.default?.[0]?.elm; - this.tribute.detach(input); - }, - methods: { - cacheAssignees() { - const isAssigneesLengthSame = - this.assignees?.length === SidebarMediator.singleton?.store?.assignees?.length; - - if (!this.assignees || !isAssigneesLengthSame) { - this.assignees = - SidebarMediator.singleton?.store?.assignees?.map((assignee) => assignee.username) || []; - } - }, - filterValues(type) { - // The assignees AJAX response can come after the user first invokes autocomplete - // so we need to check more than once if we need to update the assignee cache - this.cacheAssignees(); - - return tributeConfig[type].filterValues - ? tributeConfig[type].filterValues({ - assignees: this.assignees, - collection: this.cache[type], - fullText: this.$slots.default?.[0]?.elm?.value, - selectionStart: this.$slots.default?.[0]?.elm?.selectionStart, - }) - : this.cache[type]; - }, - getValues(type) { - return (inputText, processValues) => { - if (this.cache[type]) { - processValues(this.filterValues(type)); - } else if (type === GfmAutocompleteType.Emojis) { - Emoji.initEmojiMap() - .then(() => { - const emojis = Emoji.getValidEmojiNames(); - this.cache[type] = emojis; - processValues(emojis); - }) - .catch(() => createFlash({ message: this.$options.errorMessage })); - } else if (this.dataSources[type]) { - axios - .get(this.dataSources[type]) - .then((response) => { - this.cache[type] = response.data; - processValues(this.filterValues(type)); - }) - .catch(() => createFlash({ message: this.$options.errorMessage })); - } else { - processValues([]); - } - }; - }, - }, - render(createElement) { - return createElement('div', this.$slots.default); - }, -}; -</script> diff --git a/app/assets/javascripts/vue_shared/components/gfm_autocomplete/utils.js b/app/assets/javascripts/vue_shared/components/gfm_autocomplete/utils.js deleted file mode 100644 index 44c3fc34ba6..00000000000 --- a/app/assets/javascripts/vue_shared/components/gfm_autocomplete/utils.js +++ /dev/null @@ -1,195 +0,0 @@ -import { escape, last } from 'lodash'; -import * as Emoji from '~/emoji'; -import { spriteIcon } from '~/lib/utils/common_utils'; - -const groupType = 'Group'; // eslint-disable-line @gitlab/require-i18n-strings - -// Number of users to show in the autocomplete menu to avoid doing a mass fetch of 100+ avatars -const memberLimit = 10; - -const nonWordOrInteger = /\W|^\d+$/; - -export const menuItemLimit = 100; - -export const GfmAutocompleteType = { - Emojis: 'emojis', - Issues: 'issues', - Labels: 'labels', - Members: 'members', - MergeRequests: 'mergeRequests', - Milestones: 'milestones', - QuickActions: 'commands', - Snippets: 'snippets', -}; - -function doesCurrentLineStartWith(searchString, fullText, selectionStart) { - const currentLineNumber = fullText.slice(0, selectionStart).split('\n').length; - const currentLine = fullText.split('\n')[currentLineNumber - 1]; - return currentLine.startsWith(searchString); -} - -export const tributeConfig = { - [GfmAutocompleteType.Emojis]: { - config: { - trigger: ':', - lookup: (value) => value, - menuItemLimit, - menuItemTemplate: ({ original }) => `${original} ${Emoji.glEmojiTag(original)}`, - selectTemplate: ({ original }) => `:${original}:`, - }, - }, - - [GfmAutocompleteType.Issues]: { - config: { - trigger: '#', - lookup: (value) => `${value.iid}${value.title}`, - menuItemLimit, - menuItemTemplate: ({ original }) => - `<small>${original.reference || original.iid}</small> ${escape(original.title)}`, - selectTemplate: ({ original }) => original.reference || `#${original.iid}`, - }, - }, - - [GfmAutocompleteType.Labels]: { - config: { - trigger: '~', - lookup: 'title', - menuItemLimit, - menuItemTemplate: ({ original }) => ` - <span class="dropdown-label-box" style="background: ${escape(original.color)};"></span> - ${escape(original.title)}`, - selectTemplate: ({ original }) => - nonWordOrInteger.test(original.title) - ? `~"${escape(original.title)}"` - : `~${escape(original.title)}`, - }, - filterValues({ collection, fullText, selectionStart }) { - if (doesCurrentLineStartWith('/label', fullText, selectionStart)) { - return collection.filter((label) => !label.set); - } - - if (doesCurrentLineStartWith('/unlabel', fullText, selectionStart)) { - return collection.filter((label) => label.set); - } - - return collection; - }, - }, - - [GfmAutocompleteType.Members]: { - config: { - trigger: '@', - fillAttr: 'username', - lookup: (value) => - value.type === groupType ? last(value.name.split(' / ')) : `${value.name}${value.username}`, - menuItemLimit: memberLimit, - menuItemTemplate: ({ original }) => { - const commonClasses = 'gl-avatar gl-avatar-s32 gl-flex-shrink-0'; - const noAvatarClasses = `${commonClasses} gl-rounded-small - gl-display-flex gl-align-items-center gl-justify-content-center`; - - const avatar = original.avatar_url - ? `<img class="${commonClasses} gl-avatar-circle" src="${original.avatar_url}" alt="" />` - : `<div class="${noAvatarClasses}" aria-hidden="true"> - ${original.username.charAt(0).toUpperCase()}</div>`; - - let displayName = original.name; - let parentGroupOrUsername = `@${original.username}`; - - if (original.type === groupType) { - const splitName = original.name.split(' / '); - displayName = splitName.pop(); - parentGroupOrUsername = splitName.pop(); - } - - const count = original.count && !original.mentionsDisabled ? ` (${original.count})` : ''; - - const disabledMentionsIcon = original.mentionsDisabled - ? spriteIcon('notifications-off', 's16 gl-ml-3') - : ''; - - return ` - <div class="gl-display-flex gl-align-items-center"> - ${avatar} - <div class="gl-line-height-normal gl-ml-4"> - <div>${escape(displayName)}${count}</div> - <div class="gl-text-gray-700">${escape(parentGroupOrUsername)}</div> - </div> - ${disabledMentionsIcon} - </div> - `; - }, - }, - filterValues({ assignees, collection, fullText, selectionStart }) { - if (doesCurrentLineStartWith('/assign', fullText, selectionStart)) { - return collection.filter((member) => !assignees.includes(member.username)); - } - - if (doesCurrentLineStartWith('/unassign', fullText, selectionStart)) { - return collection.filter((member) => assignees.includes(member.username)); - } - - return collection; - }, - }, - - [GfmAutocompleteType.MergeRequests]: { - config: { - trigger: '!', - lookup: (value) => `${value.iid}${value.title}`, - menuItemLimit, - menuItemTemplate: ({ original }) => - `<small>${original.reference || original.iid}</small> ${escape(original.title)}`, - selectTemplate: ({ original }) => original.reference || `!${original.iid}`, - }, - }, - - [GfmAutocompleteType.Milestones]: { - config: { - trigger: '%', - lookup: 'title', - menuItemLimit, - menuItemTemplate: ({ original }) => escape(original.title), - selectTemplate: ({ original }) => `%"${escape(original.title)}"`, - }, - }, - - [GfmAutocompleteType.QuickActions]: { - config: { - trigger: '/', - fillAttr: 'name', - lookup: (value) => `${value.name}${value.aliases.join()}`, - menuItemLimit, - menuItemTemplate: ({ original }) => { - const aliases = original.aliases.length - ? `<small>(or /${original.aliases.join(', /')})</small>` - : ''; - - const params = original.params.length ? `<small>${original.params.join(' ')}</small>` : ''; - - let description = ''; - - if (original.warning) { - const confidentialIcon = - original.icon === 'confidential' ? spriteIcon('eye-slash', 's16 gl-mr-2') : ''; - description = `<small>${confidentialIcon}<em>${original.warning}</em></small>`; - } else if (original.description) { - description = `<small><em>${original.description}</em></small>`; - } - - return `<div>/${original.name} ${aliases} ${params}</div> - <div>${description}</div>`; - }, - }, - }, - - [GfmAutocompleteType.Snippets]: { - config: { - trigger: '$', - fillAttr: 'id', - lookup: (value) => `${value.id}${value.title}`, - menuItemLimit, - menuItemTemplate: ({ original }) => `<small>${original.id}</small> ${escape(original.title)}`, - }, - }, -}; diff --git a/app/assets/javascripts/vue_shared/components/help_popover.vue b/app/assets/javascripts/vue_shared/components/help_popover.vue index f36b9107a6e..f3b871c91b6 100644 --- a/app/assets/javascripts/vue_shared/components/help_popover.vue +++ b/app/assets/javascripts/vue_shared/components/help_popover.vue @@ -33,6 +33,9 @@ export default { <template #default> <div v-safe-html="options.content"></div> </template> + <template v-for="slot in Object.keys($slots)" #[slot]> + <slot :name="slot"></slot> + </template> </gl-popover> </span> </template> diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue index 5c86c928ce3..cbf38984e23 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/field.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue @@ -8,7 +8,6 @@ import GLForm from '~/gl_form'; import axios from '~/lib/utils/axios_utils'; import { stripHtml } from '~/lib/utils/text_utility'; import { __, sprintf } from '~/locale'; -import GfmAutocomplete from '~/vue_shared/components/gfm_autocomplete/gfm_autocomplete.vue'; import Suggestions from '~/vue_shared/components/markdown/suggestions.vue'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import MarkdownHeader from './header.vue'; @@ -20,7 +19,6 @@ function cleanUpLine(content) { export default { components: { - GfmAutocomplete, MarkdownHeader, MarkdownToolbar, GlIcon, @@ -212,15 +210,16 @@ export default { return new GLForm( $(this.$refs['gl-form']), { - emojis: this.enableAutocomplete && !this.glFeatures.tributeAutocomplete, - members: this.enableAutocomplete && !this.glFeatures.tributeAutocomplete, - issues: this.enableAutocomplete && !this.glFeatures.tributeAutocomplete, - mergeRequests: this.enableAutocomplete && !this.glFeatures.tributeAutocomplete, - epics: this.enableAutocomplete && !this.glFeatures.tributeAutocomplete, - milestones: this.enableAutocomplete && !this.glFeatures.tributeAutocomplete, - labels: this.enableAutocomplete && !this.glFeatures.tributeAutocomplete, - snippets: this.enableAutocomplete && !this.glFeatures.tributeAutocomplete, + emojis: this.enableAutocomplete, + members: this.enableAutocomplete, + issues: this.enableAutocomplete, + mergeRequests: this.enableAutocomplete, + epics: this.enableAutocomplete, + milestones: this.enableAutocomplete, + labels: this.enableAutocomplete, + snippets: this.enableAutocomplete, vulnerabilities: this.enableAutocomplete, + contacts: this.enableAutocomplete && this.glFeatures.contactsAutocomplete, }, true, ); @@ -311,10 +310,7 @@ export default { /> <div v-show="!previewMarkdown" class="md-write-holder"> <div class="zen-backdrop"> - <gfm-autocomplete v-if="glFeatures.tributeAutocomplete"> - <slot name="textarea"></slot> - </gfm-autocomplete> - <slot v-else name="textarea"></slot> + <slot name="textarea"></slot> <a class="zen-control zen-control-leave js-zen-leave gl-text-gray-500" href="#" diff --git a/app/assets/javascripts/vue_shared/components/markdown/header.vue b/app/assets/javascripts/vue_shared/components/markdown/header.vue index 3ed9de6c133..e2b6579a841 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/header.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/header.vue @@ -1,9 +1,9 @@ <script> -import { GlPopover, GlButton, GlTooltipDirective } from '@gitlab/ui'; +import { GlPopover, GlButton, GlTooltipDirective, GlTabs, GlTab } from '@gitlab/ui'; import $ from 'jquery'; import { keysFor, BOLD_TEXT, ITALIC_TEXT, LINK_TEXT } from '~/behaviors/shortcuts/keybindings'; import { getSelectedFragment } from '~/lib/utils/common_utils'; -import { s__ } from '~/locale'; +import { s__, __ } from '~/locale'; import { CopyAsGFM } from '../../../behaviors/markdown/copy_as_gfm'; import ToolbarButton from './toolbar_button.vue'; @@ -12,6 +12,8 @@ export default { ToolbarButton, GlPopover, GlButton, + GlTabs, + GlTab, }, directives: { GlTooltip: GlTooltipDirective, @@ -144,136 +146,143 @@ export default { italic: keysFor(ITALIC_TEXT), link: keysFor(LINK_TEXT), }, + i18n: { + writeTabTitle: __('Write'), + previewTabTitle: __('Preview'), + }, }; </script> <template> <div class="md-header"> - <ul class="nav-links clearfix"> - <li :class="{ active: !previewMarkdown }" class="md-header-tab"> - <button class="js-write-link" type="button" @click="writeMarkdownTab($event)"> - {{ __('Write') }} - </button> - </li> - <li :class="{ active: previewMarkdown }" class="md-header-tab"> - <button - class="js-preview-link js-md-preview-button" - type="button" - @click="previewMarkdownTab($event)" - > - {{ __('Preview') }} - </button> - </li> - <li :class="{ active: !previewMarkdown }" class="md-header-toolbar"> - <toolbar-button - tag="**" - :button-title=" - sprintf(s__('MarkdownEditor|Add bold text (%{modifierKey}B)'), { modifierKey }) - " - :shortcuts="$options.shortcuts.bold" - icon="bold" - /> - <toolbar-button - tag="_" - :button-title=" - sprintf(s__('MarkdownEditor|Add italic text (%{modifierKey}I)'), { modifierKey }) - " - :shortcuts="$options.shortcuts.italic" - icon="italic" - /> - <toolbar-button - :prepend="true" - :tag="tag" - :button-title="__('Insert a quote')" - icon="quote" - @click="handleQuote" - /> - <template v-if="canSuggest"> + <gl-tabs content-class="gl-display-none"> + <gl-tab + title-link-class="gl-pt-3 gl-px-3 js-md-write-button" + :title="$options.i18n.writeTabTitle" + :active="!previewMarkdown" + data-testid="write-tab" + @click="writeMarkdownTab($event)" + /> + <gl-tab + title-link-class="gl-pt-3 gl-px-3 js-md-preview-button" + :title="$options.i18n.previewTabTitle" + :active="previewMarkdown" + data-testid="preview-tab" + @click="previewMarkdownTab($event)" + /> + + <template v-if="!previewMarkdown" #tabs-end> + <div class="md-header-toolbar gl-ml-auto gl-pb-3 gl-justify-content-center"> + <toolbar-button + tag="**" + :button-title=" + sprintf(s__('MarkdownEditor|Add bold text (%{modifierKey}B)'), { modifierKey }) + " + :shortcuts="$options.shortcuts.bold" + icon="bold" + /> + <toolbar-button + tag="_" + :button-title=" + sprintf(s__('MarkdownEditor|Add italic text (%{modifierKey}I)'), { modifierKey }) + " + :shortcuts="$options.shortcuts.italic" + icon="italic" + /> <toolbar-button - ref="suggestButton" - :tag="mdSuggestion" :prepend="true" - :button-title="__('Insert suggestion')" - :cursor-offset="4" - :tag-content="lineContent" - icon="doc-code" - data-qa-selector="suggestion_button" - class="js-suggestion-btn" - @click="handleSuggestDismissed" + :tag="tag" + :button-title="__('Insert a quote')" + icon="quote" + @click="handleQuote" /> - <gl-popover - v-if="suggestPopoverVisible" - :target="$refs.suggestButton.$el" - :css-classes="['diff-suggest-popover']" - placement="bottom" - :show="suggestPopoverVisible" - > - <strong>{{ __('New! Suggest changes directly') }}</strong> - <p class="mb-2"> - {{ - __( - 'Suggest code changes which can be immediately applied in one click. Try it out!', - ) - }} - </p> - <gl-button - variant="info" - category="primary" - size="small" + <template v-if="canSuggest"> + <toolbar-button + ref="suggestButton" + :tag="mdSuggestion" + :prepend="true" + :button-title="__('Insert suggestion')" + :cursor-offset="4" + :tag-content="lineContent" + icon="doc-code" + data-qa-selector="suggestion_button" + class="js-suggestion-btn" @click="handleSuggestDismissed" + /> + <gl-popover + v-if="suggestPopoverVisible" + :target="$refs.suggestButton.$el" + :css-classes="['diff-suggest-popover']" + placement="bottom" + :show="suggestPopoverVisible" > - {{ __('Got it') }} - </gl-button> - </gl-popover> - </template> - <toolbar-button tag="`" tag-block="```" :button-title="__('Insert code')" icon="code" /> - <toolbar-button - tag="[{text}](url)" - tag-select="url" - :button-title=" - sprintf(s__('MarkdownEditor|Add a link (%{modifierKey}K)'), { modifierKey }) - " - :shortcuts="$options.shortcuts.link" - icon="link" - /> - <toolbar-button - :prepend="true" - tag="- " - :button-title="__('Add a bullet list')" - icon="list-bulleted" - /> - <toolbar-button - :prepend="true" - tag="1. " - :button-title="__('Add a numbered list')" - icon="list-numbered" - /> - <toolbar-button - :prepend="true" - tag="- [ ] " - :button-title="__('Add a task list')" - icon="list-task" - /> - <toolbar-button - :tag="mdCollapsibleSection" - :prepend="true" - tag-select="Click to expand" - :button-title="__('Add a collapsible section')" - icon="details-block" - /> - <toolbar-button - :tag="mdTable" - :prepend="true" - :button-title="__('Add a table')" - icon="table" - /> - <toolbar-button - class="js-zen-enter" - :prepend="true" - :button-title="__('Go full screen')" - icon="maximize" - /> - </li> - </ul> + <strong>{{ __('New! Suggest changes directly') }}</strong> + <p class="mb-2"> + {{ + __( + 'Suggest code changes which can be immediately applied in one click. Try it out!', + ) + }} + </p> + <gl-button + variant="info" + category="primary" + size="small" + @click="handleSuggestDismissed" + > + {{ __('Got it') }} + </gl-button> + </gl-popover> + </template> + <toolbar-button tag="`" tag-block="```" :button-title="__('Insert code')" icon="code" /> + <toolbar-button + tag="[{text}](url)" + tag-select="url" + :button-title=" + sprintf(s__('MarkdownEditor|Add a link (%{modifierKey}K)'), { modifierKey }) + " + :shortcuts="$options.shortcuts.link" + icon="link" + /> + <toolbar-button + :prepend="true" + tag="- " + :button-title="__('Add a bullet list')" + icon="list-bulleted" + /> + <toolbar-button + :prepend="true" + tag="1. " + :button-title="__('Add a numbered list')" + icon="list-numbered" + /> + <toolbar-button + :prepend="true" + tag="- [ ] " + :button-title="__('Add a task list')" + icon="list-task" + /> + <toolbar-button + :tag="mdCollapsibleSection" + :prepend="true" + tag-select="Click to expand" + :button-title="__('Add a collapsible section')" + icon="details-block" + /> + <toolbar-button + :tag="mdTable" + :prepend="true" + :button-title="__('Add a table')" + icon="table" + /> + <toolbar-button + class="js-zen-enter" + :prepend="true" + :button-title="__('Go full screen')" + icon="maximize" + /> + </div> + </template> + </gl-tabs> </div> </template> diff --git a/app/assets/javascripts/vue_shared/components/namespace_select/namespace_select.vue b/app/assets/javascripts/vue_shared/components/namespace_select/namespace_select.vue index 7d2af7983d1..521b1a1075a 100644 --- a/app/assets/javascripts/vue_shared/components/namespace_select/namespace_select.vue +++ b/app/assets/javascripts/vue_shared/components/namespace_select/namespace_select.vue @@ -1,34 +1,74 @@ <script> -import { GlDropdown, GlDropdownItem, GlDropdownSectionHeader, GlSearchBoxByType } from '@gitlab/ui'; +import { + GlDropdown, + GlDropdownDivider, + GlDropdownItem, + GlDropdownSectionHeader, + GlSearchBoxByType, +} from '@gitlab/ui'; import { __ } from '~/locale'; +export const EMPTY_NAMESPACE_ID = -1; export const i18n = { DEFAULT_TEXT: __('Select a new namespace'), + DEFAULT_EMPTY_NAMESPACE_TEXT: __('No namespace'), GROUPS: __('Groups'), USERS: __('Users'), }; -const filterByName = (data, searchTerm = '') => - data.filter((d) => d.humanName.toLowerCase().includes(searchTerm)); +const filterByName = (data, searchTerm = '') => { + if (!searchTerm) { + return data; + } + + return data.filter((d) => d.humanName.toLowerCase().includes(searchTerm.toLowerCase())); +}; export default { name: 'NamespaceSelect', components: { GlDropdown, + GlDropdownDivider, GlDropdownItem, GlDropdownSectionHeader, GlSearchBoxByType, }, props: { - data: { - type: Object, - required: true, + groupNamespaces: { + type: Array, + required: false, + default: () => [], + }, + userNamespaces: { + type: Array, + required: false, + default: () => [], }, fullWidth: { type: Boolean, required: false, default: false, }, + defaultText: { + type: String, + required: false, + default: i18n.DEFAULT_TEXT, + }, + includeHeaders: { + type: Boolean, + required: false, + default: true, + }, + emptyNamespaceTitle: { + type: String, + required: false, + default: i18n.DEFAULT_EMPTY_NAMESPACE_TEXT, + }, + includeEmptyNamespace: { + type: Boolean, + required: false, + default: false, + }, }, data() { return { @@ -38,21 +78,33 @@ export default { }, computed: { hasUserNamespaces() { - return this.data.user?.length; + return this.userNamespaces.length; }, hasGroupNamespaces() { - return this.data.group?.length; + return this.groupNamespaces.length; }, filteredGroupNamespaces() { if (!this.hasGroupNamespaces) return []; - return filterByName(this.data.group, this.searchTerm); + return filterByName(this.groupNamespaces, this.searchTerm); }, filteredUserNamespaces() { if (!this.hasUserNamespaces) return []; - return filterByName(this.data.user, this.searchTerm); + return filterByName(this.userNamespaces, this.searchTerm); }, selectedNamespaceText() { - return this.selectedNamespace?.humanName || this.$options.i18n.DEFAULT_TEXT; + return this.selectedNamespace?.humanName || this.defaultText; + }, + filteredEmptyNamespaceTitle() { + const { includeEmptyNamespace, emptyNamespaceTitle, searchTerm } = this; + + if (!includeEmptyNamespace) { + return ''; + } + if (!searchTerm) { + return emptyNamespaceTitle; + } + + return emptyNamespaceTitle.toLowerCase().includes(searchTerm.toLowerCase()); }, }, methods: { @@ -60,31 +112,47 @@ export default { this.selectedNamespace = item; this.$emit('select', item); }, + handleSelectEmptyNamespace() { + this.handleSelect({ id: EMPTY_NAMESPACE_ID, humanName: this.emptyNamespaceTitle }); + }, }, i18n, }; </script> <template> - <gl-dropdown :text="selectedNamespaceText" :block="fullWidth"> + <gl-dropdown :text="selectedNamespaceText" :block="fullWidth" data-qa-selector="namespaces_list"> <template #header> <gl-search-box-by-type v-model.trim="searchTerm" /> </template> - <div v-if="hasGroupNamespaces" class="qa-namespaces-list-groups"> - <gl-dropdown-section-header>{{ $options.i18n.GROUPS }}</gl-dropdown-section-header> + <div v-if="filteredEmptyNamespaceTitle"> + <gl-dropdown-item + data-qa-selector="namespaces_list_item" + @click="handleSelectEmptyNamespace()" + > + {{ emptyNamespaceTitle }} + </gl-dropdown-item> + <gl-dropdown-divider /> + </div> + <div v-if="hasGroupNamespaces" data-qa-selector="namespaces_list_groups"> + <gl-dropdown-section-header v-if="includeHeaders">{{ + $options.i18n.GROUPS + }}</gl-dropdown-section-header> <gl-dropdown-item v-for="item in filteredGroupNamespaces" :key="item.id" - class="qa-namespaces-list-item" + data-qa-selector="namespaces_list_item" @click="handleSelect(item)" >{{ item.humanName }}</gl-dropdown-item > </div> - <div v-if="hasUserNamespaces" class="qa-namespaces-list-users"> - <gl-dropdown-section-header>{{ $options.i18n.USERS }}</gl-dropdown-section-header> + <div v-if="hasUserNamespaces" data-qa-selector="namespaces_list_users"> + <gl-dropdown-section-header v-if="includeHeaders">{{ + $options.i18n.USERS + }}</gl-dropdown-section-header> <gl-dropdown-item v-for="item in filteredUserNamespaces" :key="item.id" - class="qa-namespaces-list-item" + data-qa-selector="namespaces_list_item" @click="handleSelect(item)" >{{ item.humanName }}</gl-dropdown-item > diff --git a/app/assets/javascripts/vue_shared/components/pikaday.vue b/app/assets/javascripts/vue_shared/components/pikaday.vue deleted file mode 100644 index 3c0ac32e512..00000000000 --- a/app/assets/javascripts/vue_shared/components/pikaday.vue +++ /dev/null @@ -1,48 +0,0 @@ -<script> -import { GlDatepicker } from '@gitlab/ui'; -import { pikadayToString } from '~/lib/utils/datetime_utility'; - -export default { - name: 'DatePicker', - components: { - GlDatepicker, - }, - props: { - selectedDate: { - type: Date, - required: false, - default: null, - }, - minDate: { - type: Date, - required: false, - default: null, - }, - maxDate: { - type: Date, - required: false, - default: null, - }, - }, - methods: { - selected(date) { - this.$emit('newDateSelected', pikadayToString(date)); - }, - toggled() { - this.$emit('hidePicker'); - }, - }, -}; -</script> - -<template> - <gl-datepicker - :value="selectedDate" - :min-date="minDate" - :max-date="maxDate" - start-opened - @close="toggled" - @click="toggled" - @input="selected" - /> -</template> diff --git a/app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions.vue b/app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions.vue index d886a67fff7..5d144c0d699 100644 --- a/app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions.vue +++ b/app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions.vue @@ -13,7 +13,7 @@ export default { }, modalId: 'runner-instructions-modal', i18n: { - buttonText: s__('Runners|Show Runner installation instructions'), + buttonText: s__('Runners|Show runner installation instructions'), }, data() { return { 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 deleted file mode 100644 index 460a10e08ed..00000000000 --- a/app/assets/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon.vue +++ /dev/null @@ -1,49 +0,0 @@ -<script> -import { GlIcon, GlTooltipDirective } from '@gitlab/ui'; - -export default { - name: 'CollapsedCalendarIcon', - directives: { - GlTooltip: GlTooltipDirective, - }, - components: { - GlIcon, - }, - props: { - containerClass: { - type: String, - required: false, - default: '', - }, - text: { - type: String, - required: false, - default: '', - }, - showIcon: { - type: Boolean, - required: false, - default: true, - }, - tooltipText: { - type: String, - required: false, - default: '', - }, - }, - methods: { - click() { - this.$emit('click'); - }, - }, -}; -</script> - -<template> - <div v-gl-tooltip.left.viewport="tooltipText" :class="containerClass" @click="click"> - <gl-icon v-if="showIcon" name="calendar" /> - <slot> - <span> {{ text }} </span> - </slot> - </div> -</template> diff --git a/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue b/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue deleted file mode 100644 index 4531fafbf72..00000000000 --- a/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue +++ /dev/null @@ -1,148 +0,0 @@ -<script> -import { GlLoadingIcon } from '@gitlab/ui'; -import { __ } from '~/locale'; -import { dateInWords } from '../../../lib/utils/datetime_utility'; -import datePicker from '../pikaday.vue'; -import collapsedCalendarIcon from './collapsed_calendar_icon.vue'; -import toggleSidebar from './toggle_sidebar.vue'; - -export default { - name: 'SidebarDatePicker', - components: { - datePicker, - toggleSidebar, - collapsedCalendarIcon, - GlLoadingIcon, - }, - props: { - blockClass: { - type: String, - required: false, - default: '', - }, - collapsed: { - type: Boolean, - required: false, - default: true, - }, - showToggleSidebar: { - type: Boolean, - required: false, - default: false, - }, - isLoading: { - type: Boolean, - required: false, - default: false, - }, - editable: { - type: Boolean, - required: false, - default: false, - }, - label: { - type: String, - required: false, - default: __('Date picker'), - }, - selectedDate: { - type: Date, - required: false, - default: null, - }, - minDate: { - type: Date, - required: false, - default: null, - }, - maxDate: { - type: Date, - required: false, - default: null, - }, - }, - data() { - return { - editing: false, - }; - }, - computed: { - selectedAndEditable() { - return this.selectedDate && this.editable; - }, - selectedDateWords() { - return dateInWords(this.selectedDate, true); - }, - collapsedText() { - return this.selectedDateWords ? this.selectedDateWords : __('None'); - }, - }, - methods: { - stopEditing() { - this.editing = false; - }, - toggleDatePicker() { - this.editing = !this.editing; - }, - newDateSelected(date = null) { - this.date = date; - this.editing = false; - this.$emit('saveDate', date); - }, - toggleSidebar() { - this.$emit('toggleCollapse'); - }, - }, -}; -</script> - -<template> - <div :class="blockClass" class="block"> - <div class="issuable-sidebar-header"> - <toggle-sidebar :collapsed="collapsed" @toggle="toggleSidebar" /> - </div> - <collapsed-calendar-icon :text="collapsedText" class="sidebar-collapsed-icon" /> - <div class="title"> - {{ label }} - <gl-loading-icon v-if="isLoading" size="sm" :inline="true" /> - <div class="float-right"> - <button - v-if="editable && !editing" - type="button" - class="btn-blank btn-link btn-primary-hover-link btn-sidebar-action" - @click="toggleDatePicker" - > - {{ __('Edit') }} - </button> - <toggle-sidebar v-if="showToggleSidebar" :collapsed="collapsed" @toggle="toggleSidebar" /> - </div> - </div> - <div class="value"> - <date-picker - v-if="editing" - :selected-date="selectedDate" - :min-date="minDate" - :max-date="maxDate" - :label="label" - @newDateSelected="newDateSelected" - @hidePicker="stopEditing" - /> - <span v-else class="value-content"> - <template v-if="selectedDate"> - <strong>{{ selectedDateWords }}</strong> - <span v-if="selectedAndEditable" class="no-value"> - - - <button - type="button" - class="btn-blank btn-link btn-secondary-hover-link" - @click="newDateSelected(null)" - > - {{ __('remove') }} - </button> - </span> - </template> - <span v-else class="no-value">{{ __('None') }}</span> - </span> - </div> - </div> -</template> diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue index b99083713a8..88977652556 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue @@ -117,7 +117,11 @@ export default { labelCreate: { label }, }, }, - ) => this.updateLabelsInCache(store, label), + ) => { + if (label) { + this.updateLabelsInCache(store, label); + } + }, }); if (labelCreate.errors.length) { [this.error] = labelCreate.errors; diff --git a/app/assets/javascripts/vue_shared/components/sidebar/multiselect_dropdown.vue b/app/assets/javascripts/vue_shared/components/sidebar/multiselect_dropdown.vue deleted file mode 100644 index 17904f20341..00000000000 --- a/app/assets/javascripts/vue_shared/components/sidebar/multiselect_dropdown.vue +++ /dev/null @@ -1,37 +0,0 @@ -<script> -import { GlDropdown, GlDropdownForm, GlDropdownDivider } from '@gitlab/ui'; - -export default { - components: { - GlDropdownForm, - GlDropdown, - GlDropdownDivider, - }, - props: { - headerText: { - type: String, - required: true, - }, - text: { - type: String, - required: true, - }, - }, -}; -</script> - -<template> - <gl-dropdown class="show" :text="text" @toggle="$emit('toggle')"> - <template #header> - <p class="gl-font-weight-bold gl-text-center gl-mt-2 gl-mb-4">{{ headerText }}</p> - <gl-dropdown-divider /> - <slot name="search"></slot> - </template> - <gl-dropdown-form> - <slot name="items"></slot> - </gl-dropdown-form> - <template #footer> - <slot name="footer"></slot> - </template> - </gl-dropdown> -</template> diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/constants.js b/app/assets/javascripts/vue_shared/components/source_viewer/constants.js new file mode 100644 index 00000000000..9efe0147c37 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/source_viewer/constants.js @@ -0,0 +1,111 @@ +// Language map from Rouge::Lexer to highlight.js +// Rouge::Lexer - We use it on the BE to determine the language of a source file (https://github.com/rouge-ruby/rouge/blob/master/docs/Languages.md). +// Highlight.js - We use it on the FE to highlight the syntax of a source file (https://github.com/highlightjs/highlight.js/tree/main/src/languages). +export const ROUGE_TO_HLJS_LANGUAGE_MAP = { + bsl: '1c', + actionscript: 'actionscript', + ada: 'ada', + apache: 'apache', + applescript: 'applescript', + armasm: 'armasm', + awk: 'awk', + c: 'c', + ceylon: 'ceylon', + clean: 'clean', + clojure: 'clojure', + cmake: 'cmake', + coffeescript: 'coffeescript', + coq: 'coq', + cpp: 'cpp', + crystal: 'crystal', + csharp: 'csharp', + css: 'css', + d: 'd', + dart: 'dart', + pascal: 'delphi', + diff: 'diff', + jinja: 'django', + docker: 'dockerfile', + batchfile: 'dos', + elixir: 'elixir', + elm: 'elm', + erb: 'erb', + erlang: 'erlang', + fortran: 'fortran', + fsharp: 'fsharp', + gherkin: 'gherkin', + glsl: 'glsl', + go: 'go', + gradle: 'gradle', + groovy: 'groovy', + haml: 'haml', + handlebars: 'handlebars', + haskell: 'haskell', + haxe: 'haxe', + http: 'http', + hylang: 'hy', + ini: 'ini', + isbl: 'isbl', + java: 'java', + javascript: 'javascript', + json: 'json', + julia: 'julia', + kotlin: 'kotlin', + lasso: 'lasso', + tex: 'latex', + common_lisp: 'lisp', + livescript: 'livescript', + llvm: 'llvm', + hlsl: 'lsl', + lua: 'lua', + make: 'makefile', + markdown: 'markdown', + mathematica: 'mathematica', + matlab: 'matlab', + moonscript: 'moonscript', + nginx: 'nginx', + nim: 'nim', + nix: 'nix', + objective_c: 'objectivec', + ocaml: 'ocaml', + perl: 'perl', + php: 'php', + plaintext: 'plaintext', + pony: 'pony', + powershell: 'powershell', + prolog: 'prolog', + properties: 'properties', + protobuf: 'protobuf', + puppet: 'puppet', + python: 'python', + q: 'q', + qml: 'qml', + r: 'r', + reasonml: 'reasonml', + ruby: 'ruby', + rust: 'rust', + sas: 'sas', + scala: 'scala', + scheme: 'scheme', + scss: 'scss', + shell: 'shell', + smalltalk: 'smalltalk', + sml: 'sml', + sqf: 'sqf', + sql: 'sql', + stan: 'stan', + stata: 'stata', + swift: 'swift', + tap: 'tap', + tcl: 'tcl', + twig: 'twig', + typescript: 'typescript', + vala: 'vala', + vb: 'vbnet', + verilog: 'verilog', + vhdl: 'vhdl', + viml: 'vim', + xml: 'xml', + xquery: 'xquery', + yaml: 'yaml', +}; diff --git a/app/assets/javascripts/vue_shared/components/source_viewer.vue b/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue index 99895926653..5aae1812de3 100644 --- a/app/assets/javascripts/vue_shared/components/source_viewer.vue +++ b/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue @@ -1,36 +1,31 @@ <script> -import { GlSafeHtmlDirective } from '@gitlab/ui'; +import { GlSafeHtmlDirective, GlLoadingIcon } from '@gitlab/ui'; import LineNumbers from '~/vue_shared/components/line_numbers.vue'; import { sanitize } from '~/lib/dompurify'; +import { ROUGE_TO_HLJS_LANGUAGE_MAP } from './constants'; +import { wrapLines } from './utils'; const LINE_SELECT_CLASS_NAME = 'hll'; export default { components: { LineNumbers, + GlLoadingIcon, }, directives: { SafeHtml: GlSafeHtmlDirective, }, props: { - content: { - type: String, + blob: { + type: Object, required: true, }, - language: { - type: String, - required: false, - default: 'plaintext', - }, - autoDetect: { - type: Boolean, - required: false, - default: false, - }, }, data() { return { languageDefinition: null, + content: this.blob.rawTextBlob, + language: ROUGE_TO_HLJS_LANGUAGE_MAP[this.blob.language], hljs: null, }; }, @@ -42,14 +37,14 @@ export default { let highlightedContent; if (this.hljs) { - if (this.autoDetect) { + if (!this.language) { highlightedContent = this.hljs.highlightAuto(this.content).value; } else if (this.languageDefinition) { highlightedContent = this.hljs.highlight(this.content, { language: this.language }).value; } } - return this.wrapLines(highlightedContent); + return wrapLines(highlightedContent); }, }, watch: { @@ -63,14 +58,14 @@ export default { async mounted() { this.hljs = await this.loadHighlightJS(); - if (!this.autoDetect) { + if (this.language) { this.languageDefinition = await this.loadLanguage(); } }, methods: { loadHighlightJS() { - // With auto-detect enabled we load all common languages else we load only the core (smallest footprint) - return this.autoDetect ? import('highlight.js/lib/common') : import('highlight.js/lib/core'); + // If no language can be mapped to highlight.js we load all common languages else we load only the core (smallest footprint) + return !this.language ? import('highlight.js/lib/common') : import('highlight.js/lib/core'); }, async loadLanguage() { let languageDefinition; @@ -84,15 +79,6 @@ export default { return languageDefinition; }, - wrapLines(content) { - return ( - content && - content - .split('\n') - .map((line, i) => `<span id="LC${i + 1}" class="line">${line}</span>`) - .join('\r\n') - ); - }, selectLine() { const hash = sanitize(this.$route.hash); const lineToSelect = hash && this.$el.querySelector(hash); @@ -115,9 +101,16 @@ export default { }; </script> <template> - <div class="file-content code js-syntax-highlight" :class="$options.userColorScheme"> + <gl-loading-icon v-if="!highlightedContent" size="sm" class="gl-my-5" /> + <div + v-else + class="file-content code js-syntax-highlight blob-content gl-display-flex" + :class="$options.userColorScheme" + data-type="simple" + data-qa-selector="blob_viewer_file_content" + > <line-numbers :lines="lineNumbers" /> - <pre class="code"><code v-safe-html="highlightedContent"></code> + <pre class="code gl-pb-0!"><code v-safe-html="highlightedContent"></code> </pre> </div> </template> diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/utils.js b/app/assets/javascripts/vue_shared/components/source_viewer/utils.js new file mode 100644 index 00000000000..e64e564bf61 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/source_viewer/utils.js @@ -0,0 +1,26 @@ +export const wrapLines = (content) => { + return ( + content && + content + .split('\n') + .map((line, i) => { + let formattedLine; + const idAttribute = `id="LC${i + 1}"`; + + if (line.includes('<span class="hljs') && !line.includes('</span>')) { + /** + * In some cases highlight.js will wrap multiple lines in a span, in these cases we want to append the line number to the existing span + * + * example (before): <span class="hljs-code">```bash + * example (after): <span id="LC67" class="hljs-code">```bash + */ + formattedLine = line.replace(/(?=class="hljs)/, `${idAttribute} `); + } else { + formattedLine = `<span ${idAttribute} class="line">${line}</span>`; + } + + return formattedLine; + }) + .join('\n') + ); +}; diff --git a/app/assets/javascripts/vue_shared/components/svg_gradient.vue b/app/assets/javascripts/vue_shared/components/svg_gradient.vue deleted file mode 100644 index 5ce45d492f9..00000000000 --- a/app/assets/javascripts/vue_shared/components/svg_gradient.vue +++ /dev/null @@ -1,34 +0,0 @@ -<script> -export default { - props: { - colors: { - type: Array, - required: true, - validator(value) { - return value.length === 2; - }, - }, - opacity: { - type: Array, - required: true, - validator(value) { - return value.length === 2; - }, - }, - identifierName: { - type: String, - required: true, - }, - }, -}; -</script> -<template> - <svg height="0" width="0"> - <defs> - <linearGradient :id="identifierName"> - <stop :stop-color="colors[0]" :stop-opacity="opacity[0]" offset="0%" /> - <stop :stop-color="colors[1]" :stop-opacity="opacity[1]" offset="100%" /> - </linearGradient> - </defs> - </svg> -</template> diff --git a/app/assets/javascripts/vue_shared/components/upload_dropzone/upload_dropzone.vue b/app/assets/javascripts/vue_shared/components/upload_dropzone/upload_dropzone.vue index 0a7a22ed3a8..62de76e46b5 100644 --- a/app/assets/javascripts/vue_shared/components/upload_dropzone/upload_dropzone.vue +++ b/app/assets/javascripts/vue_shared/components/upload_dropzone/upload_dropzone.vue @@ -41,6 +41,16 @@ export default { required: false, default: false, }, + inputFieldName: { + type: String, + required: false, + default: 'upload_file', + }, + shouldUpdateInputOnFileDrop: { + type: Boolean, + required: false, + default: false, + }, }, data() { return { @@ -84,6 +94,30 @@ export default { return; } + // NOTE: This is a temporary solution to integrate dropzone into a Rails + // form. On file drop if `shouldUpdateInputOnFileDrop` is true, the file + // input value is updated. So that when the form is submitted — the file + // value would be send together with the form data. This solution should + // be removed when License file upload page is fully migrated: + // https://gitlab.com/gitlab-org/gitlab/-/issues/352501 + // NOTE: as per https://caniuse.com/mdn-api_htmlinputelement_files, IE11 + // is not able to set input.files property, thought the user would still + // be able to use the file picker dialogue option, by clicking the + // "openFileUpload" button + if (this.shouldUpdateInputOnFileDrop) { + // Since FileList cannot be easily manipulated, to match requirement of + // singleFileSelection, we're throwing an error if multiple files were + // dropped on the dropzone + // NOTE: we can drop this logic together with + // `shouldUpdateInputOnFileDrop` flag + if (this.singleFileSelection && files.length > 1) { + this.$emit('error'); + return; + } + + this.$refs.fileUpload.files = files; + } + this.$emit('change', this.singleFileSelection ? files[0] : files); }, ondragenter(e) { @@ -116,6 +150,7 @@ export default { <slot> <button class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-3" + type="button" @click="openFileUpload" > <div @@ -147,7 +182,7 @@ export default { <input ref="fileUpload" type="file" - name="upload_file" + :name="inputFieldName" :accept="validFileMimetypes" class="hide" :multiple="!singleFileSelection" diff --git a/app/assets/javascripts/vue_shared/components/web_ide_link.vue b/app/assets/javascripts/vue_shared/components/web_ide_link.vue index f02cd5c4e2e..82022d1f4d6 100644 --- a/app/assets/javascripts/vue_shared/components/web_ide_link.vue +++ b/app/assets/javascripts/vue_shared/components/web_ide_link.vue @@ -1,9 +1,9 @@ <script> -import $ from 'jquery'; import { GlModal, GlSprintf, GlLink } from '@gitlab/ui'; import { s__, __ } from '~/locale'; import ActionsButton from '~/vue_shared/components/actions_button.vue'; import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue'; +import ConfirmForkModal from '~/vue_shared/components/confirm_fork_modal.vue'; const KEY_EDIT = 'edit'; const KEY_WEB_IDE = 'webide'; @@ -16,6 +16,7 @@ export default { GlModal, GlSprintf, GlLink, + ConfirmForkModal, }, i18n: { modal: { @@ -103,11 +104,22 @@ export default { required: false, default: false, }, + forkPath: { + type: String, + required: false, + default: '', + }, + forkModalId: { + type: String, + required: false, + default: '', + }, }, data() { return { selection: KEY_WEB_IDE, showEnableGitpodModal: false, + showForkModal: false, }; }, computed: { @@ -128,7 +140,7 @@ export default { return; } - this.showJQueryModal('#modal-confirm-fork-edit'); + this.showModal('showForkModal'); }, } : { href: this.editUrl }; @@ -171,7 +183,7 @@ export default { return; } - this.showJQueryModal('#modal-confirm-fork-webide'); + this.showModal('showForkModal'); }, } : { href: this.webIdeUrl }; @@ -247,9 +259,6 @@ export default { select(key) { this.selection = key; }, - showJQueryModal(id) { - $(id).modal('show'); - }, showModal(dataKey) { this[dataKey] = true; }, @@ -282,5 +291,11 @@ export default { </template> </gl-sprintf> </gl-modal> + <confirm-fork-modal + v-if="showWebIdeButton || showEditButton" + v-model="showForkModal" + :modal-id="forkModalId" + :fork-path="forkPath" + /> </div> </template> diff --git a/app/assets/javascripts/vue_shared/issuable/list/components/issuable_item.vue b/app/assets/javascripts/vue_shared/issuable/list/components/issuable_item.vue index af0235bfc69..8008b85bbdb 100644 --- a/app/assets/javascripts/vue_shared/issuable/list/components/issuable_item.vue +++ b/app/assets/javascripts/vue_shared/issuable/list/components/issuable_item.vue @@ -31,10 +31,6 @@ export default { type: Object, required: true, }, - enableLabelPermalinks: { - type: Boolean, - required: true, - }, labelFilterParam: { type: String, required: false, @@ -121,7 +117,10 @@ export default { }, showIssuableMeta() { return Boolean( - this.hasSlotContents('status') || this.showDiscussions || this.issuable.assignees, + this.hasSlotContents('status') || + this.hasSlotContents('statistics') || + this.showDiscussions || + this.issuable.assignees, ); }, issuableNotesLink() { @@ -139,11 +138,8 @@ export default { return label.title || label.name; }, labelTarget(label) { - if (this.enableLabelPermalinks) { - const value = encodeURIComponent(this.labelTitle(label)); - return `?${this.labelFilterParam}[]=${value}`; - } - return '#'; + const value = encodeURIComponent(this.labelTitle(label)); + return `?${this.labelFilterParam}[]=${value}`; }, /** * This is needed as an independent method since diff --git a/app/assets/javascripts/vue_shared/issuable/list/components/issuable_list_root.vue b/app/assets/javascripts/vue_shared/issuable/list/components/issuable_list_root.vue index 2f8401b45f0..028d48e7e8a 100644 --- a/app/assets/javascripts/vue_shared/issuable/list/components/issuable_list_root.vue +++ b/app/assets/javascripts/vue_shared/issuable/list/components/issuable_list_root.vue @@ -15,6 +15,7 @@ const VueDraggable = () => import('vuedraggable'); export default { vueDraggableAttributes: { animation: 200, + forceFallback: true, ghostClass: 'gl-visibility-hidden', tag: 'ul', }, @@ -78,6 +79,11 @@ export default { required: false, default: null, }, + truncateCounts: { + type: Boolean, + required: false, + default: false, + }, currentTab: { type: String, required: true, @@ -127,11 +133,6 @@ export default { required: false, default: 2, }, - enableLabelPermalinks: { - type: Boolean, - required: false, - default: true, - }, labelFilterParam: { type: String, required: false, @@ -261,6 +262,7 @@ export default { :tabs="tabs" :tab-counts="tabCounts" :current-tab="currentTab" + :truncate-counts="truncateCounts" @click="$emit('click-tab', $event)" > <template #nav-actions> @@ -314,7 +316,6 @@ export default { :data-qa-issuable-title="issuable.title" :issuable-symbol="issuableSymbol" :issuable="issuable" - :enable-label-permalinks="enableLabelPermalinks" :label-filter-param="labelFilterParam" :show-checkbox="showBulkEditSidebar" :checked="issuableChecked(issuable)" diff --git a/app/assets/javascripts/vue_shared/issuable/list/components/issuable_tabs.vue b/app/assets/javascripts/vue_shared/issuable/list/components/issuable_tabs.vue index 9bf54e98cc4..0691bc02b5c 100644 --- a/app/assets/javascripts/vue_shared/issuable/list/components/issuable_tabs.vue +++ b/app/assets/javascripts/vue_shared/issuable/list/components/issuable_tabs.vue @@ -1,5 +1,6 @@ <script> import { GlTabs, GlTab, GlBadge } from '@gitlab/ui'; +import { numberToMetricPrefix } from '~/lib/utils/number_utils'; import { formatNumber } from '~/locale'; export default { @@ -22,6 +23,11 @@ export default { type: String, required: true, }, + truncateCounts: { + type: Boolean, + required: false, + default: false, + }, }, methods: { isTabActive(tabName) { @@ -31,7 +37,7 @@ export default { return Number.isInteger(this.tabCounts[tab.name]); }, formatNumber(count) { - return formatNumber(count); + return this.truncateCounts ? numberToMetricPrefix(count) : formatNumber(count); }, }, }; diff --git a/app/assets/javascripts/vue_shared/issuable/show/components/issuable_header.vue b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_header.vue index d7da533d055..ee7e113af72 100644 --- a/app/assets/javascripts/vue_shared/issuable/show/components/issuable_header.vue +++ b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_header.vue @@ -102,7 +102,7 @@ export default { </div> </div> <span> - {{ __('Opened') }} + {{ __('Created') }} <time-ago-tooltip data-testid="startTimeItem" :time="createdAt" /> {{ __('by') }} </span> diff --git a/app/assets/javascripts/vue_shared/issuable/sidebar/components/issuable_sidebar_root.vue b/app/assets/javascripts/vue_shared/issuable/sidebar/components/issuable_sidebar_root.vue index 99dcccd12ed..774267639fc 100644 --- a/app/assets/javascripts/vue_shared/issuable/sidebar/components/issuable_sidebar_root.vue +++ b/app/assets/javascripts/vue_shared/issuable/sidebar/components/issuable_sidebar_root.vue @@ -1,8 +1,8 @@ <script> import { GlIcon } from '@gitlab/ui'; import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils'; -import Cookies from 'js-cookie'; -import { parseBoolean } from '~/lib/utils/common_utils'; +import { getCookie, setCookie, parseBoolean } from '~/lib/utils/common_utils'; + import { USER_COLLAPSED_GUTTER_COOKIE } from '../constants'; export default { @@ -10,7 +10,7 @@ export default { GlIcon, }, data() { - const userExpanded = !parseBoolean(Cookies.get(USER_COLLAPSED_GUTTER_COOKIE)); + const userExpanded = !parseBoolean(getCookie(USER_COLLAPSED_GUTTER_COOKIE)); // We're deliberately keeping two different props for sidebar status; // 1. userExpanded reflects value based on cookie `collapsed_gutter`. @@ -46,7 +46,7 @@ export default { this.isExpanded = !this.isExpanded; this.userExpanded = this.isExpanded; - Cookies.set(USER_COLLAPSED_GUTTER_COOKIE, !this.userExpanded); + setCookie(USER_COLLAPSED_GUTTER_COOKIE, !this.userExpanded); this.updatePageContainerClass(); }, }, diff --git a/app/assets/javascripts/vue_shared/new_namespace/new_namespace_page.vue b/app/assets/javascripts/vue_shared/new_namespace/new_namespace_page.vue index f67e590e2ce..1f3cc663848 100644 --- a/app/assets/javascripts/vue_shared/new_namespace/new_namespace_page.vue +++ b/app/assets/javascripts/vue_shared/new_namespace/new_namespace_page.vue @@ -11,7 +11,7 @@ export default { WelcomePage, LegacyContainer, CreditCardVerification: () => - import('ee_component/pages/groups/new/components/credit_card_verification.vue'), + import('ee_component/namespaces/verification/components/credit_card_verification.vue'), }, directives: { SafeHtml, diff --git a/app/assets/javascripts/vue_shared/security_configuration/components/manage_via_mr.vue b/app/assets/javascripts/vue_shared/security_configuration/components/manage_via_mr.vue index d1630c9ac13..3afd1f9410b 100644 --- a/app/assets/javascripts/vue_shared/security_configuration/components/manage_via_mr.vue +++ b/app/assets/javascripts/vue_shared/security_configuration/components/manage_via_mr.vue @@ -14,7 +14,7 @@ export default { components: { GlButton, }, - inject: ['projectPath'], + inject: ['projectFullPath'], props: { feature: { type: Object, @@ -47,7 +47,7 @@ export default { try { const { mutationSettings } = this; const { data } = await this.$apollo.mutate( - mutationSettings.getMutationPayload(this.projectPath), + mutationSettings.getMutationPayload(this.projectFullPath), ); const { errors, successPath } = data[mutationSettings.mutationId]; diff --git a/app/assets/javascripts/vue_shared/security_reports/security_reports_app.vue b/app/assets/javascripts/vue_shared/security_reports/security_reports_app.vue index 12f2bc71505..f6d85599dba 100644 --- a/app/assets/javascripts/vue_shared/security_reports/security_reports_app.vue +++ b/app/assets/javascripts/vue_shared/security_reports/security_reports_app.vue @@ -102,8 +102,8 @@ export default { error(error) { this.showError(error); }, - result({ loading }) { - if (loading) { + result({ loading, data }) { + if (loading || !data) { return; } |