diff options
Diffstat (limited to 'app/assets/javascripts/vue_shared')
59 files changed, 826 insertions, 600 deletions
diff --git a/app/assets/javascripts/vue_shared/alert_details/components/system_notes/system_note.vue b/app/assets/javascripts/vue_shared/alert_details/components/system_notes/system_note.vue index 93581dbbd40..655a16dea01 100644 --- a/app/assets/javascripts/vue_shared/alert_details/components/system_notes/system_note.vue +++ b/app/assets/javascripts/vue_shared/alert_details/components/system_notes/system_note.vue @@ -36,7 +36,7 @@ export default { <li :id="noteAnchorId" class="timeline-entry note system-note note-wrapper gl-p-0!" - data-qa-selector="alert_system_note_container" + data-testid="alert-system-note-container" > <div class="gl-display-inline-flex gl-align-items-center gl-relative"> <div diff --git a/app/assets/javascripts/vue_shared/components/awards_list.vue b/app/assets/javascripts/vue_shared/components/awards_list.vue index 59f03b41144..3c19df9c196 100644 --- a/app/assets/javascripts/vue_shared/components/awards_list.vue +++ b/app/assets/javascripts/vue_shared/components/awards_list.vue @@ -94,14 +94,12 @@ export default { return awardList.some((award) => award.user.id === this.currentUserId); }, createAwardList(name, list) { - const url = list.length ? list[0].url : null; - return { name, list, title: this.getAwardListTitle(list, name), classes: this.getAwardClassBindings(list), - html: glEmojiTag(name, { url }), + html: glEmojiTag(name), }; }, getAwardListTitle(awardsList, name) { 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 2a47e96b2e2..5a807d10f24 100644 --- a/app/assets/javascripts/vue_shared/components/changed_file_icon.vue +++ b/app/assets/javascripts/vue_shared/components/changed_file_icon.vue @@ -82,8 +82,6 @@ export default { :title="tooltipTitle" :class="{ 'ml-auto': isCentered }" class="file-changed-icon d-inline-block" - data-qa-selector="changed_file_icon_content" - :data-qa-title="tooltipTitle" > <gl-icon v-if="showIcon" :name="changedIcon" :size="size" :class="changedIconClass" /> </span> diff --git a/app/assets/javascripts/vue_shared/components/ci_icon/ci_icon.stories.js b/app/assets/javascripts/vue_shared/components/ci_icon/ci_icon.stories.js new file mode 100644 index 00000000000..66012cefeaf --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/ci_icon/ci_icon.stories.js @@ -0,0 +1,31 @@ +import CiIcon from './ci_icon.vue'; + +export default { + component: CiIcon, + title: 'vue_shared/ci_icon', +}; + +const Template = (args, { argTypes }) => ({ + components: { CiIcon }, + props: Object.keys(argTypes), + template: '<ci-icon v-bind="$props" />', +}); + +export const Default = Template.bind({}); +Default.args = { + status: { + icon: 'status_success', + text: 'Success', + detailsPath: 'https://gitab.com/', + }, +}; + +export const WithText = Template.bind({}); +WithText.args = { + status: { + icon: 'status_success', + text: 'Success', + detailsPath: 'https://gitab.com/', + }, + showStatusText: true, +}; diff --git a/app/assets/javascripts/vue_shared/components/ci_icon.vue b/app/assets/javascripts/vue_shared/components/ci_icon/ci_icon.vue index a2b6b4642c9..a2b6b4642c9 100644 --- a/app/assets/javascripts/vue_shared/components/ci_icon.vue +++ b/app/assets/javascripts/vue_shared/components/ci_icon/ci_icon.vue 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 2bdc8a174d0..e12e06a2454 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 @@ -36,11 +36,6 @@ export default { required: false, default: 'confirm-danger-button', }, - buttonQaSelector: { - type: String, - required: false, - default: null, - }, buttonVariant: { type: String, required: false, @@ -58,7 +53,6 @@ export default { :variant="buttonVariant" :disabled="disabled" :data-testid="buttonTestid" - :data-qa-selector="buttonQaSelector" >{{ buttonText }}</gl-button > <confirm-danger-modal 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 a1ef1f30ebb..5019ab901fd 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 @@ -72,7 +72,7 @@ export default { attributes: { variant: 'danger', disabled: !this.isValid, - 'data-qa-selector': 'confirm_danger_modal_button', + 'data-testid': 'confirm-danger-modal-button', }, }; }, @@ -133,8 +133,7 @@ export default { id="confirm_name_input" v-model="confirmationPhrase" class="form-control" - data-qa-selector="confirm_danger_field" - data-testid="confirm-danger-input" + data-testid="confirm-danger-field" type="text" /> </gl-form-group> diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/content_viewer.vue b/app/assets/javascripts/vue_shared/components/content_viewer/content_viewer.vue index 1370f7b2a8c..7b9ecc18ce1 100644 --- a/app/assets/javascripts/vue_shared/components/content_viewer/content_viewer.vue +++ b/app/assets/javascripts/vue_shared/components/content_viewer/content_viewer.vue @@ -54,7 +54,7 @@ export default { </script> <template> - <div class="preview-container" data-qa-selector="preview_container"> + <div class="preview-container"> <image-viewer v-if="type === 'image'" :path="path" :file-size="fileSize" /> <markdown-viewer v-if="type === 'markdown'" diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue index f28a2801bc0..332424c70ac 100644 --- a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue +++ b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue @@ -41,14 +41,7 @@ export default { {{ fileName }} <template v-if="fileSize > 0"> ({{ fileSizeReadable }}) </template> </p> - <a - :href="path" - class="btn btn-default" - rel="nofollow" - :download="fileName" - target="_blank" - data-qa-selector="download_button" - > + <a :href="path" class="btn btn-default" rel="nofollow" :download="fileName" target="_blank"> <gl-icon :size="16" name="download" class="float-left gl-mr-3" /> {{ __('Download') }} </a> diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue index 04ab0fd00aa..9742118cd5f 100644 --- a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue +++ b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue @@ -88,7 +88,7 @@ export default { </script> <template> - <div data-testid="image-viewer" data-qa-selector="image_viewer_container"> + <div data-testid="image-viewer"> <div :class="innerCssClasses" class="position-relative"> <img ref="contentImg" :src="safePath" @load="onImgLoad" /> <slot diff --git a/app/assets/javascripts/vue_shared/components/entity_select/entity_select.vue b/app/assets/javascripts/vue_shared/components/entity_select/entity_select.vue index 1a215454ab6..ea787bfe63e 100644 --- a/app/assets/javascripts/vue_shared/components/entity_select/entity_select.vue +++ b/app/assets/javascripts/vue_shared/components/entity_select/entity_select.vue @@ -57,7 +57,7 @@ export default { type: Function, required: true, }, - fetchInitialSelectionText: { + fetchInitialSelection: { type: Function, required: false, default: null, @@ -77,35 +77,23 @@ export default { searchString: '', items: [], page: 1, - selectedValue: null, - selectedText: null, + selected: this.initialSelection || '', + initialSelectedItem: {}, errorMessage: '', }; }, computed: { - selected: { - set(value) { - this.selectedValue = value; - this.selectedText = - value === null ? null : this.items.find((item) => item.value === value).text; - this.$emit('input', { - value: this.selectedValue, - text: this.selectedText, - }); - }, - get() { - return this.selectedValue; - }, + selectedItem() { + const item = this.items.find(({ value }) => value === this.selected); + + return item || this.initialSelectedItem; }, toggleText() { - return this.selectedText ?? this.defaultToggleText; + return this.selectedItem?.text ?? this.defaultToggleText; }, resetButtonLabel() { return this.clearable ? RESET_LABEL : ''; }, - inputValue() { - return this.selectedValue ? this.selectedValue : ''; - }, isSearchQueryTooShort() { return this.searchString && this.searchString.length < MINIMUM_QUERY_LENGTH; }, @@ -115,8 +103,13 @@ export default { : this.$options.i18n.noResultsText; }, }, + watch: { + selected() { + this.$emit('input', this.selectedItem); + }, + }, created() { - this.fetchInitialSelection(); + this.getInitialSelection(); }, methods: { search: debounce(function debouncedSearch(searchString) { @@ -148,23 +141,20 @@ export default { this.searching = false; this.infiniteScrollLoading = false; }, - async fetchInitialSelection() { + async getInitialSelection() { if (!this.initialSelection) { this.pristine = false; return; } - if (!this.fetchInitialSelectionText) { + if (!this.fetchInitialSelection) { throw new Error( '`initialSelection` is provided but lacks `fetchInitialSelectionText` to retrieve the corresponding text', ); } this.searching = true; - const name = await this.fetchInitialSelectionText(this.initialSelection); - - this.selectedValue = this.initialSelection; - this.selectedText = name; + this.initialSelectedItem = await this.fetchInitialSelection(this.initialSelection); this.pristine = false; this.searching = false; }, @@ -218,6 +208,6 @@ export default { <slot name="list-item" :item="item"></slot> </template> </gl-collapsible-listbox> - <input :id="inputId" data-testid="input" type="hidden" :name="inputName" :value="inputValue" /> + <input :id="inputId" data-testid="input" type="hidden" :name="inputName" :value="selected" /> </gl-form-group> </template> 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 8a338551fbe..da42c017541 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 @@ -76,11 +76,7 @@ export default { try { const url = groupsPath(this.groupsFilter, this.parentGroupID); const { data = [], headers } = await axios.get(url, { params }); - groups = data.map((group) => ({ - ...group, - text: group.full_name, - value: String(group.id), - })); + groups = data.map((group) => this.mapGroupData(group)); totalPages = parseIntPagination(normalizeHeaders(headers)).totalPages; } catch (error) { @@ -88,15 +84,19 @@ export default { } return { items: groups, totalPages }; }, - async fetchGroupName(groupId) { - let groupName = ''; + async fetchInitialGroup(groupId) { try { const group = await Api.group(groupId); - groupName = group.full_name; + + return this.mapGroupData(group); } catch (error) { this.handleError({ message: FETCH_GROUP_ERROR, error }); + + return {}; } - return groupName; + }, + mapGroupData(group) { + return { ...group, text: group.full_name, value: String(group.id) }; }, handleError({ message, error }) { Sentry.captureException(error); @@ -123,7 +123,7 @@ export default { :header-text="$options.i18n.selectGroup" :default-toggle-text="$options.i18n.toggleText" :fetch-items="fetchGroups" - :fetch-initial-selection-text="fetchGroupName" + :fetch-initial-selection="fetchInitialGroup" v-on="$listeners" > <template #error> diff --git a/app/assets/javascripts/vue_shared/components/entity_select/organization_select.vue b/app/assets/javascripts/vue_shared/components/entity_select/organization_select.vue index d068d86d95b..9f4671abbb1 100644 --- a/app/assets/javascripts/vue_shared/components/entity_select/organization_select.vue +++ b/app/assets/javascripts/vue_shared/components/entity_select/organization_select.vue @@ -1,10 +1,11 @@ <script> import { GlAlert } from '@gitlab/ui'; import * as Sentry from '~/sentry/sentry_browser_wrapper'; -import getCurrentUserOrganizationsQuery from '~/organizations/index/graphql/organizations.query.graphql'; +import getCurrentUserOrganizationsQuery from '~/organizations/shared/graphql/queries/organizations.query.graphql'; import getOrganizationQuery from '~/organizations/shared/graphql/queries/organization.query.graphql'; import { getIdFromGraphQLId, convertToGraphQLId } from '~/graphql_shared/utils'; -import { TYPENAME_ORGANIZATION } from '~/graphql_shared/constants'; +import { TYPE_ORGANIZATION } from '~/graphql_shared/constants'; +import { DEFAULT_PER_PAGE } from '~/api'; import { ORGANIZATION_TOGGLE_TEXT, ORGANIZATION_HEADER_TEXT, @@ -62,54 +63,60 @@ export default { data() { return { errorMessage: '', + endCursor: null, }; }, methods: { - async fetchOrganizations() { + async fetchOrganizations(search, page = 1) { + if (page === 1) { + this.endCursor = null; + } + try { - const { - data: { - currentUser: { - organizations: { nodes }, - }, - }, - } = await this.$apollo.query({ + const response = await this.$apollo.query({ query: getCurrentUserOrganizationsQuery, - // TODO: implement search support - https://gitlab.com/gitlab-org/gitlab/-/issues/429999. + // TODO: implement search support - https://gitlab.com/gitlab-org/gitlab/-/issues/433954. + variables: { after: this.endCursor, first: DEFAULT_PER_PAGE }, }); + const { nodes, pageInfo } = response.data.currentUser.organizations; + this.endCursor = pageInfo.endCursor; return { - items: nodes.map((organization) => ({ - text: organization.name, - value: getIdFromGraphQLId(organization.id), - })), - // TODO: implement pagination - https://gitlab.com/gitlab-org/gitlab/-/issues/429999. - totalPages: 1, + items: nodes.map((organization) => this.mapOrganizationData(organization)), + // `EntitySelect` expects a `totalPages` key but GraphQL requests don't provide this data + // because it uses keyset pagination. Since the dropdown uses infinite scroll it + // only needs to know if there is a next page. We pass `page + 1` if there is a next page, + // otherwise we just set this to the current page. + totalPages: pageInfo.hasNextPage ? page + 1 : page, }; } catch (error) { + this.endCursor = null; this.handleError({ message: FETCH_ORGANIZATIONS_ERROR, error }); return { items: [], totalPages: 0 }; } }, - async fetchOrganizationName(id) { + async fetchInitialOrganization(id) { try { - const { - data: { - organization: { name }, - }, - } = await this.$apollo.query({ + const response = await this.$apollo.query({ query: getOrganizationQuery, - variables: { id: convertToGraphQLId(TYPENAME_ORGANIZATION, id) }, + variables: { id: convertToGraphQLId(TYPE_ORGANIZATION, id) }, }); - return name; + return this.mapOrganizationData(response.data.organization); } catch (error) { this.handleError({ message: FETCH_ORGANIZATION_ERROR, error }); - return ''; + return {}; } }, + mapOrganizationData(organization) { + return { + ...organization, + text: organization.name, + value: getIdFromGraphQLId(organization.id), + }; + }, handleError({ message, error }) { Sentry.captureException(error); this.errorMessage = message; @@ -137,7 +144,7 @@ export default { :header-text="$options.i18n.selectGroup" :default-toggle-text="$options.i18n.toggleText" :fetch-items="fetchOrganizations" - :fetch-initial-selection-text="fetchOrganizationName" + :fetch-initial-selection="fetchInitialOrganization" :toggle-class="toggleClass" v-on="$listeners" > diff --git a/app/assets/javascripts/vue_shared/components/entity_select/project_select.vue b/app/assets/javascripts/vue_shared/components/entity_select/project_select.vue index 8c371e3d4ce..8c873d39496 100644 --- a/app/assets/javascripts/vue_shared/components/entity_select/project_select.vue +++ b/app/assets/javascripts/vue_shared/components/entity_select/project_select.vue @@ -120,24 +120,29 @@ export default { membership: this.membership, }); })(); - projects = data.map((item) => ({ - text: item.name_with_namespace || item.name, - value: String(item.id), - })); + projects = data.map((project) => this.mapProjectData(project)); } catch (error) { this.handleError({ message: FETCH_PROJECTS_ERROR, error }); } return { items: projects, totalPages: 1 }; }, - async fetchProjectName(projectId) { - let projectName = ''; + async fetchInitialProject(projectId) { try { - const { data: project } = await Api.project(projectId); - projectName = project.name_with_namespace; + const response = await Api.project(projectId); + + return this.mapProjectData(response.data); } catch (error) { this.handleError({ message: FETCH_PROJECT_ERROR, error }); + + return {}; } - return projectName; + }, + mapProjectData(project) { + return { + ...project, + text: project.name_with_namespace || project.name, + value: String(project.id), + }; }, handleError({ message, error }) { Sentry.captureException(error); @@ -163,7 +168,7 @@ export default { :header-text="$options.i18n.selectProject" :default-toggle-text="$options.i18n.searchForProject" :fetch-items="fetchProjects" - :fetch-initial-selection-text="fetchProjectName" + :fetch-initial-selection="fetchInitialProject" :block="block" clearable v-on="$listeners" diff --git a/app/assets/javascripts/vue_shared/components/file_icon.vue b/app/assets/javascripts/vue_shared/components/file_icon.vue index 6a10557c6bc..4738d0f5a38 100644 --- a/app/assets/javascripts/vue_shared/components/file_icon.vue +++ b/app/assets/javascripts/vue_shared/components/file_icon.vue @@ -90,12 +90,6 @@ export default { <svg v-else-if="!folder" :key="spriteHref" :class="[iconSizeClass, cssClasses]"> <use :href="spriteHref" /> </svg> - <gl-icon - v-else - :name="folderIconName" - :size="size" - class="folder-icon" - data-qa-selector="folder_icon_content" - /> + <gl-icon v-else :name="folderIconName" :size="size" class="folder-icon" /> </span> </template> diff --git a/app/assets/javascripts/vue_shared/components/file_row.vue b/app/assets/javascripts/vue_shared/components/file_row.vue index cecd1be82e9..6ac75230d88 100644 --- a/app/assets/javascripts/vue_shared/components/file_row.vue +++ b/app/assets/javascripts/vue_shared/components/file_row.vue @@ -132,11 +132,7 @@ export default { @click="clickFile" @mouseleave="$emit('mouseleave', $event)" > - <div - class="file-row-name-container" - data-qa-selector="file_row_container" - :data-qa-file-name="file.name" - > + <div class="file-row-name-container"> <span ref="textOutput" class="file-row-name" 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 c698b94749d..5362ceac9ee 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 @@ -31,6 +31,8 @@ export const OPERATORS_IS_NOT = [...OPERATORS_IS, ...OPERATORS_NOT]; export const OPERATORS_IS_NOT_OR = [...OPERATORS_IS, ...OPERATORS_NOT, ...OPERATORS_OR]; export const OPERATORS_AFTER_BEFORE = [...OPERATORS_AFTER, ...OPERATORS_BEFORE]; +export const OPERATORS_TO_GROUP = [OPERATOR_OR, OPERATOR_NOT]; + export const OPTION_NONE = { value: FILTER_NONE, text: __('None'), title: __('None') }; export const OPTION_ANY = { value: FILTER_ANY, text: __('Any'), title: __('Any') }; export const OPTION_CURRENT = { value: FILTER_CURRENT, text: __('Current') }; @@ -66,6 +68,7 @@ export const TOKEN_TITLE_CONFIDENTIAL = __('Confidential'); export const TOKEN_TITLE_CONTACT = s__('Crm|Contact'); export const TOKEN_TITLE_GROUP = __('Group'); export const TOKEN_TITLE_LABEL = __('Label'); +export const TOKEN_TITLE_PROJECT = __('Project'); export const TOKEN_TITLE_MILESTONE = __('Milestone'); export const TOKEN_TITLE_MY_REACTION = __('My-Reaction'); export const TOKEN_TITLE_ORGANIZATION = s__('Crm|Organization'); @@ -76,6 +79,7 @@ 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_VERSION = __('Version'); export const TOKEN_TITLE_SEARCH_WITHIN = __('Search Within'); export const TOKEN_TITLE_CREATED = __('Created date'); export const TOKEN_TITLE_CLOSED = __('Closed date'); @@ -91,6 +95,7 @@ export const TOKEN_TYPE_EPIC = 'epic'; // this is in the shared constants. Until we have not decoupled the EE filtered search bar // from the CE component, we need to keep this in the CE code. // https://gitlab.com/gitlab-org/gitlab/-/issues/377838 +export const TOKEN_TYPE_PROJECT = 'project'; export const TOKEN_TYPE_HEALTH = 'health'; export const TOKEN_TYPE_ITERATION = 'iteration'; export const TOKEN_TYPE_LABEL = 'label'; @@ -104,6 +109,7 @@ 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_VERSION = 'version'; export const TOKEN_TYPE_WEIGHT = 'weight'; export const TOKEN_TYPE_SEARCH_WITHIN = 'in'; export const TOKEN_TYPE_CREATED = 'created'; 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 d39e4d2ee42..364ba10e888 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 @@ -1,13 +1,5 @@ <script> -import { - GlFilteredSearch, - GlButtonGroup, - GlButton, - GlDropdown, - GlDropdownItem, - GlFormCheckbox, - GlTooltipDirective, -} from '@gitlab/ui'; +import { GlFilteredSearch, GlSorting, GlFormCheckbox, GlTooltipDirective } from '@gitlab/ui'; import RecentSearchesStorageKeys from 'ee_else_ce/filtered_search/recent_searches_storage_keys'; import RecentSearchesService from '~/filtered_search/services/recent_searches_service'; @@ -22,10 +14,7 @@ import { filterEmptySearchTerm, uniqueTokens } from './filtered_search_utils'; export default { components: { GlFilteredSearch, - GlButtonGroup, - GlButton, - GlDropdown, - GlDropdownItem, + GlSorting, GlFormCheckbox, }, directives: { @@ -118,8 +107,7 @@ export default { recentSearchesPromise: null, recentSearches: [], filterValue: this.initialFilterValue, - selectedSortOption: this.sortOptions[0], - selectedSortDirection: SORT_DIRECTION.descending, + ...this.getInitialSort(), }; }, computed: { @@ -141,15 +129,14 @@ export default { {}, ); }, - sortDirectionIcon() { - return this.selectedSortDirection === SORT_DIRECTION.ascending - ? 'sort-lowest' - : 'sort-highest'; + transformedSortOptions() { + return this.sortOptions.map(({ id: value, title: text }) => ({ value, text })); }, - sortDirectionTooltip() { - return this.selectedSortDirection === SORT_DIRECTION.ascending - ? __('Sort direction: Ascending') - : __('Sort direction: Descending'); + selectedSortDirection() { + return this.sortDirectionAscending ? SORT_DIRECTION.ascending : SORT_DIRECTION.descending; + }, + selectedSortOption() { + return this.sortOptions.find((sortOption) => sortOption.id === this.sortById); }, /** * This prop fixes a behaviour affecting GlFilteredSearch @@ -184,14 +171,13 @@ export default { this.filterValue = newValue; } }, - initialSortBy(newValue) { - if (this.syncFilterAndSort) { - this.updateSelectedSortValues(newValue); + initialSortBy(newInitialSortBy) { + if (this.syncFilterAndSort && newInitialSortBy) { + this.updateSelectedSortValues(); } }, }, created() { - this.updateSelectedSortValues(this.initialSortBy); if (this.recentSearchesStorageKey) this.setupRecentSearch(); }, methods: { @@ -273,15 +259,12 @@ export default { return filter; }); }, - handleSortOptionClick(sortBy) { - this.selectedSortOption = sortBy; - this.$emit('onSort', sortBy.sortDirection[this.selectedSortDirection]); + handleSortByChange(sortById) { + this.sortById = sortById; + this.$emit('onSort', this.selectedSortOption.sortDirection[this.selectedSortDirection]); }, - handleSortDirectionClick() { - this.selectedSortDirection = - this.selectedSortDirection === SORT_DIRECTION.ascending - ? SORT_DIRECTION.descending - : SORT_DIRECTION.ascending; + handleSortDirectionChange(isAscending) { + this.sortDirectionAscending = isAscending; this.$emit('onSort', this.selectedSortOption.sortDirection[this.selectedSortDirection]); }, handleHistoryItemSelected(filters) { @@ -328,18 +311,30 @@ export default { const cleared = true; this.$emit('onFilter', [], cleared); }, - updateSelectedSortValues(sort) { - if (!sort) { - return; + updateSelectedSortValues() { + Object.assign(this, this.getInitialSort()); + }, + getInitialSort() { + for (const sortOption of this.sortOptions) { + if (sortOption.sortDirection.ascending === this.initialSortBy) { + return { + sortById: sortOption.id, + sortDirectionAscending: true, + }; + } + + if (sortOption.sortDirection.descending === this.initialSortBy) { + return { + sortById: sortOption.id, + sortDirectionAscending: false, + }; + } } - this.selectedSortOption = this.sortOptions.find( - (sortBy) => - sortBy.sortDirection.ascending === sort || sortBy.sortDirection.descending === sort, - ); - this.selectedSortDirection = Object.keys(this.selectedSortOption?.sortDirection || {}).find( - (key) => this.selectedSortOption.sortDirection[key] === sort, - ); + return { + sortById: this.sortOptions[0]?.id, + sortDirectionAscending: false, + }; }, }, }; @@ -390,25 +385,14 @@ export default { </template> </template> </gl-filtered-search> - <gl-button-group v-if="selectedSortOption" class="sort-dropdown-container d-flex"> - <gl-dropdown :text="selectedSortOption.title" :right="true" class="w-100"> - <gl-dropdown-item - v-for="sortBy in sortOptions" - :key="sortBy.id" - is-check-item - :is-checked="sortBy.id === selectedSortOption.id" - @click="handleSortOptionClick(sortBy)" - >{{ sortBy.title }}</gl-dropdown-item - > - </gl-dropdown> - <gl-button - v-gl-tooltip - :title="sortDirectionTooltip" - :aria-label="sortDirectionTooltip" - :icon="sortDirectionIcon" - class="flex-shrink-1" - @click="handleSortDirectionClick" - /> - </gl-button-group> + <gl-sorting + v-if="selectedSortOption" + :sort-options="transformedSortOptions" + :sort-by="sortById" + :is-ascending="sortDirectionAscending" + class="sort-dropdown-container" + @sortByChange="handleSortByChange" + @sortDirectionChange="handleSortDirectionChange" + /> </div> </template> 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 3857dd9c55d..5d72ac34e73 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 @@ -11,7 +11,13 @@ import { debounce, last } from 'lodash'; import { stripQuotes } from '~/lib/utils/text_utility'; import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; -import { DEBOUNCE_DELAY, FILTERS_NONE_ANY, OPERATOR_NOT, OPERATOR_OR } from '../constants'; +import { + DEBOUNCE_DELAY, + FILTERS_NONE_ANY, + OPERATOR_NOT, + OPERATOR_OR, + OPERATORS_TO_GROUP, +} from '../constants'; import { getRecentlyUsedSuggestions, setTokenValueToRecentlyUsed } from '../filtered_search_utils'; export default { @@ -102,7 +108,7 @@ export default { }, activeTokenValue() { const data = - this.glFeatures.groupMultiSelectTokens && Array.isArray(this.value.data) + this.multiSelectEnabled && Array.isArray(this.value.data) ? last(this.value.data) : this.value.data; return this.getActiveTokenValue(this.suggestions, data); @@ -153,6 +159,22 @@ export default { ? this.activeTokenValue[this.searchBy] : undefined; }, + multiSelectEnabled() { + return ( + this.config.multiSelect && + this.glFeatures.groupMultiSelectTokens && + OPERATORS_TO_GROUP.includes(this.value.operator) + ); + }, + validatedConfig() { + if (this.config.multiSelect && !this.multiSelectEnabled) { + return { + ...this.config, + multiSelect: false, + }; + } + return this.config; + }, }, watch: { active: { @@ -199,7 +221,7 @@ export default { } }, DEBOUNCE_DELAY), handleTokenValueSelected(selectedValue) { - if (this.glFeatures.groupMultiSelectTokens) { + if (this.multiSelectEnabled) { this.$emit('token-selected', selectedValue); } @@ -228,7 +250,7 @@ export default { <template> <gl-filtered-search-token - :config="config" + :config="validatedConfig" :value="value" :active="active" :multi-select-values="multiSelectValues" diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/user_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/user_token.vue index c5326ead60d..87e295d00dd 100644 --- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/user_token.vue +++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/user_token.vue @@ -7,7 +7,7 @@ import { __ } from '~/locale'; import { WORKSPACE_GROUP, WORKSPACE_PROJECT } from '~/issues/constants'; import usersAutocompleteQuery from '~/graphql_shared/queries/users_autocomplete.query.graphql'; import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; -import { OPTIONS_NONE_ANY } from '../constants'; +import { OPERATORS_TO_GROUP, OPTIONS_NONE_ANY } from '../constants'; import BaseToken from './base_token.vue'; @@ -57,7 +57,11 @@ export default { return this.config.fetchUsers ? this.config.fetchUsers : this.fetchUsersBySearchTerm; }, multiSelectEnabled() { - return this.config.multiSelect && this.glFeatures.groupMultiSelectTokens; + return ( + this.config.multiSelect && + this.glFeatures.groupMultiSelectTokens && + OPERATORS_TO_GROUP.includes(this.value.operator) + ); }, }, watch: { @@ -94,7 +98,7 @@ export default { return user?.avatarUrl || user?.avatar_url; }, displayNameFor(username) { - return this.getActiveUser(this.allUsers, username)?.name || `@${username}`; + return this.getActiveUser(this.allUsers, username)?.name || username; }, avatarFor(username) { const user = this.getActiveUser(this.allUsers, username); 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 index 0455685627d..b03da19a896 100644 --- 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 @@ -179,7 +179,6 @@ export default { :aria-label="toggleVisibilityLabel" :icon="toggleVisibilityIcon" data-testid="toggle-visibility-button" - data-qa-selector="toggle_visibility_button" @click.stop="handleToggleVisibilityButtonClick" /> <clipboard-button diff --git a/app/assets/javascripts/vue_shared/components/keep_alive_slots.vue b/app/assets/javascripts/vue_shared/components/keep_alive_slots.vue deleted file mode 100644 index d68c4399275..00000000000 --- a/app/assets/javascripts/vue_shared/components/keep_alive_slots.vue +++ /dev/null @@ -1,51 +0,0 @@ -<script> -export default { - props: { - slotKey: { - type: String, - required: false, - default: '', - }, - }, - data() { - return { - aliveSlotsLookup: {}, - }; - }, - computed: { - aliveSlots() { - return Object.keys(this.aliveSlotsLookup); - }, - }, - watch: { - slotKey: { - handler(val) { - if (!val) { - return; - } - - this.$set(this.aliveSlotsLookup, val, true); - }, - immediate: true, - }, - }, - methods: { - isCurrentSlot(key) { - return key === this.slotKey; - }, - }, -}; -</script> - -<template> - <div> - <div - v-for="slot in aliveSlots" - v-show="isCurrentSlot(slot)" - :key="slot" - class="gl-h-full gl-w-full" - > - <slot :name="slot"></slot> - </div> - </div> -</template> diff --git a/app/assets/javascripts/vue_shared/components/list_selector/constants.js b/app/assets/javascripts/vue_shared/components/list_selector/constants.js index cff9c56a1c0..ad826c6f3e5 100644 --- a/app/assets/javascripts/vue_shared/components/list_selector/constants.js +++ b/app/assets/javascripts/vue_shared/components/list_selector/constants.js @@ -1,6 +1,26 @@ import { __ } from '~/locale'; +import UserItem from './user_item.vue'; +import GroupItem from './group_item.vue'; +import DeployKeyItem from './deploy_key_item.vue'; export const CONFIG = { - users: { title: __('Users'), icon: 'user', filterKey: 'username', showNamespaceDropdown: true }, - groups: { title: __('Groups'), icon: 'group', filterKey: 'name' }, + users: { + title: __('Users'), + icon: 'user', + filterKey: 'username', + showNamespaceDropdown: true, + component: UserItem, + }, + groups: { + title: __('Groups'), + icon: 'group', + filterKey: 'name', + component: GroupItem, + }, + deployKeys: { + title: __('Deploy keys'), + icon: 'key', + filterKey: 'name', + component: DeployKeyItem, + }, }; diff --git a/app/assets/javascripts/vue_shared/components/list_selector/deploy_key_item.vue b/app/assets/javascripts/vue_shared/components/list_selector/deploy_key_item.vue new file mode 100644 index 00000000000..4dbbd44f0b5 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/list_selector/deploy_key_item.vue @@ -0,0 +1,51 @@ +<script> +import { GlButton, GlIcon } from '@gitlab/ui'; +import { sprintf, __ } from '~/locale'; + +export default { + name: 'DeployKeyItem', + components: { GlButton, GlIcon }, + props: { + data: { + type: Object, + required: true, + }, + canDelete: { + type: Boolean, + required: false, + default: false, + }, + }, + data() { + const { title, owner, id } = this.data; + return { + deleteButtonLabel: sprintf(__('Delete %{name}'), { name: title }), + title, + owner, + id, + }; + }, +}; +</script> + +<template> + <span + class="gl-display-flex gl-align-items-center gl-gap-3" + data-testid="deploy-key-wrapper" + @click="$emit('select', id)" + > + <gl-icon name="key" /> + <span class="gl-display-flex gl-flex-direction-column gl-flex-grow-1"> + <span class="gl-font-weight-bold">{{ title }}</span> + <span class="gl-text-gray-600">@{{ owner }}</span> + </span> + + <gl-button + v-if="canDelete" + icon="remove" + :aria-label="deleteButtonLabel" + category="tertiary" + @click.stop="$emit('delete', id)" + /> + </span> +</template> diff --git a/app/assets/javascripts/vue_shared/components/list_selector/index.vue b/app/assets/javascripts/vue_shared/components/list_selector/index.vue index b8480a0c496..d79a8d6a00c 100644 --- a/app/assets/javascripts/vue_shared/components/list_selector/index.vue +++ b/app/assets/javascripts/vue_shared/components/list_selector/index.vue @@ -5,8 +5,6 @@ import { createAlert } from '~/alert'; import { __ } from '~/locale'; import groupsAutocompleteQuery from '~/graphql_shared/queries/groups_autocomplete.query.graphql'; import Api from '~/api'; -import UserItem from './user_item.vue'; -import GroupItem from './group_item.vue'; import { CONFIG } from './constants'; const I18N = { @@ -25,10 +23,6 @@ export default { GlCollapsibleListbox, }, props: { - title: { - type: String, - required: true, - }, type: { type: String, required: true, @@ -61,12 +55,6 @@ export default { config() { return CONFIG[this.type]; }, - isUserVariant() { - return this.type === 'users'; - }, - component() { - return this.isUserVariant ? UserItem : GroupItem; - }, namespaceDropdownText() { return parseBoolean(this.isProjectNamespace) ? this.$options.i18n.projectGroups @@ -77,12 +65,14 @@ export default { async handleSearchInput(search) { this.$refs.results.open(); + const searchMethod = { + users: this.fetchUsersBySearchTerm, + groups: this.fetchGroupsBySearchTerm, + deployKeys: this.fetchDeployKeysBySearchTerm, + }; + try { - if (this.isUserVariant) { - this.items = await this.fetchUsersBySearchTerm(search); - } else { - this.items = await this.fetchGroupsBySearchTerm(search); - } + this.items = await searchMethod[this.type](search); } catch (e) { createAlert({ message: this.$options.i18n.apiErrorMessage, @@ -114,6 +104,10 @@ export default { })), ); }, + fetchDeployKeysBySearchTerm() { + // TODO - implement API request (follow-up) + // https://gitlab.com/gitlab-org/gitlab/-/issues/432494 + }, getItemByKey(key) { return this.items.find((item) => item[this.config.filterKey] === key); }, @@ -139,7 +133,7 @@ export default { <gl-card header-class="gl-new-card-header gl-border-none" body-class="gl-card-footer"> <template #header ><strong data-testid="list-selector-title" - >{{ title }} + >{{ config.title }} <span class="gl-text-gray-700 gl-ml-3" ><gl-icon :name="config.icon" /> {{ selectedItems.length }}</span ></strong @@ -166,7 +160,7 @@ export default { </template> <template #list-item="{ item }"> - <component :is="component" :data="item" @select="handleSelectItem" /> + <component :is="config.component" :data="item" @select="handleSelectItem" /> </template> </gl-collapsible-listbox> @@ -180,7 +174,7 @@ export default { </div> <component - :is="component" + :is="config.component" v-for="(item, index) of selectedItems" :key="index" :class="{ 'gl-border-t': index > 0 }" 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 d99b90fa561..a7dfc1e2cdb 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 @@ -88,7 +88,7 @@ export default { placement="right" searchable size="small" - class="comment-template-dropdown gl-mr-3" + class="comment-template-dropdown gl-mr-2" positioning-strategy="fixed" :searching="$apollo.queries.savedReplies.loading" @shown="fetchCommentTemplates" diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue index 24211833026..e80f5c7f092 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/field.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue @@ -361,7 +361,7 @@ export default { <template> <div ref="gl-form" - class="js-vue-markdown-field md-area position-relative gfm-form gl-overflow-hidden" + class="js-vue-markdown-field md-area position-relative gfm-form" :data-uploads-path="uploadsPath" > <markdown-header diff --git a/app/assets/javascripts/vue_shared/components/markdown/header.vue b/app/assets/javascripts/vue_shared/components/markdown/header.vue index cc3c95a047b..cffd8471d18 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/header.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/header.vue @@ -21,6 +21,7 @@ import { updateText } from '~/lib/utils/text_markdown'; import ToolbarButton from './toolbar_button.vue'; import DrawioToolbarButton from './drawio_toolbar_button.vue'; import CommentTemplatesDropdown from './comment_templates_dropdown.vue'; +import HeaderDivider from './header_divider.vue'; export default { components: { @@ -30,6 +31,7 @@ export default { DrawioToolbarButton, CommentTemplatesDropdown, AiActionsDropdown: () => import('ee_component/ai/components/ai_actions_dropdown.vue'), + HeaderDivider, }, directives: { GlTooltip: GlTooltipDirective, @@ -188,14 +190,6 @@ export default { }) .catch(() => {}); }, - handleAttachFile(e) { - e.preventDefault(); - const $gfmForm = $(this.$el).closest('.gfm-form'); - const $gfmTextarea = $gfmForm.find('.js-gfm-input'); - - $gfmForm.find('.div-dropzone').click(); - $gfmTextarea.focus(); - }, insertIntoTextarea(text) { const textArea = this.$el.closest('.md-area')?.querySelector('textarea'); if (textArea) { @@ -254,252 +248,281 @@ export default { </script> <template> - <div class="md-header gl-border-b gl-border-gray-100 gl-px-3"> + <div + class="md-header gl-bg-white gl-border-b gl-border-gray-100 gl-rounded-lg gl-rounded-bottom-left-none gl-rounded-bottom-right-none gl-px-3" + :class="{ 'md-header-preview': previewMarkdown }" + > <div class="gl-display-flex gl-align-items-center gl-flex-wrap"> <div data-testid="md-header-toolbar" - class="md-header-toolbar gl-display-flex gl-py-3 gl-flex-wrap gl-row-gap-3" + class="md-header-toolbar gl-display-flex gl-py-3 gl-row-gap-2 gl-flex-grow-1 gl-align-items-flex-start" > - <gl-button - v-if="enablePreview" - data-testid="preview-toggle" - :value="previewMarkdown ? 'preview' : 'edit'" - :label="$options.i18n.previewTabTitle" - class="js-md-preview-button gl-flex-direction-row-reverse gl-align-items-center gl-font-weight-normal! gl-mr-2" - size="small" - category="tertiary" - @click="switchPreview" - >{{ previewMarkdown ? $options.i18n.hidePreview : $options.i18n.preview }}</gl-button - > - <template v-if="!previewMarkdown && canSuggest"> + <div class="gl-display-flex gl-flex-wrap gl-row-gap-2"> + <gl-button + v-if="enablePreview" + data-testid="preview-toggle" + :value="previewMarkdown ? 'preview' : 'edit'" + :label="$options.i18n.previewTabTitle" + class="js-md-preview-button gl-flex-direction-row-reverse gl-align-items-center gl-font-weight-normal!" + size="small" + category="tertiary" + @click="switchPreview" + >{{ previewMarkdown ? $options.i18n.hidePreview : $options.i18n.preview }}</gl-button + > + <template v-if="!previewMarkdown && canSuggest"> + <div class="gl-display-flex gl-row-gap-2"> + <header-divider :preview-markdown="previewMarkdown" /> + <toolbar-button + ref="suggestButton" + :tag="mdSuggestion" + :prepend="true" + :button-title="__('Insert suggestion')" + :cursor-offset="4" + :tag-content="lineContent" + tracking-property="codeSuggestion" + icon="doc-code" + data-testid="suggestion-button" + class="js-suggestion-btn" + @click="handleSuggestDismissed" + /> + <gl-popover + v-if="suggestPopoverVisible" + :target="$refs.suggestButton.$el" + :css-classes="['diff-suggest-popover']" + placement="bottom" + :show="suggestPopoverVisible" + triggers="" + > + <strong>{{ __('New! Suggest changes directly') }}</strong> + <p class="mb-2"> + {{ + __( + 'Suggest code changes which can be immediately applied in one click. Try it out!', + ) + }} + </p> + <gl-button + variant="confirm" + category="primary" + size="small" + data-testid="dismiss-suggestion-popover-button" + @click="handleSuggestDismissed" + > + {{ __('Got it') }} + </gl-button> + </gl-popover> + </div> + </template> + <div class="gl-display-flex gl-row-gap-2"> + <div + v-if="!previewMarkdown && editorAiActions.length" + class="gl-display-flex gl-row-gap-2" + > + <header-divider :preview-markdown="previewMarkdown" /> + <ai-actions-dropdown + :actions="editorAiActions" + @input="insertAIAction" + @replace="replaceTextarea" + /> + </div> + <header-divider :preview-markdown="previewMarkdown" /> + </div> <toolbar-button - ref="suggestButton" - :tag="mdSuggestion" + v-show="!previewMarkdown" + tag="**" + :button-title=" + /* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ + sprintf(s__('MarkdownEditor|Add bold text (%{modifierKey}B)'), { + modifierKey, + }) /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */ + " + :shortcuts="$options.shortcuts.bold" + icon="bold" + tracking-property="bold" + /> + <toolbar-button + v-show="!previewMarkdown" + tag="_" + :button-title=" + /* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ + sprintf(s__('MarkdownEditor|Add italic text (%{modifierKey}I)'), { + modifierKey, + }) /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */ + " + :shortcuts="$options.shortcuts.italic" + icon="italic" + tracking-property="italic" + /> + <div class="gl-display-flex gl-row-gap-2"> + <toolbar-button + v-if="!restrictedToolBarItems.includes('strikethrough')" + v-show="!previewMarkdown" + tag="~~" + :button-title=" + /* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ + sprintf(s__('MarkdownEditor|Add strikethrough text (%{modifierKey}%{shiftKey}X)'), { + modifierKey, + shiftKey /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */, + }) + " + :shortcuts="$options.shortcuts.strikethrough" + icon="strikethrough" + tracking-property="strike" + /> + <header-divider :preview-markdown="previewMarkdown" /> + </div> + <toolbar-button + v-if="!restrictedToolBarItems.includes('quote')" + v-show="!previewMarkdown" :prepend="true" - :button-title="__('Insert suggestion')" - :cursor-offset="4" - :tag-content="lineContent" - tracking-property="codeSuggestion" - icon="doc-code" - data-testid="suggestion-button" - class="js-suggestion-btn" - @click="handleSuggestDismissed" + :tag="tag" + :button-title="__('Insert a quote')" + icon="quote" + tracking-property="blockquote" + @click="handleQuote" /> - <gl-popover - v-if="suggestPopoverVisible" - :target="$refs.suggestButton.$el" - :css-classes="['diff-suggest-popover']" - placement="bottom" - :show="suggestPopoverVisible" - triggers="" - > - <strong>{{ __('New! Suggest changes directly') }}</strong> - <p class="mb-2"> - {{ - __( - 'Suggest code changes which can be immediately applied in one click. Try it out!', - ) - }} - </p> - <gl-button - variant="confirm" - category="primary" - size="small" - data-testid="dismiss-suggestion-popover-button" - @click="handleSuggestDismissed" - > - {{ __('Got it') }} - </gl-button> - </gl-popover> - </template> - <ai-actions-dropdown - v-if="!previewMarkdown && editorAiActions.length" - :actions="editorAiActions" - @input="insertAIAction" - @replace="replaceTextarea" - /> - <toolbar-button - v-show="!previewMarkdown" - tag="**" - :button-title=" - /* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ - sprintf(s__('MarkdownEditor|Add bold text (%{modifierKey}B)'), { - modifierKey, - }) /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */ - " - :shortcuts="$options.shortcuts.bold" - icon="bold" - tracking-property="bold" - /> - <toolbar-button - v-show="!previewMarkdown" - tag="_" - :button-title=" - /* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ - sprintf(s__('MarkdownEditor|Add italic text (%{modifierKey}I)'), { - modifierKey, - }) /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */ - " - :shortcuts="$options.shortcuts.italic" - icon="italic" - tracking-property="italic" - /> - <toolbar-button - v-if="!restrictedToolBarItems.includes('strikethrough')" - v-show="!previewMarkdown" - tag="~~" - :button-title=" - /* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ - sprintf(s__('MarkdownEditor|Add strikethrough text (%{modifierKey}%{shiftKey}X)'), { - modifierKey, - shiftKey /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */, - }) - " - :shortcuts="$options.shortcuts.strikethrough" - icon="strikethrough" - tracking-property="strike" - /> - <toolbar-button - v-if="!restrictedToolBarItems.includes('quote')" - v-show="!previewMarkdown" - :prepend="true" - :tag="tag" - :button-title="__('Insert a quote')" - icon="quote" - tracking-property="blockquote" - @click="handleQuote" - /> - <toolbar-button - v-show="!previewMarkdown" - tag="`" - tag-block="```" - :button-title="__('Insert code')" - icon="code" - tracking-property="code" - /> - <toolbar-button - v-show="!previewMarkdown" - tag="[{text}](url)" - tag-select="url" - :button-title=" - /* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ - sprintf(s__('MarkdownEditor|Add a link (%{modifierKey}K)'), { - modifierKey, - }) /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */ - " - :shortcuts="$options.shortcuts.link" - icon="link" - tracking-property="link" - /> - <toolbar-button - v-if="!restrictedToolBarItems.includes('bullet-list')" - v-show="!previewMarkdown" - :prepend="true" - tag="- " - :button-title="__('Add a bullet list')" - icon="list-bulleted" - tracking-property="bulletList" - /> - <toolbar-button - v-if="!restrictedToolBarItems.includes('numbered-list')" - v-show="!previewMarkdown" - :prepend="true" - tag="1. " - :button-title="__('Add a numbered list')" - icon="list-numbered" - tracking-property="orderedList" - /> - <toolbar-button - v-if="!restrictedToolBarItems.includes('task-list')" - v-show="!previewMarkdown" - :prepend="true" - tag="- [ ] " - :button-title="__('Add a checklist')" - icon="list-task" - tracking-property="taskList" - /> - <toolbar-button - v-if="!restrictedToolBarItems.includes('indent')" - v-show="!previewMarkdown" - class="gl-display-none" - :button-title=" - /* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ - sprintf(s__('MarkdownEditor|Indent line (%{modifierKey}])'), { - modifierKey /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */, - }) - " - :shortcuts="$options.shortcuts.indent" - command="indentLines" - icon="list-indent" - tracking-property="indent" - /> - <toolbar-button - v-if="!restrictedToolBarItems.includes('outdent')" - v-show="!previewMarkdown" - class="gl-display-none" - :button-title=" - /* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ - sprintf(s__('MarkdownEditor|Outdent line (%{modifierKey}[)'), { - modifierKey /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */, - }) - " - :shortcuts="$options.shortcuts.outdent" - command="outdentLines" - icon="list-outdent" - tracking-property="outdent" - /> - <toolbar-button - v-if="!restrictedToolBarItems.includes('collapsible-section')" - v-show="!previewMarkdown" - :tag="mdCollapsibleSection" - :prepend="true" - tag-select="Click to expand" - :button-title="__('Add a collapsible section')" - icon="details-block" - tracking-property="details" - /> - <toolbar-button - v-if="!restrictedToolBarItems.includes('table')" - v-show="!previewMarkdown" - :tag="mdTable" - :prepend="true" - :button-title="__('Add a table')" - icon="table" - tracking-property="table" - /> - <toolbar-button - v-if="!previewMarkdown && !restrictedToolBarItems.includes('attach-file')" - data-testid="button-attach-file" - :button-title="__('Attach a file or image')" - icon="paperclip" - class="gl-mr-3" - tracking-property="upload" - @click="handleAttachFile" - /> - <drawio-toolbar-button - v-if="!previewMarkdown && drawioEnabled" - :uploads-path="uploadsPath" - :markdown-preview-path="markdownPreviewPath" - /> - <!-- TODO Add icon and trigger functionality from here --> - <toolbar-button - v-if="supportsQuickActions" - v-show="!previewMarkdown" - :prepend="true" - tag="/" - :button-title="__('Add a quick action')" - icon="quick-actions" - tracking-property="quickAction" - /> - <comment-templates-dropdown - v-if="!previewMarkdown && newCommentTemplatePath" - :new-comment-template-path="newCommentTemplatePath" - @select="insertSavedReply" - /> - <div v-if="!previewMarkdown" class="full-screen"> + <toolbar-button + v-show="!previewMarkdown" + tag="`" + tag-block="```" + :button-title="__('Insert code')" + icon="code" + tracking-property="code" + /> + <toolbar-button + v-show="!previewMarkdown" + tag="[{text}](url)" + tag-select="url" + :button-title=" + /* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ + sprintf(s__('MarkdownEditor|Add a link (%{modifierKey}K)'), { + modifierKey, + }) /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */ + " + :shortcuts="$options.shortcuts.link" + icon="link" + tracking-property="link" + /> + <toolbar-button + v-if="!restrictedToolBarItems.includes('bullet-list')" + v-show="!previewMarkdown" + :prepend="true" + tag="- " + :button-title="__('Add a bullet list')" + icon="list-bulleted" + tracking-property="bulletList" + /> + <toolbar-button + v-if="!restrictedToolBarItems.includes('numbered-list')" + v-show="!previewMarkdown" + :prepend="true" + tag="1. " + :button-title="__('Add a numbered list')" + icon="list-numbered" + tracking-property="orderedList" + /> + <toolbar-button + v-if="!restrictedToolBarItems.includes('task-list')" + v-show="!previewMarkdown" + :prepend="true" + tag="- [ ] " + :button-title="__('Add a checklist')" + icon="list-task" + tracking-property="taskList" + /> + <toolbar-button + v-if="!restrictedToolBarItems.includes('indent')" + v-show="!previewMarkdown" + class="gl-display-none" + :button-title=" + /* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ + sprintf(s__('MarkdownEditor|Indent line (%{modifierKey}])'), { + modifierKey /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */, + }) + " + :shortcuts="$options.shortcuts.indent" + command="indentLines" + icon="list-indent" + tracking-property="indent" + /> + <toolbar-button + v-if="!restrictedToolBarItems.includes('outdent')" + v-show="!previewMarkdown" + class="gl-display-none" + :button-title=" + /* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ + sprintf(s__('MarkdownEditor|Outdent line (%{modifierKey}[)'), { + modifierKey /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */, + }) + " + :shortcuts="$options.shortcuts.outdent" + command="outdentLines" + icon="list-outdent" + tracking-property="outdent" + /> + <div class="gl-display-flex gl-row-gap-2"> + <toolbar-button + v-if="!restrictedToolBarItems.includes('collapsible-section')" + v-show="!previewMarkdown" + :tag="mdCollapsibleSection" + :prepend="true" + tag-select="Click to expand" + :button-title="__('Add a collapsible section')" + icon="details-block" + tracking-property="details" + /> + <header-divider :preview-markdown="previewMarkdown" /> + </div> + <toolbar-button + v-if="!restrictedToolBarItems.includes('table')" + v-show="!previewMarkdown" + :tag="mdTable" + :prepend="true" + :button-title="__('Add a table')" + icon="table" + tracking-property="table" + /> + <!-- + The attach file button's click behavior is added by + dropzone_input.js. + --> + <toolbar-button + v-if="!previewMarkdown && !restrictedToolBarItems.includes('attach-file')" + data-testid="button-attach-file" + data-button-type="attach-file" + :button-title="__('Attach a file or image')" + icon="paperclip" + class="gl-mr-2" + tracking-property="upload" + /> + <drawio-toolbar-button + v-if="!previewMarkdown && drawioEnabled" + :uploads-path="uploadsPath" + :markdown-preview-path="markdownPreviewPath" + /> + <!-- TODO Add icon and trigger functionality from here --> + <toolbar-button + v-if="supportsQuickActions" + v-show="!previewMarkdown" + :prepend="true" + tag="/" + :button-title="__('Add a quick action')" + icon="quick-actions" + tracking-property="quickAction" + /> + <comment-templates-dropdown + v-if="!previewMarkdown && newCommentTemplatePath" + :new-comment-template-path="newCommentTemplatePath" + @select="insertSavedReply" + /> + </div> + <div + v-if="!previewMarkdown" + class="full-screen gl-flex-grow-1 gl-justify-content-end gl-display-flex" + > <toolbar-button v-if="!restrictedToolBarItems.includes('full-screen')" - class="js-zen-enter" + class="js-zen-enter gl-mr-0!" icon="maximize" :button-title="__('Go full screen')" :prepend="true" diff --git a/app/assets/javascripts/vue_shared/components/markdown/header_divider.vue b/app/assets/javascripts/vue_shared/components/markdown/header_divider.vue new file mode 100644 index 00000000000..d08a3d4cd34 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/markdown/header_divider.vue @@ -0,0 +1,16 @@ +<script> +export default { + props: { + previewMarkdown: { + type: Boolean, + required: false, + default: false, + }, + }, +}; +</script> +<template> + <div v-if="!previewMarkdown" class="md-toolbar-divider gl-display-flex gl-py-2"> + <div class="gl-border-l gl-pl-3 gl-ml-2"></div> + </div> +</template> diff --git a/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue b/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue index cf484443c07..182da7945ff 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue @@ -111,7 +111,7 @@ export default { type="button" category="tertiary" size="small" - class="js-md gl-mr-3" + class="js-md gl-mr-2" data-container="body" @click="$emit('click', $event)" /> 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 3bee539688b..1ee752e8c19 100644 --- a/app/assets/javascripts/vue_shared/components/notes/noteable_warning.vue +++ b/app/assets/javascripts/vue_shared/components/notes/noteable_warning.vue @@ -72,7 +72,7 @@ export default { }; </script> <template> - <div class="issuable-note-warning"> + <div class="issuable-note-warning" data-testid="issuable-note-warning"> <gl-icon v-if="!isLockedAndConfidential" :name="warningIcon" :size="16" class="icon inline" /> <span v-if="isLockedAndConfidential" ref="lockedAndConfidential"> diff --git a/app/assets/javascripts/vue_shared/components/number_to_human_size/number_to_human_size.stories.js b/app/assets/javascripts/vue_shared/components/number_to_human_size/number_to_human_size.stories.js new file mode 100644 index 00000000000..59b1967ad31 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/number_to_human_size/number_to_human_size.stories.js @@ -0,0 +1,34 @@ +import NumberToHumanSize from './number_to_human_size.vue'; + +export default { + component: NumberToHumanSize, + title: 'vue_shared/number_to_human_size', +}; + +const Template = (args, { argTypes }) => ({ + components: { NumberToHumanSize }, + props: Object.keys(argTypes), + template: '<number-to-human-size v-bind="$props" />', +}); + +export const Default = Template.bind({}); +Default.args = { + value: 42.55 * 1024 * 1024 * 1024, + fractionDigits: 1, + labelClass: '', + plainZero: false, +}; + +export const PlainZero = Template.bind({}); +PlainZero.args = { + ...Default.args, + value: 0, + plainZero: true, +}; + +export const CustomStyles = Template.bind({}); +CustomStyles.args = { + ...Default.args, + class: 'gl-font-weight-bold', + labelClass: 'gl-font-sm gl-text-gray-500', +}; diff --git a/app/assets/javascripts/vue_shared/components/number_to_human_size/number_to_human_size.vue b/app/assets/javascripts/vue_shared/components/number_to_human_size/number_to_human_size.vue new file mode 100644 index 00000000000..d6c56b2c465 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/number_to_human_size/number_to_human_size.vue @@ -0,0 +1,48 @@ +<script> +import { numberToHumanSizeSplit } from '~/lib/utils/number_utils'; + +export default { + name: 'NumberToHumanSize', + props: { + value: { + type: Number, + required: true, + }, + fractionDigits: { + type: Number, + required: false, + default: 1, + }, + labelClass: { + type: String, + required: false, + default: null, + }, + plainZero: { + type: Boolean, + required: false, + default: false, + }, + }, + computed: { + formattedValue() { + if (this.plainZero && this.value === 0) { + return ['0']; + } + + return numberToHumanSizeSplit(this.value, this.fractionDigits); + }, + number() { + return this.formattedValue[0]; + }, + label() { + return this.formattedValue[1]; + }, + }, +}; +</script> +<template> + <span + >{{ number }}<span v-if="label" :class="labelClass"> {{ label }}</span></span + > +</template> diff --git a/app/assets/javascripts/vue_shared/components/project_selector/project_selector.vue b/app/assets/javascripts/vue_shared/components/project_selector/project_selector.vue index 67ad7769c7c..f3b483c5f53 100644 --- a/app/assets/javascripts/vue_shared/components/project_selector/project_selector.vue +++ b/app/assets/javascripts/vue_shared/components/project_selector/project_selector.vue @@ -100,7 +100,7 @@ export default { type="search" class="mb-3" autofocus - data-qa-selector="project_search_field" + data-testid="project-search-field" @input="onInput" /> <div class="d-flex flex-column"> @@ -120,7 +120,7 @@ export default { :project="project" :matcher="searchQuery" class="js-project-list-item" - data-qa-selector="project_list_item" + data-testid="project-list-item" @click="projectClicked(project)" /> </div> diff --git a/app/assets/javascripts/vue_shared/components/registry/details_row.vue b/app/assets/javascripts/vue_shared/components/registry/details_row.vue index 72e06b45561..85b4ea241ef 100644 --- a/app/assets/javascripts/vue_shared/components/registry/details_row.vue +++ b/app/assets/javascripts/vue_shared/components/registry/details_row.vue @@ -32,12 +32,14 @@ export default { <template> <div - class="gl-display-flex gl-align-items-center gl-font-monospace gl-font-sm gl-word-break-all" + class="gl-display-flex gl-align-items-top gl-font-monospace gl-font-sm gl-word-break-all" :class="[padding, borderClass]" > - <gl-icon v-if="icon" :name="icon" class="gl-mr-4" /> - <span> + <div v-if="icon" class="gl-w-5 gl-mr-4"> + <gl-icon :name="icon" /> + </div> + <div> <slot></slot> - </span> + </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 ccda8c5fea7..868e348adc0 100644 --- a/app/assets/javascripts/vue_shared/components/registry/list_item.vue +++ b/app/assets/javascripts/vue_shared/components/registry/list_item.vue @@ -70,9 +70,11 @@ export default { <slot name="left-action"></slot> </div> <div - class="gl-display-flex gl-xs-flex-direction-column gl-justify-content-space-between gl-align-items-stretch gl-flex-grow-1" + class="gl-display-flex gl-flex-direction-column gl-sm-flex-direction-row gl-justify-content-space-between gl-align-items-stretch gl-flex-grow-1" > - <div class="gl-display-flex gl-flex-direction-column gl-xs-mb-3 gl-min-w-0 gl-flex-grow-1"> + <div + class="gl-display-flex gl-flex-direction-column gl-mb-3 gl-sm-mb-0 gl-min-w-0 gl-flex-grow-1" + > <div v-if=" $slots['left-primary'] /* eslint-disable-line @gitlab/vue-prefer-dollar-scopedslots */ 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 3b6dcace8fe..89b64f03e1f 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 @@ -66,6 +66,10 @@ export default { const page = getPageParamValue(this.number); return getPageSearchString(this.blamePath, page); }, + codeStyling() { + const defaultGutterWidth = 96; + return { marginLeft: `${this.$refs.lineNumbers?.offsetWidth || defaultGutterWidth}px` }; + }, }, methods: { handleChunkAppear() { @@ -80,7 +84,7 @@ export default { </script> <template> <div class="gl-display-flex"> - <div v-if="shouldHighlight" class="gl-display-flex gl-flex-direction-column"> + <div v-if="shouldHighlight" class="gl-display-flex gl-flex-direction-column gl-absolute"> <div v-for="(n, index) in totalLines" :key="index" @@ -102,14 +106,14 @@ export default { </div> </div> - <div v-else class="line-numbers gl-p-0! gl-mr-3 gl-text-transparent"> + <div v-else ref="lineNumbers" class="line-numbers gl-p-0! gl-mr-3 gl-text-transparent"> <!-- Placeholder for line numbers while content is not highlighted --> </div> <gl-intersection-observer class="gl-w-full" @appear="handleChunkAppear"> <pre class="gl-m-0 gl-p-0! gl-w-full gl-overflow-visible! gl-border-none! code highlight gl-line-height-0" - ><code v-if="shouldHighlight" v-safe-html="highlightedContent" data-testid="content"></code><code v-else v-once class="line gl-white-space-pre-wrap! gl-ml-1" data-testid="content" v-text="rawContent"></code></pre> + ><code v-if="shouldHighlight" v-safe-html="highlightedContent" :style="codeStyling" data-testid="content"></code><code v-else v-once class="line gl-white-space-pre-wrap! gl-ml-1" data-testid="content" v-text="rawContent"></code></pre> </gl-intersection-observer> </div> </template> diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/constants.js b/app/assets/javascripts/vue_shared/components/source_viewer/constants.js index 582093e5739..47b802d9d17 100644 --- a/app/assets/javascripts/vue_shared/components/source_viewer/constants.js +++ b/app/assets/javascripts/vue_shared/components/source_viewer/constants.js @@ -14,6 +14,7 @@ export const ROUGE_TO_HLJS_LANGUAGE_MAP = { clean: 'clean', clojure: 'clojure', cmake: 'cmake', + codeowners: 'codeowners', coffeescript: 'coffeescript', coq: 'coq', cpp: 'cpp', diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/queries/blame_data.query.graphql b/app/assets/javascripts/vue_shared/components/source_viewer/queries/blame_data.query.graphql index a5f3f348cfc..c497224cde3 100644 --- a/app/assets/javascripts/vue_shared/components/source_viewer/queries/blame_data.query.graphql +++ b/app/assets/javascripts/vue_shared/components/source_viewer/queries/blame_data.query.graphql @@ -14,6 +14,7 @@ query getBlameData($fullPath: ID!, $filePath: String!, $fromLine: Int, $toLine: span commit { id + authorName titleHtml message authoredDate 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 dcefa66c403..bc46f11ab2d 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 @@ -5,7 +5,7 @@ import SafeHtml from '~/vue_shared/directives/safe_html'; import Tracking from '~/tracking'; import addBlobLinksTracking from '~/blob/blob_links_tracking'; import LineHighlighter from '~/blob/line_highlighter'; -import { EVENT_ACTION, EVENT_LABEL_VIEWER } from './constants'; +import { EVENT_ACTION, EVENT_LABEL_VIEWER, CODEOWNERS_FILE_NAME } from './constants'; import Chunk from './components/chunk_new.vue'; import Blame from './components/blame_info.vue'; import { calculateBlameOffset, shouldRender, toggleBlameClasses } from './utils'; @@ -21,6 +21,7 @@ export default { components: { Chunk, Blame, + CodeownersValidation: () => import('ee_component/blob/components/codeowners_validation.vue'), }, directives: { SafeHtml, @@ -45,6 +46,10 @@ export default { type: String, required: true, }, + currentRef: { + type: String, + required: true, + }, }, data() { return { @@ -66,6 +71,9 @@ export default { return result; }, []); }, + isCodeownersFile() { + return this.blob.name === CODEOWNERS_FILE_NAME; + }, }, watch: { showBlame: { @@ -136,11 +144,19 @@ export default { <blame v-if="showBlame && blameInfo.length" :blame-info="blameInfo" /> <div - class="file-content code js-syntax-highlight blob-content gl-display-flex gl-flex-direction-column gl-overflow-auto gl-w-full" + class="file-content code js-syntax-highlight blob-content gl-display-flex gl-flex-direction-column gl-overflow-auto gl-w-full blob-viewer" :class="$options.userColorScheme" data-type="simple" :data-path="blob.path" + data-testid="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-for="(chunk, index) in chunks" :key="index" diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/workers/highlight_utils.js b/app/assets/javascripts/vue_shared/components/source_viewer/workers/highlight_utils.js index 8d8e945cd5f..057a1c2d113 100644 --- a/app/assets/javascripts/vue_shared/components/source_viewer/workers/highlight_utils.js +++ b/app/assets/javascripts/vue_shared/components/source_viewer/workers/highlight_utils.js @@ -1,13 +1,35 @@ import hljs from 'highlight.js/lib/core'; -import json from 'highlight.js/lib/languages/json'; +import languageLoader from '~/content_editor/services/highlight_js_language_loader'; import { registerPlugins } from '../plugins/index'; import { LINES_PER_CHUNK, NEWLINE, ROUGE_TO_HLJS_LANGUAGE_MAP } from '../constants'; -const initHighlightJs = (fileType, content, language) => { - // The Highlight Worker is currently scoped to JSON files. - // See the following issue for more: https://gitlab.com/gitlab-org/gitlab/-/issues/415753 - hljs.registerLanguage(language, json); +const loadLanguage = async (language) => { + const languageDefinition = await languageLoader[language](); + hljs.registerLanguage(language, languageDefinition.default); +}; + +const loadSubLanguages = async (languageDefinition) => { + // Some files can contain sub-languages (i.e., Svelte); this ensures that sub-languages are also loaded + if (!languageDefinition?.contains) return; + + // generate list of languages to load + const languages = new Set( + languageDefinition.contains + .filter((component) => Boolean(component.subLanguage)) + .map((component) => component.subLanguage), + ); + + if (languageDefinition.subLanguage) { + languages.add(languageDefinition.subLanguage); + } + + await Promise.all([...languages].map(loadLanguage)); +}; + +const initHighlightJs = async (fileType, content, language) => { registerPlugins(hljs, fileType, content, true); + await loadLanguage(language); + await loadSubLanguages(hljs.getLanguage(language)); }; const splitByLineBreaks = (content = '') => content.split(/\r?\n/); @@ -35,12 +57,12 @@ const splitIntoChunks = (language, rawContent, highlightedContent) => { return result; }; -const highlight = (fileType, rawContent, lang) => { +const highlight = async (fileType, rawContent, lang) => { const language = ROUGE_TO_HLJS_LANGUAGE_MAP[lang.toLowerCase()]; let result; if (language) { - initHighlightJs(fileType, rawContent, language); + await initHighlightJs(fileType, rawContent, language); const highlightedContent = hljs.highlight(rawContent, { language }).value; result = splitIntoChunks(language, rawContent, highlightedContent); } diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/workers/highlight_worker.js b/app/assets/javascripts/vue_shared/components/source_viewer/workers/highlight_worker.js index 535e857d7a9..49afaba3d2f 100644 --- a/app/assets/javascripts/vue_shared/components/source_viewer/workers/highlight_worker.js +++ b/app/assets/javascripts/vue_shared/components/source_viewer/workers/highlight_worker.js @@ -4,7 +4,7 @@ import { highlight } from './highlight_utils'; * A webworker for highlighting large amounts of content with Highlight.js */ // eslint-disable-next-line no-restricted-globals -self.addEventListener('message', ({ data: { fileType, content, language } }) => { +self.addEventListener('message', async ({ data: { fileType, content, language } }) => { // eslint-disable-next-line no-restricted-globals - self.postMessage(highlight(fileType, content, language)); + self.postMessage(await highlight(fileType, content, language)); }); diff --git a/app/assets/javascripts/vue_shared/components/usage_quotas/usage_banner.vue b/app/assets/javascripts/vue_shared/components/usage_quotas/usage_banner.vue index 779a2ab5461..45d49e5339a 100644 --- a/app/assets/javascripts/vue_shared/components/usage_quotas/usage_banner.vue +++ b/app/assets/javascripts/vue_shared/components/usage_quotas/usage_banner.vue @@ -21,9 +21,11 @@ export default { <div class="gl-display-flex gl-flex-direction-column"> <div class="gl-display-flex gl-align-items-center gl-py-3"> <div - class="gl-display-flex gl-xs-flex-direction-column gl-justify-content-space-between gl-align-items-stretch gl-flex-grow-1" + class="gl-display-flex gl-flex-direction-column gl-sm-flex-direction-row gl-justify-content-space-between gl-align-items-stretch gl-flex-grow-1" > - <div class="gl-display-flex gl-flex-direction-column gl-xs-mb-3 gl-min-w-0 gl-flex-grow-1"> + <div + class="gl-display-flex gl-flex-direction-column gl-mb-3 gl-sm-mb-0 gl-min-w-0 gl-flex-grow-1" + > <div v-if=" /* eslint-disable-line @gitlab/vue-prefer-dollar-scopedslots */ $slots[ diff --git a/app/assets/javascripts/vue_shared/components/user_access_role_badge.vue b/app/assets/javascripts/vue_shared/components/user_access_role_badge.vue index e5558c038b3..43e35f2b1f0 100644 --- a/app/assets/javascripts/vue_shared/components/user_access_role_badge.vue +++ b/app/assets/javascripts/vue_shared/components/user_access_role_badge.vue @@ -12,11 +12,18 @@ export default { components: { GlBadge, }, + props: { + size: { + type: String, + required: false, + default: 'md', + }, + }, }; </script> <template> - <gl-badge class="gl-bg-transparent! gl-inset-border-1-gray-100!"> + <gl-badge :size="size" class="gl-bg-transparent! gl-inset-border-1-gray-100!"> <slot></slot> </gl-badge> </template> diff --git a/app/assets/javascripts/vue_shared/components/vuex_module_provider.vue b/app/assets/javascripts/vue_shared/components/vuex_module_provider.vue deleted file mode 100644 index 46496d2e483..00000000000 --- a/app/assets/javascripts/vue_shared/components/vuex_module_provider.vue +++ /dev/null @@ -1,18 +0,0 @@ -<script> -export default { - provide() { - return { - vuexModule: this.vuexModule, - }; - }, - props: { - vuexModule: { - type: String, - required: true, - }, - }, - render() { - return this.$scopedSlots.default?.(); - }, -}; -</script> diff --git a/app/assets/javascripts/vue_shared/components/web_ide/confirm_fork_modal.vue b/app/assets/javascripts/vue_shared/components/web_ide/confirm_fork_modal.vue index b4afb27c497..96b2bd37080 100644 --- a/app/assets/javascripts/vue_shared/components/web_ide/confirm_fork_modal.vue +++ b/app/assets/javascripts/vue_shared/components/web_ide/confirm_fork_modal.vue @@ -82,7 +82,6 @@ export default { attributes: { href: this.forkPath, variant: 'confirm', - 'data-qa-selector': 'fork_project_button', }, }, }; @@ -94,7 +93,6 @@ export default { <template> <gl-modal :visible="visible" - data-qa-selector="confirm_fork_modal" :modal-id="modalId" :title="$options.i18n.title" :action-primary="btnActions.primary" 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 441b4c31b3a..3514a9c2d5d 100644 --- a/app/assets/javascripts/vue_shared/components/web_ide_link.vue +++ b/app/assets/javascripts/vue_shared/components/web_ide_link.vue @@ -145,6 +145,11 @@ export default { required: false, default: '', }, + cssClasses: { + type: String, + required: false, + default: 'gl-sm-ml-3', + }, }, data() { return { @@ -329,7 +334,7 @@ export default { </script> <template> - <div class="gl-sm-ml-3"> + <div :class="cssClasses"> <gl-disclosure-dropdown v-if="hasActions" :variant="isBlob ? 'confirm' : 'default'" diff --git a/app/assets/javascripts/vue_shared/global_search/constants.js b/app/assets/javascripts/vue_shared/global_search/constants.js index 14ea0389bad..b3840a0adbf 100644 --- a/app/assets/javascripts/vue_shared/global_search/constants.js +++ b/app/assets/javascripts/vue_shared/global_search/constants.js @@ -1,11 +1,10 @@ -import { s__, __, sprintf } from '~/locale'; +import { s__, __ } from '~/locale'; export const AUTOCOMPLETE_ERROR_MESSAGE = s__( 'GlobalSearch|There was an error fetching search autocomplete suggestions.', ); export const ALL_GITLAB = __('All GitLab'); -export const SEARCH_GITLAB = s__('GlobalSearch|Search GitLab'); export const PLACES = s__('GlobalSearch|Places'); export const COMMAND_PALETTE = s__('GlobalSearch|Command palette'); @@ -24,17 +23,9 @@ export const SEARCH_DESCRIBED_BY_UPDATED = s__( ); export const SEARCH_RESULTS_LOADING = s__('GlobalSearch|Search results are loading'); export const SEARCH_RESULTS_SCOPE = s__('GlobalSearch|in %{scope}'); -export const KBD_HELP = sprintf( - s__('GlobalSearch|Use the shortcut key %{kbdOpen}/%{kbdClose} to start a search'), - { kbdOpen: '<kbd>', kbdClose: '</kbd>' }, - false, -); export const MIN_SEARCH_TERM = s__( 'GlobalSearch|The search term must be at least 3 characters long.', ); - -export const SCOPED_SEARCH_ITEM_ARIA_LABEL = s__('GlobalSearch| %{search} %{description} %{scope}'); - export const MSG_ISSUES_ASSIGNED_TO_ME = s__('GlobalSearch|Issues assigned to me'); export const MSG_ISSUES_IVE_CREATED = s__("GlobalSearch|Issues I've created"); @@ -76,8 +67,6 @@ export const SEARCH_RESULTS_ORDER = [ SETTINGS_CATEGORY, HELP_CATEGORY, ]; -export const DROPDOWN_ORDER = SEARCH_RESULTS_ORDER; - export const SEARCH_LABELS = s__('GlobalSearch|Search labels'); export const DROPDOWN_HEADER = s__('GlobalSearch|Labels'); diff --git a/app/assets/javascripts/vue_shared/issuable/create/components/issuable_label_selector.vue b/app/assets/javascripts/vue_shared/issuable/create/components/issuable_label_selector.vue index b4287d86289..1828208bd0f 100644 --- a/app/assets/javascripts/vue_shared/issuable/create/components/issuable_label_selector.vue +++ b/app/assets/javascripts/vue_shared/issuable/create/components/issuable_label_selector.vue @@ -18,6 +18,7 @@ export default { 'initialLabels', 'issuableType', 'labelType', + 'issuableSupportsLockOnMerge', 'variant', 'workspaceType', ], @@ -76,6 +77,7 @@ export default { :issuable-type="issuableType" :label-create-type="labelType" :selected-labels="selectedLabels" + :issuable-supports-lock-on-merge="issuableSupportsLockOnMerge" @updateSelectedLabels="handleUpdateSelectedLabels" @onLabelRemove="handleLabelRemove" > diff --git a/app/assets/javascripts/vue_shared/issuable/list/components/issuable_list_root.vue b/app/assets/javascripts/vue_shared/issuable/list/components/issuable_list_root.vue index 0db7417cebc..ad908a674d3 100644 --- a/app/assets/javascripts/vue_shared/issuable/list/components/issuable_list_root.vue +++ b/app/assets/javascripts/vue_shared/issuable/list/components/issuable_list_root.vue @@ -6,6 +6,7 @@ import PageSizeSelector from '~/vue_shared/components/page_size_selector.vue'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { updateHistory, setUrlParams } from '~/lib/utils/url_utility'; import { __ } from '~/locale'; +import { DRAG_DELAY } from '~/sortable/constants'; import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue'; import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; @@ -24,6 +25,8 @@ export default { forceFallback: true, ghostClass: 'gl-visibility-hidden', tag: 'ul', + delay: DRAG_DELAY, + delayOnTouchOnly: true, }, components: { GlAlert, diff --git a/app/assets/javascripts/vue_shared/issuable/show/components/issuable_body.vue b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_body.vue index dae3ddfe016..bac71c1eda2 100644 --- a/app/assets/javascripts/vue_shared/issuable/show/components/issuable_body.vue +++ b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_body.vue @@ -31,6 +31,10 @@ export default { type: Boolean, required: true, }, + hideEditButton: { + type: Boolean, + required: false, + }, enableAutocomplete: { type: Boolean, required: true, @@ -166,6 +170,7 @@ export default { :issuable="issuable" :status-icon="statusIcon" :enable-edit="enableEdit" + :hide-edit-button="hideEditButton" :workspace-type="workspaceType" @edit-issuable="$emit('edit-issuable', $event)" > @@ -181,12 +186,12 @@ export default { :task-list-update-path="taskListUpdatePath" /> <slot name="secondary-content"></slot> - <small v-if="isUpdated" class="edited-text gl-font-sm!"> + <small v-if="isUpdated" class="edited-text gl-font-sm! gl-text-secondary"> {{ __('Edited') }} <time-ago-tooltip :time="issuable.updatedAt" tooltip-placement="bottom" /> <span v-if="updatedBy"> {{ __('by') }} - <gl-link :href="updatedBy.webUrl" class="author-link gl-font-sm!"> + <gl-link :href="updatedBy.webUrl" class="author-link gl-font-sm! gl-text-secondary"> <span>{{ updatedBy.name }}</span> </gl-link> </span> diff --git a/app/assets/javascripts/vue_shared/issuable/show/components/issuable_edit_form.vue b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_edit_form.vue index 7c3dd5c3623..3353374310f 100644 --- a/app/assets/javascripts/vue_shared/issuable/show/components/issuable_edit_form.vue +++ b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_edit_form.vue @@ -153,7 +153,10 @@ export default { </template> </markdown-field> </gl-form-group> - <div data-testid="actions" class="col-12 gl-mt-3 gl-mb-3 gl-px-0 clearfix"> + <div + data-testid="actions" + class="col-12 gl-mt-3 gl-mb-3 gl-px-0 clearfix gl-display-flex gl-gap-3" + > <slot name="edit-form-actions" :issuable-title="title" diff --git a/app/assets/javascripts/vue_shared/issuable/show/components/issuable_header.vue b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_header.vue index 62a2b44e660..1b95a2abdf9 100644 --- a/app/assets/javascripts/vue_shared/issuable/show/components/issuable_header.vue +++ b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_header.vue @@ -155,8 +155,8 @@ export default { </script> <template> - <div class="detail-page-header gl-flex-direction-column gl-sm-flex-direction-row"> - <div class="detail-page-header-body gl-flex-wrap gl-gap-2"> + <div class="detail-page-header gl-flex-direction-column gl-md-flex-direction-row"> + <div class="detail-page-header-body gl-flex-wrap gl-column-gap-2"> <gl-badge :variant="badgeVariant" data-testid="issue-state-badge"> <gl-icon v-if="statusIcon" :name="statusIcon" :class="statusIconClass" /> <span class="gl-display-none gl-sm-display-block" :class="{ 'gl-ml-2': statusIcon }"> @@ -221,7 +221,7 @@ export default { @click="handleRightSidebarToggleClick" /> </div> - <div class="detail-page-header-actions gl-align-self-center gl-display-flex"> + <div class="detail-page-header-actions gl-align-self-center gl-display-flex gl-gap-3"> <slot name="header-actions"></slot> </div> </div> diff --git a/app/assets/javascripts/vue_shared/issuable/show/components/issuable_show_root.vue b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_show_root.vue index 040f49c7c25..1d44c4a1c14 100644 --- a/app/assets/javascripts/vue_shared/issuable/show/components/issuable_show_root.vue +++ b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_show_root.vue @@ -32,6 +32,11 @@ export default { required: false, default: false, }, + hideEditButton: { + type: Boolean, + required: false, + default: false, + }, enableAutocomplete: { type: Boolean, required: false, @@ -137,6 +142,7 @@ export default { :status-icon="statusIcon" :status-icon-class="statusIconClass" :enable-edit="enableEdit" + :hide-edit-button="hideEditButton" :enable-autocomplete="enableAutocomplete" :enable-autosave="enableAutosave" :enable-zen-mode="enableZenMode" @@ -169,6 +175,9 @@ export default { </issuable-discussion> <issuable-sidebar> + <template #right-sidebar-top-items="{ sidebarExpanded, toggleSidebar }"> + <slot name="right-sidebar-top-items" v-bind="{ sidebarExpanded, toggleSidebar }"></slot> + </template> <template #right-sidebar-items="{ sidebarExpanded, toggleSidebar }"> <slot name="right-sidebar-items" v-bind="{ sidebarExpanded, toggleSidebar }"></slot> </template> diff --git a/app/assets/javascripts/vue_shared/issuable/show/components/issuable_title.vue b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_title.vue index 5387e39e3eb..3dae894b127 100644 --- a/app/assets/javascripts/vue_shared/issuable/show/components/issuable_title.vue +++ b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_title.vue @@ -33,6 +33,10 @@ export default { type: Boolean, required: true, }, + hideEditButton: { + type: Boolean, + required: false, + }, workspaceType: { type: String, required: false, @@ -70,7 +74,7 @@ export default { data-testid="issuable-title" ></h1> <gl-button - v-if="enableEdit" + v-if="enableEdit && !hideEditButton" v-gl-tooltip.bottom :title="$options.i18n.editTitleAndDescription" :aria-label="$options.i18n.editTitleAndDescription" diff --git a/app/assets/javascripts/vue_shared/issuable/sidebar/components/issuable_sidebar_root.vue b/app/assets/javascripts/vue_shared/issuable/sidebar/components/issuable_sidebar_root.vue index 774267639fc..cb9ad6418a4 100644 --- a/app/assets/javascripts/vue_shared/issuable/sidebar/components/issuable_sidebar_root.vue +++ b/app/assets/javascripts/vue_shared/issuable/sidebar/components/issuable_sidebar_root.vue @@ -1,13 +1,17 @@ <script> -import { GlIcon } from '@gitlab/ui'; +import { GlButton, GlTooltipDirective } from '@gitlab/ui'; import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils'; import { getCookie, setCookie, parseBoolean } from '~/lib/utils/common_utils'; +import { __ } from '~/locale'; import { USER_COLLAPSED_GUTTER_COOKIE } from '../constants'; export default { components: { - GlIcon, + GlButton, + }, + directives: { + GlTooltip: GlTooltipDirective, }, data() { const userExpanded = !parseBoolean(getCookie(USER_COLLAPSED_GUTTER_COOKIE)); @@ -20,6 +24,20 @@ export default { isExpanded: userExpanded ? bp.isDesktop() : userExpanded, }; }, + computed: { + toggleLabel() { + return this.isExpanded ? __('Collapse sidebar') : __('Expand sidebar'); + }, + toggleIcon() { + return this.isExpanded ? 'chevron-double-lg-right' : 'chevron-double-lg-left'; + }, + expandedToggleClass() { + return this.isExpanded ? 'block' : ''; + }, + collapsedToggleClass() { + return !this.isExpanded ? 'block' : ''; + }, + }, mounted() { window.addEventListener('resize', this.handleWindowResize); this.updatePageContainerClass(); @@ -59,23 +77,24 @@ export default { class="right-sidebar" aria-live="polite" > - <button - class="toggle-right-sidebar-button js-toggle-right-sidebar-button w-100 gl-text-decoration-none! gl-display-flex gl-outline-0!" - data-testid="toggle-right-sidebar-button" - :title="__('Toggle sidebar')" - @click="toggleSidebar" - > - <span v-if="isExpanded" class="collapse-text gl-flex-grow-1 gl-text-left">{{ - __('Collapse sidebar') - }}</span> - <gl-icon v-show="isExpanded" data-testid="icon-collapse" name="chevron-double-lg-right" /> - <gl-icon - v-show="!isExpanded" - data-testid="icon-expand" - name="chevron-double-lg-left" - class="gl-ml-2" + <div class="right-sidebar-header" :class="expandedToggleClass"> + <gl-button + v-gl-tooltip.hover.left + category="tertiary" + size="small" + class="gl-float-right gutter-toggle toggle-right-sidebar-button js-toggle-right-sidebar-button gl-shadow-none!" + :class="collapsedToggleClass" + data-testid="toggle-right-sidebar-button" + :icon="toggleIcon" + :title="toggleLabel" + :aria-label="toggleLabel" + @click="toggleSidebar" /> - </button> + <slot + name="right-sidebar-top-items" + v-bind="{ sidebarExpanded: isExpanded, toggleSidebar }" + ></slot> + </div> <div data-testid="sidebar-items" class="issuable-sidebar"> <slot name="right-sidebar-items" diff --git a/app/assets/javascripts/vue_shared/mixins/timeago.js b/app/assets/javascripts/vue_shared/mixins/timeago.js index 61e45fa5195..438da925937 100644 --- a/app/assets/javascripts/vue_shared/mixins/timeago.js +++ b/app/assets/javascripts/vue_shared/mixins/timeago.js @@ -1,4 +1,4 @@ -import { formatDate, getTimeago, timeagoLanguageCode } from '~/lib/utils/datetime_utility'; +import { getTimeago, localeDateFormat, timeagoLanguageCode } from '~/lib/utils/datetime_utility'; /** * Mixin with time ago methods used in some vue components @@ -12,7 +12,7 @@ export default { }, tooltipTitle(time) { - return formatDate(time); + return localeDateFormat.asDateTimeFull.format(time); }, }, }; diff --git a/app/assets/javascripts/vue_shared/new_namespace/new_namespace_page.vue b/app/assets/javascripts/vue_shared/new_namespace/new_namespace_page.vue index 3412848a9b7..a5c34b4b619 100644 --- a/app/assets/javascripts/vue_shared/new_namespace/new_namespace_page.vue +++ b/app/assets/javascripts/vue_shared/new_namespace/new_namespace_page.vue @@ -86,11 +86,7 @@ export default { }, showSuperSidebarToggle() { - return gon.use_new_navigation && sidebarState.isCollapsed; - }, - - topBarClasses() { - return gon.use_new_navigation ? 'top-bar-fixed container-fluid' : ''; + return sidebarState.isCollapsed; }, }, @@ -124,7 +120,7 @@ export default { <template> <div> - <div :class="topBarClasses" data-testid="top-bar"> + <div class="top-bar-fixed container-fluid" data-testid="top-bar"> <div class="top-bar-container gl-display-flex gl-align-items-center gl-border-b-1 gl-border-b-gray-100 gl-border-b-solid" > diff --git a/app/assets/javascripts/vue_shared/security_configuration/components/manage_via_mr.vue b/app/assets/javascripts/vue_shared/security_configuration/components/manage_via_mr.vue index c1ec39e1545..dccff4a288f 100644 --- a/app/assets/javascripts/vue_shared/security_configuration/components/manage_via_mr.vue +++ b/app/assets/javascripts/vue_shared/security_configuration/components/manage_via_mr.vue @@ -1,6 +1,6 @@ <script> import { GlButton } from '@gitlab/ui'; -import { featureToMutationMap } from 'ee_else_ce/security_configuration/components/constants'; +import { featureToMutationMap } from 'ee_else_ce/security_configuration/constants'; import { parseErrorMessage } from '~/lib/utils/error_message'; import { redirectTo } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated import { sprintf, s__ } from '~/locale'; @@ -110,7 +110,6 @@ export default { :loading="isLoading" :variant="variant" :category="category" - :data-qa-selector="`${feature.type}_mr_button`" @click="mutate" >{{ $options.i18n.buttonLabel }}</gl-button > |