Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-12-20 16:37:47 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2021-12-20 16:37:47 +0300
commitaee0a117a889461ce8ced6fcf73207fe017f1d99 (patch)
tree891d9ef189227a8445d83f35c1b0fc99573f4380 /app/assets/javascripts/vue_shared/components
parent8d46af3258650d305f53b819eabf7ab18d22f59e (diff)
Add latest changes from gitlab-org/gitlab@14-6-stable-eev14.6.0-rc42
Diffstat (limited to 'app/assets/javascripts/vue_shared/components')
-rw-r--r--app/assets/javascripts/vue_shared/components/chronic_duration_input.vue133
-rw-r--r--app/assets/javascripts/vue_shared/components/clipboard_button.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/commit.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger.vue7
-rw-r--r--app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger_modal.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/confirm_modal.vue72
-rw-r--r--app/assets/javascripts/vue_shared/components/confirm_modal_eventhub.js5
-rw-r--r--app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/delete_label_modal.vue81
-rw-r--r--app/assets/javascripts/vue_shared/components/design_management/design_note_pin.vue67
-rw-r--r--app/assets/javascripts/vue_shared/components/dismissible_alert.vue1
-rw-r--r--app/assets/javascripts/vue_shared/components/dom_element_listener.vue28
-rw-r--r--app/assets/javascripts/vue_shared/components/file_icon/file_icon_map.js113
-rw-r--r--app/assets/javascripts/vue_shared/components/file_row.vue1
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js15
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/queries/epic.fragment.graphql15
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/queries/search_epics.query.graphql16
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/author_token.vue1
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue42
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/epic_token.vue129
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/iteration_token.vue138
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue1
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/weight_token.vue66
-rw-r--r--app/assets/javascripts/vue_shared/components/form/input_copy_toggle_visibility.stories.js27
-rw-r--r--app/assets/javascripts/vue_shared/components/form/input_copy_toggle_visibility.vue127
-rw-r--r--app/assets/javascripts/vue_shared/components/header_ci_component.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/issuable/init_issuable_header_warning.js22
-rw-r--r--app/assets/javascripts/vue_shared/components/issuable/issuable_header_warnings.vue61
-rw-r--r--app/assets/javascripts/vue_shared/components/issue/issue_assignees.vue107
-rw-r--r--app/assets/javascripts/vue_shared/components/issue/issue_milestone.vue91
-rw-r--r--app/assets/javascripts/vue_shared/components/issue/related_issuable_item.vue201
-rw-r--r--app/assets/javascripts/vue_shared/components/line_numbers.vue57
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/suggestions.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/toolbar.vue18
-rw-r--r--app/assets/javascripts/vue_shared/components/namespace_select/namespace_select.vue93
-rw-r--r--app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue10
-rw-r--r--app/assets/javascripts/vue_shared/components/notes/system_note.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/pagination_bar/pagination_bar.stories.js40
-rw-r--r--app/assets/javascripts/vue_shared/components/pagination_bar/pagination_bar.vue103
-rw-r--r--app/assets/javascripts/vue_shared/components/registry/list_item.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/registry/metadata_item.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue101
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue12
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_header.vue14
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_value.vue45
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_value_collapsed.vue55
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/epic_labels.query.graphql1
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/epic_update_labels.mutation.graphql4
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/issue_labels.query.graphql1
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/merge_request_labels.query.graphql1
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue21
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/queries/get_alert_assignees.query.graphql1
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_assignees.query.graphql1
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_participants.query.graphql1
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_assignees.query.graphql1
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_participants.query.graphql1
-rw-r--r--app/assets/javascripts/vue_shared/components/source_editor.vue1
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer.vue88
-rw-r--r--app/assets/javascripts/vue_shared/components/storage_counter/usage_graph.stories.js38
-rw-r--r--app/assets/javascripts/vue_shared/components/storage_counter/usage_graph.vue148
-rw-r--r--app/assets/javascripts/vue_shared/components/tooltip_on_truncate.vue69
-rw-r--r--app/assets/javascripts/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.stories.js88
-rw-r--r--app/assets/javascripts/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue85
65 files changed, 1195 insertions, 1502 deletions
diff --git a/app/assets/javascripts/vue_shared/components/chronic_duration_input.vue b/app/assets/javascripts/vue_shared/components/chronic_duration_input.vue
new file mode 100644
index 00000000000..ffbcdefc924
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/chronic_duration_input.vue
@@ -0,0 +1,133 @@
+<script>
+import * as Sentry from '@sentry/browser';
+import { GlFormInput } from '@gitlab/ui';
+import {
+ DurationParseError,
+ outputChronicDuration,
+ parseChronicDuration,
+} from '~/chronic_duration';
+import { __ } from '~/locale';
+
+export default {
+ components: {
+ GlFormInput,
+ },
+ model: {
+ prop: 'value',
+ event: 'change',
+ },
+ props: {
+ value: {
+ type: Number,
+ required: false,
+ default: null,
+ },
+ name: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ integerRequired: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ },
+ data() {
+ return {
+ numberData: this.value,
+ humanReadableData: this.convertDuration(this.value),
+ isValueValid: this.value === null ? null : true,
+ };
+ },
+ computed: {
+ numberValue: {
+ get() {
+ return this.numberData;
+ },
+ set(value) {
+ if (this.numberData !== value) {
+ this.numberData = value;
+ this.humanReadableData = this.convertDuration(value);
+ this.isValueValid = value === null ? null : true;
+ }
+ this.emitEvents();
+ },
+ },
+ humanReadableValue: {
+ get() {
+ return this.humanReadableData;
+ },
+ set(value) {
+ this.humanReadableData = value;
+ try {
+ if (value === '') {
+ this.numberData = null;
+ this.isValueValid = null;
+ } else {
+ this.numberData = parseChronicDuration(value, {
+ keepZero: true,
+ raiseExceptions: true,
+ });
+ this.isValueValid = true;
+ }
+ } catch (e) {
+ if (e instanceof DurationParseError) {
+ this.isValueValid = false;
+ } else {
+ Sentry.captureException(e);
+ }
+ }
+ this.emitEvents(true);
+ },
+ },
+ isValidDecimal() {
+ return !this.integerRequired || this.numberData === null || Number.isInteger(this.numberData);
+ },
+ feedback() {
+ if (this.isValueValid === false) {
+ return this.$options.i18n.INVALID_INPUT_FEEDBACK;
+ }
+ if (!this.isValidDecimal) {
+ return this.$options.i18n.INVALID_DECIMAL_FEEDBACK;
+ }
+ return '';
+ },
+ },
+ i18n: {
+ INVALID_INPUT_FEEDBACK: __('Please enter a valid time interval'),
+ INVALID_DECIMAL_FEEDBACK: __('An integer value is required for seconds'),
+ },
+ watch: {
+ value() {
+ this.numberValue = this.value;
+ },
+ },
+ mounted() {
+ this.emitEvents();
+ },
+ methods: {
+ convertDuration(value) {
+ return value === null ? '' : outputChronicDuration(value);
+ },
+ emitEvents(emitChange = false) {
+ if (emitChange && this.isValueValid !== false && this.isValidDecimal) {
+ this.$emit('change', this.numberData);
+ }
+ const { feedback } = this;
+ this.$refs.text.$el.setCustomValidity(feedback);
+ this.$refs.hidden.setCustomValidity(feedback);
+ this.$emit('valid', {
+ valid: this.isValueValid && this.isValidDecimal,
+ feedback,
+ });
+ },
+ },
+};
+</script>
+<template>
+ <div>
+ <gl-form-input ref="text" v-bind="$attrs" v-model="humanReadableValue" />
+ <input ref="hidden" type="hidden" :name="name" :value="numberValue" />
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/clipboard_button.vue b/app/assets/javascripts/vue_shared/components/clipboard_button.vue
index fe329b18f30..400be3ef688 100644
--- a/app/assets/javascripts/vue_shared/components/clipboard_button.vue
+++ b/app/assets/javascripts/vue_shared/components/clipboard_button.vue
@@ -66,6 +66,11 @@ export default {
required: false,
default: 'medium',
},
+ variant: {
+ type: String,
+ required: false,
+ default: 'default',
+ },
},
computed: {
clipboardText() {
@@ -92,6 +97,7 @@ export default {
:size="size"
icon="copy-to-clipboard"
:aria-label="__('Copy this value')"
+ :variant="variant"
v-on="$listeners"
>
<slot></slot>
diff --git a/app/assets/javascripts/vue_shared/components/commit.vue b/app/assets/javascripts/vue_shared/components/commit.vue
index 5f50a699034..ebbc1bfb037 100644
--- a/app/assets/javascripts/vue_shared/components/commit.vue
+++ b/app/assets/javascripts/vue_shared/components/commit.vue
@@ -2,7 +2,7 @@
import { GlTooltipDirective, GlLink, GlIcon } from '@gitlab/ui';
import { isString, isEmpty } from 'lodash';
import { __, sprintf } from '~/locale';
-import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
+import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue';
import UserAvatarLink from './user_avatar/user_avatar_link.vue';
export default {
diff --git a/app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger.vue b/app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger.vue
index 4c07cf44fed..f93415ced45 100644
--- a/app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger.vue
+++ b/app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger.vue
@@ -26,6 +26,11 @@ export default {
type: String,
required: true,
},
+ buttonClass: {
+ type: String,
+ required: false,
+ default: '',
+ },
buttonTestid: {
type: String,
required: false,
@@ -39,7 +44,7 @@ export default {
<div>
<gl-button
v-gl-modal="$options.modalId"
- class="gl-button"
+ :class="buttonClass"
variant="danger"
:disabled="disabled"
:data-testid="buttonTestid"
diff --git a/app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger_modal.vue b/app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger_modal.vue
index 30c96daf7e3..5bbe44b20b3 100644
--- a/app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger_modal.vue
+++ b/app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger_modal.vue
@@ -47,7 +47,7 @@ export default {
actionPrimary() {
return {
text: this.confirmButtonText,
- attributes: [{ variant: 'danger', disabled: !this.isValid }],
+ attributes: [{ variant: 'danger', disabled: !this.isValid, class: 'qa-confirm-button' }],
};
},
},
@@ -95,7 +95,7 @@ export default {
<gl-form-input
id="confirm_name_input"
v-model="confirmationPhrase"
- class="form-control"
+ class="form-control qa-confirm-input"
data-testid="confirm-danger-input"
type="text"
/>
diff --git a/app/assets/javascripts/vue_shared/components/confirm_modal.vue b/app/assets/javascripts/vue_shared/components/confirm_modal.vue
index 7c1d3772acd..72504e5bc50 100644
--- a/app/assets/javascripts/vue_shared/components/confirm_modal.vue
+++ b/app/assets/javascripts/vue_shared/components/confirm_modal.vue
@@ -2,10 +2,13 @@
import { GlModal, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import { uniqueId } from 'lodash';
import csrf from '~/lib/utils/csrf';
+import eventHub, { EVENT_OPEN_CONFIRM_MODAL } from './confirm_modal_eventhub';
+import DomElementListener from './dom_element_listener.vue';
export default {
components: {
GlModal,
+ DomElementListener,
},
directives: {
SafeHtml,
@@ -30,18 +33,35 @@ export default {
};
},
mounted() {
- document.querySelectorAll(this.selector).forEach((button) => {
- button.addEventListener('click', (e) => {
- e.preventDefault();
-
- this.path = button.dataset.path;
- this.method = button.dataset.method;
- this.modalAttributes = JSON.parse(button.dataset.modalAttributes);
- this.openModal();
- });
- });
+ eventHub.$on(EVENT_OPEN_CONFIRM_MODAL, this.onOpenEvent);
+ },
+ destroyed() {
+ eventHub.$off(EVENT_OPEN_CONFIRM_MODAL, this.onOpenEvent);
},
methods: {
+ onButtonPress(e) {
+ const element = e.currentTarget;
+
+ if (!element.dataset.path) {
+ return;
+ }
+
+ const modalAttributes = element.dataset.modalAttributes
+ ? JSON.parse(element.dataset.modalAttributes)
+ : {};
+
+ this.onOpenEvent({
+ path: element.dataset.path,
+ method: element.dataset.method,
+ modalAttributes,
+ });
+ },
+ onOpenEvent({ path, method, modalAttributes }) {
+ this.path = path;
+ this.method = method;
+ this.modalAttributes = modalAttributes;
+ this.openModal();
+ },
openModal() {
this.$refs.modal.show();
},
@@ -61,21 +81,23 @@ export default {
</script>
<template>
- <gl-modal
- ref="modal"
- :modal-id="modalId"
- v-bind="modalAttributes"
- @primary="submitModal"
- @cancel="closeModal"
- >
- <form ref="form" :action="path" method="post">
- <!-- Rails workaround for <form method="delete" />
+ <dom-element-listener :selector="selector" @click.prevent="onButtonPress">
+ <gl-modal
+ ref="modal"
+ :modal-id="modalId"
+ v-bind="modalAttributes"
+ @primary="submitModal"
+ @cancel="closeModal"
+ >
+ <form ref="form" :action="path" method="post">
+ <!-- Rails workaround for <form method="delete" />
https://github.com/rails/rails/blob/master/actionview/app/assets/javascripts/rails-ujs/features/method.coffee
-->
- <input type="hidden" name="_method" :value="method" />
- <input type="hidden" name="authenticity_token" :value="$options.csrf.token" />
- <div v-if="modalAttributes.messageHtml" v-safe-html="modalAttributes.messageHtml"></div>
- <div v-else>{{ modalAttributes.message }}</div>
- </form>
- </gl-modal>
+ <input type="hidden" name="_method" :value="method" />
+ <input type="hidden" name="authenticity_token" :value="$options.csrf.token" />
+ <div v-if="modalAttributes.messageHtml" v-safe-html="modalAttributes.messageHtml"></div>
+ <div v-else>{{ modalAttributes.message }}</div>
+ </form>
+ </gl-modal>
+ </dom-element-listener>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/confirm_modal_eventhub.js b/app/assets/javascripts/vue_shared/components/confirm_modal_eventhub.js
new file mode 100644
index 00000000000..f8d9d410ace
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/confirm_modal_eventhub.js
@@ -0,0 +1,5 @@
+import createEventHub from '~/helpers/event_hub_factory';
+
+export default createEventHub();
+
+export const EVENT_OPEN_CONFIRM_MODAL = Symbol('OPEN');
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 1a96cabf755..e546ca57c5e 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
@@ -3,7 +3,7 @@ import { GlIcon, GlButton, GlDropdown, GlDropdownItem, GlFormGroup } from '@gitl
import { convertToFixedRange, isEqualTimeRanges, findTimeRange } from '~/lib/utils/datetime_range';
import { __, sprintf } from '~/locale';
-import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
+import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue';
import DateTimePickerInput from './date_time_picker_input.vue';
import {
defaultTimeRanges,
diff --git a/app/assets/javascripts/vue_shared/components/delete_label_modal.vue b/app/assets/javascripts/vue_shared/components/delete_label_modal.vue
deleted file mode 100644
index 1ff0938d086..00000000000
--- a/app/assets/javascripts/vue_shared/components/delete_label_modal.vue
+++ /dev/null
@@ -1,81 +0,0 @@
-<script>
-import { GlModal, GlSprintf, GlButton } from '@gitlab/ui';
-import { uniqueId } from 'lodash';
-
-export default {
- components: {
- GlModal,
- GlSprintf,
- GlButton,
- },
- props: {
- selector: {
- type: String,
- required: true,
- },
- },
- data() {
- return {
- labelName: '',
- subjectName: '',
- destroyPath: '',
- modalId: uniqueId('modal-delete-label-'),
- };
- },
- mounted() {
- document.querySelectorAll(this.selector).forEach((button) => {
- button.addEventListener('click', (e) => {
- e.preventDefault();
-
- const { labelName, subjectName, destroyPath } = button.dataset;
- this.labelName = labelName;
- this.subjectName = subjectName;
- this.destroyPath = destroyPath;
- this.openModal();
- });
- });
- },
- methods: {
- openModal() {
- this.$refs.modal.show();
- },
- closeModal() {
- this.$refs.modal.hide();
- },
- },
-};
-</script>
-
-<template>
- <gl-modal ref="modal" :modal-id="modalId">
- <template #modal-title>
- <gl-sprintf :message="__('Delete label: %{labelName}')">
- <template #labelName>
- {{ labelName }}
- </template>
- </gl-sprintf>
- </template>
- <gl-sprintf
- :message="
- __(
- `%{strongStart}${labelName}%{strongEnd} will be permanently deleted from ${subjectName}. This cannot be undone.`,
- )
- "
- >
- <template #strong="{ content }">
- <strong>{{ content }}</strong>
- </template>
- </gl-sprintf>
- <template #modal-footer>
- <gl-button category="secondary" @click="closeModal">{{ __('Cancel') }}</gl-button>
- <gl-button
- category="primary"
- variant="danger"
- :href="destroyPath"
- data-method="delete"
- data-testid="delete-button"
- >{{ __('Delete label') }}</gl-button
- >
- </template>
- </gl-modal>
-</template>
diff --git a/app/assets/javascripts/vue_shared/components/design_management/design_note_pin.vue b/app/assets/javascripts/vue_shared/components/design_management/design_note_pin.vue
new file mode 100644
index 00000000000..cb038a8c4e1
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/design_management/design_note_pin.vue
@@ -0,0 +1,67 @@
+<script>
+import { GlIcon } from '@gitlab/ui';
+import { __, sprintf } from '~/locale';
+
+export default {
+ name: 'DesignNotePin',
+ components: {
+ GlIcon,
+ },
+ props: {
+ position: {
+ type: Object,
+ required: false,
+ default: null,
+ },
+ label: {
+ type: Number,
+ required: false,
+ default: null,
+ },
+ isResolved: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ isInactive: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ computed: {
+ isNewNote() {
+ return this.label === null;
+ },
+ pinLabel() {
+ return this.isNewNote
+ ? __('Comment form position')
+ : sprintf(__("Comment '%{label}' position"), { label: this.label });
+ },
+ },
+};
+</script>
+
+<template>
+ <button
+ :style="position"
+ :aria-label="pinLabel"
+ :class="{
+ 'btn-transparent comment-indicator': isNewNote,
+ 'js-image-badge design-note-pin': !isNewNote,
+ resolved: isResolved,
+ inactive: isInactive,
+ 'gl-absolute': position,
+ }"
+ class="gl-display-flex gl-align-items-center gl-justify-content-center gl-font-sm"
+ type="button"
+ @mousedown="$emit('mousedown', $event)"
+ @mouseup="$emit('mouseup', $event)"
+ @click="$emit('click', $event)"
+ >
+ <gl-icon v-if="isNewNote" name="image-comment-dark" :size="24" />
+ <template v-else>
+ {{ label }}
+ </template>
+ </button>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/dismissible_alert.vue b/app/assets/javascripts/vue_shared/components/dismissible_alert.vue
index 52371e42ba1..0621ec14c6c 100644
--- a/app/assets/javascripts/vue_shared/components/dismissible_alert.vue
+++ b/app/assets/javascripts/vue_shared/components/dismissible_alert.vue
@@ -24,6 +24,7 @@ export default {
methods: {
dismiss() {
this.isDismissed = true;
+ this.$emit('alertDismissed');
},
},
};
diff --git a/app/assets/javascripts/vue_shared/components/dom_element_listener.vue b/app/assets/javascripts/vue_shared/components/dom_element_listener.vue
new file mode 100644
index 00000000000..ca427ed4897
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/dom_element_listener.vue
@@ -0,0 +1,28 @@
+<script>
+export default {
+ props: {
+ selector: {
+ type: String,
+ required: true,
+ },
+ },
+ mounted() {
+ this.disposables = Array.from(document.querySelectorAll(this.selector)).flatMap((button) => {
+ return Object.entries(this.$listeners).map(([key, value]) => {
+ button.addEventListener(key, value);
+ return () => {
+ button.removeEventListener(key, value);
+ };
+ });
+ });
+ },
+ destroyed() {
+ this.disposables.forEach((x) => {
+ x();
+ });
+ },
+ render() {
+ return this.$slots.default;
+ },
+};
+</script>
diff --git a/app/assets/javascripts/vue_shared/components/file_icon/file_icon_map.js b/app/assets/javascripts/vue_shared/components/file_icon/file_icon_map.js
index e1e71639115..8686d317c8a 100644
--- a/app/assets/javascripts/vue_shared/components/file_icon/file_icon_map.js
+++ b/app/assets/javascripts/vue_shared/components/file_icon/file_icon_map.js
@@ -6,15 +6,10 @@ const fileExtensionIcons = {
jade: 'pug',
pug: 'pug',
md: 'markdown',
- 'md.rendered': 'markdown',
markdown: 'markdown',
- 'markdown.rendered': 'markdown',
mdown: 'markdown',
- 'mdown.rendered': 'markdown',
mkd: 'markdown',
- 'mkd.rendered': 'markdown',
mkdn: 'markdown',
- 'mkdn.rendered': 'markdown',
rst: 'markdown',
blink: 'blink',
css: 'css',
@@ -23,7 +18,6 @@ const fileExtensionIcons = {
less: 'less',
json: 'json',
yaml: 'yaml',
- 'YAML-tmLanguage': 'yaml',
yml: 'yaml',
xml: 'xml',
plist: 'xml',
@@ -85,10 +79,7 @@ const fileExtensionIcons = {
props: 'settings',
toml: 'settings',
prefs: 'settings',
- 'sln.dotsettings': 'settings',
- 'sln.dotsettings.user': 'settings',
ts: 'typescript',
- 'd.ts': 'typescript-def',
marko: 'markojs',
pdf: 'pdf',
xlsx: 'table',
@@ -99,7 +90,6 @@ const fileExtensionIcons = {
vscodeignore: 'vscode',
vsixmanifest: 'vscode',
vsix: 'vscode',
- 'code-workplace': 'vscode',
suo: 'visualstudio',
sln: 'visualstudio',
csproj: 'visualstudio',
@@ -118,7 +108,6 @@ const fileExtensionIcons = {
xz: 'zip',
bzip2: 'zip',
gzip: 'zip',
- '7z': 'zip',
rar: 'zip',
tgz: 'zip',
exe: 'exe',
@@ -129,7 +118,6 @@ const fileExtensionIcons = {
c: 'c',
m: 'c',
h: 'h',
- 'c++': 'cpp',
cc: 'cpp',
cpp: 'cpp',
mm: 'cpp',
@@ -231,7 +219,6 @@ const fileExtensionIcons = {
m2v: 'movie',
vdi: 'virtual',
vbox: 'virtual',
- 'vbox-prev': 'virtual',
ics: 'email',
mp3: 'music',
flac: 'music',
@@ -277,44 +264,12 @@ const fileExtensionIcons = {
ml: 'ocaml',
mli: 'ocaml',
cmx: 'ocaml',
- 'js.map': 'javascript-map',
- 'css.map': 'css-map',
lock: 'lock',
hbs: 'handlebars',
mustache: 'handlebars',
pl: 'perl',
pm: 'perl',
hx: 'haxe',
- 'spec.ts': 'test-ts',
- 'test.ts': 'test-ts',
- 'ts.snap': 'test-ts',
- 'spec.tsx': 'test-jsx',
- 'test.tsx': 'test-jsx',
- 'tsx.snap': 'test-jsx',
- 'spec.jsx': 'test-jsx',
- 'test.jsx': 'test-jsx',
- 'jsx.snap': 'test-jsx',
- 'spec.js': 'test-js',
- 'test.js': 'test-js',
- 'js.snap': 'test-js',
- 'routing.ts': 'angular-routing',
- 'routing.js': 'angular-routing',
- 'module.ts': 'angular',
- 'module.js': 'angular',
- 'ng-template': 'angular',
- 'component.ts': 'angular-component',
- 'component.js': 'angular-component',
- 'guard.ts': 'angular-guard',
- 'guard.js': 'angular-guard',
- 'service.ts': 'angular-service',
- 'service.js': 'angular-service',
- 'pipe.ts': 'angular-pipe',
- 'pipe.js': 'angular-pipe',
- 'filter.js': 'angular-pipe',
- 'directive.ts': 'angular-directive',
- 'directive.js': 'angular-directive',
- 'resolver.ts': 'angular-resolver',
- 'resolver.js': 'angular-resolver',
pp: 'puppet',
ex: 'elixir',
exs: 'elixir',
@@ -345,11 +300,8 @@ const fileExtensionIcons = {
haml: 'haml',
yang: 'yang',
tf: 'terraform',
- 'tf.json': 'terraform',
tfvars: 'terraform',
tfstate: 'terraform',
- 'blade.php': 'laravel',
- 'inky.php': 'laravel',
applescript: 'applescript',
cake: 'cake',
feature: 'cucumber',
@@ -376,16 +328,68 @@ const fileExtensionIcons = {
kv: 'kivy',
graphcool: 'graphcool',
sbt: 'sbt',
+ cr: 'crystal',
+ cu: 'cuda',
+ cuh: 'cuda',
+ log: 'log',
+};
+
+const twoFileExtensionIcons = {
+ 'gradle.kts': 'gradle',
+ 'md.rendered': 'markdown',
+ 'markdown.rendered': 'markdown',
+ 'mdown.rendered': 'markdown',
+ 'mkd.rendered': 'markdown',
+ 'mkdn.rendered': 'markdown',
+ 'YAML-tmLanguage': 'yaml',
+ 'sln.dotsettings': 'settings',
+ 'sln.dotsettings.user': 'settings',
+ 'd.ts': 'typescript-def',
+ 'code-workplace': 'vscode',
+ '7z': 'zip',
+ 'c++': 'cpp',
+ 'vbox-prev': 'virtual',
+ 'js.map': 'javascript-map',
+ 'css.map': 'css-map',
+ 'spec.ts': 'test-ts',
+ 'test.ts': 'test-ts',
+ 'ts.snap': 'test-ts',
+ 'spec.tsx': 'test-jsx',
+ 'test.tsx': 'test-jsx',
+ 'tsx.snap': 'test-jsx',
+ 'spec.jsx': 'test-jsx',
+ 'test.jsx': 'test-jsx',
+ 'jsx.snap': 'test-jsx',
+ 'spec.js': 'test-js',
+ 'test.js': 'test-js',
+ 'js.snap': 'test-js',
+ 'routing.ts': 'angular-routing',
+ 'routing.js': 'angular-routing',
+ 'module.ts': 'angular',
+ 'module.js': 'angular',
+ 'ng-template': 'angular',
+ 'component.ts': 'angular-component',
+ 'component.js': 'angular-component',
+ 'guard.ts': 'angular-guard',
+ 'guard.js': 'angular-guard',
+ 'service.ts': 'angular-service',
+ 'service.js': 'angular-service',
+ 'pipe.ts': 'angular-pipe',
+ 'pipe.js': 'angular-pipe',
+ 'filter.js': 'angular-pipe',
+ 'directive.ts': 'angular-directive',
+ 'directive.js': 'angular-directive',
+ 'resolver.ts': 'angular-resolver',
+ 'resolver.js': 'angular-resolver',
+ 'tf.json': 'terraform',
+ 'blade.php': 'laravel',
+ 'inky.php': 'laravel',
'reducer.ts': 'ngrx-reducer',
'rootReducer.ts': 'ngrx-reducer',
'state.ts': 'ngrx-state',
'actions.ts': 'ngrx-actions',
'effects.ts': 'ngrx-effects',
- cr: 'crystal',
'drone.yml': 'drone',
- cu: 'cuda',
- cuh: 'cuda',
- log: 'log',
};
const fileNameIcons = {
@@ -598,6 +602,9 @@ const fileNameIcons = {
export default function getIconForFile(name) {
return (
- fileNameIcons[name] || fileExtensionIcons[name ? name.split('.').pop().toLowerCase() : ''] || ''
+ fileNameIcons[name] ||
+ twoFileExtensionIcons[name ? name.split('.').slice(-2).join('.') : ''] ||
+ fileExtensionIcons[name ? name.split('.').pop().toLowerCase() : ''] ||
+ ''
);
}
diff --git a/app/assets/javascripts/vue_shared/components/file_row.vue b/app/assets/javascripts/vue_shared/components/file_row.vue
index 0b0a416b7ef..2227047a909 100644
--- a/app/assets/javascripts/vue_shared/components/file_row.vue
+++ b/app/assets/javascripts/vue_shared/components/file_row.vue
@@ -146,6 +146,7 @@ export default {
ref="textOutput"
:style="levelIndentation"
class="file-row-name"
+ :title="file.name"
data-qa-selector="file_name_content"
:data-qa-file-name="file.name"
data-testid="file-row-name-container"
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 d9290e86bca..810d9f782b9 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
@@ -2,7 +2,6 @@ import { __ } from '~/locale';
export const DEBOUNCE_DELAY = 200;
export const MAX_RECENT_TOKENS_SIZE = 3;
-export const WEIGHT_TOKEN_SUGGESTIONS_SIZE = 21;
export const FILTER_NONE = 'None';
export const FILTER_ANY = 'Any';
@@ -24,22 +23,11 @@ export const DEFAULT_LABEL_NONE = { value: FILTER_NONE, text: __('None'), title:
export const DEFAULT_LABEL_ANY = { value: FILTER_ANY, text: __('Any'), title: __('Any') };
export const DEFAULT_NONE_ANY = [DEFAULT_LABEL_NONE, DEFAULT_LABEL_ANY];
-export const DEFAULT_ITERATIONS = DEFAULT_NONE_ANY.concat([
- { value: FILTER_CURRENT, text: __('Current') },
-]);
-
export const DEFAULT_MILESTONES = DEFAULT_NONE_ANY.concat([
{ value: FILTER_UPCOMING, text: __('Upcoming'), title: __('Upcoming') },
{ value: FILTER_STARTED, text: __('Started'), title: __('Started') },
]);
-export const DEFAULT_MILESTONES_GRAPHQL = [
- { value: 'any', text: __('Any'), title: __('Any') },
- { value: 'none', text: __('None'), title: __('None') },
- { value: '#upcoming', text: __('Upcoming'), title: __('Upcoming') },
- { value: '#started', text: __('Started'), title: __('Started') },
-];
-
export const SortDirection = {
descending: 'descending',
ascending: 'ascending',
@@ -56,6 +44,3 @@ export const TOKEN_TITLE_TYPE = __('Type');
export const TOKEN_TITLE_RELEASE = __('Release');
export const TOKEN_TITLE_MY_REACTION = __('My-Reaction');
export const TOKEN_TITLE_CONFIDENTIAL = __('Confidential');
-export const TOKEN_TITLE_ITERATION = __('Iteration');
-export const TOKEN_TITLE_EPIC = __('Epic');
-export const TOKEN_TITLE_WEIGHT = __('Weight');
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/queries/epic.fragment.graphql b/app/assets/javascripts/vue_shared/components/filtered_search_bar/queries/epic.fragment.graphql
deleted file mode 100644
index 9e9bda8ad3e..00000000000
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/queries/epic.fragment.graphql
+++ /dev/null
@@ -1,15 +0,0 @@
-fragment EpicNode on Epic {
- id
- iid
- group {
- fullPath
- }
- title
- state
- reference
- referencePath: reference(full: true)
- webPath
- webUrl
- createdAt
- closedAt
-}
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/queries/search_epics.query.graphql b/app/assets/javascripts/vue_shared/components/filtered_search_bar/queries/search_epics.query.graphql
deleted file mode 100644
index 4bb4b586fc9..00000000000
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/queries/search_epics.query.graphql
+++ /dev/null
@@ -1,16 +0,0 @@
-#import "./epic.fragment.graphql"
-
-query searchEpics($fullPath: ID!, $search: String, $state: EpicState) {
- group(fullPath: $fullPath) {
- epics(
- search: $search
- state: $state
- includeAncestorGroups: true
- includeDescendantGroups: false
- ) {
- nodes {
- ...EpicNode
- }
- }
- }
-}
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 b3b3d5c88c6..06478a89721 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
@@ -87,7 +87,6 @@ export default {
:get-active-token-value="getActiveAuthor"
:default-suggestions="defaultAuthors"
:preloaded-suggestions="preloadedAuthors"
- :recent-suggestions-storage-key="config.recentSuggestionsStorageKey"
@fetch-suggestions="fetchAuthors"
v-on="$listeners"
>
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue
index cee7c40aa83..bbc1888bc0b 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue
@@ -4,12 +4,17 @@ import {
GlFilteredSearchSuggestion,
GlDropdownDivider,
GlDropdownSectionHeader,
+ GlDropdownText,
GlLoadingIcon,
} from '@gitlab/ui';
import { debounce } from 'lodash';
import { DEBOUNCE_DELAY, FILTER_NONE_ANY, OPERATOR_IS_NOT } from '../constants';
-import { getRecentlyUsedSuggestions, setTokenValueToRecentlyUsed } from '../filtered_search_utils';
+import {
+ getRecentlyUsedSuggestions,
+ setTokenValueToRecentlyUsed,
+ stripQuotes,
+} from '../filtered_search_utils';
export default {
components: {
@@ -17,6 +22,7 @@ export default {
GlFilteredSearchSuggestion,
GlDropdownDivider,
GlDropdownSectionHeader,
+ GlDropdownText,
GlLoadingIcon,
},
props: {
@@ -57,11 +63,6 @@ export default {
required: false,
default: () => [],
},
- recentSuggestionsStorageKey: {
- type: String,
- required: false,
- default: '',
- },
valueIdentifier: {
type: String,
required: false,
@@ -76,14 +77,14 @@ export default {
data() {
return {
searchKey: '',
- recentSuggestions: this.recentSuggestionsStorageKey
- ? getRecentlyUsedSuggestions(this.recentSuggestionsStorageKey)
+ recentSuggestions: this.config.recentSuggestionsStorageKey
+ ? getRecentlyUsedSuggestions(this.config.recentSuggestionsStorageKey)
: [],
};
},
computed: {
isRecentSuggestionsEnabled() {
- return Boolean(this.recentSuggestionsStorageKey);
+ return Boolean(this.config.recentSuggestionsStorageKey);
},
recentTokenIds() {
return this.recentSuggestions.map((tokenValue) => tokenValue[this.valueIdentifier]);
@@ -119,6 +120,9 @@ export default {
showDefaultSuggestions() {
return this.availableDefaultSuggestions.length > 0;
},
+ showNoMatchesText() {
+ return this.searchKey && !this.availableSuggestions.length;
+ },
showRecentSuggestions() {
return (
this.isRecentSuggestionsEnabled && this.recentSuggestions.length > 0 && !this.searchKey
@@ -163,11 +167,20 @@ export default {
this.searchKey = data;
if (!this.suggestionsLoading && !this.activeTokenValue) {
- const search = this.searchTerm ? this.searchTerm : data;
+ let search = this.searchTerm ? this.searchTerm : data;
+
+ if (search.startsWith('"') && search.endsWith('"')) {
+ search = stripQuotes(search);
+ } else if (search.startsWith('"')) {
+ search = search.slice(1, search.length);
+ }
+
this.$emit('fetch-suggestions', search);
}
}, DEBOUNCE_DELAY),
- handleTokenValueSelected(activeTokenValue) {
+ handleTokenValueSelected(selectedValue) {
+ const activeTokenValue = this.getActiveTokenValue(this.suggestions, selectedValue);
+
// Make sure that;
// 1. Recently used values feature is enabled
// 2. User has actually selected a value
@@ -177,7 +190,7 @@ export default {
activeTokenValue &&
!this.preloadedTokenIds.includes(activeTokenValue[this.valueIdentifier])
) {
- setTokenValueToRecentlyUsed(this.recentSuggestionsStorageKey, activeTokenValue);
+ setTokenValueToRecentlyUsed(this.config.recentSuggestionsStorageKey, activeTokenValue);
}
},
},
@@ -192,7 +205,7 @@ export default {
v-bind="$attrs"
v-on="$listeners"
@input="handleInput"
- @select="handleTokenValueSelected(activeTokenValue)"
+ @select="handleTokenValueSelected"
>
<template #view-token="viewTokenProps">
<slot name="view-token" :view-token-props="{ ...viewTokenProps, activeTokenValue }"></slot>
@@ -222,6 +235,9 @@ export default {
:suggestions="preloadedSuggestions"
></slot>
<gl-loading-icon v-if="suggestionsLoading" size="sm" />
+ <gl-dropdown-text v-else-if="showNoMatchesText">
+ {{ __('No matches found') }}
+ </gl-dropdown-text>
<template v-else>
<slot name="suggestions-list" :suggestions="availableSuggestions"></slot>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/epic_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/epic_token.vue
deleted file mode 100644
index 9c2f5306654..00000000000
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/epic_token.vue
+++ /dev/null
@@ -1,129 +0,0 @@
-<script>
-import { GlFilteredSearchSuggestion } from '@gitlab/ui';
-import createFlash from '~/flash';
-import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import { __ } from '~/locale';
-import { DEFAULT_NONE_ANY, FILTER_NONE_ANY, OPERATOR_IS_NOT } from '../constants';
-import searchEpicsQuery from '../queries/search_epics.query.graphql';
-
-import BaseToken from './base_token.vue';
-
-export default {
- prefix: '&',
- separator: '::',
- components: {
- BaseToken,
- GlFilteredSearchSuggestion,
- },
- props: {
- config: {
- type: Object,
- required: true,
- },
- value: {
- type: Object,
- required: true,
- },
- active: {
- type: Boolean,
- required: true,
- },
- },
- data() {
- return {
- epics: this.config.initialEpics || [],
- loading: false,
- };
- },
- computed: {
- idProperty() {
- return this.config.idProperty || 'iid';
- },
- currentValue() {
- const epicIid = Number(this.value.data);
- if (epicIid) {
- return epicIid;
- }
- return this.value.data;
- },
- defaultEpics() {
- return this.config.defaultEpics || DEFAULT_NONE_ANY;
- },
- availableDefaultEpics() {
- if (this.value.operator === OPERATOR_IS_NOT) {
- return this.defaultEpics.filter(
- (suggestion) => !FILTER_NONE_ANY.includes(suggestion.value),
- );
- }
- return this.defaultEpics;
- },
- },
- methods: {
- fetchEpics(search = '') {
- return this.$apollo
- .query({
- query: searchEpicsQuery,
- variables: { fullPath: this.config.fullPath, search },
- })
- .then(({ data }) => data.group?.epics.nodes);
- },
- fetchEpicsBySearchTerm(search) {
- this.loading = true;
- this.fetchEpics(search)
- .then((response) => {
- this.epics = Array.isArray(response) ? response : response?.data;
- })
- .catch(() => createFlash({ message: __('There was a problem fetching epics.') }))
- .finally(() => {
- this.loading = false;
- });
- },
- getActiveEpic(epics, data) {
- if (data && epics.length) {
- return epics.find((epic) => this.getValue(epic) === data);
- }
- return undefined;
- },
- getValue(epic) {
- return this.getEpicIdProperty(epic).toString();
- },
- displayValue(epic) {
- return `${this.$options.prefix}${this.getEpicIdProperty(epic)}${this.$options.separator}${
- epic?.title
- }`;
- },
- getEpicIdProperty(epic) {
- return getIdFromGraphQLId(epic[this.idProperty]);
- },
- },
-};
-</script>
-
-<template>
- <base-token
- :config="config"
- :value="value"
- :active="active"
- :suggestions-loading="loading"
- :suggestions="epics"
- :get-active-token-value="getActiveEpic"
- :default-suggestions="availableDefaultEpics"
- :recent-suggestions-storage-key="config.recentSuggestionsStorageKey"
- search-by="title"
- @fetch-suggestions="fetchEpicsBySearchTerm"
- v-on="$listeners"
- >
- <template #view="{ viewTokenProps: { inputValue, activeTokenValue } }">
- {{ activeTokenValue ? displayValue(activeTokenValue) : inputValue }}
- </template>
- <template #suggestions-list="{ suggestions }">
- <gl-filtered-search-suggestion
- v-for="epic in suggestions"
- :key="epic.id"
- :value="getValue(epic)"
- >
- {{ epic.title }}
- </gl-filtered-search-suggestion>
- </template>
- </base-token>
-</template>
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/iteration_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/iteration_token.vue
deleted file mode 100644
index aff93ebc9c0..00000000000
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/iteration_token.vue
+++ /dev/null
@@ -1,138 +0,0 @@
-<script>
-import { GlDropdownDivider, GlDropdownSectionHeader, GlFilteredSearchSuggestion } from '@gitlab/ui';
-import createFlash from '~/flash';
-import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import { __ } from '~/locale';
-import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
-import { formatDate } from '~/lib/utils/datetime_utility';
-import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import { DEFAULT_ITERATIONS } from '../constants';
-
-export default {
- components: {
- BaseToken,
- GlDropdownDivider,
- GlDropdownSectionHeader,
- GlFilteredSearchSuggestion,
- },
- mixins: [glFeatureFlagMixin()],
- props: {
- active: {
- type: Boolean,
- required: true,
- },
- config: {
- type: Object,
- required: true,
- },
- value: {
- type: Object,
- required: true,
- },
- },
- data() {
- return {
- iterations: this.config.initialIterations || [],
- loading: false,
- };
- },
- computed: {
- defaultIterations() {
- return this.config.defaultIterations || DEFAULT_ITERATIONS;
- },
- },
- methods: {
- getActiveIteration(iterations, data) {
- return iterations.find((iteration) => this.getValue(iteration) === data);
- },
- groupIterationsByCadence(iterations) {
- const cadences = [];
- iterations.forEach((iteration) => {
- if (!iteration.iterationCadence) {
- return;
- }
- const { title } = iteration.iterationCadence;
- const cadenceIteration = {
- id: iteration.id,
- title: iteration.title,
- period: this.getIterationPeriod(iteration),
- };
- const cadence = cadences.find((cad) => cad.title === title);
- if (cadence) {
- cadence.iterations.push(cadenceIteration);
- } else {
- cadences.push({ title, iterations: [cadenceIteration] });
- }
- });
- return cadences;
- },
- fetchIterations(searchTerm) {
- this.loading = true;
- this.config
- .fetchIterations(searchTerm)
- .then((response) => {
- this.iterations = Array.isArray(response) ? response : response.data;
- })
- .catch(() => {
- createFlash({ message: __('There was a problem fetching iterations.') });
- })
- .finally(() => {
- this.loading = false;
- });
- },
- getValue(iteration) {
- return String(getIdFromGraphQLId(iteration.id));
- },
- /**
- * TODO: https://gitlab.com/gitlab-org/gitlab/-/issues/344619
- * This method also exists as a utility function in ee/../iterations/utils.js
- * Remove the duplication when iteration token is moved to EE.
- */
- getIterationPeriod({ startDate, dueDate }) {
- const start = formatDate(startDate, 'mmm d, yyyy', true);
- const due = formatDate(dueDate, 'mmm d, yyyy', true);
- return `${start} - ${due}`;
- },
- },
-};
-</script>
-
-<template>
- <base-token
- :active="active"
- :config="config"
- :value="value"
- :default-suggestions="defaultIterations"
- :suggestions="iterations"
- :suggestions-loading="loading"
- :get-active-token-value="getActiveIteration"
- @fetch-suggestions="fetchIterations"
- v-on="$listeners"
- >
- <template #view="{ viewTokenProps: { inputValue, activeTokenValue } }">
- {{ activeTokenValue ? activeTokenValue.title : inputValue }}
- </template>
- <template #suggestions-list="{ suggestions }">
- <template v-for="(cadence, index) in groupIterationsByCadence(suggestions)">
- <gl-dropdown-divider v-if="index !== 0" :key="index" />
- <gl-dropdown-section-header
- :key="cadence.title"
- class="gl-overflow-hidden"
- :title="cadence.title"
- >
- {{ cadence.title }}
- </gl-dropdown-section-header>
- <gl-filtered-search-suggestion
- v-for="iteration in cadence.iterations"
- :key="iteration.id"
- :value="getValue(iteration)"
- >
- {{ iteration.title }}
- <div v-if="glFeatures.iterationCadences" class="gl-text-gray-400">
- {{ iteration.period }}
- </div>
- </gl-filtered-search-suggestion>
- </template>
- </template>
- </base-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 c31f3a25fb1..3f7a8920f48 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
@@ -104,7 +104,6 @@ export default {
:suggestions="labels"
:get-active-token-value="getActiveLabel"
:default-suggestions="defaultLabels"
- :recent-suggestions-storage-key="config.recentSuggestionsStorageKey"
@fetch-suggestions="fetchLabels"
v-on="$listeners"
>
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 523438f459c..0d3394788fa 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 { GlFilteredSearchSuggestion } from '@gitlab/ui';
import createFlash from '~/flash';
import { __ } from '~/locale';
-import { sortMilestonesByDueDate } from '~/milestones/milestone_utils';
+import { sortMilestonesByDueDate } from '~/milestones/utils';
import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
import { DEFAULT_MILESTONES } from '../constants';
import { stripQuotes } from '../filtered_search_utils';
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/weight_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/weight_token.vue
deleted file mode 100644
index 280fb234576..00000000000
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/weight_token.vue
+++ /dev/null
@@ -1,66 +0,0 @@
-<script>
-import { GlFilteredSearchSuggestion } from '@gitlab/ui';
-import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
-import { DEFAULT_NONE_ANY, WEIGHT_TOKEN_SUGGESTIONS_SIZE } from '../constants';
-
-const weights = Array.from(Array(WEIGHT_TOKEN_SUGGESTIONS_SIZE), (_, index) => index.toString());
-
-export default {
- components: {
- BaseToken,
- GlFilteredSearchSuggestion,
- },
- props: {
- active: {
- type: Boolean,
- required: true,
- },
- config: {
- type: Object,
- required: true,
- },
- value: {
- type: Object,
- required: true,
- },
- },
- data() {
- return {
- weights,
- };
- },
- computed: {
- defaultWeights() {
- return this.config.defaultWeights || DEFAULT_NONE_ANY;
- },
- },
- methods: {
- getActiveWeight(weightSuggestions, data) {
- return weightSuggestions.find((weight) => weight === data);
- },
- updateWeights(searchTerm) {
- const weight = parseInt(searchTerm, 10);
- this.weights = Number.isNaN(weight) ? weights : [String(weight)];
- },
- },
-};
-</script>
-
-<template>
- <base-token
- :active="active"
- :config="config"
- :value="value"
- :default-suggestions="defaultWeights"
- :suggestions="weights"
- :get-active-token-value="getActiveWeight"
- @fetch-suggestions="updateWeights"
- v-on="$listeners"
- >
- <template #suggestions-list="{ suggestions }">
- <gl-filtered-search-suggestion v-for="weight of suggestions" :key="weight" :value="weight">
- {{ weight }}
- </gl-filtered-search-suggestion>
- </template>
- </base-token>
-</template>
diff --git a/app/assets/javascripts/vue_shared/components/form/input_copy_toggle_visibility.stories.js b/app/assets/javascripts/vue_shared/components/form/input_copy_toggle_visibility.stories.js
new file mode 100644
index 00000000000..cdd7a074f34
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/form/input_copy_toggle_visibility.stories.js
@@ -0,0 +1,27 @@
+import InputCopyToggleVisibility from './input_copy_toggle_visibility.vue';
+
+export default {
+ component: InputCopyToggleVisibility,
+ title: 'vue_shared/components/form/input_copy_toggle_visibility',
+};
+
+const defaultProps = {
+ value: 'hR8x1fuJbzwu5uFKLf9e',
+ formInputGroupProps: { class: 'gl-form-input-xl' },
+};
+
+const Template = (args, { argTypes }) => ({
+ components: { InputCopyToggleVisibility },
+ props: Object.keys(argTypes),
+ template: `<input-copy-toggle-visibility
+ :value="value"
+ :initial-visibility="initialVisibility"
+ :show-toggle-visibility-button="showToggleVisibilityButton"
+ :show-copy-button="showCopyButton"
+ :form-input-group-props="formInputGroupProps"
+ :copy-button-title="copyButtonTitle"
+ />`,
+});
+
+export const Default = Template.bind({});
+Default.args = defaultProps;
diff --git a/app/assets/javascripts/vue_shared/components/form/input_copy_toggle_visibility.vue b/app/assets/javascripts/vue_shared/components/form/input_copy_toggle_visibility.vue
new file mode 100644
index 00000000000..06949b59823
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/form/input_copy_toggle_visibility.vue
@@ -0,0 +1,127 @@
+<script>
+import { GlFormInputGroup, GlFormGroup, GlButton, GlTooltipDirective } from '@gitlab/ui';
+
+import { __ } from '~/locale';
+import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
+
+export default {
+ name: 'InputCopyToggleVisibility',
+ i18n: {
+ toggleVisibilityLabelHide: __('Click to hide'),
+ toggleVisibilityLabelReveal: __('Click to reveal'),
+ },
+ components: {
+ GlFormInputGroup,
+ GlFormGroup,
+ GlButton,
+ ClipboardButton,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ props: {
+ value: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ initialVisibility: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ showToggleVisibilityButton: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ showCopyButton: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ copyButtonTitle: {
+ type: String,
+ required: false,
+ default: __('Copy'),
+ },
+ formInputGroupProps: {
+ type: Object,
+ required: false,
+ default() {
+ return {};
+ },
+ },
+ },
+ data() {
+ return {
+ valueIsVisible: this.initialVisibility,
+ };
+ },
+ computed: {
+ toggleVisibilityLabel() {
+ return this.valueIsVisible
+ ? this.$options.i18n.toggleVisibilityLabelHide
+ : this.$options.i18n.toggleVisibilityLabelReveal;
+ },
+ toggleVisibilityIcon() {
+ return this.valueIsVisible ? 'eye-slash' : 'eye';
+ },
+ computedValueIsVisible() {
+ return !this.showToggleVisibilityButton || this.valueIsVisible;
+ },
+ displayedValue() {
+ return this.computedValueIsVisible ? this.value : '*'.repeat(this.value.length || 20);
+ },
+ },
+ methods: {
+ handleToggleVisibilityButtonClick() {
+ this.valueIsVisible = !this.valueIsVisible;
+
+ this.$emit('visibility-change', this.valueIsVisible);
+ },
+ handleCopyButtonClick() {
+ this.$emit('copy');
+ },
+ handleFormInputCopy(event) {
+ if (this.computedValueIsVisible) {
+ return;
+ }
+
+ event.clipboardData.setData('text/plain', this.value);
+ event.preventDefault();
+ },
+ },
+};
+</script>
+<template>
+ <gl-form-group v-bind="$attrs">
+ <gl-form-input-group
+ :value="displayedValue"
+ input-class="gl-font-monospace! gl-cursor-default!"
+ select-on-click
+ readonly
+ v-bind="formInputGroupProps"
+ @copy="handleFormInputCopy"
+ >
+ <template v-if="showToggleVisibilityButton || showCopyButton" #append>
+ <gl-button
+ v-if="showToggleVisibilityButton"
+ v-gl-tooltip.hover="toggleVisibilityLabel"
+ :aria-label="toggleVisibilityLabel"
+ :icon="toggleVisibilityIcon"
+ @click="handleToggleVisibilityButtonClick"
+ />
+ <clipboard-button
+ v-if="showCopyButton"
+ :text="value"
+ :title="copyButtonTitle"
+ @click="handleCopyButtonClick"
+ />
+ </template>
+ </gl-form-input-group>
+ <template v-for="slot in Object.keys($slots)" #[slot]>
+ <slot :name="slot"></slot>
+ </template>
+ </gl-form-group>
+</template>
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 6ace0bd88f8..9bff469b670 100644
--- a/app/assets/javascripts/vue_shared/components/header_ci_component.vue
+++ b/app/assets/javascripts/vue_shared/components/header_ci_component.vue
@@ -5,6 +5,7 @@ import {
GlSafeHtmlDirective,
GlAvatarLink,
GlAvatarLabeled,
+ GlTooltip,
} from '@gitlab/ui';
import { isGid, getIdFromGraphQLId } from '~/graphql_shared/utils';
import { glEmojiTag } from '../../emoji';
@@ -26,6 +27,7 @@ export default {
GlButton,
GlAvatarLink,
GlAvatarLabeled,
+ GlTooltip,
},
directives: {
GlTooltip: GlTooltipDirective,
diff --git a/app/assets/javascripts/vue_shared/components/issuable/init_issuable_header_warning.js b/app/assets/javascripts/vue_shared/components/issuable/init_issuable_header_warning.js
deleted file mode 100644
index 28aa93d6680..00000000000
--- a/app/assets/javascripts/vue_shared/components/issuable/init_issuable_header_warning.js
+++ /dev/null
@@ -1,22 +0,0 @@
-import Vue from 'vue';
-import { parseBoolean } from '~/lib/utils/common_utils';
-import IssuableHeaderWarnings from './issuable_header_warnings.vue';
-
-export default function issuableHeaderWarnings(store) {
- const el = document.getElementById('js-issuable-header-warnings');
-
- if (!el) {
- return false;
- }
-
- const { hidden } = el.dataset;
-
- return new Vue({
- el,
- store,
- provide: { hidden: parseBoolean(hidden) },
- render(createElement) {
- return createElement(IssuableHeaderWarnings);
- },
- });
-}
diff --git a/app/assets/javascripts/vue_shared/components/issuable/issuable_header_warnings.vue b/app/assets/javascripts/vue_shared/components/issuable/issuable_header_warnings.vue
deleted file mode 100644
index 82223ab9ef4..00000000000
--- a/app/assets/javascripts/vue_shared/components/issuable/issuable_header_warnings.vue
+++ /dev/null
@@ -1,61 +0,0 @@
-<script>
-import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
-import { mapGetters } from 'vuex';
-import { __ } from '~/locale';
-
-export default {
- components: {
- GlIcon,
- },
- directives: {
- GlTooltip: GlTooltipDirective,
- },
- inject: ['hidden'],
- computed: {
- ...mapGetters(['getNoteableData']),
- isLocked() {
- return this.getNoteableData.discussion_locked;
- },
- isConfidential() {
- return this.getNoteableData.confidential;
- },
- warningIconsMeta() {
- return [
- {
- iconName: 'lock',
- visible: this.isLocked,
- dataTestId: 'locked',
- },
- {
- iconName: 'eye-slash',
- visible: this.isConfidential,
- dataTestId: 'confidential',
- },
- {
- iconName: 'spam',
- visible: this.hidden,
- dataTestId: 'hidden',
- tooltip: __('This issue is hidden because its author has been banned'),
- },
- ];
- },
- },
-};
-</script>
-
-<template>
- <div class="gl-display-inline-block">
- <template v-for="meta in warningIconsMeta">
- <div
- v-if="meta.visible"
- :key="meta.iconName"
- v-gl-tooltip
- :data-testid="meta.dataTestId"
- :title="meta.tooltip || null"
- class="issuable-warning-icon inline"
- >
- <gl-icon :name="meta.iconName" class="icon" />
- </div>
- </template>
- </div>
-</template>
diff --git a/app/assets/javascripts/vue_shared/components/issue/issue_assignees.vue b/app/assets/javascripts/vue_shared/components/issue/issue_assignees.vue
deleted file mode 100644
index 5955f31fc70..00000000000
--- a/app/assets/javascripts/vue_shared/components/issue/issue_assignees.vue
+++ /dev/null
@@ -1,107 +0,0 @@
-<script>
-import { GlTooltipDirective } from '@gitlab/ui';
-import { __, sprintf } from '~/locale';
-import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
-
-export default {
- components: {
- UserAvatarLink,
- },
- directives: {
- GlTooltip: GlTooltipDirective,
- },
- props: {
- assignees: {
- type: Array,
- required: true,
- },
- iconSize: {
- type: Number,
- required: false,
- default: 24,
- },
- imgCssClasses: {
- type: String,
- required: false,
- default: '',
- },
- maxVisible: {
- type: Number,
- required: false,
- default: 3,
- },
- },
- data() {
- return {
- maxAssignees: 99,
- };
- },
- computed: {
- assigneesToShow() {
- const numShownAssignees = this.assignees.length - this.numHiddenAssignees;
- return this.assignees.slice(0, numShownAssignees);
- },
- assigneesCounterTooltip() {
- return sprintf(__('%{count} more assignees'), { count: this.numHiddenAssignees });
- },
- numHiddenAssignees() {
- if (this.assignees.length > this.maxVisible) {
- return this.assignees.length - this.maxVisible + 1;
- }
- return 0;
- },
- assigneeCounterLabel() {
- if (this.numHiddenAssignees > this.maxAssignees) {
- return `${this.maxAssignees}+`;
- }
-
- return `+${this.numHiddenAssignees}`;
- },
- },
- methods: {
- avatarUrlTitle(assignee) {
- return sprintf(__('Assigned to %{assigneeName}'), {
- assigneeName: assignee.name,
- });
- },
- // This method is for backward compat
- // since Graph query would return camelCase
- // props while Rails would return snake_case
- webUrl(assignee) {
- return assignee.web_url || assignee.webUrl;
- },
- avatarUrl(assignee) {
- return assignee.avatar_url || assignee.avatarUrl;
- },
- },
-};
-</script>
-<template>
- <div>
- <user-avatar-link
- v-for="assignee in assigneesToShow"
- :key="assignee.id"
- :link-href="webUrl(assignee)"
- :img-alt="avatarUrlTitle(assignee)"
- :img-css-classes="imgCssClasses"
- :img-src="avatarUrl(assignee)"
- :img-size="iconSize"
- class="js-no-trigger author-link"
- tooltip-placement="bottom"
- data-qa-selector="assignee_link"
- >
- <span class="js-assignee-tooltip">
- <span class="bold d-block">{{ __('Assignee') }}</span> {{ assignee.name }}
- <span v-if="assignee.username" class="text-white-50">@{{ assignee.username }}</span>
- </span>
- </user-avatar-link>
- <span
- v-if="numHiddenAssignees > 0"
- v-gl-tooltip.bottom
- :title="assigneesCounterTooltip"
- class="avatar-counter"
- data-qa-selector="avatar_counter_content"
- >{{ assigneeCounterLabel }}</span
- >
- </div>
-</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
deleted file mode 100644
index 6a0c21602bd..00000000000
--- a/app/assets/javascripts/vue_shared/components/issue/issue_milestone.vue
+++ /dev/null
@@ -1,91 +0,0 @@
-<script>
-import { GlTooltip, GlIcon } from '@gitlab/ui';
-import { timeFor, parsePikadayDate, dateInWords } from '~/lib/utils/datetime_utility';
-import { __, sprintf } from '~/locale';
-import timeagoMixin from '~/vue_shared/mixins/timeago';
-
-export default {
- components: {
- GlIcon,
- GlTooltip,
- },
- mixins: [timeagoMixin],
- props: {
- milestone: {
- type: Object,
- required: true,
- },
- },
- computed: {
- milestoneDue() {
- const dueDate = this.milestone.due_date || this.milestone.dueDate;
-
- return dueDate ? parsePikadayDate(dueDate) : null;
- },
- milestoneStart() {
- const startDate = this.milestone.start_date || this.milestone.startDate;
-
- return startDate ? parsePikadayDate(startDate) : null;
- },
- isMilestoneStarted() {
- if (!this.milestoneStart) {
- return false;
- }
- return Date.now() > this.milestoneStart;
- },
- isMilestonePastDue() {
- if (!this.milestoneDue) {
- return false;
- }
- return Date.now() > this.milestoneDue;
- },
- milestoneDatesAbsolute() {
- if (this.milestoneDue) {
- return `(${dateInWords(this.milestoneDue)})`;
- } else if (this.milestoneStart) {
- return `(${dateInWords(this.milestoneStart)})`;
- }
- return '';
- },
- milestoneDatesHuman() {
- if (this.milestoneStart || this.milestoneDue) {
- if (this.milestoneDue) {
- return timeFor(
- this.milestoneDue,
- sprintf(__('Expired %{expiredOn}'), {
- expiredOn: this.timeFormatted(this.milestoneDue),
- }),
- );
- }
-
- return sprintf(
- this.isMilestoneStarted ? __('Started %{startsIn}') : __('Starts %{startsIn}'),
- {
- startsIn: this.timeFormatted(this.milestoneStart),
- },
- );
- }
- return '';
- },
- },
-};
-</script>
-<template>
- <div ref="milestoneDetails" class="issue-milestone-details">
- <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 />
- <span>{{ milestone.title }}</span> <br />
- <span
- v-if="milestoneStart || milestoneDue"
- :class="{
- 'text-danger-muted': isMilestonePastDue,
- 'text-tertiary': !isMilestonePastDue,
- }"
- ><span>{{ milestoneDatesHuman }}</span
- ><br /><span>{{ milestoneDatesAbsolute }}</span>
- </span>
- </gl-tooltip>
- </div>
-</template>
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
deleted file mode 100644
index 8aeff9257a5..00000000000
--- a/app/assets/javascripts/vue_shared/components/issue/related_issuable_item.vue
+++ /dev/null
@@ -1,201 +0,0 @@
-<script>
-import '~/commons/bootstrap';
-import {
- GlIcon,
- GlTooltip,
- GlTooltipDirective,
- GlButton,
- GlSafeHtmlDirective as SafeHtml,
-} from '@gitlab/ui';
-import IssueDueDate from '~/boards/components/issue_due_date.vue';
-import { sprintf } from '~/locale';
-import relatedIssuableMixin from '../../mixins/related_issuable_mixin';
-import CiIcon from '../ci_icon.vue';
-import IssueAssignees from './issue_assignees.vue';
-import IssueMilestone from './issue_milestone.vue';
-
-export default {
- name: 'IssueItem',
- components: {
- IssueMilestone,
- IssueAssignees,
- CiIcon,
- GlIcon,
- GlTooltip,
- IssueWeight: () => import('ee_component/boards/components/issue_card_weight.vue'),
- IssueDueDate,
- GlButton,
- },
- directives: {
- GlTooltip: GlTooltipDirective,
- SafeHtml,
- },
- mixins: [relatedIssuableMixin],
- props: {
- canReorder: {
- type: Boolean,
- required: false,
- default: false,
- },
- isLocked: {
- type: Boolean,
- required: false,
- default: false,
- },
- lockedMessage: {
- type: String,
- required: false,
- default: '',
- },
- },
- computed: {
- stateTitle() {
- return sprintf(
- '<span class="bold">%{state}</span> %{timeInWords}<br/><span class="text-tertiary">%{timestamp}</span>',
- {
- state: this.stateText,
- timeInWords: this.stateTimeInWords,
- timestamp: this.stateTimestamp,
- },
- );
- },
- iconClasses() {
- return `${this.iconClass} ic-${this.iconName}`;
- },
- },
-};
-</script>
-
-<template>
- <div
- :class="{
- 'issuable-info-container': !canReorder,
- 'card-body': canReorder,
- }"
- class="item-body d-flex align-items-center py-2 px-3"
- >
- <div
- class="item-contents gl-display-flex gl-align-items-center gl-flex-wrap gl-flex-grow-1 flex-xl-nowrap gl-min-h-7"
- >
- <!-- Title area: Status icon (XL) and title -->
- <div class="item-title d-flex align-items-xl-center mb-xl-0 gl-min-w-0">
- <div ref="iconElementXL">
- <gl-icon
- v-if="hasState"
- ref="iconElementXL"
- class="mr-2 d-block"
- :class="iconClasses"
- :name="iconName"
- :title="stateTitle"
- :aria-label="state"
- />
- </div>
- <gl-tooltip :target="() => $refs.iconElementXL">
- <span v-safe-html="stateTitle"></span>
- </gl-tooltip>
- <gl-icon
- v-if="confidential"
- v-gl-tooltip
- name="eye-slash"
- :title="__('Confidential')"
- class="confidential-icon gl-mr-2 align-self-baseline align-self-md-auto mt-xl-0"
- :aria-label="__('Confidential')"
- />
- <a :href="computedPath" class="sortable-link gl-font-weight-normal">{{ title }}</a>
- </div>
-
- <!-- Info area: meta, path, and assignees -->
- <div class="item-info-area d-flex flex-xl-grow-1 flex-shrink-0">
- <!-- Meta area: path and attributes -->
- <!-- If there is no room beside the path, meta attributes are put ABOVE it (flex-wrap-reverse). -->
- <!-- See design: https://gitlab-org.gitlab.io/gitlab-design/hosted/pedro/%2383-issue-mr-rows-cards-spec-previews/#artboard16 -->
- <div
- class="item-meta d-flex flex-wrap-reverse justify-content-start justify-content-md-between"
- >
- <!-- Path area: status icon (<XL), path, issue # -->
- <div
- class="item-path-area item-path-id d-flex align-items-center mr-2 mt-2 mt-xl-0 ml-xl-2"
- >
- <gl-tooltip :target="() => this.$refs.iconElement">
- <span v-safe-html="stateTitle"></span>
- </gl-tooltip>
- <span v-gl-tooltip :title="itemPath" class="path-id-text d-inline-block">{{
- itemPath
- }}</span>
- <span>{{ pathIdSeparator }}{{ itemId }}</span>
- </div>
-
- <!-- Attributes area: CI, epic count, weight, milestone -->
- <!-- They have a different order on large screen sizes -->
- <div class="item-attributes-area d-flex align-items-center mt-2 mt-xl-0">
- <span v-if="hasPipeline" class="mr-ci-status order-md-last">
- <a :href="pipelineStatus.details_path">
- <ci-icon v-gl-tooltip :status="pipelineStatus" :title="pipelineStatusTooltip" />
- </a>
- </span>
-
- <issue-milestone
- v-if="hasMilestone"
- :milestone="milestone"
- class="d-flex align-items-center item-milestone order-md-first ml-md-0"
- />
-
- <!-- Flex order for slots is defined in the parent component: e.g. related_issues_block.vue -->
- <span v-if="weight > 0" class="order-md-1">
- <issue-weight
- :weight="weight"
- class="item-weight gl-display-flex gl-align-items-center"
- tag-name="span"
- />
- </span>
-
- <span v-if="dueDate" class="order-md-1">
- <issue-due-date
- :date="dueDate"
- :closed="Boolean(closedAt)"
- tooltip-placement="top"
- css-class="item-due-date gl-display-flex gl-align-items-center"
- />
- </span>
-
- <issue-assignees
- v-if="hasAssignees"
- :assignees="assignees"
- class="item-assignees align-items-center align-self-end flex-shrink-0 order-md-2 d-none d-md-flex"
- />
- </div>
- </div>
-
- <!-- Assignees. On small layouts, these are put here, at the end of the card. -->
- <issue-assignees
- v-if="assignees.length !== 0"
- :assignees="assignees"
- class="item-assignees d-flex align-items-center align-self-end flex-shrink-0 d-md-none ml-2"
- />
- </div>
- </div>
-
- <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"
- class="js-issue-item-remove-button gl-ml-3"
- data-qa-selector="remove_related_issue_button"
- :title="__('Remove')"
- :aria-label="__('Remove')"
- @click="onRemoveRequest"
- />
- </div>
-</template>
diff --git a/app/assets/javascripts/vue_shared/components/line_numbers.vue b/app/assets/javascripts/vue_shared/components/line_numbers.vue
new file mode 100644
index 00000000000..7e17cca3dcc
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/line_numbers.vue
@@ -0,0 +1,57 @@
+<script>
+import { GlIcon, GlLink } from '@gitlab/ui';
+
+export default {
+ components: {
+ GlIcon,
+ GlLink,
+ },
+ props: {
+ lines: {
+ type: Number,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ currentlyHighlightedLine: null,
+ };
+ },
+ mounted() {
+ this.scrollToLine();
+ },
+ methods: {
+ scrollToLine(hash = window.location.hash) {
+ const lineToHighlight = hash && this.$el.querySelector(hash);
+
+ if (!lineToHighlight) {
+ return;
+ }
+
+ if (this.currentlyHighlightedLine) {
+ this.currentlyHighlightedLine.classList.remove('hll');
+ }
+
+ lineToHighlight.classList.add('hll');
+ this.currentlyHighlightedLine = lineToHighlight;
+ lineToHighlight.scrollIntoView({ behavior: 'smooth', block: 'center' });
+ },
+ },
+};
+</script>
+<template>
+ <div class="line-numbers">
+ <gl-link
+ v-for="line in lines"
+ :id="`L${line}`"
+ :key="line"
+ class="diff-line-num"
+ :href="`#L${line}`"
+ :data-line-number="line"
+ @click="scrollToLine(`#L${line}`)"
+ >
+ <gl-icon :size="12" name="link" />
+ {{ line }}
+ </gl-link>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue b/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue
index e36cfb3b275..2f6776f835e 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue
@@ -165,6 +165,6 @@ export default {
<template>
<div>
<div class="flash-container js-suggestions-flash"></div>
- <div v-show="isRendered" ref="container" v-safe-html="noteHtml" class="md"></div>
+ <div v-show="isRendered" ref="container" v-safe-html="noteHtml" class="md suggestions"></div>
</div>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue
index 912aa8ce294..f1c293c87f4 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue
@@ -1,18 +1,13 @@
<script>
import { GlButton, GlLink, GlLoadingIcon, GlSprintf, GlIcon } from '@gitlab/ui';
-import { isExperimentVariant } from '~/experimentation/utils';
-import InviteMembersTrigger from '~/invite_members/components/invite_members_trigger.vue';
-import { INVITE_MEMBERS_IN_COMMENT } from '~/invite_members/constants';
export default {
- inviteMembersInComment: INVITE_MEMBERS_IN_COMMENT,
components: {
GlButton,
GlLink,
GlLoadingIcon,
GlSprintf,
GlIcon,
- InviteMembersTrigger,
},
props: {
markdownDocsPath: {
@@ -34,9 +29,6 @@ export default {
hasQuickActionsDocsPath() {
return this.quickActionsDocsPath !== '';
},
- inviteCommentEnabled() {
- return isExperimentVariant(INVITE_MEMBERS_IN_COMMENT, 'invite_member_link');
- },
},
};
</script>
@@ -67,16 +59,6 @@ export default {
</template>
</div>
<span v-if="canAttachFile" class="uploading-container">
- <invite-members-trigger
- v-if="inviteCommentEnabled"
- classes="gl-mr-3 gl-vertical-align-text-bottom"
- :display-text="s__('InviteMember|Invite Member')"
- icon="assignee"
- variant="link"
- :track-experiment="$options.inviteMembersInComment"
- :trigger-source="$options.inviteMembersInComment"
- data-track-action="comment_invite_click"
- />
<span class="uploading-progress-container hide">
<gl-icon name="media" />
<span class="attaching-file-message"></span>
diff --git a/app/assets/javascripts/vue_shared/components/namespace_select/namespace_select.vue b/app/assets/javascripts/vue_shared/components/namespace_select/namespace_select.vue
new file mode 100644
index 00000000000..7d2af7983d1
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/namespace_select/namespace_select.vue
@@ -0,0 +1,93 @@
+<script>
+import { GlDropdown, GlDropdownItem, GlDropdownSectionHeader, GlSearchBoxByType } from '@gitlab/ui';
+import { __ } from '~/locale';
+
+export const i18n = {
+ DEFAULT_TEXT: __('Select a new namespace'),
+ GROUPS: __('Groups'),
+ USERS: __('Users'),
+};
+
+const filterByName = (data, searchTerm = '') =>
+ data.filter((d) => d.humanName.toLowerCase().includes(searchTerm));
+
+export default {
+ name: 'NamespaceSelect',
+ components: {
+ GlDropdown,
+ GlDropdownItem,
+ GlDropdownSectionHeader,
+ GlSearchBoxByType,
+ },
+ props: {
+ data: {
+ type: Object,
+ required: true,
+ },
+ fullWidth: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ data() {
+ return {
+ searchTerm: '',
+ selectedNamespace: null,
+ };
+ },
+ computed: {
+ hasUserNamespaces() {
+ return this.data.user?.length;
+ },
+ hasGroupNamespaces() {
+ return this.data.group?.length;
+ },
+ filteredGroupNamespaces() {
+ if (!this.hasGroupNamespaces) return [];
+ return filterByName(this.data.group, this.searchTerm);
+ },
+ filteredUserNamespaces() {
+ if (!this.hasUserNamespaces) return [];
+ return filterByName(this.data.user, this.searchTerm);
+ },
+ selectedNamespaceText() {
+ return this.selectedNamespace?.humanName || this.$options.i18n.DEFAULT_TEXT;
+ },
+ },
+ methods: {
+ handleSelect(item) {
+ this.selectedNamespace = item;
+ this.$emit('select', item);
+ },
+ },
+ i18n,
+};
+</script>
+<template>
+ <gl-dropdown :text="selectedNamespaceText" :block="fullWidth">
+ <template #header>
+ <gl-search-box-by-type v-model.trim="searchTerm" />
+ </template>
+ <div v-if="hasGroupNamespaces" class="qa-namespaces-list-groups">
+ <gl-dropdown-section-header>{{ $options.i18n.GROUPS }}</gl-dropdown-section-header>
+ <gl-dropdown-item
+ v-for="item in filteredGroupNamespaces"
+ :key="item.id"
+ class="qa-namespaces-list-item"
+ @click="handleSelect(item)"
+ >{{ item.humanName }}</gl-dropdown-item
+ >
+ </div>
+ <div v-if="hasUserNamespaces" class="qa-namespaces-list-users">
+ <gl-dropdown-section-header>{{ $options.i18n.USERS }}</gl-dropdown-section-header>
+ <gl-dropdown-item
+ v-for="item in filteredUserNamespaces"
+ :key="item.id"
+ class="qa-namespaces-list-item"
+ @click="handleSelect(item)"
+ >{{ item.humanName }}</gl-dropdown-item
+ >
+ </div>
+ </gl-dropdown>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue b/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue
index 9ea14ed506c..624dbcc6d8e 100644
--- a/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue
+++ b/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue
@@ -39,6 +39,11 @@ export default {
required: false,
default: null,
},
+ isOverviewTab: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
computed: {
...mapGetters(['getUserData']),
@@ -46,9 +51,10 @@ export default {
return renderMarkdown(this.note.body);
},
avatarSize() {
- if (this.line) {
- return 16;
+ if (this.line && !this.isOverviewTab) {
+ return 24;
}
+
return 40;
},
},
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 8877cfa39fb..1963d1aa7fe 100644
--- a/app/assets/javascripts/vue_shared/components/notes/system_note.vue
+++ b/app/assets/javascripts/vue_shared/components/notes/system_note.vue
@@ -141,6 +141,7 @@ export default {
variant="link"
:icon="descriptionVersionToggleIcon"
data-testid="compare-btn"
+ class="gl-vertical-align-text-bottom"
@click="toggleDescriptionVersion"
>{{ __('Compare with previous version') }}</gl-button
>
@@ -149,6 +150,7 @@ export default {
:icon="showLines ? 'chevron-up' : 'chevron-down'"
variant="link"
data-testid="outdated-lines-change-btn"
+ class="gl-vertical-align-text-bottom"
@click="toggleDiff"
>
{{ __('Compare changes') }}
diff --git a/app/assets/javascripts/vue_shared/components/pagination_bar/pagination_bar.stories.js b/app/assets/javascripts/vue_shared/components/pagination_bar/pagination_bar.stories.js
new file mode 100644
index 00000000000..e31446f4bb8
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/pagination_bar/pagination_bar.stories.js
@@ -0,0 +1,40 @@
+/* eslint-disable @gitlab/require-i18n-strings */
+import PaginationBar from './pagination_bar.vue';
+
+export default {
+ component: PaginationBar,
+ title: 'vue_shared/components/pagination_bar/pagination_bar',
+};
+
+const Template = (args, { argTypes }) => ({
+ components: { PaginationBar },
+ props: Object.keys(argTypes),
+ template: `<pagination-bar v-bind="$props" v-on="{ 'set-page-size': setPageSize, 'set-page': setPage }" />`,
+});
+
+export const Default = Template.bind({});
+
+Default.args = {
+ pageInfo: {
+ perPage: 20,
+ page: 2,
+ total: 83,
+ totalPages: 5,
+ },
+ pageSizes: [20, 50, 100],
+};
+
+Default.argTypes = {
+ pageInfo: {
+ description: 'Page info object',
+ control: { type: 'object' },
+ },
+ pageSizes: {
+ description: 'Array of possible page sizes',
+ control: { type: 'array' },
+ },
+
+ // events
+ setPageSize: { action: 'set-page-size' },
+ setPage: { action: 'set-page' },
+};
diff --git a/app/assets/javascripts/vue_shared/components/pagination_bar/pagination_bar.vue b/app/assets/javascripts/vue_shared/components/pagination_bar/pagination_bar.vue
new file mode 100644
index 00000000000..b4d565991f5
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/pagination_bar/pagination_bar.vue
@@ -0,0 +1,103 @@
+<script>
+import { GlDropdown, GlDropdownItem, GlIcon, GlSprintf } from '@gitlab/ui';
+import { __ } from '~/locale';
+import PaginationLinks from '~/vue_shared/components/pagination_links.vue';
+
+const DEFAULT_PAGE_SIZES = [20, 50, 100];
+
+export default {
+ components: {
+ PaginationLinks,
+ GlDropdown,
+ GlDropdownItem,
+ GlIcon,
+ GlSprintf,
+ },
+ props: {
+ pageInfo: {
+ required: true,
+ type: Object,
+ },
+ pageSizes: {
+ required: false,
+ type: Array,
+ default: () => DEFAULT_PAGE_SIZES,
+ },
+ },
+
+ computed: {
+ humanizedTotal() {
+ return this.pageInfo.total >= 1000 ? __('1000+') : this.pageInfo.total;
+ },
+
+ paginationInfo() {
+ const { page, perPage, totalPages, total } = this.pageInfo;
+ const itemsCount = page === totalPages ? total - (page - 1) * perPage : perPage;
+ const start = (page - 1) * perPage + 1;
+ const end = start + itemsCount - 1;
+
+ return { start, end };
+ },
+ },
+
+ methods: {
+ setPage(page) {
+ // eslint-disable-next-line spaced-comment
+ /**
+ * Emitted when selected page is updated
+ *
+ * @event set-page
+ **/
+ this.$emit('set-page', page);
+ },
+
+ setPageSize(pageSize) {
+ // eslint-disable-next-line spaced-comment
+ /**
+ * Emitted when page size is updated
+ *
+ * @event set-page-size
+ **/
+ this.$emit('set-page-size', pageSize);
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="gl-display-flex gl-align-items-center">
+ <pagination-links :change="setPage" :page-info="pageInfo" class="gl-m-0" />
+ <gl-dropdown category="tertiary" class="gl-ml-auto" data-testid="page-size">
+ <template #button-content>
+ <span class="gl-font-weight-bold">
+ <gl-sprintf :message="__('%{count} items per page')">
+ <template #count>
+ {{ pageInfo.perPage }}
+ </template>
+ </gl-sprintf>
+ </span>
+ <gl-icon class="gl-button-icon dropdown-chevron" name="chevron-down" />
+ </template>
+ <gl-dropdown-item v-for="size in pageSizes" :key="size" @click="setPageSize(size)">
+ <gl-sprintf :message="__('%{count} items per page')">
+ <template #count>
+ {{ size }}
+ </template>
+ </gl-sprintf>
+ </gl-dropdown-item>
+ </gl-dropdown>
+ <div class="gl-ml-2" data-testid="information">
+ <gl-sprintf :message="s__('BulkImport|Showing %{start}-%{end} of %{total}')">
+ <template #start>
+ {{ paginationInfo.start }}
+ </template>
+ <template #end>
+ {{ paginationInfo.end }}
+ </template>
+ <template #total>
+ {{ humanizedTotal }}
+ </template>
+ </gl-sprintf>
+ </div>
+ </div>
+</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
index 933a215112b..6bb321713d5 100644
--- a/app/assets/javascripts/vue_shared/components/registry/list_item.vue
+++ b/app/assets/javascripts/vue_shared/components/registry/list_item.vue
@@ -54,10 +54,10 @@ export default {
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-3 gl-px-5">
+ <div class="gl-display-flex gl-align-items-center gl-py-3">
<div
v-if="$slots['left-action']"
- class="gl-w-7 gl-display-none gl-sm-display-flex gl-justify-content-start gl-pl-2"
+ class="gl-w-7 gl-display-flex gl-justify-content-start gl-pl-2"
>
<slot name="left-action"></slot>
</div>
@@ -105,7 +105,7 @@ export default {
</div>
<div
v-if="$slots['right-action']"
- class="gl-w-9 gl-display-none gl-sm-display-flex gl-justify-content-end gl-pr-1"
+ class="gl-w-9 gl-display-flex gl-justify-content-end gl-pr-1"
>
<slot name="right-action"></slot>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/registry/metadata_item.vue b/app/assets/javascripts/vue_shared/components/registry/metadata_item.vue
index 93396219a54..4c2816b63b2 100644
--- a/app/assets/javascripts/vue_shared/components/registry/metadata_item.vue
+++ b/app/assets/javascripts/vue_shared/components/registry/metadata_item.vue
@@ -1,6 +1,6 @@
<script>
import { GlIcon, GlLink, GlTooltipDirective } from '@gitlab/ui';
-import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
+import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue';
export default {
name: 'MetadataItem',
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue b/app/assets/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue
deleted file mode 100644
index a1dca65a423..00000000000
--- a/app/assets/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue
+++ /dev/null
@@ -1,101 +0,0 @@
-<script>
-import { dateInWords, timeFor } from '~/lib/utils/datetime_utility';
-import { __ } from '~/locale';
-import timeagoMixin from '~/vue_shared/mixins/timeago';
-import collapsedCalendarIcon from './collapsed_calendar_icon.vue';
-
-export default {
- name: 'SidebarCollapsedGroupedDatePicker',
- components: {
- collapsedCalendarIcon,
- },
- mixins: [timeagoMixin],
- props: {
- collapsed: {
- type: Boolean,
- required: false,
- default: true,
- },
- minDate: {
- type: Date,
- required: false,
- default: null,
- },
- maxDate: {
- type: Date,
- required: false,
- default: null,
- },
- disableClickableIcons: {
- type: Boolean,
- required: false,
- default: false,
- },
- },
- computed: {
- hasMinAndMaxDates() {
- return this.minDate && this.maxDate;
- },
- hasNoMinAndMaxDates() {
- return !this.minDate && !this.maxDate;
- },
- showMinDateBlock() {
- return this.minDate || this.hasNoMinAndMaxDates;
- },
- showFromText() {
- return !this.maxDate && this.minDate;
- },
- iconClass() {
- const disabledClass = this.disableClickableIcons ? 'disabled' : '';
- return `sidebar-collapsed-icon calendar-icon ${disabledClass}`;
- },
- },
- methods: {
- toggleSidebar() {
- this.$emit('toggleCollapse');
- },
- dateText(dateType = 'min') {
- const date = this[`${dateType}Date`];
- const dateWords = dateInWords(date, true);
- const parsedDateWords = dateWords ? dateWords.replace(',', '') : dateWords;
-
- return date ? parsedDateWords : __('None');
- },
- tooltipText(dateType = 'min') {
- const defaultText = dateType === 'min' ? __('Start date') : __('Due date');
- const date = this[`${dateType}Date`];
- const timeAgo = dateType === 'min' ? this.timeFormatted(date) : timeFor(date);
- const dateText = date ? [this.dateText(dateType), `(${timeAgo})`].join(' ') : '';
-
- if (date) {
- return [defaultText, dateText].join('<br />');
- }
- return __('Start and due date');
- },
- },
-};
-</script>
-
-<template>
- <div class="block sidebar-grouped-item gl-cursor-pointer" role="button" @click="toggleSidebar">
- <collapsed-calendar-icon
- v-if="showMinDateBlock"
- :container-class="iconClass"
- :tooltip-text="tooltipText('min')"
- >
- <span class="sidebar-collapsed-value">
- <span v-if="showFromText">{{ __('From') }}</span> <span>{{ dateText('min') }}</span>
- </span>
- </collapsed-calendar-icon>
- <div v-if="hasMinAndMaxDates" class="text-center sidebar-collapsed-divider">-</div>
- <collapsed-calendar-icon
- v-if="maxDate"
- :container-class="iconClass"
- :tooltip-text="tooltipText('max')"
- >
- <span class="sidebar-collapsed-value">
- <span v-if="!minDate">{{ __('Until') }}</span> <span>{{ dateText('max') }}</span>
- </span>
- </collapsed-calendar-icon>
- </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 4234bc72f3a..7e259cb8b96 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
@@ -179,6 +179,8 @@ export default {
document.addEventListener('mousedown', this.handleDocumentMousedown);
document.addEventListener('click', this.handleDocumentClick);
+
+ this.updateLabelsSetState();
},
beforeDestroy() {
document.removeEventListener('mousedown', this.handleDocumentMousedown);
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue
index f7485de0342..13a6dd43207 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue
@@ -172,6 +172,13 @@ export default {
showDropdown() {
this.$refs.dropdown.show();
},
+ clearSearch() {
+ if (!this.allowMultiselect || this.isStandalone) {
+ return;
+ }
+ this.searchKey = '';
+ this.setFocus();
+ },
},
};
</script>
@@ -188,12 +195,12 @@ export default {
>
<template #header>
<dropdown-header
- v-if="!isStandalone"
ref="header"
- v-model="searchKey"
+ :search-key="searchKey"
:labels-create-title="labelsCreateTitle"
:labels-list-title="labelsListTitle"
:show-dropdown-contents-create-view="showDropdownContentsCreateView"
+ :is-standalone="isStandalone"
@toggleDropdownContentsCreateView="toggleDropdownContent"
@closeDropdown="$emit('closeDropdown')"
@input="debouncedSearchKeyUpdate"
@@ -210,6 +217,7 @@ export default {
:attr-workspace-path="attrWorkspacePath"
:label-create-type="labelCreateType"
@hideCreateView="toggleDropdownContent"
+ @input="clearSearch"
/>
</template>
<template #footer>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_header.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_header.vue
index 10064b01648..7a0f20b0c83 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_header.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_header.vue
@@ -6,9 +6,6 @@ export default {
GlButton,
GlSearchBoxByType,
},
- model: {
- prop: 'searchKey',
- },
props: {
labelsCreateTitle: {
type: String,
@@ -31,6 +28,11 @@ export default {
type: String,
required: true,
},
+ isStandalone: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
computed: {
dropdownTitle() {
@@ -47,7 +49,11 @@ export default {
<template>
<div data-testid="dropdown-header">
- <div class="dropdown-title gl-display-flex gl-align-items-center gl-pt-0 gl-pb-3!">
+ <div
+ v-if="!isStandalone"
+ class="dropdown-title gl-display-flex gl-align-items-center gl-pt-0 gl-pb-3!"
+ data-testid="dropdown-header-title"
+ >
<gl-button
v-if="showDropdownContentsCreateView"
:aria-label="__('Go back')"
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_value.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_value.vue
index aed5bc303ee..57ee816c4c7 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_value.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_value.vue
@@ -1,10 +1,15 @@
<script>
-import { GlLabel } from '@gitlab/ui';
+import { GlIcon, GlLabel, GlTooltipDirective } from '@gitlab/ui';
import { sortBy } from 'lodash';
import { isScopedLabel } from '~/lib/utils/common_utils';
+import { s__, sprintf } from '~/locale';
export default {
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
components: {
+ GlIcon,
GlLabel,
},
inject: ['allowScopedLabels'],
@@ -35,6 +40,23 @@ export default {
sortedSelectedLabels() {
return sortBy(this.selectedLabels, (label) => (isScopedLabel(label) ? 0 : 1));
},
+ labelsList() {
+ const labelsString = this.selectedLabels.length
+ ? this.selectedLabels
+ .slice(0, 5)
+ .map((label) => label.title)
+ .join(', ')
+ : s__('LabelSelect|Labels');
+
+ if (this.selectedLabels.length > 5) {
+ return sprintf(s__('LabelSelect|%{labelsString}, and %{remainingLabelCount} more'), {
+ labelsString,
+ remainingLabelCount: this.selectedLabels.length - 5,
+ });
+ }
+
+ return labelsString;
+ },
},
methods: {
labelFilterUrl(label) {
@@ -48,6 +70,9 @@ export default {
removeLabel(labelId) {
this.$emit('onLabelRemove', labelId);
},
+ handleCollapsedClick() {
+ this.$emit('onCollapsedValueClick');
+ },
},
};
</script>
@@ -57,16 +82,30 @@ export default {
:class="{
'has-labels': selectedLabels.length,
}"
- class="hide-collapsed value issuable-show-labels js-value"
+ class="value issuable-show-labels js-value"
data-testid="value-wrapper"
>
- <span v-if="!selectedLabels.length" class="text-secondary" data-testid="empty-placeholder">
+ <div
+ v-gl-tooltip.left.viewport
+ :title="labelsList"
+ class="sidebar-collapsed-icon"
+ @click="handleCollapsedClick"
+ >
+ <gl-icon name="labels" />
+ <span class="gl-font-base gl-line-height-24">{{ selectedLabels.length }}</span>
+ </div>
+ <span
+ v-if="!selectedLabels.length"
+ class="text-secondary hide-collapsed"
+ data-testid="empty-placeholder"
+ >
<slot></slot>
</span>
<template v-else>
<gl-label
v-for="label in sortedSelectedLabels"
:key="label.id"
+ class="hide-collapsed"
data-qa-selector="selected_label_content"
:data-qa-label-name="label.title"
:title="label.title"
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_value_collapsed.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_value_collapsed.vue
deleted file mode 100644
index 122250d1ce7..00000000000
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_value_collapsed.vue
+++ /dev/null
@@ -1,55 +0,0 @@
-<script>
-import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
-import { s__, sprintf } from '~/locale';
-
-export default {
- directives: {
- GlTooltip: GlTooltipDirective,
- },
- components: {
- GlIcon,
- },
- props: {
- labels: {
- type: Array,
- required: true,
- },
- },
- computed: {
- labelsList() {
- const labelsString = this.labels.length
- ? this.labels
- .slice(0, 5)
- .map((label) => label.title)
- .join(', ')
- : s__('LabelSelect|Labels');
-
- if (this.labels.length > 5) {
- return sprintf(s__('LabelSelect|%{labelsString}, and %{remainingLabelCount} more'), {
- labelsString,
- remainingLabelCount: this.labels.length - 5,
- });
- }
-
- return labelsString;
- },
- },
- methods: {
- handleClick() {
- this.$emit('onValueClick');
- },
- },
-};
-</script>
-
-<template>
- <div
- v-gl-tooltip.left.viewport
- :title="labelsList"
- class="sidebar-collapsed-icon"
- @click="handleClick"
- >
- <gl-icon name="labels" />
- <span>{{ labels.length }}</span>
- </div>
-</template>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/epic_labels.query.graphql b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/epic_labels.query.graphql
index c130cc426dc..c442c17eb88 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/epic_labels.query.graphql
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/epic_labels.query.graphql
@@ -2,6 +2,7 @@
query epicLabels($fullPath: ID!, $iid: ID) {
workspace: group(fullPath: $fullPath) {
+ id
issuable: epic(iid: $iid) {
id
labels {
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/epic_update_labels.mutation.graphql b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/epic_update_labels.mutation.graphql
index 45fcb50732e..cb054e2968f 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/epic_update_labels.mutation.graphql
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/epic_update_labels.mutation.graphql
@@ -1,8 +1,8 @@
#import "~/graphql_shared/fragments/label.fragment.graphql"
mutation updateEpicLabels($input: UpdateEpicInput!) {
- updateEpic(input: $input) {
- epic {
+ updateIssuableLabels: updateEpic(input: $input) {
+ issuable: epic {
id
labels {
nodes {
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/issue_labels.query.graphql b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/issue_labels.query.graphql
index e471d279b24..2904857270e 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/issue_labels.query.graphql
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/issue_labels.query.graphql
@@ -2,6 +2,7 @@
query issueLabels($fullPath: ID!, $iid: String) {
workspace: project(fullPath: $fullPath) {
+ id
issuable: issue(iid: $iid) {
id
labels {
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/merge_request_labels.query.graphql b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/merge_request_labels.query.graphql
index dd80e89c8a7..e0cdfd91658 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/merge_request_labels.query.graphql
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/merge_request_labels.query.graphql
@@ -2,6 +2,7 @@
query mergeRequestLabels($fullPath: ID!, $iid: String!) {
workspace: project(fullPath: $fullPath) {
+ id
issuable: mergeRequest(iid: $iid) {
id
labels {
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue
index 97a65c13933..3adda69b892 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue
@@ -2,14 +2,13 @@
import { debounce } from 'lodash';
import { MutationOperationMode, getIdFromGraphQLId } from '~/graphql_shared/utils';
import createFlash from '~/flash';
-import { IssuableType } from '~/issue_show/constants';
+import { IssuableType } from '~/issues/constants';
import { __ } from '~/locale';
import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
import { issuableLabelsQueries } from '~/sidebar/constants';
import { DEBOUNCE_DROPDOWN_DELAY, DropdownVariant } from './constants';
import DropdownContents from './dropdown_contents.vue';
import DropdownValue from './dropdown_value.vue';
-import DropdownValueCollapsed from './dropdown_value_collapsed.vue';
import {
isDropdownVariantSidebar,
isDropdownVariantStandalone,
@@ -20,7 +19,6 @@ export default {
components: {
DropdownValue,
DropdownContents,
- DropdownValueCollapsed,
SidebarEditableItem,
},
inject: {
@@ -225,15 +223,13 @@ export default {
variables: { input: inputVariables },
})
.then(({ data }) => {
- const { mutationName } = issuableLabelsQueries[this.issuableType];
-
- if (data[mutationName]?.errors?.length) {
+ if (data.updateIssuableLabels?.errors?.length) {
throw new Error();
}
this.$emit('updateSelectedLabels', {
- id: data[mutationName]?.[this.issuableType]?.id,
- labels: data[mutationName]?.[this.issuableType]?.labels?.nodes,
+ id: data.updateIssuableLabels?.issuable?.id,
+ labels: data.updateIssuableLabels?.issuable?.labels?.nodes,
});
})
.catch((error) =>
@@ -288,18 +284,14 @@ export default {
<template>
<div
- class="labels-select-wrapper position-relative"
+ class="labels-select-wrapper gl-relative"
:class="{
'is-standalone': isDropdownVariantStandalone(variant),
'is-embedded': isDropdownVariantEmbedded(variant),
}"
+ data-qa-selector="labels_block"
>
<template v-if="isDropdownVariantSidebar(variant)">
- <dropdown-value-collapsed
- ref="dropdownButtonCollapsed"
- :labels="issuableLabels"
- @onValueClick="handleCollapsedValueClick"
- />
<sidebar-editable-item
ref="editable"
:title="__('Labels')"
@@ -315,6 +307,7 @@ export default {
:labels-filter-base-path="labelsFilterBasePath"
:labels-filter-param="labelsFilterParam"
@onLabelRemove="handleLabelRemove"
+ @onCollapsedValueClick="handleCollapsedValueClick"
>
<slot></slot>
</dropdown-value>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_alert_assignees.query.graphql b/app/assets/javascripts/vue_shared/components/sidebar/queries/get_alert_assignees.query.graphql
index d99fc125012..bb6c7181e5c 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_alert_assignees.query.graphql
+++ b/app/assets/javascripts/vue_shared/components/sidebar/queries/get_alert_assignees.query.graphql
@@ -7,6 +7,7 @@ query alertAssignees(
$iid: String!
) {
workspace: project(fullPath: $fullPath) {
+ id
issuable: alertManagementAlert(domain: $domain, iid: $iid) {
iid
assignees {
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_assignees.query.graphql b/app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_assignees.query.graphql
index 93b9833bb7d..be270e440ed 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_assignees.query.graphql
+++ b/app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_assignees.query.graphql
@@ -4,6 +4,7 @@
query issueAssignees($fullPath: ID!, $iid: String!) {
workspace: project(fullPath: $fullPath) {
__typename
+ id
issuable: issue(iid: $iid) {
__typename
id
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_participants.query.graphql b/app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_participants.query.graphql
index 48787305459..96a40e597ee 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_participants.query.graphql
+++ b/app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_participants.query.graphql
@@ -4,6 +4,7 @@
query issueParticipants($fullPath: ID!, $iid: String!) {
workspace: project(fullPath: $fullPath) {
__typename
+ id
issuable: issue(iid: $iid) {
__typename
id
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_assignees.query.graphql b/app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_assignees.query.graphql
index 53f7381760e..81e19e48d75 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_assignees.query.graphql
+++ b/app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_assignees.query.graphql
@@ -3,6 +3,7 @@
query getMrAssignees($fullPath: ID!, $iid: String!) {
workspace: project(fullPath: $fullPath) {
+ id
issuable: mergeRequest(iid: $iid) {
id
assignees {
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_participants.query.graphql b/app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_participants.query.graphql
index 6adbd4098f2..3496d5f4a2e 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_participants.query.graphql
+++ b/app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_participants.query.graphql
@@ -3,6 +3,7 @@
query getMrParticipants($fullPath: ID!, $iid: String!) {
workspace: project(fullPath: $fullPath) {
+ id
issuable: mergeRequest(iid: $iid) {
id
participants {
diff --git a/app/assets/javascripts/vue_shared/components/source_editor.vue b/app/assets/javascripts/vue_shared/components/source_editor.vue
index fdf0c9baee3..8a0fef36079 100644
--- a/app/assets/javascripts/vue_shared/components/source_editor.vue
+++ b/app/assets/javascripts/vue_shared/components/source_editor.vue
@@ -96,6 +96,7 @@ export default {
:id="`source-editor-${fileGlobalId}`"
ref="editor"
data-editor-loading
+ data-qa-selector="source_editor_container"
@[$options.readyEvent]="$emit($options.readyEvent)"
>
<pre class="editor-loading-content">{{ value }}</pre>
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer.vue b/app/assets/javascripts/vue_shared/components/source_viewer.vue
new file mode 100644
index 00000000000..8f0d051543f
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/source_viewer.vue
@@ -0,0 +1,88 @@
+<script>
+import { GlSafeHtmlDirective } from '@gitlab/ui';
+import LineNumbers from '~/vue_shared/components/line_numbers.vue';
+
+export default {
+ components: {
+ LineNumbers,
+ },
+ directives: {
+ SafeHtml: GlSafeHtmlDirective,
+ },
+ props: {
+ content: {
+ type: String,
+ required: true,
+ },
+ language: {
+ type: String,
+ required: false,
+ default: 'plaintext',
+ },
+ autoDetect: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ data() {
+ return {
+ languageDefinition: null,
+ hljs: null,
+ };
+ },
+ computed: {
+ lineNumbers() {
+ return this.content.split('\n').length;
+ },
+ highlightedContent() {
+ let highlightedContent;
+
+ if (this.hljs) {
+ if (this.autoDetect) {
+ highlightedContent = this.hljs.highlightAuto(this.content).value;
+ } else if (this.languageDefinition) {
+ highlightedContent = this.hljs.highlight(this.content, { language: this.language }).value;
+ }
+ }
+
+ return highlightedContent;
+ },
+ },
+ async mounted() {
+ this.hljs = await this.loadHighlightJS();
+
+ if (!this.autoDetect) {
+ this.languageDefinition = await this.loadLanguage();
+ }
+ },
+ methods: {
+ loadHighlightJS() {
+ // With auto-detect enabled we load all common languages else we load only the core (smallest footprint)
+ return this.autoDetect ? import('highlight.js/lib/common') : import('highlight.js/lib/core');
+ },
+ async loadLanguage() {
+ let languageDefinition;
+
+ try {
+ languageDefinition = await import(`highlight.js/lib/languages/${this.language}`);
+ this.hljs.registerLanguage(this.language, languageDefinition.default);
+ } catch (message) {
+ this.$emit('error', message);
+ }
+
+ return languageDefinition;
+ },
+ },
+ userColorScheme: window.gon.user_color_scheme,
+};
+</script>
+<template>
+ <div class="file-content code" :class="$options.userColorScheme">
+ <line-numbers :lines="lineNumbers" />
+ <pre
+ class="code gl-pl-3!"
+ ><code v-safe-html="highlightedContent" class="gl-white-space-pre-wrap!"></code>
+ </pre>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/storage_counter/usage_graph.stories.js b/app/assets/javascripts/vue_shared/components/storage_counter/usage_graph.stories.js
deleted file mode 100644
index 00aa5519ec6..00000000000
--- a/app/assets/javascripts/vue_shared/components/storage_counter/usage_graph.stories.js
+++ /dev/null
@@ -1,38 +0,0 @@
-/* eslint-disable @gitlab/require-i18n-strings */
-import '@gitlab/ui/dist/utility_classes.css';
-import UsageGraph from './usage_graph.vue';
-
-export default {
- component: UsageGraph,
- title: 'vue_shared/components/storage_counter/usage_graph',
-};
-
-const Template = (args, { argTypes }) => ({
- components: { UsageGraph },
- props: Object.keys(argTypes),
- template: '<usage-graph v-bind="$props" />',
-});
-
-export const Default = Template.bind({});
-Default.argTypes = {
- rootStorageStatistics: {
- description: 'The statistics object with all its fields',
- type: { name: 'object', required: true },
- defaultValue: {
- buildArtifactsSize: 400000,
- pipelineArtifactsSize: 38000,
- lfsObjectsSize: 4800000,
- packagesSize: 3800000,
- repositorySize: 39000000,
- snippetsSize: 2000112,
- storageSize: 39930000,
- uploadsSize: 7000,
- wikiSize: 300000,
- },
- },
- limit: {
- description:
- 'When a limit is set, users will see how much of their storage usage (limit) is used. In case the limit is 0 or the current usage exceeds the limit, it just renders the distribution',
- defaultValue: 0,
- },
-};
diff --git a/app/assets/javascripts/vue_shared/components/storage_counter/usage_graph.vue b/app/assets/javascripts/vue_shared/components/storage_counter/usage_graph.vue
deleted file mode 100644
index c33d065ff4b..00000000000
--- a/app/assets/javascripts/vue_shared/components/storage_counter/usage_graph.vue
+++ /dev/null
@@ -1,148 +0,0 @@
-<script>
-import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
-import { numberToHumanSize } from '~/lib/utils/number_utils';
-import { s__ } from '~/locale';
-
-export default {
- components: {
- GlIcon,
- },
- directives: {
- GlTooltip: GlTooltipDirective,
- },
- props: {
- rootStorageStatistics: {
- required: true,
- type: Object,
- },
- limit: {
- required: true,
- type: Number,
- },
- },
- computed: {
- storageTypes() {
- const {
- buildArtifactsSize,
- pipelineArtifactsSize,
- lfsObjectsSize,
- packagesSize,
- repositorySize,
- storageSize,
- wikiSize,
- snippetsSize,
- uploadsSize,
- } = this.rootStorageStatistics;
- const artifactsSize = buildArtifactsSize + pipelineArtifactsSize;
-
- if (storageSize === 0) {
- return null;
- }
-
- return [
- {
- name: s__('UsageQuota|Repositories'),
- style: this.usageStyle(this.barRatio(repositorySize)),
- class: 'gl-bg-data-viz-blue-500',
- size: repositorySize,
- },
- {
- name: s__('UsageQuota|LFS Objects'),
- style: this.usageStyle(this.barRatio(lfsObjectsSize)),
- class: 'gl-bg-data-viz-orange-600',
- size: lfsObjectsSize,
- },
- {
- name: s__('UsageQuota|Packages'),
- style: this.usageStyle(this.barRatio(packagesSize)),
- class: 'gl-bg-data-viz-aqua-500',
- size: packagesSize,
- },
- {
- name: s__('UsageQuota|Artifacts'),
- style: this.usageStyle(this.barRatio(artifactsSize)),
- class: 'gl-bg-data-viz-green-600',
- size: artifactsSize,
- tooltip: s__('UsageQuota|Artifacts is a sum of build and pipeline artifacts.'),
- },
- {
- name: s__('UsageQuota|Wikis'),
- style: this.usageStyle(this.barRatio(wikiSize)),
- class: 'gl-bg-data-viz-magenta-500',
- size: wikiSize,
- },
- {
- name: s__('UsageQuota|Snippets'),
- style: this.usageStyle(this.barRatio(snippetsSize)),
- class: 'gl-bg-data-viz-orange-800',
- size: snippetsSize,
- },
- {
- name: s__('UsageQuota|Uploads'),
- style: this.usageStyle(this.barRatio(uploadsSize)),
- class: 'gl-bg-data-viz-aqua-700',
- size: uploadsSize,
- },
- ]
- .filter((data) => data.size !== 0)
- .sort((a, b) => b.size - a.size);
- },
- },
- methods: {
- formatSize(size) {
- return numberToHumanSize(size);
- },
- usageStyle(ratio) {
- return { flex: ratio };
- },
- barRatio(size) {
- let max = this.rootStorageStatistics.storageSize;
-
- if (this.limit !== 0 && max <= this.limit) {
- max = this.limit;
- }
-
- return size / max;
- },
- },
-};
-</script>
-<template>
- <div v-if="storageTypes" class="gl-display-flex gl-flex-direction-column w-100">
- <div class="gl-h-6 gl-my-5 gl-bg-gray-50 gl-rounded-base gl-display-flex">
- <div
- v-for="storageType in storageTypes"
- :key="storageType.name"
- class="storage-type-usage gl-h-full gl-display-inline-block"
- :class="storageType.class"
- :style="storageType.style"
- data-testid="storage-type-usage"
- ></div>
- </div>
- <div class="row py-0">
- <div
- v-for="storageType in storageTypes"
- :key="storageType.name"
- class="col-md-auto gl-display-flex gl-align-items-center"
- data-testid="storage-type-legend"
- >
- <div class="gl-h-2 gl-w-5 gl-mr-2 gl-display-inline-block" :class="storageType.class"></div>
- <span class="gl-mr-2 gl-font-weight-bold gl-font-sm">
- {{ storageType.name }}
- </span>
- <span class="gl-text-gray-500 gl-font-sm">
- {{ formatSize(storageType.size) }}
- </span>
- <span
- v-if="storageType.tooltip"
- v-gl-tooltip
- :title="storageType.tooltip"
- :aria-label="storageType.tooltip"
- class="gl-ml-2"
- >
- <gl-icon name="question" :size="12" />
- </span>
- </div>
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/vue_shared/components/tooltip_on_truncate.vue b/app/assets/javascripts/vue_shared/components/tooltip_on_truncate.vue
deleted file mode 100644
index c5fdb5fc242..00000000000
--- a/app/assets/javascripts/vue_shared/components/tooltip_on_truncate.vue
+++ /dev/null
@@ -1,69 +0,0 @@
-<script>
-import { GlTooltipDirective as GlTooltip } from '@gitlab/ui';
-import { isFunction } from 'lodash';
-import { hasHorizontalOverflow } from '~/lib/utils/dom_utils';
-
-export default {
- directives: {
- GlTooltip,
- },
- props: {
- title: {
- type: String,
- required: false,
- default: '',
- },
- placement: {
- type: String,
- required: false,
- default: 'top',
- },
- truncateTarget: {
- type: [String, Function],
- required: false,
- default: '',
- },
- },
- data() {
- return {
- showTooltip: false,
- };
- },
- watch: {
- title() {
- // Wait on $nextTick in case of slot width changes
- this.$nextTick(this.updateTooltip);
- },
- },
- mounted() {
- this.updateTooltip();
- },
- methods: {
- selectTarget() {
- if (isFunction(this.truncateTarget)) {
- return this.truncateTarget(this.$el);
- } else if (this.truncateTarget === 'child') {
- return this.$el.childNodes[0];
- }
-
- return this.$el;
- },
- updateTooltip() {
- const target = this.selectTarget();
- this.showTooltip = hasHorizontalOverflow(target);
- },
- },
-};
-</script>
-
-<template>
- <span
- v-if="showTooltip"
- v-gl-tooltip="{ placement }"
- :title="title"
- class="js-show-tooltip gl-min-w-0"
- >
- <slot></slot>
- </span>
- <span v-else class="gl-min-w-0"> <slot></slot> </span>
-</template>
diff --git a/app/assets/javascripts/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.stories.js b/app/assets/javascripts/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.stories.js
new file mode 100644
index 00000000000..f27901a30a9
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.stories.js
@@ -0,0 +1,88 @@
+/* eslint-disable @gitlab/require-i18n-strings */
+import TooltipOnTruncate from './tooltip_on_truncate.vue';
+
+const defaultWidth = '250px';
+
+export default {
+ component: TooltipOnTruncate,
+ title: 'vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue',
+};
+
+const createStory = ({ ...options }) => {
+ return (_, { argTypes }) => {
+ const comp = {
+ components: { TooltipOnTruncate },
+ props: Object.keys(argTypes),
+ template: `
+ <div class="gl-bg-blue-50" :style="{ width }">
+ <tooltip-on-truncate :title="title" :placement="placement" class="gl-display-block gl-text-truncate">
+ {{title}}
+ </tooltip-on-truncate>
+ </div>
+ `,
+ ...options,
+ };
+
+ return comp;
+ };
+};
+
+export const Default = createStory();
+Default.args = {
+ width: defaultWidth,
+ title: 'Hover on this text to see the content in a tooltip.',
+};
+
+export const NoOverflow = createStory();
+NoOverflow.args = {
+ width: defaultWidth,
+ title: "Short text doesn't need a tooltip.",
+};
+
+export const Placement = createStory();
+Placement.args = {
+ width: defaultWidth,
+ title: 'Use `placement="right"` to display this tooltip at the right.',
+ placement: 'right',
+};
+
+const TIMEOUT_S = 3;
+
+export const LiveUpdates = createStory({
+ props: ['width', 'placement'],
+ data() {
+ return {
+ title: `(loading in ${TIMEOUT_S}s)`,
+ };
+ },
+ mounted() {
+ setTimeout(() => {
+ this.title = 'Content updated! The content is now overflowing so we use a tooltip!';
+ }, TIMEOUT_S * 1000);
+ },
+});
+LiveUpdates.args = {
+ width: defaultWidth,
+};
+LiveUpdates.argTypes = {
+ title: {
+ control: false,
+ },
+};
+
+export const TruncateTarget = createStory({
+ template: `
+ <div class="gl-bg-black" :style="{ width }">
+ <tooltip-on-truncate class="gl-display-flex" :truncate-target="truncateTarget" :title="title">
+ <div class="gl-m-5 gl-bg-blue-50 gl-text-truncate">
+ {{ title }}
+ </div>
+ </tooltip-on-truncate>
+ </div>
+ `,
+});
+TruncateTarget.args = {
+ width: defaultWidth,
+ truncateTarget: 'child',
+ title: 'Wrap in container and use `truncate-target="child"` prop.',
+};
diff --git a/app/assets/javascripts/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue b/app/assets/javascripts/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue
new file mode 100644
index 00000000000..09414e679bb
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue
@@ -0,0 +1,85 @@
+<script>
+import { GlTooltipDirective, GlResizeObserverDirective } from '@gitlab/ui';
+import { isFunction, debounce } from 'lodash';
+import { hasHorizontalOverflow } from '~/lib/utils/dom_utils';
+
+const UPDATE_TOOLTIP_DEBOUNCED_WAIT_MS = 300;
+
+export default {
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ GlResizeObserver: GlResizeObserverDirective,
+ },
+ props: {
+ title: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ placement: {
+ type: String,
+ required: false,
+ default: 'top',
+ },
+ truncateTarget: {
+ type: [String, Function],
+ required: false,
+ default: '',
+ },
+ },
+ data() {
+ return {
+ tooltipDisabled: true,
+ };
+ },
+ computed: {
+ classes() {
+ if (this.tooltipDisabled) {
+ return '';
+ }
+ return 'js-show-tooltip';
+ },
+ tooltip() {
+ return {
+ title: this.title,
+ placement: this.placement,
+ disabled: this.tooltipDisabled,
+ };
+ },
+ },
+ watch: {
+ title() {
+ // Wait on $nextTick in case the slot width changes
+ this.$nextTick(this.updateTooltip);
+ },
+ },
+ created() {
+ this.updateTooltipDebounced = debounce(this.updateTooltip, UPDATE_TOOLTIP_DEBOUNCED_WAIT_MS);
+ },
+ mounted() {
+ this.updateTooltip();
+ },
+ methods: {
+ selectTarget() {
+ if (isFunction(this.truncateTarget)) {
+ return this.truncateTarget(this.$el);
+ } else if (this.truncateTarget === 'child') {
+ return this.$el.childNodes[0];
+ }
+ return this.$el;
+ },
+ updateTooltip() {
+ this.tooltipDisabled = !hasHorizontalOverflow(this.selectTarget());
+ },
+ onResize() {
+ this.updateTooltipDebounced();
+ },
+ },
+};
+</script>
+
+<template>
+ <span v-gl-tooltip="tooltip" v-gl-resize-observer="onResize" :class="classes" class="gl-min-w-0">
+ <slot></slot>
+ </span>
+</template>