Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2024-04-23 09:11:56 +00:00
parent ea1aa2dade
commit a288697b47
46 changed files with 933 additions and 497 deletions

View File

@ -164,12 +164,12 @@ include:
.rules:test:smoke-for-omnibus-mr:
rules:
- if: '$CI_PROJECT_NAME == "omnibus-gitlab" && $PIPELINE_TYPE =~ /TRIGGERED_(CE|EE)_PIPELINE/ && $QA_OMNIBUS_MR_TESTS == "only-smoke-reliable"'
- if: '$CI_PROJECT_NAME == "omnibus-gitlab" && $PIPELINE_TYPE =~ /TRIGGERED_(CE|EE)_PIPELINE/ && $QA_OMNIBUS_MR_TESTS == "only-smoke"'
variables:
QA_RSPEC_TAGS: "--tag smoke --tag reliable --tag ~orchestrated --tag ~skip_live_env"
- if: '$CI_PROJECT_NAME == "omnibus-gitlab" && $PIPELINE_TYPE =~ /TRIGGERED_(CE|EE)_PIPELINE/ && $QA_OMNIBUS_MR_TESTS == "except-smoke-reliable"'
QA_RSPEC_TAGS: "--tag smoke --tag ~orchestrated --tag ~skip_live_env"
- if: '$CI_PROJECT_NAME == "omnibus-gitlab" && $PIPELINE_TYPE =~ /TRIGGERED_(CE|EE)_PIPELINE/ && $QA_OMNIBUS_MR_TESTS == "except-smoke"'
variables:
QA_RSPEC_TAGS: "--tag ~smoke --tag ~reliable --tag ~orchestrated --tag ~skip_live_env --tag ~transient"
QA_RSPEC_TAGS: "--tag ~smoke --tag ~orchestrated --tag ~skip_live_env --tag ~transient"
# ------------------------------------------
# Report

View File

@ -13,6 +13,6 @@ variables:
QA_RUN_ALL_TESTS: "true"
# Used by gitlab-qa to set up a volume for `${CI_PROJECT_DIR}/qa/rspec:/home/gitlab/qa/rspec/`
QA_RSPEC_REPORT_PATH: "${CI_PROJECT_DIR}/qa/rspec"
QA_OMNIBUS_MR_TESTS: "only-smoke-reliable"
QA_OMNIBUS_MR_TESTS: "only-smoke"
# Retry failed specs in separate process
QA_RETRY_FAILED_SPECS: "true"

View File

@ -3010,6 +3010,20 @@
- <<: *if-merge-request-labels-run-all-e2e
- <<: *if-merge-request-labels-pipeline-expedite
when: never
- <<: *if-merge-request-targeting-stable-branch
changes: *setup-test-env-patterns
- <<: *if-automated-merge-request
changes: *db-patterns
- <<: *if-automated-merge-request
changes: *backend-patterns
- <<: *if-automated-merge-request
changes: *code-backstage-patterns
- <<: *if-security-merge-request
changes: *backend-patterns
- <<: *if-security-merge-request
changes: *code-backstage-qa-patterns
- <<: *if-security-merge-request
changes: *db-patterns
- <<: *if-merge-request-not-approved
when: never
- <<: *if-merge-request-labels-frontend-and-feature-flag
@ -3029,20 +3043,6 @@
changes: *redis-patterns
- <<: *if-merge-request
changes: *feature-flag-development-config-patterns
- <<: *if-merge-request-targeting-stable-branch
changes: *setup-test-env-patterns
- <<: *if-automated-merge-request
changes: *db-patterns
- <<: *if-automated-merge-request
changes: *backend-patterns
- <<: *if-automated-merge-request
changes: *code-backstage-patterns
- <<: *if-security-merge-request
changes: *backend-patterns
- <<: *if-security-merge-request
changes: *code-backstage-qa-patterns
- <<: *if-security-merge-request
changes: *db-patterns
.as-if-foss:rules:start-as-if-foss:allow-failure:manual:
rules:
@ -3076,6 +3076,34 @@
when: manual
- <<: *if-merge-request-labels-pipeline-expedite
when: never
- <<: *if-merge-request-targeting-stable-branch
changes: *setup-test-env-patterns
allow_failure: true
when: manual
- <<: *if-automated-merge-request
changes: *db-patterns
allow_failure: true
when: manual
- <<: *if-automated-merge-request
changes: *backend-patterns
allow_failure: true
when: manual
- <<: *if-automated-merge-request
changes: *code-backstage-patterns
allow_failure: true
when: manual
- <<: *if-security-merge-request
changes: *backend-patterns
allow_failure: true
when: manual
- <<: *if-security-merge-request
changes: *code-backstage-qa-patterns
allow_failure: true
when: manual
- <<: *if-security-merge-request
changes: *db-patterns
allow_failure: true
when: manual
- <<: *if-merge-request-not-approved
when: never
- <<: *if-merge-request-labels-frontend-and-feature-flag
@ -3113,34 +3141,6 @@
changes: *feature-flag-development-config-patterns
allow_failure: true
when: manual
- <<: *if-merge-request-targeting-stable-branch
changes: *setup-test-env-patterns
allow_failure: true
when: manual
- <<: *if-automated-merge-request
changes: *db-patterns
allow_failure: true
when: manual
- <<: *if-automated-merge-request
changes: *backend-patterns
allow_failure: true
when: manual
- <<: *if-automated-merge-request
changes: *code-backstage-patterns
allow_failure: true
when: manual
- <<: *if-security-merge-request
changes: *backend-patterns
allow_failure: true
when: manual
- <<: *if-security-merge-request
changes: *code-backstage-qa-patterns
allow_failure: true
when: manual
- <<: *if-security-merge-request
changes: *db-patterns
allow_failure: true
when: manual
.as-if-foss:rules:start-as-if-foss:allow-failure:
rules:
@ -3167,6 +3167,27 @@
allow_failure: true
- <<: *if-merge-request-labels-pipeline-expedite
when: never
- <<: *if-merge-request-targeting-stable-branch
changes: *setup-test-env-patterns
allow_failure: true
- <<: *if-automated-merge-request
changes: *db-patterns
allow_failure: true
- <<: *if-automated-merge-request
changes: *backend-patterns
allow_failure: true
- <<: *if-automated-merge-request
changes: *code-backstage-patterns
allow_failure: true
- <<: *if-security-merge-request
changes: *backend-patterns
allow_failure: true
- <<: *if-security-merge-request
changes: *code-backstage-qa-patterns
allow_failure: true
- <<: *if-security-merge-request
changes: *db-patterns
allow_failure: true
- <<: *if-merge-request-not-approved
when: never
- <<: *if-merge-request-labels-frontend-and-feature-flag
@ -3195,27 +3216,6 @@
- <<: *if-merge-request
changes: *feature-flag-development-config-patterns
allow_failure: true
- <<: *if-merge-request-targeting-stable-branch
changes: *setup-test-env-patterns
allow_failure: true
- <<: *if-automated-merge-request
changes: *db-patterns
allow_failure: true
- <<: *if-automated-merge-request
changes: *backend-patterns
allow_failure: true
- <<: *if-automated-merge-request
changes: *code-backstage-patterns
allow_failure: true
- <<: *if-security-merge-request
changes: *backend-patterns
allow_failure: true
- <<: *if-security-merge-request
changes: *code-backstage-qa-patterns
allow_failure: true
- <<: *if-security-merge-request
changes: *db-patterns
allow_failure: true
##################
# as-if-jh rules #

View File

@ -18,7 +18,7 @@ include:
.rules:gdk:qa-parallel:
rules:
# To account for cases where a group label is set which may trigger selective execution
# But we want to execute full reliable suite on gdk in case of code-pattern-changes
# But we want to execute full blocking suite on gdk in case of code-pattern-changes
- <<: *code-pattern-changes
variables:
QA_TESTS: ""

View File

