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:
Diffstat (limited to 'app/assets/javascripts/vue_shared/components')
-rw-r--r--app/assets/javascripts/vue_shared/components/blob_viewers/constants.js4
-rw-r--r--app/assets/javascripts/vue_shared/components/blob_viewers/rich_viewer.vue85
-rw-r--r--app/assets/javascripts/vue_shared/components/changed_file_icon.vue9
-rw-r--r--app/assets/javascripts/vue_shared/components/ci_badge_link.vue12
-rw-r--r--app/assets/javascripts/vue_shared/components/confidentiality_badge.vue28
-rw-r--r--app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger_modal.vue12
-rw-r--r--app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker.vue283
-rw-r--r--app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker_input.vue77
-rw-r--r--app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker_lib.js91
-rw-r--r--app/assets/javascripts/vue_shared/components/diff_viewer/diff_viewer.vue3
-rw-r--r--app/assets/javascripts/vue_shared/components/diff_viewer/viewers/empty_file.vue3
-rw-r--r--app/assets/javascripts/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.vue11
-rw-r--r--app/assets/javascripts/vue_shared/components/entity_select/group_select.vue25
-rw-r--r--app/assets/javascripts/vue_shared/components/entity_select/utils.js19
-rw-r--r--app/assets/javascripts/vue_shared/components/file_finder/index.vue3
-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.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue3
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue21
-rw-r--r--app/assets/javascripts/vue_shared/components/groups_list/groups_list.vue1
-rw-r--r--app/assets/javascripts/vue_shared/components/groups_list/groups_list_item.vue203
-rw-r--r--app/assets/javascripts/vue_shared/components/header_ci_component.vue172
-rw-r--r--app/assets/javascripts/vue_shared/components/incidents/utils.js3
-rw-r--r--app/assets/javascripts/vue_shared/components/list_actions/constants.js16
-rw-r--r--app/assets/javascripts/vue_shared/components/list_actions/list_actions.stories.js44
-rw-r--r--app/assets/javascripts/vue_shared/components/list_actions/list_actions.vue52
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/apply_suggestion.vue45
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/comment_templates_dropdown.vue12
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/constants.js1
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/field_view.vue20
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/mount_markdown_editor.js4
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/notes/noteable_warning.vue3
-rw-r--r--app/assets/javascripts/vue_shared/components/paginated_table_with_search_and_tabs/constants.js6
-rw-r--r--app/assets/javascripts/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/pagination/table_pagination.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/pagination_bar/pagination_bar.vue45
-rw-r--r--app/assets/javascripts/vue_shared/components/projects_list/constants.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/projects_list/projects_list_item.vue49
-rw-r--r--app/assets/javascripts/vue_shared/components/source_editor.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_new.vue1
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue66
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/source_viewer_new.vue11
-rw-r--r--app/assets/javascripts/vue_shared/components/split_button.vue85
-rw-r--r--app/assets/javascripts/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue3
-rw-r--r--app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/user_select/user_select.vue12
-rw-r--r--app/assets/javascripts/vue_shared/components/web_ide_link.vue6
49 files changed, 613 insertions, 963 deletions
diff --git a/app/assets/javascripts/vue_shared/components/blob_viewers/constants.js b/app/assets/javascripts/vue_shared/components/blob_viewers/constants.js
index 106dd7a3b97..957e642fcb8 100644
--- a/app/assets/javascripts/vue_shared/components/blob_viewers/constants.js
+++ b/app/assets/javascripts/vue_shared/components/blob_viewers/constants.js
@@ -1 +1,5 @@
export const HIGHLIGHT_CLASS_NAME = 'hll';
+export const MARKUP_FILE_TYPE = 'markup';
+export const MARKUP_CONTENT_SELECTOR = '.js-markup-content';
+export const ELEMENTS_PER_CHUNK = 20;
+export const CONTENT_LOADED_EVENT = 'richContentLoaded';
diff --git a/app/assets/javascripts/vue_shared/components/blob_viewers/rich_viewer.vue b/app/assets/javascripts/vue_shared/components/blob_viewers/rich_viewer.vue
index 11ce6afbb1d..27bdcc69120 100644
--- a/app/assets/javascripts/vue_shared/components/blob_viewers/rich_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/blob_viewers/rich_viewer.vue
@@ -3,7 +3,14 @@ import SafeHtml from '~/vue_shared/directives/safe_html';
import { handleBlobRichViewer } from '~/blob/viewer';
import MarkdownFieldView from '~/vue_shared/components/markdown/field_view.vue';
import { handleLocationHash } from '~/lib/utils/common_utils';
+import { sanitize } from '~/lib/dompurify';
import ViewerMixin from './mixins';
+import {
+ MARKUP_FILE_TYPE,
+ MARKUP_CONTENT_SELECTOR,
+ ELEMENTS_PER_CHUNK,
+ CONTENT_LOADED_EVENT,
+} from './constants';
export default {
components: {
@@ -16,21 +23,77 @@ export default {
data() {
return {
isLoading: true,
+ initialContent: null,
+ remainingContent: [],
};
},
+ computed: {
+ rawContent() {
+ return this.initialContent || this.richViewer || this.content;
+ },
+ isMarkup() {
+ return this.type === MARKUP_FILE_TYPE;
+ },
+ },
+ created() {
+ this.optimizeMarkupRendering();
+ },
mounted() {
- window.requestIdleCallback(async () => {
+ this.renderRemainingMarkup();
+ handleBlobRichViewer(this.$refs.content, this.type);
+ handleLocationHash();
+ },
+ methods: {
+ optimizeMarkupRendering() {
+ /**
+ * If content is markup we optimize rendering by splitting it into two parts:
+ * - initialContent (top section of the file - is rendered right away)
+ * - remainingContent (remaining content - is rendered over a longer time period)
+ *
+ * This is done so that the browser doesn't render the whole file at once (improves TBT)
+ */
+
+ if (!this.isMarkup) return;
+
+ const tmpWrapper = document.createElement('div');
+ tmpWrapper.innerHTML = sanitize(this.rawContent, this.$options.safeHtmlConfig);
+
+ const fileContent = tmpWrapper.querySelector(MARKUP_CONTENT_SELECTOR);
+ if (!fileContent) return;
+
+ const initialContent = [...fileContent.childNodes].slice(0, ELEMENTS_PER_CHUNK);
+ this.remainingContent = [...fileContent.childNodes].slice(ELEMENTS_PER_CHUNK);
+
+ fileContent.innerHTML = '';
+ fileContent.append(...initialContent);
+ this.initialContent = tmpWrapper.outerHTML;
+ },
+ renderRemainingMarkup() {
/**
- * Rendering Markdown usually takes long due to the amount of HTML being parsed.
- * This ensures that content is loaded only when the browser goes into idle.
+ * Rendering large Markdown files can block the main thread due to the amount of HTML being parsed.
+ * The optimization below ensures that content is rendered over a longer time period instead of all at once.
* More details here: https://gitlab.com/gitlab-org/gitlab/-/issues/331448
* */
- this.isLoading = false;
- await this.$nextTick();
- handleBlobRichViewer(this.$refs.content, this.type);
- handleLocationHash();
- this.$emit('richContentLoaded');
- });
+
+ if (!this.isMarkup || !this.remainingContent.length) {
+ this.$emit(CONTENT_LOADED_EVENT);
+ this.isLoading = false;
+ return;
+ }
+
+ const fileContent = this.$refs.content.$el.querySelector(MARKUP_CONTENT_SELECTOR);
+
+ for (let i = 0; i < this.remainingContent.length; i += ELEMENTS_PER_CHUNK) {
+ const nextChunkEnd = i + ELEMENTS_PER_CHUNK;
+ const content = this.remainingContent.slice(i, nextChunkEnd);
+ setTimeout(() => {
+ fileContent.append(...content);
+ if (nextChunkEnd < this.remainingContent.length) return;
+ this.$emit(CONTENT_LOADED_EVENT);
+ this.isLoading = false;
+ }, i);
+ }
+ },
},
safeHtmlConfig: {
ADD_TAGS: ['gl-emoji', 'copy-code'],
@@ -39,8 +102,8 @@ export default {
</script>
<template>
<markdown-field-view
- v-if="!isLoading"
ref="content"
- v-safe-html:[$options.safeHtmlConfig]="richViewer || content"
+ v-safe-html:[$options.safeHtmlConfig]="rawContent"
+ :is-loading="isLoading"
/>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/changed_file_icon.vue b/app/assets/javascripts/vue_shared/components/changed_file_icon.vue
index 14e99977a85..2a47e96b2e2 100644
--- a/app/assets/javascripts/vue_shared/components/changed_file_icon.vue
+++ b/app/assets/javascripts/vue_shared/components/changed_file_icon.vue
@@ -50,11 +50,14 @@ export default {
tooltipTitle() {
if (!this.showTooltip) {
return undefined;
- } else if (this.file.deleted) {
+ }
+ if (this.file.deleted) {
return __('Deleted');
- } else if (this.file.tempFile) {
+ }
+ if (this.file.tempFile) {
return __('Added');
- } else if (this.file.changed) {
+ }
+ if (this.file.changed) {
return __('Modified');
}
diff --git a/app/assets/javascripts/vue_shared/components/ci_badge_link.vue b/app/assets/javascripts/vue_shared/components/ci_badge_link.vue
index 9aa7a7d6c49..1f45b4c5c9d 100644
--- a/app/assets/javascripts/vue_shared/components/ci_badge_link.vue
+++ b/app/assets/javascripts/vue_shared/components/ci_badge_link.vue
@@ -1,6 +1,7 @@
<script>
import { GlBadge, GlTooltipDirective } from '@gitlab/ui';
import CiIcon from './ci_icon.vue';
+
/**
* Renders CI Badge link with CI icon and status text based on
* API response shared between all places where it is used.
@@ -48,7 +49,7 @@ export default {
required: false,
default: true,
},
- badgeSize: {
+ size: {
type: String,
required: false,
default: badgeSizeOptions.md,
@@ -59,7 +60,7 @@ export default {
},
computed: {
isSmallBadgeSize() {
- return this.badgeSize === badgeSizeOptions.sm;
+ return this.size === badgeSizeOptions.sm;
},
title() {
return !this.showText ? this.status?.text : '';
@@ -120,13 +121,12 @@ export default {
<template>
<gl-badge
v-gl-tooltip
- :class="{ 'gl-pl-0!': isSmallBadgeSize }"
+ :class="{ 'gl-pl-2': isSmallBadgeSize }"
:title="title"
:href="detailsPath"
- :size="badgeSize"
+ :size="size"
:variant="badgeStyles.variant"
- :data-testid="`ci-badge-${status.text}`"
- data-qa-selector="status_badge_link"
+ data-testid="ci-badge-link"
@click="$emit('ciStatusBadgeClick')"
>
<ci-icon :status="status" />
diff --git a/app/assets/javascripts/vue_shared/components/confidentiality_badge.vue b/app/assets/javascripts/vue_shared/components/confidentiality_badge.vue
index 31c98d1e3a7..025e38a55ad 100644
--- a/app/assets/javascripts/vue_shared/components/confidentiality_badge.vue
+++ b/app/assets/javascripts/vue_shared/components/confidentiality_badge.vue
@@ -1,10 +1,11 @@
<script>
-import { GlBadge, GlTooltipDirective } from '@gitlab/ui';
+import { GlBadge, GlTooltipDirective, GlIcon } from '@gitlab/ui';
import { confidentialityInfoText } from '../constants';
export default {
components: {
GlBadge,
+ GlIcon,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -18,22 +19,31 @@ export default {
type: String,
required: true,
},
+ hideTextInSmallScreens: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
computed: {
confidentialTooltip() {
return confidentialityInfoText(this.workspaceType, this.issuableType);
},
+ confidentialTextClass() {
+ return {
+ 'gl-display-none gl-sm-display-block': this.hideTextInSmallScreens,
+ 'gl-ml-2': true,
+ };
+ },
},
};
</script>
<template>
- <gl-badge
- v-gl-tooltip.bottom
- :title="confidentialTooltip"
- icon="eye-slash"
- variant="warning"
- class="gl-display-inline gl-mr-3"
- >{{ __('Confidential') }}</gl-badge
- >
+ <gl-badge v-gl-tooltip :title="confidentialTooltip" variant="warning">
+ <gl-icon name="eye-slash" :size="16" />
+ <span data-testid="confidential-badge-text" :class="confidentialTextClass">{{
+ __('Confidential')
+ }}</span>
+ </gl-badge>
</template>
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 65a601ed927..a1ef1f30ebb 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
@@ -38,7 +38,16 @@ export default {
default: CONFIRM_DANGER_MODAL_CANCEL,
},
},
+ model: {
+ prop: 'visible',
+ event: 'change',
+ },
props: {
+ visible: {
+ type: Boolean,
+ required: false,
+ default: null,
+ },
modalId: {
type: String,
required: true,
@@ -89,12 +98,15 @@ export default {
<template>
<gl-modal
ref="modal"
+ :visible="visible"
:modal-id="modalId"
:data-testid="modalId"
:title="$options.i18n.CONFIRM_DANGER_MODAL_TITLE"
:action-primary="actionPrimary"
:action-cancel="actionCancel"
+ size="sm"
@primary="$emit('confirm')"
+ @change="$emit('change', $event)"
>
<gl-alert
v-if="confirmDangerMessage"
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
deleted file mode 100644
index d8a2789a419..00000000000
--- a/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker.vue
+++ /dev/null
@@ -1,283 +0,0 @@
-<script>
-import { GlIcon, GlButton, GlDropdown, GlDropdownItem, GlFormGroup } from '@gitlab/ui';
-import { convertToFixedRange, isEqualTimeRanges, findTimeRange } from '~/lib/utils/datetime_range';
-import { __, sprintf } from '~/locale';
-
-import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue';
-import DateTimePickerInput from './date_time_picker_input.vue';
-import {
- defaultTimeRanges,
- defaultTimeRange,
- isValidInputString,
- inputStringToIsoDate,
- isoDateToInputString,
-} from './date_time_picker_lib';
-
-const events = {
- input: 'input',
- invalid: 'invalid',
-};
-
-export default {
- components: {
- GlIcon,
- GlButton,
- GlDropdown,
- GlDropdownItem,
- GlFormGroup,
- TooltipOnTruncate,
- DateTimePickerInput,
- },
- props: {
- value: {
- type: Object,
- required: false,
- default: () => defaultTimeRange,
- },
- options: {
- type: Array,
- required: false,
- default: () => defaultTimeRanges,
- },
- customEnabled: {
- type: Boolean,
- required: false,
- default: true,
- },
- utc: {
- type: Boolean,
- required: false,
- default: false,
- },
- },
- data() {
- return {
- timeRange: this.value,
-
- /**
- * Valid start iso date string, null if not valid value
- */
- startDate: null,
- /**
- * Invalid start date string as input by the user
- */
- startFallbackVal: '',
-
- /**
- * Valid end iso date string, null if not valid value
- */
- endDate: null,
- /**
- * Invalid end date string as input by the user
- */
- endFallbackVal: '',
- };
- },
- computed: {
- startInputValid() {
- return isValidInputString(this.startDate);
- },
- endInputValid() {
- return isValidInputString(this.endDate);
- },
- isValid() {
- return this.startInputValid && this.endInputValid;
- },
-
- startInput: {
- get() {
- return this.dateToInput(this.startDate) || this.startFallbackVal;
- },
- set(val) {
- try {
- this.startDate = this.inputToDate(val);
- this.startFallbackVal = null;
- } catch (e) {
- this.startDate = null;
- this.startFallbackVal = val;
- }
- this.timeRange = null;
- },
- },
- endInput: {
- get() {
- return this.dateToInput(this.endDate) || this.endFallbackVal;
- },
- set(val) {
- try {
- this.endDate = this.inputToDate(val);
- this.endFallbackVal = null;
- } catch (e) {
- this.endDate = null;
- this.endFallbackVal = val;
- }
- this.timeRange = null;
- },
- },
-
- timeWindowText() {
- try {
- const timeRange = findTimeRange(this.value, this.options);
- if (timeRange) {
- return timeRange.label;
- }
-
- const { start, end } = convertToFixedRange(this.value);
- if (isValidInputString(start) && isValidInputString(end)) {
- return sprintf(__('%{start} to %{end}'), {
- start: this.stripZerosInDateTime(this.dateToInput(start)),
- end: this.stripZerosInDateTime(this.dateToInput(end)),
- });
- }
- } catch {
- return __('Invalid date range');
- }
- return '';
- },
-
- customLabel() {
- if (this.utc) {
- return __('Custom range (UTC)');
- }
- return __('Custom range');
- },
- },
- watch: {
- value(newValue) {
- const { start, end } = convertToFixedRange(newValue);
- this.timeRange = this.value;
- this.startDate = start;
- this.endDate = end;
- },
- },
- mounted() {
- try {
- const { start, end } = convertToFixedRange(this.timeRange);
- this.startDate = start;
- this.endDate = end;
- } catch {
- // when dates cannot be parsed, emit error.
- this.$emit(events.invalid);
- }
-
- // Validate on mounted, and trigger an update if needed
- if (!this.isValid) {
- this.$emit(events.invalid);
- }
- },
- methods: {
- dateToInput(date) {
- if (date === null) {
- return null;
- }
- return isoDateToInputString(date, this.utc);
- },
- inputToDate(value) {
- return inputStringToIsoDate(value, this.utc);
- },
- stripZerosInDateTime(str = '') {
- return str.replace(' 00:00:00', '');
- },
- closeDropdown() {
- this.$refs.dropdown.hide();
- },
- isOptionActive(option) {
- return isEqualTimeRanges(option, this.timeRange);
- },
- setQuickRange(option) {
- this.timeRange = option;
- this.$emit(events.input, this.timeRange);
- },
- setFixedRange() {
- this.timeRange = convertToFixedRange({
- start: this.startDate,
- end: this.endDate,
- });
- this.$emit(events.input, this.timeRange);
- },
- },
-};
-</script>
-<template>
- <tooltip-on-truncate
- :title="timeWindowText"
- :truncate-target="(elem) => elem.querySelector('.gl-dropdown-toggle-text')"
- placement="top"
- class="d-inline-block"
- >
- <gl-dropdown
- ref="dropdown"
- :text="timeWindowText"
- v-bind="$attrs"
- class="date-time-picker w-100"
- menu-class="date-time-picker-menu"
- toggle-class="date-time-picker-toggle text-truncate"
- >
- <template #button-content>
- <span class="gl-flex-grow-1 text-truncate">{{ timeWindowText }}</span>
- <span v-if="utc" class="gl-text-gray-500 gl-font-weight-bold gl-font-sm">{{
- __('UTC')
- }}</span>
- <gl-icon class="gl-dropdown-caret" name="chevron-down" />
- </template>
-
- <div class="d-flex justify-content-between gl-p-2">
- <gl-form-group
- v-if="customEnabled"
- :label="customLabel"
- label-for="custom-from-time"
- label-class="gl-pb-2"
- class="custom-time-range-form-group col-md-7 gl-pl-2 gl-pr-0 m-0"
- >
- <div class="gl-pt-3">
- <date-time-picker-input
- id="custom-time-from"
- v-model="startInput"
- :label="__('From')"
- :state="startInputValid"
- />
- <date-time-picker-input
- id="custom-time-to"
- v-model="endInput"
- :label="__('To')"
- :state="endInputValid"
- />
- </div>
- <gl-form-group>
- <gl-button data-testid="cancelButton" @click="closeDropdown">{{
- __('Cancel')
- }}</gl-button>
- <gl-button
- variant="confirm"
- category="primary"
- :disabled="!isValid"
- @click="setFixedRange()"
- >
- {{ __('Apply') }}
- </gl-button>
- </gl-form-group>
- </gl-form-group>
- <gl-form-group label-for="group-id-dropdown" class="col-md-5 gl-px-2 m-0">
- <template #label>
- <span class="gl-pl-7">{{ __('Quick range') }}</span>
- </template>
-
- <gl-dropdown-item
- v-for="(option, index) in options"
- :key="index"
- :active="isOptionActive(option)"
- active-class="active"
- @click="setQuickRange(option)"
- >
- <gl-icon
- name="mobile-issue-close"
- class="align-bottom"
- :class="{ invisible: !isOptionActive(option) }"
- />
- {{ option.label }}
- </gl-dropdown-item>
- </gl-form-group>
- </div>
- </gl-dropdown>
- </tooltip-on-truncate>
-</template>
diff --git a/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker_input.vue b/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker_input.vue
deleted file mode 100644
index 190d4e1f104..00000000000
--- a/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker_input.vue
+++ /dev/null
@@ -1,77 +0,0 @@
-<script>
-import { GlFormGroup, GlFormInput } from '@gitlab/ui';
-import { uniqueId } from 'lodash';
-import { __, sprintf } from '~/locale';
-import { dateFormats } from './date_time_picker_lib';
-
-const inputGroupText = {
- invalidFeedback: sprintf(__('Format: %{dateFormat}'), {
- dateFormat: dateFormats.inputFormat,
- }),
- placeholder: dateFormats.inputFormat,
-};
-
-export default {
- components: {
- GlFormGroup,
- GlFormInput,
- },
- props: {
- state: {
- default: null,
- required: true,
- validator: (prop) => typeof prop === 'boolean' || prop === null,
- },
- value: {
- default: null,
- required: false,
- validator: (prop) => typeof prop === 'string' || prop === null,
- },
- label: {
- type: String,
- default: '',
- required: true,
- },
- id: {
- type: String,
- required: false,
- default: () => uniqueId('dateTimePicker_'),
- },
- },
- data() {
- return {
- inputGroupText,
- };
- },
- computed: {
- invalidFeedback() {
- return this.state ? '' : this.inputGroupText.invalidFeedback;
- },
- inputState() {
- // When the state is valid we want to show no
- // green outline. Hence passing null and not true.
- if (this.state === true) {
- return null;
- }
- return this.state;
- },
- },
- methods: {
- onInputBlur(e) {
- this.$emit('input', e.target.value.trim() || null);
- },
- },
-};
-</script>
-
-<template>
- <gl-form-group :label="label" label-size="sm" :label-for="id" :invalid-feedback="invalidFeedback">
- <gl-form-input
- :id="id"
- :value="value"
- :state="inputState"
- :placeholder="inputGroupText.placeholder"
- @blur="onInputBlur"
- />
- </gl-form-group>
-</template>
diff --git a/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker_lib.js b/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker_lib.js
deleted file mode 100644
index 38b1a587b34..00000000000
--- a/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker_lib.js
+++ /dev/null
@@ -1,91 +0,0 @@
-import dateformat from '~/lib/dateformat';
-import { __ } from '~/locale';
-
-/**
- * Default time ranges for the date picker.
- * @see app/assets/javascripts/lib/utils/datetime_range.js
- */
-export const defaultTimeRanges = [
- {
- duration: { seconds: 60 * 30 },
- label: __('30 minutes'),
- },
- {
- duration: { seconds: 60 * 60 * 3 },
- label: __('3 hours'),
- },
- {
- duration: { seconds: 60 * 60 * 8 },
- label: __('8 hours'),
- default: true,
- },
- {
- duration: { seconds: 60 * 60 * 24 * 1 },
- label: __('1 day'),
- },
-];
-
-export const defaultTimeRange = defaultTimeRanges.find((tr) => tr.default);
-
-export const dateFormats = {
- /**
- * Format used by users to input dates
- *
- * Note: Should be a format that can be parsed by Date.parse.
- */
- inputFormat: 'yyyy-mm-dd HH:MM:ss',
- /**
- * Format used to strip timezone from inputs
- */
- stripTimezoneFormat: "yyyy-mm-dd'T'HH:MM:ss'Z'",
-};
-
-/**
- * Returns true if the date can be parsed succesfully after
- * being typed by a user.
- *
- * It allows some ambiguity so validation is not strict.
- *
- * @param {string} value - Value as typed by the user
- * @returns true if the value can be parsed as a valid date, false otherwise
- */
-export const isValidInputString = (value) => {
- try {
- // dateformat throws error that can be caught.
- // This is better than using `new Date()`
- if (value && value.trim()) {
- dateformat(value, 'isoDateTime');
- return true;
- }
- return false;
- } catch (e) {
- return false;
- }
-};
-
-/**
- * Convert the input in time picker component to an ISO date.
- *
- * @param {string} value
- * @param {Boolean} utc - If true, it forces the date to by
- * formatted using UTC format, ignoring the local time.
- * @returns {Date}
- */
-export const inputStringToIsoDate = (value, utc = false) => {
- let date = new Date(value);
- if (utc) {
- // Forces date to be interpreted as UTC by stripping the timezone
- // by formatting to a string with 'Z' and skipping timezone
- date = dateformat(date, dateFormats.stripTimezoneFormat);
- }
- return dateformat(date, 'isoUtcDateTime');
-};
-
-/**
- * Converts a iso date string to a formatted string for the Time picker component.
- *
- * @param {String} ISO Formatted date
- * @returns {string}
- */
-export const isoDateToInputString = (date, utc = false) =>
- dateformat(date, dateFormats.inputFormat, utc);
diff --git a/app/assets/javascripts/vue_shared/components/diff_viewer/diff_viewer.vue b/app/assets/javascripts/vue_shared/components/diff_viewer/diff_viewer.vue
index 7080e046b30..535f1c5f645 100644
--- a/app/assets/javascripts/vue_shared/components/diff_viewer/diff_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/diff_viewer/diff_viewer.vue
@@ -65,7 +65,8 @@ export default {
viewer() {
if (this.diffViewerMode === diffViewerModes.renamed) {
return RenamedFile;
- } else if (this.diffMode === diffModes.mode_changed) {
+ }
+ if (this.diffMode === diffModes.mode_changed) {
return ModeChanged;
}
diff --git a/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/empty_file.vue b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/empty_file.vue
deleted file mode 100644
index 53210cbcc93..00000000000
--- a/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/empty_file.vue
+++ /dev/null
@@ -1,3 +0,0 @@
-<template>
- <div class="nothing-here-block">{{ __('Empty file') }}</div>
-</template>
diff --git a/app/assets/javascripts/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.vue b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.vue
index 3bb168e9051..b34a6b11092 100644
--- a/app/assets/javascripts/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.vue
+++ b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.vue
@@ -64,6 +64,11 @@ export default {
required: false,
default: undefined,
},
+ noOptionsText: {
+ type: String,
+ required: false,
+ default: __('No options found'),
+ },
},
computed: {
isSearchEmpty() {
@@ -72,6 +77,9 @@ export default {
noOptionsFound() {
return !this.isSearchEmpty && this.options.length === 0;
},
+ noOptions() {
+ return this.isSearchEmpty && this.options.length === 0;
+ },
},
methods: {
selectOption(option) {
@@ -177,6 +185,9 @@ export default {
<gl-dropdown-item v-if="noOptionsFound" class="gl-pl-6!">
{{ $options.i18n.noMatchingResults }}
</gl-dropdown-item>
+ <gl-dropdown-item v-if="noOptions">
+ {{ noOptionsText }}
+ </gl-dropdown-item>
</template>
</gl-dropdown-form>
</slot>
diff --git a/app/assets/javascripts/vue_shared/components/entity_select/group_select.vue b/app/assets/javascripts/vue_shared/components/entity_select/group_select.vue
index 71e3bf4ff63..eb7b20fa4c1 100644
--- a/app/assets/javascripts/vue_shared/components/entity_select/group_select.vue
+++ b/app/assets/javascripts/vue_shared/components/entity_select/group_select.vue
@@ -19,6 +19,11 @@ export default {
EntitySelect,
},
props: {
+ apiParams: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
label: {
type: String,
required: false,
@@ -48,7 +53,7 @@ export default {
default: null,
},
groupsFilter: {
- type: String,
+ type: String, // Two supported values: `descendant_groups` and `subgroups` See app/assets/javascripts/vue_shared/components/entity_select/utils.js.
required: false,
default: null,
},
@@ -62,17 +67,15 @@ export default {
async fetchGroups(searchString = '', page = 1) {
let groups = [];
let totalPages = 0;
+ const params = {
+ search: searchString,
+ per_page: DEFAULT_PER_PAGE,
+ page,
+ ...this.apiParams,
+ };
try {
- const { data = [], headers } = await axios.get(
- Api.buildUrl(groupsPath(this.groupsFilter, this.parentGroupID)),
- {
- params: {
- search: searchString,
- per_page: DEFAULT_PER_PAGE,
- page,
- },
- },
- );
+ const url = groupsPath(this.groupsFilter, this.parentGroupID);
+ const { data = [], headers } = await axios.get(url, { params });
groups = data.map((group) => ({
...group,
text: group.full_name,
diff --git a/app/assets/javascripts/vue_shared/components/entity_select/utils.js b/app/assets/javascripts/vue_shared/components/entity_select/utils.js
index 0a4622269f4..857a3ab4c74 100644
--- a/app/assets/javascripts/vue_shared/components/entity_select/utils.js
+++ b/app/assets/javascripts/vue_shared/components/entity_select/utils.js
@@ -1,15 +1,26 @@
import Api from '~/api';
+/**
+ * @param {'descendant_groups'|'subgroups'|null} [groupsFilter] - type of group filtering
+ * @param {string|null} [parentGroupID] - parent group is needed for 'descendant_groups' and 'subgroups' filtering.
+ */
export const groupsPath = (groupsFilter, parentGroupID) => {
- if (groupsFilter !== undefined && parentGroupID === undefined) {
+ if (groupsFilter && !parentGroupID) {
throw new Error('Cannot use groupsFilter without a parentGroupID');
}
+
+ let url = '';
switch (groupsFilter) {
case 'descendant_groups':
- return Api.descendantGroupsPath.replace(':id', parentGroupID);
+ url = Api.descendantGroupsPath.replace(':id', parentGroupID);
+ break;
case 'subgroups':
- return Api.subgroupsPath.replace(':id', parentGroupID);
+ url = Api.subgroupsPath.replace(':id', parentGroupID);
+ break;
default:
- return Api.groupsPath;
+ url = Api.groupsPath;
+ break;
}
+
+ return Api.buildUrl(url);
};
diff --git a/app/assets/javascripts/vue_shared/components/file_finder/index.vue b/app/assets/javascripts/vue_shared/components/file_finder/index.vue
index db0b0ea185b..226f44a1541 100644
--- a/app/assets/javascripts/vue_shared/components/file_finder/index.vue
+++ b/app/assets/javascripts/vue_shared/components/file_finder/index.vue
@@ -145,7 +145,8 @@ export default {
el.classList.contains('inputarea')
) {
return true;
- } else if (combo === 'mod+p') {
+ }
+ if (combo === 'mod+p') {
return false;
}
diff --git a/app/assets/javascripts/vue_shared/components/file_row.vue b/app/assets/javascripts/vue_shared/components/file_row.vue
index 721f87ff4d6..cecd1be82e9 100644
--- a/app/assets/javascripts/vue_shared/components/file_row.vue
+++ b/app/assets/javascripts/vue_shared/components/file_row.vue
@@ -141,7 +141,6 @@ export default {
ref="textOutput"
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"
:class="[fileClasses, { 'str-truncated': !truncateMiddle, 'gl-min-w-0': truncateMiddle }]"
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 2b3d1b2c1f5..c698b94749d 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
@@ -73,6 +73,7 @@ export const TOKEN_TITLE_RELEASE = __('Release');
export const TOKEN_TITLE_REVIEWER = s__('SearchToken|Reviewer');
export const TOKEN_TITLE_SOURCE_BRANCH = __('Source Branch');
export const TOKEN_TITLE_STATUS = __('Status');
+export const TOKEN_TITLE_JOBS_RUNNER_TYPE = s__('Job|Runner type');
export const TOKEN_TITLE_TARGET_BRANCH = __('Target Branch');
export const TOKEN_TITLE_TYPE = __('Type');
export const TOKEN_TITLE_SEARCH_WITHIN = __('Search Within');
@@ -100,6 +101,7 @@ export const TOKEN_TYPE_RELEASE = 'release';
export const TOKEN_TYPE_REVIEWER = 'reviewer';
export const TOKEN_TYPE_SOURCE_BRANCH = 'source-branch';
export const TOKEN_TYPE_STATUS = 'status';
+export const TOKEN_TYPE_JOBS_RUNNER_TYPE = 'jobs-runner-type';
export const TOKEN_TYPE_TARGET_BRANCH = 'target-branch';
export const TOKEN_TYPE_TYPE = 'type';
export const TOKEN_TYPE_WEIGHT = 'weight';
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue
index f31d4d53a23..346384e3023 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue
@@ -73,7 +73,8 @@ export default {
},
searchInputPlaceholder: {
type: String,
- required: true,
+ required: false,
+ default: __('Search or filter results…'),
},
suggestionsListClass: {
type: String,
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 8322fe92de4..77108ad3628 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,6 +2,8 @@
import { GlFilteredSearchSuggestion } from '@gitlab/ui';
import { createAlert } from '~/alert';
import { __ } from '~/locale';
+import { WORKSPACE_GROUP, WORKSPACE_PROJECT } from '~/issues/constants';
+import searchMilestonesQuery from '~/issues/list/queries/search_milestones.query.graphql';
import { sortMilestonesByDueDate } from '~/milestones/utils';
import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
import { stripQuotes } from '~/lib/utils/text_utility';
@@ -36,6 +38,14 @@ export default {
defaultMilestones() {
return this.config.defaultMilestones || DEFAULT_MILESTONES;
},
+ namespace() {
+ return this.config.isProject ? WORKSPACE_PROJECT : WORKSPACE_GROUP;
+ },
+ fetchMilestonesQuery() {
+ return this.config.fetchMilestones
+ ? this.config.fetchMilestones
+ : this.fetchMilestonesBySearchTerm;
+ },
},
methods: {
getActiveMilestone(milestones, data) {
@@ -51,10 +61,17 @@ export default {
) || this.defaultMilestones.find(({ value }) => value === data)
);
},
+ fetchMilestonesBySearchTerm(search) {
+ return this.$apollo
+ .query({
+ query: searchMilestonesQuery,
+ variables: { fullPath: this.config.fullPath, search, isProject: this.config.isProject },
+ })
+ .then(({ data }) => data[this.namespace]?.milestones.nodes);
+ },
fetchMilestones(searchTerm) {
this.loading = true;
- this.config
- .fetchMilestones(searchTerm)
+ this.fetchMilestonesQuery(searchTerm)
.then((response) => {
const data = Array.isArray(response) ? response : response.data;
diff --git a/app/assets/javascripts/vue_shared/components/groups_list/groups_list.vue b/app/assets/javascripts/vue_shared/components/groups_list/groups_list.vue
index 7da45169fee..a375a167c68 100644
--- a/app/assets/javascripts/vue_shared/components/groups_list/groups_list.vue
+++ b/app/assets/javascripts/vue_shared/components/groups_list/groups_list.vue
@@ -24,6 +24,7 @@ export default {
:key="group.id"
:group="group"
:show-group-icon="showGroupIcon"
+ @delete="$emit('delete', $event)"
/>
</ul>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/groups_list/groups_list_item.vue b/app/assets/javascripts/vue_shared/components/groups_list/groups_list_item.vue
index 8a301cd0dd0..ca1e7400f2d 100644
--- a/app/assets/javascripts/vue_shared/components/groups_list/groups_list_item.vue
+++ b/app/assets/javascripts/vue_shared/components/groups_list/groups_list_item.vue
@@ -1,5 +1,6 @@
<script>
import { GlAvatarLabeled, GlIcon, GlTooltipDirective, GlTruncateText } from '@gitlab/ui';
+import uniqueId from 'lodash/uniqueId';
import { VISIBILITY_TYPE_ICON, GROUP_VISIBILITY_TYPE } from '~/visibility_level/constants';
import { ACCESS_LEVEL_LABELS } from '~/access_level/constants';
@@ -7,6 +8,9 @@ import UserAccessRoleBadge from '~/vue_shared/components/user_access_role_badge.
import { __ } from '~/locale';
import { numberToMetricPrefix } from '~/lib/utils/number_utils';
import SafeHtml from '~/vue_shared/directives/safe_html';
+import { ACTION_EDIT, ACTION_DELETE } from '~/vue_shared/components/list_actions/constants';
+import ListActions from '~/vue_shared/components/list_actions/list_actions.vue';
+import DangerConfirmModal from '~/vue_shared/components/confirm_danger/confirm_danger_modal.vue';
export default {
i18n: {
@@ -25,6 +29,8 @@ export default {
GlIcon,
UserAccessRoleBadge,
GlTruncateText,
+ ListActions,
+ DangerConfirmModal,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -41,6 +47,12 @@ export default {
default: false,
},
},
+ data() {
+ return {
+ isDeleteModalVisible: false,
+ modalId: uniqueId('groups-list-item-modal-id-'),
+ };
+ },
computed: {
visibility() {
return this.group.visibility;
@@ -75,94 +87,131 @@ export default {
groupMembersCount() {
return numberToMetricPrefix(this.group.groupMembersCount);
},
+ actions() {
+ return {
+ [ACTION_EDIT]: {
+ href: this.group.editPath,
+ },
+ [ACTION_DELETE]: {
+ action: this.onActionDelete,
+ },
+ };
+ },
+ hasActions() {
+ return this.group.availableActions?.length;
+ },
+ hasActionDelete() {
+ return this.group.availableActions?.includes(ACTION_DELETE);
+ },
+ },
+ methods: {
+ onActionDelete() {
+ this.isDeleteModalVisible = true;
+ },
},
};
</script>
<template>
- <li class="groups-list-item gl-py-5 gl-md-display-flex gl-align-items-center gl-border-b">
- <div class="gl-display-flex gl-flex-grow-1">
- <gl-icon
- v-if="showGroupIcon"
- class="gl-mr-3 gl-mt-3 gl-md-mt-5 gl-flex-shrink-0 gl-text-secondary"
- :name="groupIconName"
- />
- <gl-avatar-labeled
- :entity-id="group.id"
- :entity-name="group.fullName"
- :label="group.fullName"
- :label-link="group.webUrl"
- shape="rect"
- :size="$options.avatarSize"
- >
- <template #meta>
- <div class="gl-px-2">
- <div class="gl-mx-n2 gl-display-flex gl-align-items-center gl-flex-wrap">
- <div class="gl-px-2">
- <gl-icon
- v-if="visibility"
- v-gl-tooltip="visibilityTooltip"
- :name="visibilityIcon"
- class="gl-text-secondary"
- />
- </div>
- <div class="gl-px-2">
- <user-access-role-badge v-if="shouldShowAccessLevel">{{
- accessLevelLabel
- }}</user-access-role-badge>
+ <li class="groups-list-item gl-py-5 gl-border-b gl-display-flex gl-align-items-flex-start">
+ <div class="gl-md-display-flex gl-align-items-center gl-flex-grow-1">
+ <div class="gl-display-flex gl-flex-grow-1">
+ <gl-icon
+ v-if="showGroupIcon"
+ class="gl-mr-3 gl-mt-3 gl-md-mt-5 gl-flex-shrink-0 gl-text-secondary"
+ :name="groupIconName"
+ />
+ <gl-avatar-labeled
+ :entity-id="group.id"
+ :entity-name="group.fullName"
+ :label="group.fullName"
+ :label-link="group.webUrl"
+ shape="rect"
+ :size="$options.avatarSize"
+ >
+ <template #meta>
+ <div class="gl-px-2">
+ <div class="gl-mx-n2 gl-display-flex gl-align-items-center gl-flex-wrap">
+ <div class="gl-px-2">
+ <gl-icon
+ v-if="visibility"
+ v-gl-tooltip="visibilityTooltip"
+ :name="visibilityIcon"
+ class="gl-text-secondary"
+ />
+ </div>
+ <div class="gl-px-2">
+ <user-access-role-badge v-if="shouldShowAccessLevel">{{
+ accessLevelLabel
+ }}</user-access-role-badge>
+ </div>
</div>
</div>
+ </template>
+ <gl-truncate-text
+ v-if="group.descriptionHtml"
+ :lines="2"
+ :mobile-lines="2"
+ :show-more-text="$options.i18n.showMore"
+ :show-less-text="$options.i18n.showLess"
+ class="gl-mt-2"
+ >
+ <div
+ v-safe-html:[$options.safeHtmlConfig]="group.descriptionHtml"
+ class="gl-font-sm md"
+ data-testid="group-description"
+ ></div>
+ </gl-truncate-text>
+ </gl-avatar-labeled>
+ </div>
+ <div
+ class="gl-md-display-flex gl-flex-direction-column gl-align-items-flex-end gl-flex-shrink-0 gl-mt-3 gl-md-pl-0 gl-md-mt-0 gl-md-ml-3"
+ :class="statsPadding"
+ >
+ <div class="gl-display-flex gl-align-items-center gl-gap-x-3">
+ <div
+ v-gl-tooltip="$options.i18n.subgroups"
+ :aria-label="$options.i18n.subgroups"
+ class="gl-text-secondary"
+ data-testid="subgroups-count"
+ >
+ <gl-icon name="subgroup" />
+ <span>{{ descendantGroupsCount }}</span>
</div>
- </template>
- <gl-truncate-text
- v-if="group.descriptionHtml"
- :lines="2"
- :mobile-lines="2"
- :show-more-text="$options.i18n.showMore"
- :show-less-text="$options.i18n.showLess"
- class="gl-mt-2"
- >
<div
- v-safe-html:[$options.safeHtmlConfig]="group.descriptionHtml"
- class="gl-font-sm md"
- data-testid="group-description"
- ></div>
- </gl-truncate-text>
- </gl-avatar-labeled>
- </div>
- <div
- class="gl-md-display-flex gl-flex-direction-column gl-align-items-flex-end gl-flex-shrink-0 gl-mt-3 gl-md-pl-0 gl-md-mt-0 gl-md-ml-3"
- :class="statsPadding"
- >
- <div class="gl-display-flex gl-align-items-center gl-gap-x-3">
- <div
- v-gl-tooltip="$options.i18n.subgroups"
- :aria-label="$options.i18n.subgroups"
- class="gl-text-secondary"
- data-testid="subgroups-count"
- >
- <gl-icon name="subgroup" />
- <span>{{ descendantGroupsCount }}</span>
- </div>
- <div
- v-gl-tooltip="$options.i18n.projects"
- :aria-label="$options.i18n.projects"
- class="gl-text-secondary"
- data-testid="projects-count"
- >
- <gl-icon name="project" />
- <span>{{ projectsCount }}</span>
- </div>
- <div
- v-gl-tooltip="$options.i18n.directMembers"
- :aria-label="$options.i18n.directMembers"
- class="gl-text-secondary"
- data-testid="members-count"
- >
- <gl-icon name="users" />
- <span>{{ groupMembersCount }}</span>
+ v-gl-tooltip="$options.i18n.projects"
+ :aria-label="$options.i18n.projects"
+ class="gl-text-secondary"
+ data-testid="projects-count"
+ >
+ <gl-icon name="project" />
+ <span>{{ projectsCount }}</span>
+ </div>
+ <div
+ v-gl-tooltip="$options.i18n.directMembers"
+ :aria-label="$options.i18n.directMembers"
+ class="gl-text-secondary"
+ data-testid="members-count"
+ >
+ <gl-icon name="users" />
+ <span>{{ groupMembersCount }}</span>
+ </div>
</div>
</div>
</div>
+ <list-actions
+ v-if="hasActions"
+ class="gl-ml-3 gl-md-align-self-center"
+ :actions="actions"
+ :available-actions="group.availableActions"
+ />
+
+ <danger-confirm-modal
+ v-if="hasActionDelete"
+ v-model="isDeleteModalVisible"
+ :modal-id="modalId"
+ :phrase="group.fullName"
+ @confirm="$emit('delete', group)"
+ />
</li>
</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
deleted file mode 100644
index 1adda905006..00000000000
--- a/app/assets/javascripts/vue_shared/components/header_ci_component.vue
+++ /dev/null
@@ -1,172 +0,0 @@
-<script>
-import { GlTooltipDirective, GlButton, GlAvatarLink, GlAvatarLabeled, GlTooltip } from '@gitlab/ui';
-import SafeHtml from '~/vue_shared/directives/safe_html';
-import { isGid, getIdFromGraphQLId } from '~/graphql_shared/utils';
-import { glEmojiTag } from '~/emoji';
-import { __, sprintf } from '~/locale';
-import CiBadgeLink from './ci_badge_link.vue';
-import TimeagoTooltip from './time_ago_tooltip.vue';
-
-/**
- * Renders header component for job and pipeline page based on UI mockups
- *
- * Used in:
- * - job show page
- * - pipeline show page
- */
-export default {
- components: {
- CiBadgeLink,
- TimeagoTooltip,
- GlButton,
- GlAvatarLink,
- GlAvatarLabeled,
- GlTooltip,
- },
- directives: {
- GlTooltip: GlTooltipDirective,
- SafeHtml,
- },
- EMOJI_REF: 'EMOJI_REF',
- props: {
- status: {
- type: Object,
- required: true,
- },
- itemName: {
- type: String,
- required: true,
- },
- itemId: {
- type: String,
- required: false,
- default: '',
- },
- time: {
- type: String,
- required: true,
- },
- user: {
- type: Object,
- required: false,
- default: () => ({}),
- },
- hasSidebarButton: {
- type: Boolean,
- required: false,
- default: false,
- },
- shouldRenderTriggeredLabel: {
- type: Boolean,
- required: false,
- default: true,
- },
- },
-
- computed: {
- userAvatarAltText() {
- return sprintf(__(`%{username}'s avatar`), { username: this.user.name });
- },
- userPath() {
- // GraphQL returns `webPath` and Rest `path`
- return this.user?.webPath || this.user?.path;
- },
- avatarUrl() {
- // GraphQL returns `avatarUrl` and Rest `avatar_url`
- return this.user?.avatarUrl || this.user?.avatar_url;
- },
- webUrl() {
- // GraphQL returns `webUrl` and Rest `web_url`
- return this.user?.webUrl || this.user?.web_url;
- },
- statusTooltipHTML() {
- // Rest `status_tooltip_html` which is a ready to work
- // html for the emoji and the status text inside a tooltip.
- // GraphQL returns `status.emoji` and `status.message` which
- // needs to be combined to make the html we want.
- const { emoji } = this.user?.status || {};
- const emojiHtml = emoji ? glEmojiTag(emoji) : '';
-
- return emojiHtml || this.user?.status_tooltip_html;
- },
- message() {
- return this.user?.status?.message;
- },
- item() {
- if (this.itemId) {
- return `${this.itemName} #${this.itemId}`;
- }
-
- return this.itemName;
- },
- userId() {
- return isGid(this.user?.id) ? getIdFromGraphQLId(this.user?.id) : this.user?.id;
- },
- },
-
- methods: {
- onClickSidebarButton() {
- this.$emit('clickedSidebarButton');
- },
- },
- safeHtmlConfig: { ADD_TAGS: ['gl-emoji'] },
-};
-</script>
-
-<template>
- <header class="page-content-header gl-md-display-flex gl-min-h-7" data-testid="ci-header-content">
- <section class="header-main-content gl-mr-3">
- <ci-badge-link class="gl-mr-3" :status="status" />
-
- <strong data-testid="ci-header-item-text">{{ item }}</strong>
-
- <template v-if="shouldRenderTriggeredLabel">{{ __('triggered') }}</template>
- <template v-else>{{ __('created') }}</template>
-
- <timeago-tooltip :time="time" />
-
- {{ __('by') }}
-
- <template v-if="user">
- <gl-avatar-link
- :data-user-id="userId"
- :data-username="user.username"
- :data-name="user.name"
- :href="webUrl"
- target="_blank"
- class="js-user-link gl-vertical-align-middle gl-mx-2 gl-align-items-center"
- >
- <gl-avatar-labeled
- :size="24"
- :src="avatarUrl"
- :label="user.name"
- class="gl-display-none gl-sm-display-inline-flex gl-mx-1"
- />
- <strong class="author gl-display-inline gl-sm-display-none!">@{{ user.username }}</strong>
- <gl-tooltip v-if="message" :target="() => $refs[$options.EMOJI_REF]">
- {{ message }}
- </gl-tooltip>
- <span
- v-if="statusTooltipHTML"
- :ref="$options.EMOJI_REF"
- v-safe-html:[$options.safeHtmlConfig]="statusTooltipHTML"
- class="gl-ml-2"
- :data-testid="message"
- ></span>
- </gl-avatar-link>
- </template>
- </section>
-
- <!-- eslint-disable-next-line @gitlab/vue-prefer-dollar-scopedslots -->
- <section v-if="$slots.default" data-testid="ci-header-action-buttons" class="gl-display-flex">
- <slot></slot>
- </section>
- <gl-button
- v-if="hasSidebarButton"
- class="gl-md-display-none gl-ml-auto gl-align-self-start js-sidebar-build-toggle"
- icon="chevron-double-lg-left"
- :aria-label="__('Toggle sidebar')"
- @click="onClickSidebarButton"
- />
- </header>
-</template>
diff --git a/app/assets/javascripts/vue_shared/components/incidents/utils.js b/app/assets/javascripts/vue_shared/components/incidents/utils.js
deleted file mode 100644
index bcb578a6ba6..00000000000
--- a/app/assets/javascripts/vue_shared/components/incidents/utils.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import { noop } from 'lodash';
-
-export const isValidSlaDueAt = noop;
diff --git a/app/assets/javascripts/vue_shared/components/list_actions/constants.js b/app/assets/javascripts/vue_shared/components/list_actions/constants.js
new file mode 100644
index 00000000000..b1506ae1e93
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/list_actions/constants.js
@@ -0,0 +1,16 @@
+import { __ } from '~/locale';
+
+export const ACTION_EDIT = 'edit';
+export const ACTION_DELETE = 'delete';
+
+export const BASE_ACTIONS = {
+ [ACTION_EDIT]: {
+ text: __('Edit'),
+ },
+ [ACTION_DELETE]: {
+ text: __('Delete'),
+ extraAttrs: {
+ class: 'gl-text-red-500!',
+ },
+ },
+};
diff --git a/app/assets/javascripts/vue_shared/components/list_actions/list_actions.stories.js b/app/assets/javascripts/vue_shared/components/list_actions/list_actions.stories.js
new file mode 100644
index 00000000000..d34729c2373
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/list_actions/list_actions.stories.js
@@ -0,0 +1,44 @@
+import { makeContainer } from 'storybook_addons/make_container';
+import ListActions from './list_actions.vue';
+import { ACTION_DELETE, ACTION_EDIT } from './constants';
+
+export default {
+ component: ListActions,
+ title: 'vue_shared/list_actions',
+ decorators: [makeContainer({ height: '115px' })],
+ parameters: {
+ docs: {
+ description: {
+ component: `
+This component renders actions used by lists of resources such as groups and projects.
+Currently it is used by \`ProjectsListItem\`. There are base actions defined in \`~/vue_shared/components/list_actions\`
+that help reduce the amount of boilerplate needed for common actions such as edit and delete. This component accepts an
+\`actions\` prop that can extend the base actions and/or add custom actions. These actions should follow the format of
+a [disclosure dropdown item](https://gitlab-org.gitlab.io/gitlab-ui/?path=/docs/base-new-dropdowns-disclosure--docs#setting-disclosure-dropdown-items).
+The \`availableActions\` prop defines what actions to render and in what order. This prop will generally be set by checking
+permissions of the current user.
+`,
+ },
+ },
+ },
+};
+
+const Template = (args, { argTypes }) => ({
+ components: { ListActions },
+ props: Object.keys(argTypes),
+ template: '<list-actions v-bind="$props" />',
+});
+
+export const Default = Template.bind({});
+Default.args = {
+ actions: {
+ [ACTION_EDIT]: {
+ href: '/?path=/story/vue-shared-list-actions--default',
+ },
+ [ACTION_DELETE]: {
+ // eslint-disable-next-line no-console
+ action: () => console.log('Deleted'),
+ },
+ },
+ availableActions: [ACTION_EDIT, ACTION_DELETE],
+};
diff --git a/app/assets/javascripts/vue_shared/components/list_actions/list_actions.vue b/app/assets/javascripts/vue_shared/components/list_actions/list_actions.vue
new file mode 100644
index 00000000000..7b78cc1da8f
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/list_actions/list_actions.vue
@@ -0,0 +1,52 @@
+<script>
+import { GlDisclosureDropdown } from '@gitlab/ui';
+import { __ } from '~/locale';
+import { BASE_ACTIONS } from './constants';
+
+export default {
+ name: 'ListActions',
+ i18n: {
+ actions: __('Actions'),
+ },
+ components: {
+ GlDisclosureDropdown,
+ },
+ props: {
+ // Can extend `BASE_ACTIONS` and/or add new actions.
+ // Expected format: https://gitlab-org.gitlab.io/gitlab-ui/?path=/docs/base-new-dropdowns-disclosure--docs#setting-disclosure-dropdown-items
+ actions: {
+ type: Object,
+ required: true,
+ },
+ availableActions: {
+ type: Array,
+ required: true,
+ },
+ },
+ computed: {
+ items() {
+ return this.availableActions.reduce((accumulator, action) => {
+ return [
+ ...accumulator,
+ {
+ ...BASE_ACTIONS[action],
+ ...this.actions[action],
+ },
+ ];
+ }, []);
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-disclosure-dropdown
+ :items="items"
+ icon="ellipsis_v"
+ no-caret
+ :toggle-text="$options.i18n.actions"
+ text-sr-only
+ placement="right"
+ category="tertiary"
+ />
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/apply_suggestion.vue b/app/assets/javascripts/vue_shared/components/markdown/apply_suggestion.vue
index a570abae9d3..05ce007e615 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/apply_suggestion.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/apply_suggestion.vue
@@ -1,9 +1,9 @@
<script>
-import { GlDropdown, GlDropdownForm, GlFormTextarea, GlButton, GlAlert } from '@gitlab/ui';
+import { GlDisclosureDropdown, GlForm, GlFormTextarea, GlButton, GlAlert } from '@gitlab/ui';
import { __, n__ } from '~/locale';
export default {
- components: { GlDropdown, GlDropdownForm, GlFormTextarea, GlButton, GlAlert },
+ components: { GlDisclosureDropdown, GlForm, GlFormTextarea, GlButton, GlAlert },
props: {
disabled: {
type: Boolean,
@@ -39,43 +39,58 @@ export default {
return n__('Apply %d suggestion', 'Apply %d suggestions', this.batchSuggestionsCount);
},
+ helperText() {
+ if (this.batchSuggestionsCount <= 1) {
+ return __('This also resolves this thread');
+ }
+
+ return __('This also resolves all related threads');
+ },
},
methods: {
onApply() {
this.$emit('apply', this.message);
},
+ focusCommitMessageInput() {
+ this.$refs.commitMessage.$el.focus();
+ },
},
};
</script>
<template>
- <gl-dropdown
- :text="dropdownText"
- :disabled="disabled"
- size="small"
- boundary="window"
- right
- lazy
- menu-class="gl-w-full!"
+ <gl-disclosure-dropdown
data-qa-selector="apply_suggestion_dropdown"
- @shown="$refs.commitMessage.$el.focus()"
+ fluid-width
+ placement="right"
+ size="small"
+ :disabled="disabled"
+ :toggle-text="dropdownText"
+ @shown="focusCommitMessageInput"
>
- <gl-dropdown-form class="gl-px-4! gl-m-0!">
+ <gl-form class="gl-display-flex gl-flex-direction-column gl-px-4! gl-mx-0! gl-my-2!">
<label for="commit-message">{{ __('Commit message') }}</label>
<gl-alert v-if="errorMessage" variant="danger" :dismissible="false" class="gl-mb-4">
{{ errorMessage }}
</gl-alert>
+
<gl-form-textarea
id="commit-message"
ref="commitMessage"
v-model="message"
+ class="apply-suggestions-input-min-width"
:placeholder="defaultCommitMessage"
submit-on-enter
data-qa-selector="commit_message_field"
@submit="onApply"
/>
+
+ <span class="gl-mt-2 gl-text-secondary">
+ {{ helperText }}
+ </span>
+
<gl-button
- class="gl-w-auto! gl-mt-3 gl-text-center! gl-transition-medium! float-right"
+ class="gl-w-auto! gl-mt-3 gl-align-self-end"
category="primary"
variant="confirm"
data-qa-selector="commit_with_custom_message_button"
@@ -83,6 +98,6 @@ export default {
>
{{ __('Apply') }}
</gl-button>
- </gl-dropdown-form>
- </gl-dropdown>
+ </gl-form>
+ </gl-disclosure-dropdown>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/comment_templates_dropdown.vue b/app/assets/javascripts/vue_shared/components/markdown/comment_templates_dropdown.vue
index b1c6f5e6056..f7f5ccdbf31 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/comment_templates_dropdown.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/comment_templates_dropdown.vue
@@ -4,7 +4,11 @@ import fuzzaldrinPlus from 'fuzzaldrin-plus';
import { getDerivedMergeRequestInformation } from '~/diffs/utils/merge_request';
import { InternalEvents } from '~/tracking';
import savedRepliesQuery from './saved_replies.query.graphql';
-import { TRACKING_SAVED_REPLIES_USE, TRACKING_SAVED_REPLIES_USE_IN_MR } from './constants';
+import {
+ TRACKING_SAVED_REPLIES_USE,
+ TRACKING_SAVED_REPLIES_USE_IN_MR,
+ TRACKING_SAVED_REPLIES_USE_IN_OTHER,
+} from './constants';
export default {
apollo: {
@@ -61,9 +65,9 @@ export default {
if (savedReply) {
this.$emit('select', savedReply.content);
this.track_event(TRACKING_SAVED_REPLIES_USE);
- if (isInMr) {
- this.track_event(TRACKING_SAVED_REPLIES_USE_IN_MR);
- }
+ this.track_event(
+ isInMr ? TRACKING_SAVED_REPLIES_USE_IN_MR : TRACKING_SAVED_REPLIES_USE_IN_OTHER,
+ );
}
},
},
diff --git a/app/assets/javascripts/vue_shared/components/markdown/constants.js b/app/assets/javascripts/vue_shared/components/markdown/constants.js
index 47ef7cccbc2..7b31c4a59e3 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/constants.js
+++ b/app/assets/javascripts/vue_shared/components/markdown/constants.js
@@ -1,2 +1,3 @@
export const TRACKING_SAVED_REPLIES_USE = 'i_code_review_saved_replies_use';
export const TRACKING_SAVED_REPLIES_USE_IN_MR = 'i_code_review_saved_replies_use_in_mr';
+export const TRACKING_SAVED_REPLIES_USE_IN_OTHER = 'i_code_review_saved_replies_use_in_other';
diff --git a/app/assets/javascripts/vue_shared/components/markdown/field_view.vue b/app/assets/javascripts/vue_shared/components/markdown/field_view.vue
index 84d40db07bb..c70197c6715 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/field_view.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/field_view.vue
@@ -2,8 +2,26 @@
import { renderGFM } from '~/behaviors/markdown/render_gfm';
export default {
+ props: {
+ isLoading: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ watch: {
+ isLoading() {
+ this.handleGFM();
+ },
+ },
mounted() {
- renderGFM(this.$el);
+ this.handleGFM();
+ },
+ methods: {
+ handleGFM() {
+ if (this.isLoading) return;
+ renderGFM(this.$el);
+ },
},
};
</script>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue b/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue
index 493b329f1b1..fc7e0a7c732 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue
@@ -119,9 +119,11 @@ export default {
},
},
data() {
+ const editingMode =
+ localStorage.getItem(this.$options.EDITING_MODE_KEY) || EDITING_MODE_MARKDOWN_FIELD;
return {
markdown: this.value || (this.autosaveKey ? getDraft(this.autosaveKey) : '') || '',
- editingMode: EDITING_MODE_MARKDOWN_FIELD,
+ editingMode,
autofocused: false,
};
},
diff --git a/app/assets/javascripts/vue_shared/components/markdown/mount_markdown_editor.js b/app/assets/javascripts/vue_shared/components/markdown/mount_markdown_editor.js
index 0b0867ae84c..6c2f084591e 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/mount_markdown_editor.js
+++ b/app/assets/javascripts/vue_shared/components/markdown/mount_markdown_editor.js
@@ -72,7 +72,7 @@ export function mountMarkdownEditor(options = {}) {
quickActionsDocsPath,
formFieldPlaceholder,
formFieldClasses,
- qaSelector,
+ testid,
newIssuePath,
} = el.dataset;
@@ -115,7 +115,7 @@ export function mountMarkdownEditor(options = {}) {
id: formFieldId,
name: formFieldName,
class: formFieldClasses,
- 'data-qa-selector': qaSelector,
+ 'data-testid': testid,
},
autosaveKey,
enableAutocomplete,
diff --git a/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue b/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue
index 855c7a449c4..8a0ca8ebac1 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue
@@ -77,9 +77,7 @@ export default {
return this.inapplicableReason;
}
- return this.batchSuggestionsCount > 1
- ? __('This also resolves all related threads')
- : __('This also resolves this thread');
+ return false;
},
isDisableButton() {
return this.isApplying || !this.canApply;
diff --git a/app/assets/javascripts/vue_shared/components/notes/noteable_warning.vue b/app/assets/javascripts/vue_shared/components/notes/noteable_warning.vue
index 9179331cdec..0ec8b6e2a0a 100644
--- a/app/assets/javascripts/vue_shared/components/notes/noteable_warning.vue
+++ b/app/assets/javascripts/vue_shared/components/notes/noteable_warning.vue
@@ -6,6 +6,9 @@ const noteableTypeText = {
Issue: __('issue'),
Epic: __('epic'),
MergeRequest: __('merge request'),
+ Task: __('task'),
+ KeyResult: __('key result'),
+ Objective: __('objective'),
};
export default {
diff --git a/app/assets/javascripts/vue_shared/components/paginated_table_with_search_and_tabs/constants.js b/app/assets/javascripts/vue_shared/components/paginated_table_with_search_and_tabs/constants.js
index df1188d365b..77fd197978f 100644
--- a/app/assets/javascripts/vue_shared/components/paginated_table_with_search_and_tabs/constants.js
+++ b/app/assets/javascripts/vue_shared/components/paginated_table_with_search_and_tabs/constants.js
@@ -1,5 +1,3 @@
-import { __ } from '~/locale';
-
export const tdClass =
'table-col gl-display-flex d-md-table-cell gl-align-items-center gl-white-space-nowrap';
export const thClass = 'gl-hover-bg-blue-50';
@@ -15,7 +13,3 @@ export const initialPaginationState = {
firstPageSize: defaultPageSize,
lastPageSize: null,
};
-
-export const defaultI18n = {
- searchPlaceholder: __('Search or filter results…'),
-};
diff --git a/app/assets/javascripts/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs.vue b/app/assets/javascripts/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs.vue
index ab9e6e092d9..0c3d175684c 100644
--- a/app/assets/javascripts/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs.vue
+++ b/app/assets/javascripts/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs.vue
@@ -14,11 +14,10 @@ import {
} from '~/vue_shared/components/filtered_search_bar/constants';
import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
import UserToken from '~/vue_shared/components/filtered_search_bar/tokens/user_token.vue';
-import { initialPaginationState, defaultI18n, defaultPageSize } from './constants';
+import { initialPaginationState, defaultPageSize } from './constants';
import { isAny } from './utils';
export default {
- defaultI18n,
components: {
GlAlert,
GlBadge,
@@ -300,7 +299,6 @@ export default {
<div class="filtered-search-wrapper">
<filtered-search-bar
:namespace="projectPath"
- :search-input-placeholder="$options.defaultI18n.searchPlaceholder"
:tokens="filteredSearchTokens"
:initial-filter-value="filteredSearchValue"
initial-sortby="created_desc"
diff --git a/app/assets/javascripts/vue_shared/components/pagination/table_pagination.vue b/app/assets/javascripts/vue_shared/components/pagination/table_pagination.vue
index e1f042f78ab..76bedc0feeb 100644
--- a/app/assets/javascripts/vue_shared/components/pagination/table_pagination.vue
+++ b/app/assets/javascripts/vue_shared/components/pagination/table_pagination.vue
@@ -64,7 +64,7 @@ export default {
<template>
<gl-pagination
v-if="showPagination"
- class="gl-mt-3"
+ class="gl-mt-5"
v-bind="$attrs"
align="center"
:value="pageInfo.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
index c1246b2bf44..4f580d4a848 100644
--- a/app/assets/javascripts/vue_shared/components/pagination_bar/pagination_bar.vue
+++ b/app/assets/javascripts/vue_shared/components/pagination_bar/pagination_bar.vue
@@ -1,5 +1,11 @@
<script>
-import { GlDropdown, GlDropdownItem, GlIcon, GlSprintf } from '@gitlab/ui';
+import {
+ GlButton,
+ GlDisclosureDropdown,
+ GlDisclosureDropdownItem,
+ GlIcon,
+ GlSprintf,
+} from '@gitlab/ui';
import { __ } from '~/locale';
import PaginationLinks from '~/vue_shared/components/pagination_links.vue';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
@@ -9,8 +15,9 @@ const DEFAULT_PAGE_SIZES = [20, 50, 100];
export default {
components: {
PaginationLinks,
- GlDropdown,
- GlDropdownItem,
+ GlButton,
+ GlDisclosureDropdown,
+ GlDisclosureDropdownItem,
GlIcon,
GlSprintf,
LocalStorageSync,
@@ -80,25 +87,31 @@ export default {
@input="setPageSize"
/>
<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-disclosure-dropdown category="tertiary" class="gl-ml-auto" data-testid="page-size">
+ <template #toggle>
+ <gl-button class="gl-font-weight-bold" category="tertiary">
<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" />
+ <gl-icon class="gl-button-icon dropdown-chevron" name="chevron-down" />
+ </gl-button>
</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>
+ <gl-disclosure-dropdown-item
+ v-for="size in pageSizes"
+ :key="size"
+ @action="setPageSize(size)"
+ >
+ <template #list-item>
+ <gl-sprintf :message="__('%{count} items per page')">
+ <template #count>
+ {{ size }}
+ </template>
+ </gl-sprintf>
+ </template>
+ </gl-disclosure-dropdown-item>
+ </gl-disclosure-dropdown>
<div class="gl-ml-2" data-testid="information">
<gl-sprintf :message="s__('BulkImport|Showing %{start}-%{end} of %{total}')">
<template #start>
diff --git a/app/assets/javascripts/vue_shared/components/projects_list/constants.js b/app/assets/javascripts/vue_shared/components/projects_list/constants.js
deleted file mode 100644
index aa0b1418a06..00000000000
--- a/app/assets/javascripts/vue_shared/components/projects_list/constants.js
+++ /dev/null
@@ -1,2 +0,0 @@
-export const ACTION_EDIT = 'edit';
-export const ACTION_DELETE = 'delete';
diff --git a/app/assets/javascripts/vue_shared/components/projects_list/projects_list_item.vue b/app/assets/javascripts/vue_shared/components/projects_list/projects_list_item.vue
index 9fc4571b0dc..ce75e305473 100644
--- a/app/assets/javascripts/vue_shared/components/projects_list/projects_list_item.vue
+++ b/app/assets/javascripts/vue_shared/components/projects_list/projects_list_item.vue
@@ -7,7 +7,6 @@ import {
GlTooltipDirective,
GlPopover,
GlSprintf,
- GlDisclosureDropdown,
} from '@gitlab/ui';
import uniqueId from 'lodash/uniqueId';
@@ -20,8 +19,9 @@ import { numberToMetricPrefix } from '~/lib/utils/number_utils';
import { truncate } from '~/lib/utils/text_utility';
import SafeHtml from '~/vue_shared/directives/safe_html';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
+import ListActions from '~/vue_shared/components/list_actions/list_actions.vue';
+import { ACTION_EDIT, ACTION_DELETE } from '~/vue_shared/components/list_actions/constants';
import DeleteModal from '~/projects/components/shared/delete_modal.vue';
-import { ACTION_EDIT, ACTION_DELETE } from './constants';
const MAX_TOPICS_TO_SHOW = 3;
const MAX_TOPIC_TITLE_LENGTH = 15;
@@ -51,8 +51,8 @@ export default {
GlPopover,
GlSprintf,
TimeAgoTooltip,
- GlDisclosureDropdown,
DeleteModal,
+ ListActions,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -163,30 +163,21 @@ export default {
return numberToMetricPrefix(this.project.openIssuesCount);
},
- actionsDropdownItems() {
- return [
- {
- id: ACTION_EDIT,
- text: __('Edit'),
+ actions() {
+ return {
+ [ACTION_EDIT]: {
href: this.project.editPath,
},
- {
- id: ACTION_DELETE,
- text: __('Delete'),
- extraAttrs: {
- class: 'gl-text-red-500!',
- },
- action: () => {
- this.isDeleteModalVisible = true;
- },
+ [ACTION_DELETE]: {
+ action: this.onActionDelete,
},
- ].filter(({ id }) => this.project.actions?.includes(id));
+ };
},
hasActions() {
- return this.actionsDropdownItems.length;
+ return this.project.availableActions?.length;
},
- hasDeleteAction() {
- return this.actionsDropdownItems.find((action) => action.id === ACTION_DELETE);
+ hasActionDelete() {
+ return this.project.availableActions?.includes(ACTION_DELETE);
},
},
methods: {
@@ -204,6 +195,9 @@ export default {
return null;
},
+ onActionDelete() {
+ this.isDeleteModalVisible = true;
+ },
},
};
</script>
@@ -336,20 +330,15 @@ export default {
</div>
</div>
</div>
- <gl-disclosure-dropdown
+ <list-actions
v-if="hasActions"
class="gl-ml-3 gl-md-align-self-center"
- :items="actionsDropdownItems"
- icon="ellipsis_v"
- no-caret
- :toggle-text="$options.i18n.actions"
- text-sr-only
- placement="right"
- category="tertiary"
+ :actions="actions"
+ :available-actions="project.availableActions"
/>
<delete-modal
- v-if="hasDeleteAction"
+ v-if="hasActionDelete"
v-model="isDeleteModalVisible"
:confirm-phrase="project.name"
:is-fork="project.isForked"
diff --git a/app/assets/javascripts/vue_shared/components/source_editor.vue b/app/assets/javascripts/vue_shared/components/source_editor.vue
index 7b7d3d48d9e..53c16fccba1 100644
--- a/app/assets/javascripts/vue_shared/components/source_editor.vue
+++ b/app/assets/javascripts/vue_shared/components/source_editor.vue
@@ -104,7 +104,7 @@ export default {
:id="`source-editor-${fileGlobalId}`"
ref="editor"
data-editor-loading
- data-qa-selector="source_editor_container"
+ data-testid="source-editor-container"
@[$options.readyEvent]="$emit($options.readyEvent, $event)"
>
<pre class="editor-loading-content">{{ value }}</pre>
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_new.vue b/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_new.vue
index b89fa3f8292..8dac6327a99 100644
--- a/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_new.vue
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_new.vue
@@ -82,6 +82,7 @@ export default {
methods: {
handleChunkAppear() {
this.hasAppeared = true;
+ this.$emit('appear');
},
calculateLineNumber(index) {
return this.startingFrom + index + 1;
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue b/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue
index a4d50466f8f..797a38d8171 100644
--- a/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue
@@ -33,6 +33,7 @@ export default {
components: {
GlLoadingIcon,
Chunk,
+ CodeownersValidation: () => import('ee_component/blob/components/codeowners_validation.vue'),
},
mixins: [Tracking.mixin()],
props: {
@@ -40,6 +41,14 @@ export default {
type: Object,
required: true,
},
+ projectPath: {
+ type: String,
+ required: true,
+ },
+ currentRef: {
+ type: String,
+ required: true,
+ },
},
data() {
return {
@@ -49,7 +58,6 @@ export default {
firstChunk: null,
chunks: {},
isLoading: true,
- isLineSelected: false,
lineHighlighter: null,
};
},
@@ -66,7 +74,8 @@ export default {
if (this.blob.name && this.blob.name.endsWith(`.${SVELTE_LANGUAGE}`)) {
// override for svelte files until https://github.com/rouge-ruby/rouge/issues/1717 is resolved
return SVELTE_LANGUAGE;
- } else if (this.blob.name === this.$options.codeownersFileName) {
+ }
+ if (this.isCodeownersFile) {
// override for codeowners files
return this.$options.codeownersLanguage;
}
@@ -87,6 +96,9 @@ export default {
totalChunks() {
return Object.keys(this.chunks).length;
},
+ isCodeownersFile() {
+ return this.blob.name === CODEOWNERS_FILE_NAME;
+ },
},
async created() {
if (this.isLfsBlob) {
@@ -121,7 +133,7 @@ export default {
this.generateRemainingChunks();
this.isLoading = false;
await this.$nextTick();
- this.lineHighlighter = new LineHighlighter({ scrollBehavior: 'auto' });
+ this.selectLine();
});
},
methods: {
@@ -227,18 +239,16 @@ export default {
return languageDefinition;
},
async selectLine() {
- if (this.isLineSelected || !this.lineHighlighter) {
- return;
+ if (!this.lineHighlighter) {
+ this.lineHighlighter = new LineHighlighter({ scrollBehavior: 'auto' });
}
-
- this.isLineSelected = true;
await this.$nextTick();
- this.lineHighlighter.highlightHash(this.$route.hash);
+ const scrollEnabled = false;
+ this.lineHighlighter.highlightHash(this.$route.hash, scrollEnabled);
},
},
userColorScheme: window.gon.user_color_scheme,
currentlySelectedLine: null,
- codeownersFileName: CODEOWNERS_FILE_NAME,
codeownersLanguage: CODEOWNERS_LANGUAGE,
};
</script>
@@ -250,6 +260,13 @@ export default {
:data-path="blob.path"
data-qa-selector="blob_viewer_file_content"
>
+ <codeowners-validation
+ v-if="isCodeownersFile"
+ class="gl-text-black-normal"
+ :current-ref="currentRef"
+ :project-path="projectPath"
+ :file-path="blob.path"
+ />
<chunk
v-if="firstChunk"
:lines="firstChunk.lines"
@@ -263,20 +280,21 @@ export default {
/>
<gl-loading-icon v-if="isLoading" size="sm" class="gl-my-5" />
- <chunk
- v-for="(chunk, key, index) in chunks"
- v-else
- :key="key"
- :lines="chunk.lines"
- :content="chunk.content"
- :total-lines="chunk.totalLines"
- :starting-from="chunk.startingFrom"
- :is-highlighted="chunk.isHighlighted"
- :chunk-index="index"
- :language="chunk.language"
- :blame-path="blob.blamePath"
- :total-chunks="totalChunks"
- @appear="highlightChunk"
- />
+ <template v-else>
+ <chunk
+ v-for="(chunk, key, index) in chunks"
+ :key="key"
+ :lines="chunk.lines"
+ :content="chunk.content"
+ :total-lines="chunk.totalLines"
+ :starting-from="chunk.startingFrom"
+ :is-highlighted="chunk.isHighlighted"
+ :chunk-index="index"
+ :language="chunk.language"
+ :blame-path="blob.blamePath"
+ :total-chunks="totalChunks"
+ @appear="highlightChunk"
+ />
+ </template>
</div>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer_new.vue b/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer_new.vue
index 0fb6e577f32..c7353ed6785 100644
--- a/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer_new.vue
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer_new.vue
@@ -41,8 +41,14 @@ export default {
addBlobLinksTracking();
},
mounted() {
- const { hash } = this.$route;
- this.lineHighlighter.highlightHash(hash);
+ this.selectLine();
+ },
+ methods: {
+ async selectLine() {
+ await this.$nextTick();
+ const scrollEnabled = false;
+ this.lineHighlighter.highlightHash(this.$route.hash, scrollEnabled);
+ },
},
userColorScheme: window.gon.user_color_scheme,
};
@@ -66,6 +72,7 @@ export default {
:total-lines="chunk.totalLines"
:starting-from="chunk.startingFrom"
:blame-path="blob.blamePath"
+ @appear="selectLine"
/>
</div>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/split_button.vue b/app/assets/javascripts/vue_shared/components/split_button.vue
deleted file mode 100644
index c0aef42b0f2..00000000000
--- a/app/assets/javascripts/vue_shared/components/split_button.vue
+++ /dev/null
@@ -1,85 +0,0 @@
-<script>
-import { GlDropdown, GlDropdownDivider, GlDropdownItem } from '@gitlab/ui';
-import { isString } from 'lodash';
-
-const isValidItem = (item) =>
- isString(item.eventName) && isString(item.title) && isString(item.description);
-
-export default {
- components: {
- GlDropdown,
- GlDropdownDivider,
- GlDropdownItem,
- },
-
- props: {
- actionItems: {
- type: Array,
- required: true,
- validator(value) {
- return value.length > 1 && value.every(isValidItem);
- },
- },
- menuClass: {
- type: String,
- required: false,
- default: '',
- },
- variant: {
- type: String,
- required: false,
- default: 'default',
- },
- },
-
- data() {
- return {
- selectedItem: this.actionItems[0],
- };
- },
-
- computed: {
- dropdownToggleText() {
- return this.selectedItem.title;
- },
- },
-
- methods: {
- triggerEvent() {
- this.$emit(this.selectedItem.eventName);
- },
- changeSelectedItem(item) {
- this.selectedItem = item;
- this.$emit('change', item);
- },
- },
-};
-</script>
-
-<template>
- <gl-dropdown
- :menu-class="menuClass"
- split
- :text="dropdownToggleText"
- :variant="variant"
- v-bind="$attrs"
- @click="triggerEvent"
- >
- <template v-for="(item, itemIndex) in actionItems">
- <gl-dropdown-item
- :key="item.eventName"
- is-check-item
- :is-checked="selectedItem === item"
- @click="changeSelectedItem(item)"
- >
- <strong>{{ item.title }}</strong>
- <div>{{ item.description }}</div>
- </gl-dropdown-item>
-
- <gl-dropdown-divider
- v-if="itemIndex < actionItems.length - 1"
- :key="`${item.eventName}-divider`"
- />
- </template>
- </gl-dropdown>
-</template>
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
index bda88a48e48..9ba5e8724f9 100644
--- 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
@@ -70,7 +70,8 @@ export default {
selectTarget() {
if (isFunction(this.truncateTarget)) {
return this.truncateTarget(this.$el);
- } else if (this.truncateTarget === 'child') {
+ }
+ if (this.truncateTarget === 'child') {
return this.$el.childNodes[0];
}
return this.$el;
diff --git a/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue b/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue
index 446c8c97df0..30f616dd8e1 100644
--- a/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue
+++ b/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue
@@ -83,9 +83,11 @@ export default {
if (this.user.status.emoji && this.user.status.message_html) {
return `${glEmojiTag(this.user.status.emoji)} ${this.user.status.message_html}`;
- } else if (this.user.status.message_html) {
+ }
+ if (this.user.status.message_html) {
return this.user.status.message_html;
- } else if (this.user.status.emoji) {
+ }
+ if (this.user.status.emoji) {
return glEmojiTag(this.user.status.emoji);
}
diff --git a/app/assets/javascripts/vue_shared/components/user_select/user_select.vue b/app/assets/javascripts/vue_shared/components/user_select/user_select.vue
index 4879baced0d..863c43b0e55 100644
--- a/app/assets/javascripts/vue_shared/components/user_select/user_select.vue
+++ b/app/assets/javascripts/vue_shared/components/user_select/user_select.vue
@@ -13,7 +13,7 @@ import { __ } from '~/locale';
import SidebarParticipant from '~/sidebar/components/assignees/sidebar_participant.vue';
import { TYPE_ISSUE, TYPE_MERGE_REQUEST } from '~/issues/constants';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
-import { participantsQueries, userSearchQueries } from '~/sidebar/constants';
+import { participantsQueries, userSearchQueries } from '~/sidebar/queries/constants';
import { TYPENAME_MERGE_REQUEST } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
@@ -130,11 +130,11 @@ export default {
},
update(data) {
return (
- data.workspace?.users?.nodes
- .filter((x) => x?.user)
- .map((node) => ({
- ...node.user,
- canMerge: node.mergeRequestInteraction?.canMerge || false,
+ data.workspace?.users
+ .filter((user) => user)
+ .map((user) => ({
+ ...user,
+ canMerge: user.mergeRequestInteraction?.canMerge || false,
})) || []
);
},
diff --git a/app/assets/javascripts/vue_shared/components/web_ide_link.vue b/app/assets/javascripts/vue_shared/components/web_ide_link.vue
index 79d14b5f2d0..beb8321a271 100644
--- a/app/assets/javascripts/vue_shared/components/web_ide_link.vue
+++ b/app/assets/javascripts/vue_shared/components/web_ide_link.vue
@@ -195,9 +195,11 @@ export default {
webIdeActionText() {
if (this.webIdeText) {
return this.webIdeText;
- } else if (this.isBlob) {
+ }
+ if (this.isBlob) {
return __('Open in Web IDE');
- } else if (this.isFork) {
+ }
+ if (this.isFork) {
return __('Edit fork in Web IDE');
}