summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/vue_shared/components
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2022-02-18 09:45:46 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2022-02-18 09:45:46 +0000
commita7b3560714b4d9cc4ab32dffcd1f74a284b93580 (patch)
tree7452bd5c3545c2fa67a28aa013835fb4fa071baf /app/assets/javascripts/vue_shared/components
parentee9173579ae56a3dbfe5afe9f9410c65bb327ca7 (diff)
downloadgitlab-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/components')
-rw-r--r--app/assets/javascripts/vue_shared/components/awards_list.vue7
-rw-r--r--app/assets/javascripts/vue_shared/components/blob_viewers/simple_viewer.vue18
-rw-r--r--app/assets/javascripts/vue_shared/components/ci_cd_analytics/ci_cd_analytics_charts.vue3
-rw-r--r--app/assets/javascripts/vue_shared/components/confirm_fork_modal.vue68
-rw-r--r--app/assets/javascripts/vue_shared/components/design_management/design_note_pin.vue28
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js14
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue23
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue7
-rw-r--r--app/assets/javascripts/vue_shared/components/gfm_autocomplete/gfm_autocomplete.vue106
-rw-r--r--app/assets/javascripts/vue_shared/components/gfm_autocomplete/utils.js195
-rw-r--r--app/assets/javascripts/vue_shared/components/help_popover.vue3
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/field.vue24
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/header.vue255
-rw-r--r--app/assets/javascripts/vue_shared/components/namespace_select/namespace_select.vue104
-rw-r--r--app/assets/javascripts/vue_shared/components/pikaday.vue48
-rw-r--r--app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon.vue49
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue148
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/multiselect_dropdown.vue37
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/constants.js111
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue (renamed from app/assets/javascripts/vue_shared/components/source_viewer.vue)51
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/utils.js26
-rw-r--r--app/assets/javascripts/vue_shared/components/svg_gradient.vue34
-rw-r--r--app/assets/javascripts/vue_shared/components/upload_dropzone/upload_dropzone.vue37
-rw-r--r--app/assets/javascripts/vue_shared/components/web_ide_link.vue27
26 files changed, 603 insertions, 828 deletions
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>