@ -96,7 +96,7 @@ export default {
return getTimeago().format(this.latestVersion?.createdAt);
},
resourceId() {
return cleanLeadingSeparator(this.resource.webPath);
return this.resource?.fullPath;
},
starCount() {
return this.resource?.starCount || 0;

View File

@ -1,6 +1,7 @@
fragment CatalogResourceFields on CiCatalogResource {
id
description
fullPath
icon
name
starCount

View File

@ -13,6 +13,11 @@ export default {
GlSprintf,
},
props: {
visible: {
type: Boolean,
default: false,
required: false,
},
issueCount: {
type: Number,
required: true,
@ -98,8 +103,14 @@ Once deleted, it cannot be undone or recovered.`),
});
}
throw error;
})
.finally(() => {
this.onClose();
});
},
onClose() {
this.$emit('deleteModalVisible', false);
},
},
primaryProps: {
text: s__('Milestones|Delete milestone'),
@ -113,11 +124,13 @@ Once deleted, it cannot be undone or recovered.`),
<template>
<gl-modal
:visible="visible"
modal-id="delete-milestone-modal"
:title="title"
:action-primary="$options.primaryProps"
:action-cancel="$options.cancelProps"
@primary="onSubmit"
@hide="onClose"
>
<gl-sprintf :message="text">
<template #milestoneTitle>

View File

@ -0,0 +1,237 @@
<script>
import {
GlButton,
GlIcon,
GlDisclosureDropdownItem,
GlDisclosureDropdownGroup,
GlDisclosureDropdown,
GlTooltipDirective,
} from '@gitlab/ui';
import { __, s__, sprintf } from '~/locale';
import PromoteMilestoneModal from '~/milestones/components/promote_milestone_modal.vue';
import DeleteMilestoneModal from '~/milestones/components/delete_milestone_modal.vue';
export default {
components: {
GlButton,
GlIcon,
GlDisclosureDropdownItem,
GlDisclosureDropdownGroup,
GlDisclosureDropdown,
PromoteMilestoneModal,
DeleteMilestoneModal,
},
directives: {
GlTooltip: GlTooltipDirective,
},
inject: [
'id',
'title',
'isActive',
'showDelete',
'isDetailPage',
'canReadMilestone',
'milestoneUrl',
'editUrl',
'closeUrl',
'reopenUrl',
'promoteUrl',
'groupName',
'issueCount',
'mergeRequestCount',
],
data() {
return {
isDropdownVisible: false,
isPromotionModalVisible: false,
isDeleteModalVisible: false,
isPromoteModalVisible: false,
};
},
computed: {
hasUrl() {
return this.editUrl || this.closeUrl || this.reopenUrl || this.promoteUrl;
},
copiedToClipboard() {
return this.$options.i18n.copiedToClipboard;
},
editItem() {
return {
text: this.$options.i18n.edit,
href: this.editUrl,
extraAttrs: {
'data-testid': 'milestone-edit-item',
},
};
},
promoteItem() {
return {
text: this.$options.i18n.promote,
extraAttrs: {
'data-testid': 'milestone-promote-item',
},
};
},
closeItem() {
return {
text: this.$options.i18n.close,
href: this.closeUrl,
extraAttrs: {
class: { 'gl-sm-display-none!': this.isDetailPage },
'data-testid': 'milestone-close-item',
'data-method': 'put',
rel: 'nofollow',
},
};
},
reopenItem() {
return {
text: this.$options.i18n.reopen,
href: this.reopenUrl,
extraAttrs: {
class: { 'gl-sm-display-none!': this.isDetailPage },
'data-testid': 'milestone-reopen-item',
'data-method': 'put',
rel: 'nofollow',
},
};
},
deleteItem() {
return {
text: this.$options.i18n.delete,
extraAttrs: {
class: 'gl-text-red-500!',
'data-testid': 'milestone-delete-item',
},
};
},
copyIdItem() {
return {
text: sprintf(this.$options.i18n.copyTitle, { id: this.id }),
action: () => {
this.$toast.show(this.copiedToClipboard);
},
extraAttrs: {
'data-testid': 'copy-milestone-id',
itemprop: 'identifier',
},
};
},
showDropdownTooltip() {
return !this.isDropdownVisible ? this.$options.i18n.actionsLabel : '';
},
showTestIdIfNotDetailPage() {
return !this.isDetailPage ? 'milestone-more-actions-dropdown-toggle' : false;
},
},
methods: {
showDropdown() {
this.isDropdownVisible = true;
},
hideDropdown() {
this.isDropdownVisible = false;
},
setDeleteModalVisibility(visibility = false) {
this.isDeleteModalVisible = visibility;
},
setPromoteModalVisibility(visibility = false) {
this.isPromoteModalVisible = visibility;
},
},
primaryAction: {
text: s__('Milestones|Promote Milestone'),
attributes: { variant: 'confirm' },
},
cancelAction: {
text: __('Cancel'),
attributes: {},
},
i18n: {
actionsLabel: s__('Milestone|Milestone actions'),
close: __('Close'),
delete: __('Delete'),
edit: __('Edit'),
promote: __('Promote'),
reopen: __('Reopen'),
copyTitle: s__('Milestone|Copy milestone ID: %{id}'),
copiedToClipboard: s__('Milestone|Milestone ID copied to clipboard.'),
},
};
</script>
<template>
<gl-disclosure-dropdown
v-gl-tooltip="showDropdownTooltip"
category="tertiary"
icon="ellipsis_v"
placement="bottom-end"
block
no-caret
:toggle-text="$options.i18n.actionsLabel"
text-sr-only
class="gl-relative gl-w-full gl-sm-w-auto gl-min-w-7"
:data-testid="showTestIdIfNotDetailPage"
@shown="showDropdown"
@hidden="hideDropdown"
>
<template v-if="isDetailPage" #toggle>
<div class="gl-min-h-7">
<gl-button
class="gl-md-display-none! gl-new-dropdown-toggle gl-absolute gl-top-0 gl-left-0 gl-w-full gl-sm-w-auto"
button-text-classes="gl-w-full"
category="secondary"
:aria-label="$options.i18n.actionsLabel"
:title="$options.i18n.actionsLabel"
>
<span class="gl-new-dropdown-button-text">{{ $options.i18n.actionsLabel }}</span>
<gl-icon class="dropdown-chevron" name="chevron-down" />
</gl-button>
<gl-button
class="gl-display-none gl-md-display-flex! gl-new-dropdown-toggle gl-new-dropdown-icon-only gl-new-dropdown-toggle-no-caret"
category="tertiary"
icon="ellipsis_v"
:aria-label="$options.i18n.actionsLabel"
:title="$options.i18n.actionsLabel"
data-testid="milestone-more-actions-dropdown-toggle"
/>
</div>
</template>
<gl-disclosure-dropdown-item v-if="isActive" :item="closeItem" />
<gl-disclosure-dropdown-item v-else :item="reopenItem" />
<gl-disclosure-dropdown-item v-if="editUrl" :item="editItem" />
<gl-disclosure-dropdown-item
v-if="promoteUrl"
:item="promoteItem"
@action="setPromoteModalVisibility(true)"
/>
<gl-disclosure-dropdown-group v-if="canReadMilestone" bordered class="gl-border-t-gray-200!">
<gl-disclosure-dropdown-item :item="copyIdItem" :data-clipboard-text="id" />
</gl-disclosure-dropdown-group>
<gl-disclosure-dropdown-group v-if="showDelete" bordered class="gl-border-t-gray-200!">
<gl-disclosure-dropdown-item :item="deleteItem" @action="setDeleteModalVisibility(true)" />
</gl-disclosure-dropdown-group>
<promote-milestone-modal
:visible="isPromoteModalVisible"
:milestone-title="title"
:promote-url="promoteUrl"
:group-name="groupName"
@promotionModalVisible="setPromoteModalVisibility"
/>
<delete-milestone-modal
:visible="isDeleteModalVisible"
:issue-count="issueCount"
:merge-request-count="mergeRequestCount"
:milestone-id="id"
:milestone-title="title"
:milestone-url="milestoneUrl"
@deleteModalVisible="setDeleteModalVisibility"
/>
</gl-disclosure-dropdown>
</template>

View File

@ -9,14 +9,24 @@ export default {
components: {
GlModal,
},
data() {
return {
milestoneTitle: '',
url: '',
groupName: '',
currentButton: null,
visible: false,
};
props: {
visible: {
type: Boolean,
default: false,
required: false,
},
milestoneTitle: {
type: String,
required: true,
},
promoteUrl: {
type: String,
required: true,
},
groupName: {
type: String,
required: true,
},
},
computed: {
title() {
@ -32,33 +42,10 @@ export default {
);
},
},
mounted() {
this.getButtons().forEach((button) => {
button.addEventListener('click', this.onPromoteButtonClick);
button.removeAttribute('disabled');
});
},
beforeDestroy() {
this.getButtons().forEach((button) => {
button.removeEventListener('click', this.onPromoteButtonClick);
});
},
methods: {
onPromoteButtonClick({ currentTarget }) {
const { milestoneTitle, url, groupName } = currentTarget.dataset;
currentTarget.setAttribute('disabled', '');
this.visible = true;
this.milestoneTitle = milestoneTitle;
this.url = url;
this.groupName = groupName;
this.currentButton = currentTarget;
},
getButtons() {
return document.querySelectorAll('.js-promote-project-milestone-button');
},
onSubmit() {
return axios
.post(this.url, { params: { format: 'json' } })
.post(this.promoteUrl, { params: { format: 'json' } })
.then((response) => {
visitUrl(response.data.url);
})
@ -68,14 +55,11 @@ export default {
});
})
.finally(() => {
this.visible = false;
this.onClose();
});
},
onClose() {
this.visible = false;
if (this.currentButton) {
this.currentButton.removeAttribute('disabled');
}
this.$emit('promotionModalVisible', false);
},
},
primaryAction: {
@ -92,9 +76,9 @@ export default {
<gl-modal
:visible="visible"
modal-id="promote-milestone-modal"
:title="title"
:action-primary="$options.primaryAction"
:action-cancel="$options.cancelAction"
:title="title"
@primary="onSubmit"
@hide="onClose"
>

View File

@ -1,20 +1,14 @@
import Vue from 'vue';
import initDatePicker from '~/behaviors/date_picker';
import { BV_SHOW_MODAL } from '~/lib/utils/constants';
import Milestone from '~/milestones/milestone';
import { renderGFM } from '~/behaviors/markdown/render_gfm';
import { mountMarkdownEditor } from '~/vue_shared/components/markdown/mount_markdown_editor';
import Sidebar from '~/right_sidebar';
import MountMilestoneSidebar from '~/sidebar/mount_milestone_sidebar';
import Translate from '~/vue_shared/translate';
import ZenMode from '~/zen_mode';
import TaskList from '~/task_list';
import { TYPE_MILESTONE } from '~/issues/constants';
import { createAlert } from '~/alert';
import { __ } from '~/locale';
import DeleteMilestoneModal from './components/delete_milestone_modal.vue';
import PromoteMilestoneModal from './components/promote_milestone_modal.vue';
import eventHub from './event_hub';
// See app/views/shared/milestones/_description.html.haml
export const MILESTONE_DESCRIPTION_ELEMENT = '.milestone-detail .description';
@ -54,88 +48,3 @@ export function initShow() {
},
});
}
export function initPromoteMilestoneModal() {
Vue.use(Translate);
const promoteMilestoneModal = document.getElementById('promote-milestone-modal');
if (!promoteMilestoneModal) {
return null;
}
return new Vue({
el: promoteMilestoneModal,
name: 'PromoteMilestoneModalRoot',
render(createElement) {
return createElement(PromoteMilestoneModal);
},
});
}
export function initDeleteMilestoneModal() {
Vue.use(Translate);
const onRequestFinished = ({ milestoneUrl, successful }) => {
const button = document.querySelector(
`.js-delete-milestone-button[data-milestone-url="${milestoneUrl}"]`,
);
if (!successful) {
button.removeAttribute('disabled');
}
};
const deleteMilestoneButtons = document.querySelectorAll('.js-delete-milestone-button');
const onRequestStarted = (milestoneUrl) => {
const button = document.querySelector(
`.js-delete-milestone-button[data-milestone-url="${milestoneUrl}"]`,
);
button.setAttribute('disabled', '');
eventHub.$once('deleteMilestoneModal.requestFinished', onRequestFinished);
};
return new Vue({
el: '#js-delete-milestone-modal',
name: 'DeleteMilestoneModalRoot',
data() {
return {
modalProps: {
milestoneId: -1,
milestoneTitle: '',
milestoneUrl: '',
issueCount: -1,
mergeRequestCount: -1,
},
};
},
mounted() {
eventHub.$on('deleteMilestoneModal.props', this.setModalProps);
deleteMilestoneButtons.forEach((button) => {
button.removeAttribute('disabled');
button.addEventListener('click', () => {
this.$root.$emit(BV_SHOW_MODAL, 'delete-milestone-modal');
eventHub.$once('deleteMilestoneModal.requestStarted', onRequestStarted);
this.setModalProps({
milestoneId: parseInt(button.dataset.milestoneId, 10),
milestoneTitle: button.dataset.milestoneTitle,
milestoneUrl: button.dataset.milestoneUrl,
issueCount: parseInt(button.dataset.milestoneIssueCount, 10),
mergeRequestCount: parseInt(button.dataset.milestoneMergeRequestCount, 10),
});
});
});
},
methods: {
setModalProps(modalProps) {
this.modalProps = modalProps;
},
},
render(createElement) {
return createElement(DeleteMilestoneModal, {
props: this.modalProps,
});
},
});
}

View File

@ -0,0 +1,52 @@
import Vue from 'vue';
import { parseBoolean } from '~/lib/utils/common_utils';
import MoreActionsDropdown from '~/milestones/components/more_actions_dropdown.vue';
export default function InitMoreActionsDropdown() {
const containers = document.querySelectorAll('.js-vue-milestone-actions');
if (!containers.length) {
return false;
}
return containers.forEach((el) => {
const {
id,
title,
isActive,
showDelete,
isDetailPage,
canReadMilestone,
milestoneUrl,
editUrl,
closeUrl,
reopenUrl,
promoteUrl,
groupName,
issueCount,
mergeRequestCount,
} = el.dataset;
return new Vue({
el,
name: 'MoreActionsDropdownRoot',
provide: {
id: Number(id),
title,
isActive: parseBoolean(isActive),
showDelete: parseBoolean(showDelete),
isDetailPage: parseBoolean(isDetailPage),
canReadMilestone: parseBoolean(canReadMilestone),
milestoneUrl,
editUrl,
closeUrl,
reopenUrl,
promoteUrl,
groupName,
issueCount: Number(issueCount),
mergeRequestCount: Number(mergeRequestCount),
},
render: (createElement) => createElement(MoreActionsDropdown),
});
});
}

View File

@ -139,6 +139,9 @@ export default {
isLoading() {
return this.$apollo.queries.packageFiles.loading || this.mutationLoading;
},
isLastPage() {
return !this.pageInfo.hasPreviousPage && !this.pageInfo.hasNextPage;
},
filesTableHeaderFields() {
return [
{
@ -263,7 +266,7 @@ export default {
},
handleFileDelete(files) {
this.track(REQUEST_DELETE_PACKAGE_FILE_TRACKING_ACTION);
if (files.length === this.packageFiles.length && !this.pageInfo.hasNextPage) {
if (files.length === this.packageFiles.length && this.isLastPage) {
this.$emit(
'delete-all-files',
this.hasOneItem(files)

View File

@ -1,6 +1,7 @@
import { initNewResourceDropdown } from '~/vue_shared/components/new_resource_dropdown/init_new_resource_dropdown';
import { RESOURCE_TYPE_MILESTONE } from '~/vue_shared/components/new_resource_dropdown/constants';
import searchUserGroupsAndProjects from '~/vue_shared/components/new_resource_dropdown/graphql/search_user_groups_and_projects.query.graphql';
import InitMoreActionsDropdown from '~/milestones/init_more_actions_dropdown';
initNewResourceDropdown({
resourceType: RESOURCE_TYPE_MILESTONE,
@ -10,3 +11,4 @@ initNewResourceDropdown({
...(data?.projects?.nodes ?? []),
],
});
InitMoreActionsDropdown();

View File

@ -1,3 +1,3 @@
import { initDeleteMilestoneModal } from '~/milestones';
import InitMoreActionsDropdown from '~/milestones/init_more_actions_dropdown';
initDeleteMilestoneModal();
InitMoreActionsDropdown();

View File

@ -1,4 +1,5 @@
import { initDeleteMilestoneModal, initShow } from '~/milestones';
import { initShow } from '~/milestones';
import InitMoreActionsDropdown from '~/milestones/init_more_actions_dropdown';
initShow();
initDeleteMilestoneModal();
InitMoreActionsDropdown();

View File

@ -1,4 +1,3 @@
import { initDeleteMilestoneModal, initPromoteMilestoneModal } from '~/milestones';
import InitMoreActionsDropdown from '~/milestones/init_more_actions_dropdown';
initDeleteMilestoneModal();
initPromoteMilestoneModal();
InitMoreActionsDropdown();

View File

@ -1,5 +1,5 @@
import { initDeleteMilestoneModal, initPromoteMilestoneModal, initShow } from '~/milestones';
import { initShow } from '~/milestones';
import InitMoreActionsDropdown from '~/milestones/init_more_actions_dropdown';
initShow();
initDeleteMilestoneModal();
initPromoteMilestoneModal();
InitMoreActionsDropdown();

View File

@ -23,9 +23,9 @@ module Types
field :project, ::Types::ProjectType, null: true, description: 'Project of the pipeline schedule.'
field :next_run_at, Types::TimeType, null: false, description: 'Time when the next pipeline will run.'
field :next_run_at, Types::TimeType, null: true, description: 'Time when the next pipeline will run.'
field :real_next_run, Types::TimeType, null: false, description: 'Time when the next pipeline will run.'
field :real_next_run, Types::TimeType, null: true, description: 'Time when the next pipeline will run.'
field :last_pipeline, PipelineType, null: true, description: 'Last pipeline object.'

View File

@ -17,23 +17,7 @@ module Members
validates :new_access_level, presence: true
validates :user, presence: true
validates :member_namespace, presence: true
validate :validate_unique_pending_approval, on: [:create, :update]
scope :pending_member_approvals, ->(member_namespace_id) do
where(member_namespace_id: member_namespace_id).where(status: statuses[:pending])
end
private
def validate_unique_pending_approval
return unless pending?
scope = self.class.where(user_id: user_id, member_namespace_id: member_namespace_id,
new_access_level: new_access_level, status: self.class.statuses[:pending])
scope = scope.where.not(id: id) if persisted?
return unless scope.exists?
errors.add(:base, 'A pending approval for the same user, namespace, and access level already exists.')
end
end
end
Members::MemberApproval.prepend_mod

View File

@ -92,8 +92,6 @@ module Deployments
end
def link_fast_forward_merge_requests(commits)
return if Feature.disabled?(:link_fast_forward_merge_requests_to_deployment, project, type: :gitlab_com_derisk)
deployment.link_merge_requests(merge_requests_by_head_commit_sha(commits))
end

View File

@ -1,6 +0,0 @@
- milestone_url = Gitlab::UrlBuilder.build(milestone, only_path: true)
= render Pajamas::ButtonComponent.new(category: :tertiary, button_options: { class: 'menu-item js-delete-milestone-button', data: { milestone_id: milestone.id, milestone_title: markdown_field(milestone, :title), milestone_url: milestone_url, milestone_issue_count: milestone.total_issues_count, milestone_merge_request_count: milestone.total_merge_requests_count }, disabled: true }) do
.gl-dropdown-item-text-wrapper.gl-text-red-500
= _('Delete')
#js-delete-milestone-modal

View File

@ -1,11 +1,6 @@
.detail-page-description.milestone-detail.gl-py-4
%h2.gl-m-0{ data: { testid: "milestone-title-content" } }
= markdown_field(milestone, :title)
.gl-font-sm.gl-text-secondary.gl-font-base.gl-font-weight-normal.gl-line-height-normal{ itemprop: 'identifier' }
- if can?(current_user, :read_milestone, @milestone)
%span.gl-display-inline-block.gl-vertical-align-middle
= s_('MilestonePage|Milestone ID: %{milestone_id}') % { milestone_id: @milestone.id }
= clipboard_button(title: s_('MilestonePage|Copy milestone ID'), text: @milestone.id)
- if milestone.try(:description).present?
%div{ data: { testid: "milestone-description-content" } }

View File

@ -11,7 +11,10 @@
= render Pajamas::ButtonComponent.new(icon: 'chevron-double-lg-left', button_options: { 'aria-label' => _('Toggle sidebar'), class: 'btn-grouped !gl-float-right gl-sm-display-none js-sidebar-toggle' })
- if can?(current_user, :admin_milestone, @group || @project)
.milestone-buttons.detail-page-header-actions.gl-display-flex.gl-align-self-start
- can_promote = @project && can_admin_group_milestones? && milestone.project
- can_read_milestone = can?(current_user, :read_milestone, @milestone)
.milestone-buttons.detail-page-header-actions.gl-display-flex.gl-align-self-start.gl-gap-3
- if milestone.active?
= render Pajamas::ButtonComponent.new(href: update_milestone_path(milestone, { state_event: :close }), method: :put, button_options: { class: 'btn-close gl-display-none gl-md-display-inline-block' }) do
= _('Close milestone')
@ -19,38 +22,18 @@
= render Pajamas::ButtonComponent.new(href: update_milestone_path(milestone, { state_event: :activate }), method: :put, button_options: { class: 'gl-display-none gl-md-display-inline-block' }) do
= _('Reopen milestone')
.gl-md-ml-3.gl-display-flex.dropdown.gl-dropdown.gl-md-w-auto.gl-w-full
= render Pajamas::ButtonComponent.new(category: :tertiary, icon: 'ellipsis_v', button_options: { class: 'has-tooltip gl-display-none! gl-md-display-inline-flex!', 'aria-label': _('Milestone actions'), data: { toggle: 'dropdown', title: _('Milestone actions'), testid: 'milestone-actions' } })
= render Pajamas::ButtonComponent.new(button_options: { class: 'btn-block gl-md-display-none!', data: { toggle: 'dropdown' } }) do
= _('Milestone actions')
= sprite_icon "chevron-down", size: 16, css_class: "dropdown-icon gl-icon"
.dropdown-menu.dropdown-menu-right
.gl-dropdown-inner
.gl-dropdown-contents
%ul
%li.gl-dropdown-item
= link_to edit_milestone_path(milestone), class: 'menu-item' do
.gl-dropdown-item-text-wrapper
= _('Edit')
- if milestone.project_milestone? && milestone.project.group
%li.gl-dropdown-item
%button.js-promote-project-milestone-button{ data: { milestone_title: milestone.title,
group_name: milestone.project.group.name,
url: promote_project_milestone_path(milestone.project, milestone)},
disabled: true,
type: 'button' }
.gl-dropdown-item-text-wrapper
= _('Promote')
#promote-milestone-modal
- if milestone.active?
%li.gl-dropdown-item{ class: "gl-md-display-none!" }
= link_to update_milestone_path(milestone, { state_event: :close }), method: :put, class: 'menu-item' do
.gl-dropdown-item-text-wrapper
= _('Close milestone')
- else
%li.gl-dropdown-item{ class: "gl-md-display-none!" }
= link_to update_milestone_path(milestone, { state_event: :activate }), method: :put, class: 'menu-item' do
.gl-dropdown-item-text-wrapper
= _('Reopen milestone')
%li.gl-dropdown-item
= render 'shared/milestones/delete_button', milestone: @milestone
.js-vue-milestone-actions{ data: { id: @milestone.id,
title: milestone.title,
is_active: milestone.active?.to_s,
show_delete: 'true',
is_detail_page: 'true',
can_read_milestone: can_read_milestone.to_s,
milestone_url: Gitlab::UrlBuilder.build(milestone, only_path: true),
edit_url: edit_milestone_path(milestone),
close_url: update_milestone_path(milestone, { state_event: :close }),
reopen_url: update_milestone_path(milestone, { state_event: :activate }),
promote_url: can_promote ? promote_project_milestone_path(milestone.project, milestone) : '',
group_name: can_promote && milestone.project_milestone? && milestone.project.group ? milestone.project.group.name : '',
issue_count: @milestone.issues.count,
merge_request_count: @milestone.merge_requests.count
} }

View File

@ -48,39 +48,19 @@
.float-lg-right.light
= format(s_('Milestone|%{percentage}%{percent} complete'), percentage: milestone.percent_complete, percent: '%')
- if can_admin_milestone
- show_delete = @project.present? || @group.present?
.col-1.order-2.order-md-3
.milestone-actions.d-flex.justify-content-sm-start.justify-content-md-end
= render Pajamas::ButtonComponent.new(category: :tertiary,
size: :small,
icon: 'ellipsis_v',
button_options: { class: 'gl-ml-3 has-tooltip', 'aria_label': _('Milestone actions'), title: _('Milestone actions'), data: { toggle: 'dropdown' } })
.dropdown-menu.dropdown-menu-right
.gl-dropdown-inner
.gl-dropdown-contents
%ul
%li.gl-dropdown-item
- if milestone.closed?
= render Pajamas::ButtonComponent.new(category: :tertiary,
href: milestone_path(milestone, milestone: { state_event: :activate }),
method: :put,
variant: :link) do
= s_('Milestones|Reopen')
- else
= render Pajamas::ButtonComponent.new(category: :tertiary,
href: milestone_path(milestone, milestone: { state_event: :close }),
method: :put,
variant: :link) do
= s_('Milestones|Close')
%li.gl-dropdown-item
= render Pajamas::ButtonComponent.new(category: :tertiary,
href: edit_milestone_path(milestone),
variant: :link) do
= _('Edit')
- if can_promote
%li.gl-dropdown-item
= render Pajamas::ButtonComponent.new(category: :tertiary, variant: :link,
button_options: { class: 'js-promote-project-milestone-button', disabled: true, data: { toggle: 'tooltip', container: 'body', url: promote_project_milestone_path(milestone.project, milestone), milestone_title: milestone.title, group_name: @project.group.name } }) do
= s_('Promote')
- if @project || @group
%li.gl-dropdown-item
= render 'shared/milestones/delete_button', milestone: milestone
.gl-display-flex.gl-justify-content-end
.js-vue-milestone-actions{ data: { id: milestone.id,
title: milestone.title,
is_active: milestone.active?.to_s,
show_delete: show_delete.to_s,
milestone_url: Gitlab::UrlBuilder.build(milestone, only_path: true),
edit_url: edit_milestone_path(milestone),
close_url: milestone_path(milestone, milestone: { state_event: :close }),
reopen_url: milestone_path(milestone, milestone: { state_event: :activate }),
promote_url: can_promote ? promote_project_milestone_path(milestone.project, milestone) : '',
group_name: can_promote ? @project.group.name : '',
issue_count: milestone.issues.count,
merge_request_count: milestone.merge_requests.count
} }

View File

@ -1,9 +0,0 @@
---
name: link_fast_forward_merge_requests_to_deployment
feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/384104
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/145211
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/442377
milestone: '16.10'
group: group::environments
type: gitlab_com_derisk
default_enabled: false

View File

@ -0,0 +1,23 @@
# frozen_string_literal: true
class UpdateUniqueIndexOnMemberApprovals < Gitlab::Database::Migration[2.2]
milestone '17.0'
disable_ddl_transaction!
OLD_INDEX_NAME = 'unique_idx_member_approvals_on_pending_status'
NEW_INDEX_NAME = 'unique_index_member_approvals_on_pending_status'
def up
remove_concurrent_index_by_name :member_approvals, OLD_INDEX_NAME
add_concurrent_index :member_approvals, [:user_id, :member_namespace_id, :new_access_level, :member_role_id],
unique: true, where: "status = 0", name: NEW_INDEX_NAME
end
def down
remove_concurrent_index_by_name :member_approvals, NEW_INDEX_NAME
add_concurrent_index :member_approvals, [:user_id, :member_namespace_id, :new_access_level],
unique: true, where: "status = 0", name: OLD_INDEX_NAME
end
end

View File

@ -0,0 +1 @@
e115fde7eaa73b72417f8e6ab24676b9e11d934c6ce696cd99873041ac3185b6

View File

@ -28036,8 +28036,6 @@ CREATE UNIQUE INDEX unique_external_audit_event_destination_namespace_id_and_nam
CREATE UNIQUE INDEX unique_google_cloud_logging_configurations_on_namespace_id ON audit_events_google_cloud_logging_configurations USING btree (namespace_id, google_project_id_name, log_id_name);
CREATE UNIQUE INDEX unique_idx_member_approvals_on_pending_status ON member_approvals USING btree (user_id, member_namespace_id, new_access_level) WHERE (status = 0);
CREATE UNIQUE INDEX unique_idx_namespaces_storage_limit_exclusions_on_namespace_id ON namespaces_storage_limit_exclusions USING btree (namespace_id);
CREATE UNIQUE INDEX unique_import_source_users_source_identifier_and_import_source ON import_source_users USING btree (source_user_identifier, namespace_id, source_hostname, import_type);
@ -28048,6 +28046,8 @@ CREATE UNIQUE INDEX unique_index_for_credit_card_validation_payment_method_xid O
CREATE UNIQUE INDEX unique_index_for_project_pages_unique_domain ON project_settings USING btree (pages_unique_domain) WHERE (pages_unique_domain IS NOT NULL);
CREATE UNIQUE INDEX unique_index_member_approvals_on_pending_status ON member_approvals USING btree (user_id, member_namespace_id, new_access_level, member_role_id) WHERE (status = 0);
CREATE UNIQUE INDEX unique_index_ml_model_metadata_name ON ml_model_metadata USING btree (model_id, name);
CREATE UNIQUE INDEX unique_index_ml_model_version_metadata_name ON ml_model_version_metadata USING btree (model_version_id, name);

View File

@ -26111,10 +26111,10 @@ Represents a pipeline schedule.
| <a id="pipelineschedulefortag"></a>`forTag` | [`Boolean!`](#boolean) | Indicates if a pipelines schedule belongs to a tag. |
| <a id="pipelinescheduleid"></a>`id` | [`ID!`](#id) | ID of the pipeline schedule. |
| <a id="pipelineschedulelastpipeline"></a>`lastPipeline` | [`Pipeline`](#pipeline) | Last pipeline object. |
| <a id="pipelineschedulenextrunat"></a>`nextRunAt` | [`Time!`](#time) | Time when the next pipeline will run. |
| <a id="pipelineschedulenextrunat"></a>`nextRunAt` | [`Time`](#time) | Time when the next pipeline will run. |
| <a id="pipelinescheduleowner"></a>`owner` | [`UserCore`](#usercore) | Owner of the pipeline schedule. |
| <a id="pipelinescheduleproject"></a>`project` | [`Project`](#project) | Project of the pipeline schedule. |
| <a id="pipelineschedulerealnextrun"></a>`realNextRun` | [`Time!`](#time) | Time when the next pipeline will run. |
| <a id="pipelineschedulerealnextrun"></a>`realNextRun` | [`Time`](#time) | Time when the next pipeline will run. |
| <a id="pipelinescheduleref"></a>`ref` | [`String`](#string) | Ref of the pipeline schedule. |
| <a id="pipelineschedulereffordisplay"></a>`refForDisplay` | [`String`](#string) | Git ref for the pipeline schedule. |
| <a id="pipelineschedulerefpath"></a>`refPath` | [`String`](#string) | Path to the ref that triggered the pipeline. |

View File

@ -311,37 +311,27 @@ You can find the play button in the pipelines, environments, deployments, and jo
## Track newly included merge requests per deployment
> - Feature flag `link_fast_forward_merge_requests_to_deployment` [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/384104) in GitLab 16.10. Disabled by default.
GitLab can track newly included merge requests per deployment.
When a deployment succeeds, the system calculates commit-diffs between the latest deployment and the previous deployment.
You can fetch tracking information with the [Deployment API](../../api/deployments.md#list-of-merge-requests-associated-with-a-deployment)
or view it at a post-merge pipeline in [merge request pages](../../user/project/merge_requests/index.md).
To enable tracking:
To enable tracking configure your environment so either:
1. Set your [project's merge method](../../user/project/merge_requests/methods/index.md).
The merge method:
- The [environment name](../yaml/index.md#environmentname) doesn't use folders with `/` (long-lived or top-level environments).
- The [environment tier](#deployment-tier-of-environments) is either `production` or `staging`.
- Must _not_ be **Fast-forward merge**.
- Can be **Fast-forward merge** if the `link_fast_forward_merge_requests_to_deployment` feature flag is enabled.
Here are some example configurations using the [`environment` keyword](../yaml/index.md#environment) in `.gitlab-ci.yml`:
1. Configure your environment so either:
```yaml
# Trackable
environment: production
environment: production/aws
environment: development
- The [environment name](../yaml/index.md#environmentname) doesn't use folders with `/` (long-lived or top-level environments).
- The [environment tier](#deployment-tier-of-environments) is either `production` or `staging`.
Here are some example configurations using the [`environment` keyword](../yaml/index.md#environment) in `.gitlab-ci.yml`:
```yaml
# Trackable
environment: production
environment: production/aws
environment: development
# Non Trackable
environment: review/$CI_COMMIT_REF_SLUG
environment: testing/aws
# Non Trackable
environment: review/$CI_COMMIT_REF_SLUG
environment: testing/aws
```
Configuration changes apply only to new deployments. Existing deployment records do not have merge requests linked or unlinked from them.

View File

@ -207,10 +207,12 @@ the webhooks yourself.
> - Introduced in GitLab 15.2 [with a flag](../../../administration/feature_flags.md) named `webhooks_failed_callout`. Disabled by default.
> - [Generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/365535) in GitLab 15.7. Feature flag `webhooks_failed_callout` removed.
When a webhook is temporarily disabled, a `Webhook fails to connect` error appears at the top
with information on when the webhook is re-enabled automatically.
When a webhook is permanently disabled, a `Webhook failed to connect` error appears at the top
with information on how to re-enable the webhook yourself.
Webhooks can be temporarily or permanently disabled:
- When a webhook is **temporarily disabled**, a `Webhook fails to connect` error appears
with information on when the webhook is re-enabled automatically.
- When a webhook is **permanently disabled**, a `Webhook failed to connect` error appears
with information on how to re-enable the webhook yourself.
To re-enable a temporarily or permanently disabled webhook manually, [send a test request](#test-a-webhook).
If the test request returns a response code in the `2xx` range, the webhook is re-enabled.

View File

@ -32404,9 +32404,6 @@ msgid_plural "Milestones"
msgstr[0] ""
msgstr[1] ""
msgid "Milestone actions"
msgstr ""
msgid "Milestone due date"
msgstr ""
@ -32431,12 +32428,6 @@ msgstr ""
msgid "MilestoneCombobox|Select milestone"
msgstr ""
msgid "MilestonePage|Copy milestone ID"
msgstr ""
msgid "MilestonePage|Milestone ID: %{milestone_id}"
msgstr ""
msgid "MilestoneSidebar|Closed:"
msgstr ""
@ -32503,9 +32494,6 @@ msgstr ""
msgid "Milestones| Youre about to permanently delete the milestone %{milestoneTitle}. This milestone is not currently used in any issues or merge requests."
msgstr ""
msgid "Milestones|Close"
msgstr ""
msgid "Milestones|Completed Issues (closed)"
msgstr ""
@ -32545,9 +32533,6 @@ msgstr ""
msgid "Milestones|Promoting %{milestoneTitle} will make it available for all projects inside %{groupName}. Existing project milestones with the same title will be merged."
msgstr ""
msgid "Milestones|Reopen"
msgstr ""
msgid "Milestones|There are no closed milestones"
msgstr ""
@ -32566,6 +32551,15 @@ msgstr ""
msgid "Milestone|%{percentage}%{percent} complete"
msgstr ""
msgid "Milestone|Copy milestone ID: %{id}"
msgstr ""
msgid "Milestone|Milestone ID copied to clipboard."
msgstr ""
msgid "Milestone|Milestone actions"
msgstr ""
msgid "Min Value"
msgstr ""

View File

@ -600,10 +600,6 @@ module QA
enabled?(ENV['QA_VALIDATE_RESOURCE_REUSE'], default: false)
end
def skip_smoke_reliable?
enabled?(ENV['QA_SKIP_SMOKE_RELIABLE'], default: false)
end
def fips?
enabled?(ENV['FIPS'], default: false)
end

View File

@ -33,7 +33,6 @@ module QA
tags_for_rspec.push(%w[--tag ~geo]) unless QA::Runtime::Env.geo_environment?
tags_for_rspec.push(%w[--tag ~skip_signup_disabled]) if QA::Runtime::Env.signup_disabled?
tags_for_rspec.push(%w[--tag ~smoke --tag ~reliable]) if QA::Runtime::Env.skip_smoke_reliable?
tags_for_rspec.push(%w[--tag ~skip_live_env]) if QA::Specs::Helpers::ContextSelector.dot_com?
QA::Runtime::Env.supported_features.each_key do |key|

View File

@ -70,7 +70,7 @@ RSpec.describe 'Group milestones', feature_category: :team_planning do
end
end
context 'when milestones exists' do
context 'when milestones exists', :js do
let_it_be(:other_project) { create(:project_empty_repo, group: group) }
let_it_be(:active_project_milestone1) do
@ -116,12 +116,42 @@ RSpec.describe 'Group milestones', feature_category: :team_planning do
end
page.within('.detail-page-header') do
find_by_testid('milestone-more-actions-dropdown-toggle').click
click_link('Edit')
end
expect(page).to have_selector('.milestone-form')
end
it 'shows milestone id' do
page.within(".milestones #milestone_#{active_group_milestone.id}") do
click_link(active_group_milestone.title)
end
page.within('.detail-page-header') do
find_by_testid('milestone-more-actions-dropdown-toggle').click
end
expect(page).to have_selector('[data-testid="copy-milestone-id"]')
expect(page).to have_content("Copy milestone ID: #{active_group_milestone.id}")
end
it 'delete a milestone' do
page.within(".milestones #milestone_#{active_group_milestone.id}") do
click_link(active_group_milestone.title)
end
page.within('.detail-page-header') do
find_by_testid('milestone-more-actions-dropdown-toggle').click
click_button('Delete')
end
click_button('Delete milestone')
expect(page).to have_selector('.milestones')
expect(page).not_to have_selector(".milestones #milestone_#{active_group_milestone.id}")
end
it 'renders milestones' do
expect(page).to have_content('v1.0')
expect(page).to have_content('v1.1')

View File

@ -105,18 +105,18 @@ RSpec.describe 'Milestone', feature_category: :team_planning do
end
end
describe 'Deleting a milestone' do
describe 'Deleting a milestone', :js do
it "the delete milestone button does not show for unauthorized users" do
create(:milestone, project: project, title: 8.7)
sign_out(user)
visit group_milestones_path(group)
expect(page).to have_selector('.js-delete-milestone-button', count: 0)
expect(page).to have_selector('[data-testid="milestone-delete-item"]', count: 0)
end
end
describe 'reopen closed milestones' do
describe 'reopen closed milestones', :js do
before do
create(:milestone, :closed, project: project)
end
@ -125,6 +125,7 @@ RSpec.describe 'Milestone', feature_category: :team_planning do
it 'reopens the milestone' do
visit group_milestones_path(group, { state: 'closed' })
find_by_testid('milestone-more-actions-dropdown-toggle').click
click_link 'Reopen'
expect(page).not_to have_selector('.badge-danger')
@ -132,10 +133,11 @@ RSpec.describe 'Milestone', feature_category: :team_planning do
end
end
describe 'project milestones page' do
describe 'project milestones page', :js do
it 'reopens the milestone' do
visit project_milestones_path(project, { state: 'closed' })
find_by_testid('milestone-more-actions-dropdown-toggle').click
click_link 'Reopen'
expect(page).not_to have_selector('.badge-danger')

View File

@ -15,8 +15,10 @@ RSpec.describe 'User promotes milestone', feature_category: :team_planning do
visit(project_milestones_path(project))
end
it "shows milestone promote button" do
expect(page).to have_selector('.js-promote-project-milestone-button')
it "shows milestone promote button", :js do
find_by_testid('milestone-more-actions-dropdown-toggle').click
expect(page).to have_selector('[data-testid="milestone-promote-item"]')
end
end
@ -27,8 +29,10 @@ RSpec.describe 'User promotes milestone', feature_category: :team_planning do
visit(project_milestones_path(project))
end
it "does not show milestone promote button" do
expect(page).not_to have_selector('.js-promote-project-milestone-button')
it "does not show milestone promote button", :js do
find_by_testid('milestone-more-actions-dropdown-toggle').click
expect(page).not_to have_selector('[data-testid="milestone-promote-item"]')
end
end
end

View File

@ -3,7 +3,6 @@ import VueRouter from 'vue-router';
import { update, cloneDeep } from 'lodash';
import { GlAvatar, GlBadge, GlSprintf, GlTruncate } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { cleanLeadingSeparator } from '~/lib/utils/url_utility';
import { createRouter } from '~/ci/catalog/router/index';
import CiResourcesListItem from '~/ci/catalog/components/list/ci_resources_list_item.vue';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
@ -75,7 +74,7 @@ describe('CiResourcesListItem', () => {
it('renders the resource name and link', () => {
expect(findResourceName().exists()).toBe(true);
expect(findResourceName().attributes().href).toBe(defaultProps.resource.webPath);
expect(findResourceName().attributes().href).toBe(`/${defaultProps.resource.fullPath}`);
});
it('renders the resource version badge', () => {
@ -229,7 +228,7 @@ describe('CiResourcesListItem', () => {
await findResourceName().vm.$emit('click', defaultEvent);
expect(routerPush).toHaveBeenCalledWith({
path: cleanLeadingSeparator(resource.webPath),
path: resource.fullPath,
});
});
});
@ -258,7 +257,7 @@ describe('CiResourcesListItem', () => {
});
it('navigates to the details page', () => {
expect(routerPush).toHaveBeenCalledWith({ path: cleanLeadingSeparator(resource.webPath) });
expect(routerPush).toHaveBeenCalledWith({ path: resource.fullPath });
});
});

View File

@ -102,6 +102,7 @@ export const catalogResponseBody = {
__typename: 'CiCatalogResourceVersionConnection',
},
webPath: '/frontend-fixtures/project-42',
fullPath: 'namespace/frontend-fixtures/project-42',
__typename: 'CiCatalogResource',
},
{
@ -117,6 +118,7 @@ export const catalogResponseBody = {
__typename: 'CiCatalogResourceVersionConnection',
},
webPath: '/frontend-fixtures/project-41',
fullPath: 'namespace/frontend-fixtures/project-41',
__typename: 'CiCatalogResource',
},
{
@ -132,6 +134,7 @@ export const catalogResponseBody = {
__typename: 'CiCatalogResourceVersionConnection',
},
webPath: '/frontend-fixtures/project-42',
fullPath: 'namespace/frontend-fixtures/project-42',
__typename: 'CiCatalogResource',
},
{
@ -147,6 +150,7 @@ export const catalogResponseBody = {
__typename: 'CiCatalogResourceVersionConnection',
},
webPath: '/frontend-fixtures/project-39',
fullPath: 'namespace/frontend-fixtures/project-39',
__typename: 'CiCatalogResource',
},
{
@ -162,6 +166,7 @@ export const catalogResponseBody = {
__typename: 'CiCatalogResourceVersionConnection',
},
webPath: '/frontend-fixtures/project-38',
fullPath: 'namespace/frontend-fixtures/project-38',
__typename: 'CiCatalogResource',
},
{
@ -177,6 +182,7 @@ export const catalogResponseBody = {
__typename: 'CiCatalogResourceVersionConnection',
},
webPath: '/frontend-fixtures/project-37',
fullPath: 'namespace/frontend-fixtures/project-37',
__typename: 'CiCatalogResource',
},
{
@ -192,6 +198,7 @@ export const catalogResponseBody = {
__typename: 'CiCatalogResourceVersionConnection',
},
webPath: '/frontend-fixtures/project-36',
fullPath: 'namespace/frontend-fixtures/project-36',
__typename: 'CiCatalogResource',
},
{
@ -207,6 +214,7 @@ export const catalogResponseBody = {
__typename: 'CiCatalogResourceVersionConnection',
},
webPath: '/frontend-fixtures/project-35',
fullPath: 'namespace/frontend-fixtures/project-35',
__typename: 'CiCatalogResource',
},
{
@ -222,6 +230,7 @@ export const catalogResponseBody = {
__typename: 'CiCatalogResourceVersionConnection',
},
webPath: '/frontend-fixtures/project-34',
fullPath: 'namespace/frontend-fixtures/project-34',
__typename: 'CiCatalogResource',
},
{
@ -237,6 +246,7 @@ export const catalogResponseBody = {
__typename: 'CiCatalogResourceVersionConnection',
},
webPath: '/frontend-fixtures/project-33',
fullPath: 'namespace/frontend-fixtures/project-33',
__typename: 'CiCatalogResource',
},
{
@ -252,6 +262,7 @@ export const catalogResponseBody = {
__typename: 'CiCatalogResourceVersionConnection',
},
webPath: '/frontend-fixtures/project-32',
fullPath: 'namespace/frontend-fixtures/project-32',
__typename: 'CiCatalogResource',
},
{
@ -267,6 +278,7 @@ export const catalogResponseBody = {
__typename: 'CiCatalogResourceVersionConnection',
},
webPath: '/frontend-fixtures/project-31',
fullPath: 'namespace/frontend-fixtures/project-31',
__typename: 'CiCatalogResource',
},
{
@ -282,6 +294,7 @@ export const catalogResponseBody = {
__typename: 'CiCatalogResourceVersionConnection',
},
webPath: '/frontend-fixtures/project-30',
fullPath: 'namespace/frontend-fixtures/project-30',
__typename: 'CiCatalogResource',
},
{
@ -297,6 +310,7 @@ export const catalogResponseBody = {
__typename: 'CiCatalogResourceVersionConnection',
},
webPath: '/frontend-fixtures/project-29',
fullPath: 'namespace/frontend-fixtures/project-29',
__typename: 'CiCatalogResource',
},
{
@ -312,6 +326,7 @@ export const catalogResponseBody = {
__typename: 'CiCatalogResourceVersionConnection',
},
webPath: '/frontend-fixtures/project-28',
fullPath: 'namespace/frontend-fixtures/project-28',
__typename: 'CiCatalogResource',
},
{
@ -327,6 +342,7 @@ export const catalogResponseBody = {
__typename: 'CiCatalogResourceVersionConnection',
},
webPath: '/frontend-fixtures/project-27',
fullPath: 'namespace/frontend-fixtures/project-27',
__typename: 'CiCatalogResource',
},
{
@ -342,6 +358,7 @@ export const catalogResponseBody = {
__typename: 'CiCatalogResourceVersionConnection',
},
webPath: '/frontend-fixtures/project-26',
fullPath: 'namespace/frontend-fixtures/project-26',
__typename: 'CiCatalogResource',
},
{
@ -357,6 +374,7 @@ export const catalogResponseBody = {
__typename: 'CiCatalogResourceVersionConnection',
},
webPath: '/frontend-fixtures/project-25',
fullPath: 'namespace/frontend-fixtures/project-25',
__typename: 'CiCatalogResource',
},
{
@ -372,6 +390,7 @@ export const catalogResponseBody = {
__typename: 'CiCatalogResourceVersionConnection',
},
webPath: '/frontend-fixtures/project-24',
fullPath: 'namespace/frontend-fixtures/project-24',
__typename: 'CiCatalogResource',
},
{
@ -387,6 +406,7 @@ export const catalogResponseBody = {
__typename: 'CiCatalogResourceVersionConnection',
},
webPath: '/frontend-fixtures/project-23',
fullPath: 'namespace/frontend-fixtures/project-23',
__typename: 'CiCatalogResource',
},
],
@ -437,6 +457,7 @@ export const catalogSinglePageResponse = {
],
},
webPath: '/frontend-fixtures/project-45',
fullPath: 'namespace/frontend-fixtures/project-45',
__typename: 'CiCatalogResource',
},
{
@ -451,6 +472,7 @@ export const catalogSinglePageResponse = {
__typename: 'CiCatalogResourceVersionConnection',
},
webPath: '/frontend-fixtures/project-44',
fullPath: 'namespace/frontend-fixtures/project-44',
__typename: 'CiCatalogResource',
},
{
@ -465,6 +487,7 @@ export const catalogSinglePageResponse = {
__typename: 'CiCatalogResourceVersionConnection',
},
webPath: '/frontend-fixtures/project-43',
fullPath: 'namespace/frontend-fixtures/project-43',
__typename: 'CiCatalogResource',
},
],
@ -505,6 +528,7 @@ export const catalogSharedDataMock = {
],
},
webPath: '/path/to/project',
fullPath: 'namespace/path/to/project',
},
},
};
@ -554,6 +578,7 @@ const generateResourcesNodes = (count = 20, startId = 0) => {
],
},
webPath: 'path/to/project',
fullPath: 'namespace/path/to/project',
});
}

View File

@ -1,9 +1,13 @@
import { initAllowRunnerRegistrationTokenToggle } from '~/group_settings/allow_runner_registration_token_toggle';
import { GlToggle } from '@gitlab/ui';
import { createWrapper } from '@vue/test-utils';
import waitForPromises from 'helpers/wait_for_promises';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import { initAllowRunnerRegistrationTokenToggle } from '~/group_settings/allow_runner_registration_token_toggle';
describe('initAllowRunnerRegistrationTokenToggle', () => {
let form;
let wrapper;
let requestSubmitMock;
const setFormFixture = ({
@ -20,14 +24,16 @@ describe('initAllowRunnerRegistrationTokenToggle', () => {
</form>
`);
initAllowRunnerRegistrationTokenToggle();
const toggle = initAllowRunnerRegistrationTokenToggle();
form = document.querySelector('form');
wrapper = createWrapper(toggle);
requestSubmitMock = jest.spyOn(form, 'requestSubmit').mockImplementation(() => {});
};
const findInput = () => form.querySelector('[name="group[allow_runner_registration_token]"]');
const findToggle = () => form.querySelector('[data-testid="toggle-wrapper"] button');
const findToggle = () => wrapper.findComponent(GlToggle);
afterEach(() => {
resetHTMLFixture();
@ -38,7 +44,7 @@ describe('initAllowRunnerRegistrationTokenToggle', () => {
expect(form.textContent).toContain('Toggle Label');
expect(findToggle()).toBeDefined();
expect(findToggle().exists()).toBeDefined();
expect(findInput()).toBeDefined();
});
@ -48,30 +54,38 @@ describe('initAllowRunnerRegistrationTokenToggle', () => {
});
it('shows an "on" toggle', () => {
expect(findToggle().props('value')).toBe(true);
expect(findInput().value).toBe('true');
expect(findToggle().getAttribute('aria-checked')).toBe('true');
});
it('when clicked, toggles the setting', () => {
findToggle().click();
it('when clicked, toggles the setting', async () => {
findToggle().vm.$emit('change', false);
await waitForPromises();
expect(findToggle().props('isLoading')).toBe(true);
expect(findInput().value).toBe('false');
expect(requestSubmitMock).toHaveBeenCalledTimes(1);
});
});
describe('when setting is disabled', () => {
beforeEach(() => {
setFormFixture({ hiddenInputValue: 'false', toggleIsChecked: 'false' });
});
it('shows an "off toggle"', () => {
expect(findToggle().props('value')).toBe(false);
expect(findInput().value).toBe('false');
expect(findToggle().getAttribute('aria-checked')).toBe('false');
});
it('when clicked, toggles the setting', () => {
findToggle().click();
it('when clicked, toggles the setting', async () => {
findToggle().vm.$emit('change', true);
await waitForPromises();
expect(findToggle().props('isLoading')).toBe(true);
expect(findInput().value).toBe('true');
expect(requestSubmitMock).toHaveBeenCalledTimes(1);
});

View File

@ -0,0 +1,293 @@
import { GlDisclosureDropdownItem, GlDisclosureDropdown } from '@gitlab/ui';
import { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import moreActionsDropdown from '~/milestones/components/more_actions_dropdown.vue';
import DeleteMilestoneModal from '~/milestones/components/delete_milestone_modal.vue';
import PromoteMilestoneModal from '~/milestones/components/promote_milestone_modal.vue';
describe('moreActionsDropdown', () => {
let wrapper;
const defaultProvide = {
id: 1,
title: 'Milestone 1',
isActive: true,
showDelete: true,
canReadMilestone: true,
milestoneUrl: '/milestone-url',
editUrl: '/edit-url',
closeUrl: '/close-url',
reopenUrl: '/reopen-url',
promoteUrl: '/promote-url',
groupName: 'test-group',
issueCount: 1,
mergeRequestCount: 2,
isDetailPage: false,
};
const createComponent = ({ provideData = {}, propsData = {} } = {}) => {
wrapper = shallowMountExtended(moreActionsDropdown, {
directives: {
GlTooltip: createMockDirective('gl-tooltip'),
},
provide: {
...defaultProvide,
...provideData,
},
propsData,
stubs: {
GlDisclosureDropdownItem,
DeleteMilestoneModal,
PromoteMilestoneModal,
},
});
};
const findDropdown = () => wrapper.findComponent(GlDisclosureDropdown);
const showDropdown = () => {
findDropdown().vm.$emit('show');
};
const findDropdownTooltip = () => getBinding(findDropdown().element, 'gl-tooltip');
const findEditItem = () => wrapper.findByTestId('milestone-edit-item');
const findPromoteItem = () => wrapper.findByTestId('milestone-promote-item');
const findPromoteMilestoneModal = () => wrapper.findComponent(PromoteMilestoneModal);
const findCloseItem = () => wrapper.findByTestId('milestone-close-item');
const findReopenItem = () => wrapper.findByTestId('milestone-reopen-item');
const findDeleteItem = () => wrapper.findByTestId('milestone-delete-item');
const findMilestoneIdItem = () => wrapper.findByTestId('copy-milestone-id');
const findDeleteMilestoneModal = () => wrapper.findComponent(DeleteMilestoneModal);
describe('dropdown group', () => {
it('renders tooltip', () => {
createComponent();
expect(findDropdownTooltip().value).toBe('Milestone actions');
});
});
describe('edit item', () => {
it('renders with correct value if `editUrl` is set', () => {
const provideData = {
editUrl: '/my-edit-url',
};
createComponent({
provideData,
});
expect(findEditItem().attributes('href')).toBe(provideData.editUrl);
});
it('does not render if `editUrl` is false', () => {
createComponent({
provideData: {
editUrl: '',
},
});
expect(findEditItem().exists()).toBe(false);
});
});
describe('promote item', () => {
const provideData = {
promoteUrl: '/my-promote-url',
groupName: 'promote-group',
title: 'Milestone to promote',
};
it('renders with correct values if `promoteUrl` is set', () => {
createComponent({
provideData,
});
expect(findPromoteItem().exists()).toBe(true);
expect(findPromoteMilestoneModal().props()).toMatchObject({
visible: false,
milestoneTitle: provideData.title,
promoteUrl: provideData.promoteUrl,
groupName: provideData.groupName,
});
});
it('click on promote opens confirm modal with correct props', async () => {
createComponent({
provideData,
});
expect(findPromoteMilestoneModal().props('visible')).toBe(false);
findPromoteItem().trigger('click');
await nextTick();
expect(findPromoteMilestoneModal().props()).toMatchObject({
visible: true,
milestoneTitle: provideData.title,
promoteUrl: provideData.promoteUrl,
groupName: provideData.groupName,
});
});
it('does not render if `promoteUrl` is false', () => {
createComponent({
provideData: {
promoteUrl: '',
},
});
expect(findPromoteItem().exists()).toBe(false);
});
});
describe('close item', () => {
it('renders with correct values if `isActive` is set', () => {
const provideData = {
isActive: true,
closeUrl: '/my-close-url',
};
createComponent({
provideData,
});
expect(findCloseItem().exists()).toBe(true);
expect(findReopenItem().exists()).toBe(false);
expect(findCloseItem().attributes('href')).toBe(provideData.closeUrl);
});
it('does not render if `isActive` is false', () => {
createComponent({
provideData: {
isActive: false,
},
});
expect(findCloseItem().exists()).toBe(false);
expect(findReopenItem().exists()).toBe(true);
});
it('has correct class if `isDetailPage` is true', () => {
createComponent({
provideData: {
isDetailPage: true,
},
});
expect(findCloseItem().attributes('class')).toContain('gl-sm-display-none!');
});
});
describe('reopen item', () => {
it('renders with correct values if `isActive` is set', () => {
const provideData = {
isActive: false,
reopenUrl: '/my-reopen-url',
};
createComponent({
provideData,
});
expect(findReopenItem().exists()).toBe(true);
expect(findCloseItem().exists()).toBe(false);
expect(findReopenItem().attributes('href')).toBe(provideData.reopenUrl);
});
it('does not render if `isActive` is false', () => {
createComponent({
provideData: {
isActive: true,
},
});
expect(findReopenItem().exists()).toBe(false);
expect(findCloseItem().exists()).toBe(true);
});
it('has correct class if `isDetailPage` is true', () => {
createComponent({
provideData: {
isActive: false,
isDetailPage: true,
},
});
expect(findReopenItem().attributes('class')).toContain('gl-sm-display-none!');
});
});
describe('delete item', () => {
const provideData = {
issueCount: 1,
mergeRequestCount: 2,
milestoneId: 1,
milestoneTitle: 'Milestone 1',
milestoneUrl: '/milestone-url',
};
it('renders with correct values', () => {
createComponent();
expect(findDeleteItem().exists()).toBe(true);
expect(findDeleteMilestoneModal().props()).toMatchObject({
visible: false,
issueCount: provideData.issueCount,
mergeRequestCount: provideData.mergeRequestCount,
milestoneId: provideData.milestoneId,
milestoneTitle: provideData.milestoneTitle,
milestoneUrl: provideData.milestoneUrl,
});
});
it('click on delete opens confirm modal with correct props', async () => {
createComponent();
expect(findDeleteMilestoneModal().props('visible')).toBe(false);
findDeleteItem().trigger('click');
await nextTick();
expect(findDeleteMilestoneModal().props()).toMatchObject({
visible: true,
issueCount: provideData.issueCount,
mergeRequestCount: provideData.mergeRequestCount,
milestoneId: provideData.milestoneId,
milestoneTitle: provideData.milestoneTitle,
milestoneUrl: provideData.milestoneUrl,
});
});
});
describe('copy milestone id item', () => {
it('renders copy milestone id with correct id', () => {
createComponent({
provideData: {
id: 22,
},
});
showDropdown();
expect(findMilestoneIdItem().text()).toBe('Copy milestone ID: 22');
});
it('renders if `canReadMilestone` is true', () => {
createComponent({
provideData: {
canReadMilestone: true,
},
});
expect(findMilestoneIdItem().exists()).toBe(true);
});
it('does not render if `canReadMilestone` is false', () => {
createComponent({
provideData: {
canReadMilestone: false,
},
});
expect(findMilestoneIdItem().exists()).toBe(false);
});
});
});

View File

@ -1,6 +1,5 @@
import { GlModal } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { setHTMLFixture } from 'helpers/fixtures';
import { TEST_HOST } from 'helpers/test_constants';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/alert';
@ -16,44 +15,29 @@ describe('Promote milestone modal', () => {
let wrapper;
const milestoneMockData = {
milestoneTitle: 'v1.0',
url: `${TEST_HOST}/dummy/promote/milestones`,
promoteUrl: `${TEST_HOST}/dummy/promote/milestones`,
groupName: 'group',
};
const promoteButton = () => document.querySelector('.js-promote-project-milestone-button');
beforeEach(() => {
setHTMLFixture(`<button
class="js-promote-project-milestone-button"
data-group-name="${milestoneMockData.groupName}"
data-milestone-title="${milestoneMockData.milestoneTitle}"
data-url="${milestoneMockData.url}">
Promote
</button>`);
wrapper = shallowMount(PromoteMilestoneModal);
});
describe('Modal opener button', () => {
it('button gets disabled when the modal opens', () => {
expect(promoteButton().disabled).toBe(false);
promoteButton().click();
expect(promoteButton().disabled).toBe(true);
const createComponent = ({ propsData = {} } = {}) => {
wrapper = shallowMount(PromoteMilestoneModal, {
propsData,
stubs: {
PromoteMilestoneModal,
},
});
it('button gets enabled when the modal closes', () => {
promoteButton().click();
wrapper.findComponent(GlModal).vm.$emit('hide');
expect(promoteButton().disabled).toBe(false);
});
});
};
describe('Modal title and description', () => {
beforeEach(() => {
promoteButton().click();
createComponent({
propsData: {
visible: true,
milestoneTitle: milestoneMockData.milestoneTitle,
promoteUrl: milestoneMockData.promoteUrl,
groupName: milestoneMockData.groupName,
},
});
});
it('contains the proper description', () => {
@ -63,19 +47,28 @@ describe('Promote milestone modal', () => {
});
it('contains the correct title', () => {
expect(wrapper.vm.title).toBe('Promote v1.0 to group milestone?');
expect(wrapper.vm.title).toBe(
`Promote ${milestoneMockData.milestoneTitle} to group milestone?`,
);
});
});
describe('When requesting a milestone promotion', () => {
beforeEach(() => {
promoteButton().click();
createComponent({
propsData: {
visible: true,
milestoneTitle: milestoneMockData.milestoneTitle,
promoteUrl: milestoneMockData.promoteUrl,
groupName: milestoneMockData.groupName,
},
});
});
it('redirects when a milestone is promoted', async () => {
const responseURL = `${TEST_HOST}/dummy/endpoint`;
jest.spyOn(axios, 'post').mockImplementation((url) => {
expect(url).toBe(milestoneMockData.url);
expect(url).toBe(milestoneMockData.promoteUrl);
return Promise.resolve({
data: {
url: responseURL,
@ -93,7 +86,7 @@ describe('Promote milestone modal', () => {
const dummyError = new Error('promoting milestone failed');
dummyError.response = { status: HTTP_STATUS_INTERNAL_SERVER_ERROR };
jest.spyOn(axios, 'post').mockImplementation((url) => {
expect(url).toBe(milestoneMockData.url);
expect(url).toBe(milestoneMockData.promoteUrl);
return Promise.reject(dummyError);
});

View File

@ -397,6 +397,7 @@ describe('Package Files', () => {
packageFilesQuery({
files: [file],
extendPagination: {
hasPreviousPage: false,
hasNextPage: false,
},
}),
@ -422,6 +423,7 @@ describe('Package Files', () => {
resolver: jest.fn().mockResolvedValue(
packageFilesQuery({
extendPagination: {
hasPreviousPage: false,
hasNextPage: false,
},
}),

View File

@ -15,48 +15,5 @@ RSpec.describe Members::MemberApproval, feature_category: :groups_and_projects d
it { is_expected.to validate_presence_of(:new_access_level) }
it { is_expected.to validate_presence_of(:user) }
it { is_expected.to validate_presence_of(:member_namespace) }
context 'when uniqness is enforced' do
let!(:user) { create(:user) }
let!(:group) { create(:group) }
let!(:member_approval) { create(:member_approval, user: user, member_namespace: group) }
context 'with same user, namespace, and access level and pending status' do
let(:message) { 'A pending approval for the same user, namespace, and access level already exists.' }
it 'disallows on create' do
duplicate_approval = build(:member_approval, user: user, member_namespace: group)
expect(duplicate_approval).not_to be_valid
expect(duplicate_approval.errors[:base]).to include(message)
end
it 'disallows on update' do
duplicate_approval = create(:member_approval, user: user, member_namespace: group, status: :approved)
expect(duplicate_approval).to be_valid
duplicate_approval.status = ::Members::MemberApproval.statuses[:pending]
expect(duplicate_approval).not_to be_valid
expect(duplicate_approval.errors[:base]).to include(message)
end
end
it 'allows duplicate member approvals with different statuses' do
member_approval.update!(status: ::Members::MemberApproval.statuses[:approved])
pending_approval = build(:member_approval, user: user, member_namespace: group)
expect(pending_approval).to be_valid
end
it 'allows duplicate member approvals with different access levels' do
different_approval = build(:member_approval,
user: user,
member_namespace: group,
new_access_level: ::Gitlab::Access::MAINTAINER)
expect(different_approval).to be_valid
end
end
end
end

View File

@ -243,18 +243,6 @@ RSpec.describe Deployments::LinkMergeRequestsService, feature_category: :continu
expect(deploy.merge_requests).to match_array([merge_request_1, merge_request_2])
end
context 'when :link_fast_forward_merge_requests_to_deployment FF is disabled' do
before do
stub_feature_flags(link_fast_forward_merge_requests_to_deployment: false)
end
it 'does not link merge requests' do
link_merge_requests_for_range
expect(deploy.merge_requests).to be_empty
end
end
end
end

View File

@ -574,16 +574,14 @@ RSpec.shared_examples 'work items iteration' do
expect(page).to be_axe_clean.within(work_item_iteration_selector)
end
# TODO, add test for automated accessibility after it is fixed in GlCollapsibleListBox
# Invalid ARIA attribute value: aria-owns="listbox-##" when searchable
# it 'passes axe automated accessibility testing in open state' do
# within(work_item_iteration) do
# click_button _('Edit')
# wait_for_requests
it 'passes axe automated accessibility testing in open state' do
within(work_item_iteration_selector) do
click_button _('Edit')
wait_for_requests
# expect(page).to be_axe_clean.within(work_item_iteration)
# end
# end
expect(page).to be_axe_clean.within(work_item_iteration_selector)
end
end
end
context 'when edit is clicked' do