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