Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
4f27fd0e82
commit
0e48c192ad
|
|
@ -82,7 +82,7 @@ workflow:
|
|||
QA_ALLOW_LOCAL_REQUESTS: "true"
|
||||
QA_SUITE_STATUS_ENV_FILE: $CI_PROJECT_DIR/suite_status.env
|
||||
CNG_FORCE_COLOR: "true"
|
||||
QA_RUN_IN_PARALLEL: "true"
|
||||
QA_RUN_IN_PARALLEL: "false" # TODO: consider re-enabling after https://gitlab.com/groups/gitlab-org/quality/quality-engineering/-/epics/99 is completed
|
||||
QA_PARALLEL_PROCESSES: 4
|
||||
before_script:
|
||||
- echo "SUITE_RAN=true" > "$QA_SUITE_STATUS_ENV_FILE"
|
||||
|
|
|
|||
|
|
@ -1235,3 +1235,10 @@ Gitlab/NoFindInWorkers:
|
|||
Include:
|
||||
- app/workers/**/*
|
||||
- ee/app/workers/**/*
|
||||
|
||||
Tailwind/StringInterpolation:
|
||||
Enabled: true
|
||||
Include:
|
||||
- '{,ee/,jh/}app/helpers/**/*.rb'
|
||||
- '{,ee/,jh/}app/components/**/*.{haml,rb}'
|
||||
- '{,ee/,jh/}app/views/**/*.haml'
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ Layout/ExtraSpacing:
|
|||
- 'ee/app/graphql/mutations/geo/registries/update.rb'
|
||||
- 'ee/app/models/epic_issue.rb'
|
||||
- 'ee/app/models/merge_request/predictions.rb'
|
||||
- 'ee/app/models/search/namespace_index_assignment.rb'
|
||||
- 'ee/app/services/dependencies/export_serializers/group_dependencies_service.rb'
|
||||
- 'ee/app/workers/pull_mirrors/reenable_configuration_worker.rb'
|
||||
- 'ee/lib/ee/gitlab/ci/pipeline/quota/size.rb'
|
||||
|
|
|
|||
|
|
@ -1647,7 +1647,6 @@ Layout/LineLength:
|
|||
- 'ee/spec/models/merge_requests/compliance_violation_spec.rb'
|
||||
- 'ee/spec/models/merge_requests/external_status_check_spec.rb'
|
||||
- 'ee/spec/models/namespace_setting_spec.rb'
|
||||
- 'ee/spec/models/note_spec.rb'
|
||||
- 'ee/spec/models/packages/package_file_spec.rb'
|
||||
- 'ee/spec/models/project_import_data_spec.rb'
|
||||
- 'ee/spec/models/project_import_state_spec.rb'
|
||||
|
|
|
|||
|
|
@ -1345,7 +1345,6 @@ Style/InlineDisableAnnotation:
|
|||
- 'ee/app/models/protected_environments/approval_rules/summarizable.rb'
|
||||
- 'ee/app/models/protected_environments/authorizable.rb'
|
||||
- 'ee/app/models/requirements_management/requirement.rb'
|
||||
- 'ee/app/models/search/namespace_index_assignment.rb'
|
||||
- 'ee/app/models/security/training.rb'
|
||||
- 'ee/app/models/vulnerabilities/finding.rb'
|
||||
- 'ee/app/policies/merge_request_diff_policy.rb'
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ Style/RedundantParentheses:
|
|||
- 'ee/app/controllers/groups/billings_controller.rb'
|
||||
- 'ee/app/helpers/ee/projects_helper.rb'
|
||||
- 'ee/app/models/push_rule.rb'
|
||||
- 'ee/app/models/search/index.rb'
|
||||
- 'ee/app/services/concerns/approval_rules/updater.rb'
|
||||
- 'ee/app/services/merge_trains/refresh_service.rb'
|
||||
- 'ee/lib/ee/gitlab/git_access.rb'
|
||||
|
|
|
|||
|
|
@ -202,7 +202,6 @@ Style/RedundantSelf:
|
|||
- 'ee/lib/ee/gitlab/auth/ldap/sync/proxy.rb'
|
||||
- 'ee/lib/ee/gitlab/auth/ldap/sync/users.rb'
|
||||
- 'ee/lib/ee/gitlab/background_migration/delete_invalid_epic_issues.rb'
|
||||
- 'ee/lib/elastic/instance_proxy_util.rb'
|
||||
- 'ee/lib/elastic/latest/commit_config.rb'
|
||||
- 'ee/lib/elastic/latest/issue_config.rb'
|
||||
- 'ee/lib/elastic/latest/note_config.rb'
|
||||
|
|
|
|||
|
|
@ -439,7 +439,7 @@
|
|||
{"name":"numerizer","version":"0.2.0","platform":"ruby","checksum":"e58076d5ee5370417b7e52d9cb25836d62acd1b8d9a194c308707986c1705d7b"},
|
||||
{"name":"oauth","version":"0.5.6","platform":"ruby","checksum":"4085fe28e0c5e2434135e00a6555294fd2a4ff96a98d1bdecdcd619fc6368dff"},
|
||||
{"name":"oauth2","version":"2.0.9","platform":"ruby","checksum":"b21f9defcf52dc1610e0dfab4c868342173dcd707fd15c777d9f4f04e153f7fb"},
|
||||
{"name":"octokit","version":"9.1.0","platform":"ruby","checksum":"7849a659d2722c629181f48d1d7e567c9539f1a85c9676144dbdbfc6ce288253"},
|
||||
{"name":"octokit","version":"9.2.0","platform":"ruby","checksum":"4fa47ff35ce654127edf2c836ab9269bcc8829f5542dc1e86871f697ce7f4316"},
|
||||
{"name":"ohai","version":"18.1.3","platform":"ruby","checksum":"980cfd6a6597f897e157532ba2168d29afb83a8f5e125f682ec3248c3407df95"},
|
||||
{"name":"oj","version":"3.13.23","platform":"ruby","checksum":"206dfdc4020ad9974705037f269cfba211d61b7662a58c717cce771829ccef51"},
|
||||
{"name":"oj-introspect","version":"0.7.2","platform":"ruby","checksum":"c415a44567ed2870d8e963a69421d9322128e194fab7867e37e54d5a25d5333d"},
|
||||
|
|
|
|||
|
|
@ -1202,7 +1202,7 @@ GEM
|
|||
rack (>= 1.2, < 4)
|
||||
snaky_hash (~> 2.0)
|
||||
version_gem (~> 1.1)
|
||||
octokit (9.1.0)
|
||||
octokit (9.2.0)
|
||||
faraday (>= 1, < 3)
|
||||
sawyer (~> 0.9)
|
||||
ohai (18.1.3)
|
||||
|
|
|
|||
|
|
@ -444,7 +444,7 @@
|
|||
{"name":"numerizer","version":"0.2.0","platform":"ruby","checksum":"e58076d5ee5370417b7e52d9cb25836d62acd1b8d9a194c308707986c1705d7b"},
|
||||
{"name":"oauth","version":"0.5.6","platform":"ruby","checksum":"4085fe28e0c5e2434135e00a6555294fd2a4ff96a98d1bdecdcd619fc6368dff"},
|
||||
{"name":"oauth2","version":"2.0.9","platform":"ruby","checksum":"b21f9defcf52dc1610e0dfab4c868342173dcd707fd15c777d9f4f04e153f7fb"},
|
||||
{"name":"octokit","version":"9.1.0","platform":"ruby","checksum":"7849a659d2722c629181f48d1d7e567c9539f1a85c9676144dbdbfc6ce288253"},
|
||||
{"name":"octokit","version":"9.2.0","platform":"ruby","checksum":"4fa47ff35ce654127edf2c836ab9269bcc8829f5542dc1e86871f697ce7f4316"},
|
||||
{"name":"ohai","version":"18.1.3","platform":"ruby","checksum":"980cfd6a6597f897e157532ba2168d29afb83a8f5e125f682ec3248c3407df95"},
|
||||
{"name":"oj","version":"3.13.23","platform":"ruby","checksum":"206dfdc4020ad9974705037f269cfba211d61b7662a58c717cce771829ccef51"},
|
||||
{"name":"oj-introspect","version":"0.7.2","platform":"ruby","checksum":"c415a44567ed2870d8e963a69421d9322128e194fab7867e37e54d5a25d5333d"},
|
||||
|
|
|
|||
|
|
@ -1217,7 +1217,7 @@ GEM
|
|||
rack (>= 1.2, < 4)
|
||||
snaky_hash (~> 2.0)
|
||||
version_gem (~> 1.1)
|
||||
octokit (9.1.0)
|
||||
octokit (9.2.0)
|
||||
faraday (>= 1, < 3)
|
||||
sawyer (~> 0.9)
|
||||
ohai (18.1.3)
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import isEmpty from 'lodash/isEmpty';
|
|||
import { GlAlert, GlLink, GlSprintf } from '@gitlab/ui';
|
||||
import * as Sentry from '~/sentry/sentry_browser_wrapper';
|
||||
import { __ } from '~/locale';
|
||||
import { sanitize } from '~/lib/dompurify';
|
||||
|
||||
export const VARIANT_SUCCESS = 'success';
|
||||
export const VARIANT_WARNING = 'warning';
|
||||
|
|
@ -76,6 +77,7 @@ export const createAlert = ({
|
|||
error = null,
|
||||
messageLinks = null,
|
||||
dismissible = true,
|
||||
renderMessageHTML = false,
|
||||
}) => {
|
||||
if (captureError && error) Sentry.captureException(error);
|
||||
|
||||
|
|
@ -90,6 +92,19 @@ export const createAlert = ({
|
|||
}
|
||||
|
||||
const createMessageNodes = (h) => {
|
||||
if (renderMessageHTML) {
|
||||
return [
|
||||
h('div', {
|
||||
domProps: {
|
||||
innerHTML: sanitize(message, {
|
||||
ALLOWED_TAGS: ['a'],
|
||||
ALLOWED_ATTR: ['href', 'rel', 'target'],
|
||||
}),
|
||||
},
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
if (isEmpty(messageLinks)) {
|
||||
return message;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { uniqueId } from 'lodash';
|
|||
import { sprintf, __ } from '~/locale';
|
||||
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||
import getRefMixin from '../mixins/get_ref';
|
||||
import DeleteBlobModal from './delete_blob_modal.vue';
|
||||
import CommitChangesModal from './commit_changes_modal.vue';
|
||||
import UploadBlobModal from './upload_blob_modal.vue';
|
||||
|
||||
const REPLACE_BLOB_MODAL_ID = 'modal-replace-blob';
|
||||
|
|
@ -19,7 +19,7 @@ export default {
|
|||
GlButtonGroup,
|
||||
GlButton,
|
||||
UploadBlobModal,
|
||||
DeleteBlobModal,
|
||||
CommitChangesModal,
|
||||
LockButton: () => import('ee_component/repository/components/lock_button.vue'),
|
||||
},
|
||||
mixins: [getRefMixin, glFeatureFlagMixin()],
|
||||
|
|
@ -89,7 +89,7 @@ export default {
|
|||
deleteModalId() {
|
||||
return uniqueId('delete-modal');
|
||||
},
|
||||
deleteModalTitle() {
|
||||
deleteModalCommitMessage() {
|
||||
return sprintf(__('Delete %{name}'), { name: this.name });
|
||||
},
|
||||
lockBtnTestId() {
|
||||
|
|
@ -141,12 +141,11 @@ export default {
|
|||
:replace-path="replacePath"
|
||||
:primary-btn-text="$options.i18n.replacePrimaryBtnText"
|
||||
/>
|
||||
<delete-blob-modal
|
||||
<commit-changes-modal
|
||||
:ref="deleteModalId"
|
||||
:modal-id="deleteModalId"
|
||||
:modal-title="deleteModalTitle"
|
||||
:delete-path="deletePath"
|
||||
:commit-message="deleteModalTitle"
|
||||
:commit-message="deleteModalCommitMessage"
|
||||
:target-branch="targetBranch || ref"
|
||||
:original-branch="originalBranch || ref"
|
||||
:can-push-code="canPushCode"
|
||||
|
|
|
|||
|
|
@ -1,47 +1,51 @@
|
|||
<script>
|
||||
import {
|
||||
GlModal,
|
||||
GlFormCheckbox,
|
||||
GlFormGroup,
|
||||
GlFormInput,
|
||||
GlFormTextarea,
|
||||
GlToggle,
|
||||
GlFormRadio,
|
||||
GlFormRadioGroup,
|
||||
GlForm,
|
||||
GlSprintf,
|
||||
GlLink,
|
||||
} from '@gitlab/ui';
|
||||
import csrf from '~/lib/utils/csrf';
|
||||
import { __, s__ } from '~/locale';
|
||||
import validation from '~/vue_shared/directives/validation';
|
||||
import validation, { initFormField } from '~/vue_shared/directives/validation';
|
||||
import { helpPagePath } from '~/helpers/help_page_helper';
|
||||
import {
|
||||
SECONDARY_OPTIONS_TEXT,
|
||||
COMMIT_LABEL,
|
||||
TARGET_BRANCH_LABEL,
|
||||
TOGGLE_CREATE_MR_LABEL,
|
||||
COMMIT_MESSAGE_SUBJECT_MAX_LENGTH,
|
||||
COMMIT_MESSAGE_BODY_MAX_LENGTH,
|
||||
} from '../constants';
|
||||
|
||||
const initFormField = ({ value, required = true, skipValidation = false }) => ({
|
||||
value,
|
||||
required,
|
||||
state: skipValidation ? true : null,
|
||||
feedback: null,
|
||||
});
|
||||
|
||||
export default {
|
||||
csrf,
|
||||
components: {
|
||||
GlModal,
|
||||
GlFormCheckbox,
|
||||
GlFormGroup,
|
||||
GlFormInput,
|
||||
GlFormRadio,
|
||||
GlFormRadioGroup,
|
||||
GlFormTextarea,
|
||||
GlToggle,
|
||||
GlForm,
|
||||
GlSprintf,
|
||||
GlLink,
|
||||
},
|
||||
i18n: {
|
||||
BRANCH: __('Branch'),
|
||||
CURRENT_BRANCH_LABEL: __('Commit to the current %{branchName} branch'),
|
||||
COMMIT_CHANGES: __('Commit changes'),
|
||||
COMMIT_LABEL,
|
||||
COMMIT_MESSAGE_HINT: __(
|
||||
'Try to keep the first line under 52 characters and the others under 72.',
|
||||
),
|
||||
NEW_BRANCH_LABEl: __('Commit to a new branch'),
|
||||
CREATE_MR_LABEL: __('Create a merge request for this change'),
|
||||
LFS_WARNING_TITLE: __("The file you're about to delete is tracked by LFS"),
|
||||
LFS_WARNING_PRIMARY_CONTENT: s__(
|
||||
'BlobViewer|If you delete the file, it will be removed from the branch %{branch}.',
|
||||
|
|
@ -51,14 +55,7 @@ export default {
|
|||
),
|
||||
LFS_CONTINUE_TEXT: __('Continue…'),
|
||||
LFS_CANCEL_TEXT: __('Cancel'),
|
||||
PRIMARY_OPTIONS_TEXT: __('Delete file'),
|
||||
SECONDARY_OPTIONS_TEXT,
|
||||
COMMIT_LABEL,
|
||||
TARGET_BRANCH_LABEL,
|
||||
TOGGLE_CREATE_MR_LABEL,
|
||||
COMMIT_MESSAGE_HINT: __(
|
||||
'Try to keep the first line under 52 characters and the others under 72.',
|
||||
),
|
||||
},
|
||||
directives: {
|
||||
validation: validation(),
|
||||
|
|
@ -68,10 +65,6 @@ export default {
|
|||
type: String,
|
||||
required: true,
|
||||
},
|
||||
modalTitle: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
deletePath: {
|
||||
type: String,
|
||||
required: true,
|
||||
|
|
@ -113,23 +106,29 @@ export default {
|
|||
fields: {
|
||||
// fields key must match case of form name for validation directive to work
|
||||
commit_message: initFormField({ value: this.commitMessage }),
|
||||
branch_name: initFormField({ value: this.targetBranch, skipValidation: !this.canPushCode }),
|
||||
branch_name: initFormField({
|
||||
value: this.canPushToBranch ? this.originalBranch : '',
|
||||
// Branch name is pre-filled with the current branch name in two scenarios and therefore doesn't need validation:
|
||||
// 1. When the user doesn't have permission to push to the repo (e.g., guest user)
|
||||
// 2. When the user can push directly to the current branch
|
||||
skipValidation: !this.canPushCode || this.canPushToBranch,
|
||||
}),
|
||||
},
|
||||
};
|
||||
return {
|
||||
lfsWarningDismissed: false,
|
||||
loading: false,
|
||||
createNewBranch: false,
|
||||
createNewMr: true,
|
||||
error: '',
|
||||
form,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
primaryOptions() {
|
||||
const defaultOptions = {
|
||||
text: this.$options.i18n.PRIMARY_OPTIONS_TEXT,
|
||||
text: this.$options.i18n.COMMIT_CHANGES,
|
||||
attributes: {
|
||||
variant: 'danger',
|
||||
variant: 'confirm',
|
||||
loading: this.loading,
|
||||
disabled: this.loading || !this.form.state,
|
||||
},
|
||||
|
|
@ -150,12 +149,6 @@ export default {
|
|||
},
|
||||
};
|
||||
},
|
||||
showCreateNewMrToggle() {
|
||||
return this.canPushCode && this.form.fields.branch_name.value !== this.originalBranch;
|
||||
},
|
||||
formCompleted() {
|
||||
return this.form.fields.commit_message.value && this.form.fields.branch_name.value;
|
||||
},
|
||||
showHint() {
|
||||
const splitCommitMessageByLineBreak = this.form.fields.commit_message.value
|
||||
.trim()
|
||||
|
|
@ -177,12 +170,29 @@ export default {
|
|||
return this.isUsingLfs && !this.lfsWarningDismissed;
|
||||
},
|
||||
title() {
|
||||
return this.showLfsWarning ? this.$options.i18n.LFS_WARNING_TITLE : this.modalTitle;
|
||||
return this.showLfsWarning
|
||||
? this.$options.i18n.LFS_WARNING_TITLE
|
||||
: this.$options.i18n.COMMIT_CHANGES;
|
||||
},
|
||||
showDeleteForm() {
|
||||
return !this.isUsingLfs || (this.isUsingLfs && this.lfsWarningDismissed);
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
createNewBranch: {
|
||||
handler(newValue) {
|
||||
if (newValue) {
|
||||
this.form.fields.branch_name.value = '';
|
||||
} else {
|
||||
this.form.fields.branch_name = {
|
||||
...this.form.fields.branch_name,
|
||||
value: this.originalBranch,
|
||||
state: true,
|
||||
};
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
show() {
|
||||
this.$refs[this.modalId].show();
|
||||
|
|
@ -259,12 +269,7 @@ export default {
|
|||
</template>
|
||||
<template v-else>
|
||||
<input type="hidden" name="original_branch" :value="originalBranch" />
|
||||
<input
|
||||
v-if="createNewMr || !canPushToBranch"
|
||||
type="hidden"
|
||||
name="create_merge_request"
|
||||
value="1"
|
||||
/>
|
||||
<input v-if="createNewMr" type="hidden" name="create_merge_request" value="1" />
|
||||
<gl-form-group
|
||||
:label="$options.i18n.COMMIT_LABEL"
|
||||
label-for="commit_message"
|
||||
|
|
@ -288,26 +293,66 @@ export default {
|
|||
</gl-form-group>
|
||||
<gl-form-group
|
||||
v-if="canPushCode"
|
||||
:label="$options.i18n.TARGET_BRANCH_LABEL"
|
||||
label-for="branch_name"
|
||||
:invalid-feedback="form.fields['branch_name'].feedback"
|
||||
:label="$options.i18n.BRANCH"
|
||||
label-for="branch_selection"
|
||||
>
|
||||
<gl-form-input
|
||||
id="branch_name"
|
||||
v-model="form.fields['branch_name'].value"
|
||||
v-validation:[form.showValidation]
|
||||
:state="form.fields['branch_name'].state"
|
||||
:disabled="loading"
|
||||
name="branch_name"
|
||||
required
|
||||
/>
|
||||
<template v-if="canPushToBranch">
|
||||
<gl-form-radio-group
|
||||
v-model="createNewBranch"
|
||||
name="branch_selection"
|
||||
:label="$options.i18n.BRANCH"
|
||||
>
|
||||
<gl-form-radio :value="false">
|
||||
<gl-sprintf :message="$options.i18n.CURRENT_BRANCH_LABEL">
|
||||
<template #branchName
|
||||
><code>{{ originalBranch }}</code>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</gl-form-radio>
|
||||
<gl-form-radio :value="true">
|
||||
{{ $options.i18n.NEW_BRANCH_LABEl }}
|
||||
</gl-form-radio>
|
||||
</gl-form-radio-group>
|
||||
<div v-if="createNewBranch" class="gl-ml-6">
|
||||
<gl-form-input
|
||||
v-model="form.fields['branch_name'].value"
|
||||
v-validation:[form.showValidation]
|
||||
:state="form.fields['branch_name'].state"
|
||||
:disabled="loading"
|
||||
name="branch_name"
|
||||
required
|
||||
:placeholder="__('example-branch-name')"
|
||||
class="gl-mt-2"
|
||||
/>
|
||||
<gl-form-checkbox v-if="createNewBranch" v-model="createNewMr" class="gl-mt-4">
|
||||
<span>
|
||||
{{ $options.i18n.CREATE_MR_LABEL }}
|
||||
</span>
|
||||
</gl-form-checkbox>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<span>
|
||||
{{ $options.i18n.NEW_BRANCH_LABEl }}
|
||||
</span>
|
||||
<gl-form-input
|
||||
v-model="form.fields['branch_name'].value"
|
||||
v-validation:[form.showValidation]
|
||||
:state="form.fields['branch_name'].state"
|
||||
:disabled="loading"
|
||||
name="branch_name"
|
||||
required
|
||||
:placeholder="__('example-branch-name')"
|
||||
class="gl-mt-2"
|
||||
/>
|
||||
<gl-form-checkbox v-model="createNewMr" class="gl-mt-4">
|
||||
<span>
|
||||
{{ $options.i18n.CREATE_MR_LABEL }}
|
||||
</span>
|
||||
</gl-form-checkbox>
|
||||
</template>
|
||||
</gl-form-group>
|
||||
<gl-toggle
|
||||
v-if="showCreateNewMrToggle"
|
||||
v-model="createNewMr"
|
||||
:disabled="loading"
|
||||
:label="$options.i18n.TOGGLE_CREATE_MR_LABEL"
|
||||
/>
|
||||
</template>
|
||||
</gl-form>
|
||||
</div>
|
||||
|
|
@ -154,7 +154,7 @@ export default function initValidation(customFeedbackMap = {}) {
|
|||
* @param {*} fieldValues
|
||||
* @returns formObject
|
||||
*/
|
||||
const initFormField = ({ value, required = true, skipValidation = false }) => ({
|
||||
export const initFormField = ({ value, required = true, skipValidation = false }) => ({
|
||||
value,
|
||||
required,
|
||||
state: skipValidation ? true : null,
|
||||
|
|
|
|||
|
|
@ -53,6 +53,9 @@ export default {
|
|||
i18n: {
|
||||
enableConfidentiality: s__('WorkItem|Turn on confidentiality'),
|
||||
disableConfidentiality: s__('WorkItem|Turn off confidentiality'),
|
||||
confidentialParentTooltip: s__(
|
||||
'WorkItem|Child items of a confidential parent must be confidential. Turn off confidentiality on the parent item first.',
|
||||
),
|
||||
notifications: s__('WorkItem|Notifications'),
|
||||
notificationOn: s__('WorkItem|Notifications turned on.'),
|
||||
notificationOff: s__('WorkItem|Notifications turned off.'),
|
||||
|
|
@ -236,10 +239,18 @@ export default {
|
|||
canPromoteToObjective() {
|
||||
return this.canUpdate && this.workItemType === WORK_ITEM_TYPE_VALUE_KEY_RESULT;
|
||||
},
|
||||
confidentialItemText() {
|
||||
return this.isConfidential
|
||||
? this.$options.i18n.disableConfidentiality
|
||||
: this.$options.i18n.enableConfidentiality;
|
||||
confidentialItem() {
|
||||
return {
|
||||
text: this.isConfidential
|
||||
? this.$options.i18n.disableConfidentiality
|
||||
: this.$options.i18n.enableConfidentiality,
|
||||
extraAttrs: {
|
||||
disabled: this.isParentConfidential,
|
||||
},
|
||||
};
|
||||
},
|
||||
confidentialTooltip() {
|
||||
return this.isParentConfidential ? this.$options.i18n.confidentialParentTooltip : '';
|
||||
},
|
||||
lockDiscussionText() {
|
||||
return this.isDiscussionLocked ? __('Unlock discussion') : __('Lock discussion');
|
||||
|
|
@ -474,12 +485,12 @@ export default {
|
|||
</gl-disclosure-dropdown-item>
|
||||
|
||||
<gl-disclosure-dropdown-item
|
||||
v-if="canUpdate && !isParentConfidential"
|
||||
v-if="canUpdate"
|
||||
v-gl-tooltip.left.viewport.d0="confidentialTooltip"
|
||||
:item="confidentialItem"
|
||||
:data-testid="$options.confidentialityTestId"
|
||||
@action="handleToggleWorkItemConfidentiality"
|
||||
>
|
||||
<template #list-item>{{ confidentialItemText }}</template>
|
||||
</gl-disclosure-dropdown-item>
|
||||
/>
|
||||
|
||||
<gl-disclosure-dropdown-item
|
||||
:data-testid="$options.copyReferenceTestId"
|
||||
|
|
|
|||
|
|
@ -169,6 +169,8 @@ export default {
|
|||
'.pika-single',
|
||||
'.atwho-container',
|
||||
'.tippy-content .gl-new-dropdown-panel',
|
||||
'#blocked-by-issues-modal',
|
||||
'#open-children-warning-modal',
|
||||
],
|
||||
};
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -13,10 +13,11 @@ import {
|
|||
LINKED_CATEGORIES_MAP,
|
||||
i18n,
|
||||
} from '../constants';
|
||||
import { findLinkedItemsWidget } from '../utils';
|
||||
import { findHierarchyWidgets, findLinkedItemsWidget } from '../utils';
|
||||
import updateWorkItemMutation from '../graphql/update_work_item.mutation.graphql';
|
||||
import workItemByIidQuery from '../graphql/work_item_by_iid.query.graphql';
|
||||
import workItemLinkedItemsQuery from '../graphql/work_item_linked_items.query.graphql';
|
||||
import workItemOpenChildCountQuery from '../graphql/open_child_count.query.graphql';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
|
@ -63,6 +64,7 @@ export default {
|
|||
return {
|
||||
updateInProgress: false,
|
||||
blockerItems: [],
|
||||
openChildItemsCount: 0,
|
||||
};
|
||||
},
|
||||
apollo: {
|
||||
|
|
@ -101,7 +103,7 @@ export default {
|
|||
update({ workspace }) {
|
||||
if (!workspace?.workItem) return [];
|
||||
|
||||
const linkedWorkItems = findLinkedItemsWidget(workspace.workItem).linkedItems?.nodes || [];
|
||||
const linkedWorkItems = findLinkedItemsWidget(workspace.workItem)?.linkedItems?.nodes || [];
|
||||
|
||||
return linkedWorkItems.filter((item) => {
|
||||
return item.linkType === LINKED_CATEGORIES_MAP.IS_BLOCKED_BY;
|
||||
|
|
@ -113,6 +115,32 @@ export default {
|
|||
Sentry.captureException(new Error(msg));
|
||||
},
|
||||
},
|
||||
openChildItemsCount: {
|
||||
query: workItemOpenChildCountQuery,
|
||||
variables() {
|
||||
return {
|
||||
fullPath: this.fullPath,
|
||||
iid: this.workItemIid,
|
||||
};
|
||||
},
|
||||
skip() {
|
||||
return !this.workItemIid;
|
||||
},
|
||||
update({ namespace }) {
|
||||
if (!namespace?.workItem) return 0;
|
||||
|
||||
/** @type {Array<{countsByState: { opened : number }}> } */
|
||||
const countsByType = findHierarchyWidgets(namespace.workItem.widgets)?.rolledUpCountsByType;
|
||||
|
||||
if (!countsByType) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const total = countsByType.reduce((acc, curr) => acc + curr.countsByState.opened, 0);
|
||||
|
||||
return total;
|
||||
},
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
isWorkItemOpen() {
|
||||
|
|
@ -146,24 +174,46 @@ export default {
|
|||
isBlocked() {
|
||||
return this.blockerItems.length > 0;
|
||||
},
|
||||
hasOpenChildren() {
|
||||
return this.openChildItemsCount > 0;
|
||||
},
|
||||
action() {
|
||||
if (this.isBlocked && this.isWorkItemOpen) {
|
||||
return () => this.$refs.blockedByIssuesModal.show();
|
||||
if (this.isWorkItemOpen) {
|
||||
if (this.isBlocked) {
|
||||
return () => this.$refs.blockedByIssuesModal.show();
|
||||
}
|
||||
if (this.hasOpenChildren) {
|
||||
return () => this.$refs.openChildrenWarningModal.show();
|
||||
}
|
||||
}
|
||||
return this.updateWorkItem;
|
||||
},
|
||||
modalTitle() {
|
||||
blockedByModalTitle() {
|
||||
return sprintfWorkItem(
|
||||
s__('WorkItem|Are you sure you want to close this blocked %{workItemType}?'),
|
||||
this.workItemType,
|
||||
);
|
||||
},
|
||||
modalBody() {
|
||||
blockedByModalBody() {
|
||||
return sprintfWorkItem(
|
||||
s__('WorkItem|This %{workItemType} is currently blocked by the following items:'),
|
||||
this.workItemType,
|
||||
);
|
||||
},
|
||||
openChildrenModalTitle() {
|
||||
return sprintfWorkItem(
|
||||
s__('WorkItem|Are you sure you want to close this %{workItemType}?'),
|
||||
this.workItemType,
|
||||
);
|
||||
},
|
||||
openChildrenModalBody() {
|
||||
return sprintfWorkItem(
|
||||
s__(
|
||||
'WorkItem|This %{workItemType} has open child items. If you close this %{workItemType}, they will remain open.',
|
||||
),
|
||||
this.workItemType,
|
||||
);
|
||||
},
|
||||
modalActionCancel() {
|
||||
return {
|
||||
text: __('Cancel'),
|
||||
|
|
@ -235,17 +285,30 @@ export default {
|
|||
<gl-modal
|
||||
ref="blockedByIssuesModal"
|
||||
modal-id="blocked-by-issues-modal"
|
||||
data-testid="blocked-by-issues-modal"
|
||||
:action-cancel="modalActionCancel"
|
||||
:action-primary="modalActionPrimary"
|
||||
:title="modalTitle"
|
||||
:title="blockedByModalTitle"
|
||||
@primary="updateWorkItem"
|
||||
>
|
||||
<p>{{ modalBody }}</p>
|
||||
<p>{{ blockedByModalBody }}</p>
|
||||
<ul>
|
||||
<li v-for="issue in blockerItems" :key="issue.workItem.iid">
|
||||
<gl-link :href="issue.workItem.webUrl">#{{ issue.workItem.iid }}</gl-link>
|
||||
</li>
|
||||
</ul>
|
||||
</gl-modal>
|
||||
|
||||
<gl-modal
|
||||
ref="openChildrenWarningModal"
|
||||
modal-id="open-children-warning-modal"
|
||||
data-testid="open-children-warning-modal"
|
||||
:action-cancel="modalActionCancel"
|
||||
:action-primary="modalActionPrimary"
|
||||
:title="openChildrenModalTitle"
|
||||
@primary="updateWorkItem"
|
||||
>
|
||||
<p>{{ openChildrenModalBody }}</p>
|
||||
</gl-modal>
|
||||
</span>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,25 @@
|
|||
query openChildItemCount($fullPath: ID!, $iid: String!) {
|
||||
namespace(fullPath: $fullPath) {
|
||||
id
|
||||
workItem(iid: $iid) {
|
||||
id
|
||||
widgets {
|
||||
... on WorkItemWidgetHierarchy {
|
||||
type
|
||||
rolledUpCountsByType {
|
||||
countsByState {
|
||||
opened
|
||||
all
|
||||
closed
|
||||
}
|
||||
workItemType {
|
||||
id
|
||||
name
|
||||
iconName
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -32,6 +32,7 @@ fragment WorkItemHierarchy on WorkItem {
|
|||
}
|
||||
rolledUpCountsByType {
|
||||
countsByState {
|
||||
opened
|
||||
all
|
||||
closed
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ fragment WorkItemWidgets on WorkItemWidget {
|
|||
hasParent
|
||||
rolledUpCountsByType {
|
||||
countsByState {
|
||||
opened
|
||||
all
|
||||
closed
|
||||
}
|
||||
|
|
|
|||
|
|
@ -71,10 +71,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
@mixin btn-white {
|
||||
@include btn-color($white, $gray-200, $gray-50, $gray-200, $gray-100, $gray-300, $gl-text-color);
|
||||
}
|
||||
|
||||
@mixin btn-with-margin {
|
||||
margin-left: $gl-spacing-scale-3;
|
||||
float: left;
|
||||
|
|
@ -86,7 +82,7 @@
|
|||
|
||||
.btn {
|
||||
@include btn-default;
|
||||
@include btn-white;
|
||||
@include btn-color($white, $gray-200, $gray-50, $gray-200, $gray-100, $gray-300, $gl-text-color);
|
||||
|
||||
color: $gl-text-color;
|
||||
white-space: nowrap;
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ module Pajamas
|
|||
end
|
||||
|
||||
def base_class
|
||||
classes = ["gl-alert-#{@variant}"]
|
||||
classes = ["gl-alert-#{@variant}"] # rubocop:disable Tailwind/StringInterpolation -- Not a CSS utility class
|
||||
classes.push('gl-alert-not-dismissible') unless @dismissible
|
||||
classes.push('gl-alert-no-icon') unless @show_icon
|
||||
classes.push('gl-alert-has-title') if @title
|
||||
|
|
@ -48,7 +48,7 @@ module Pajamas
|
|||
end
|
||||
|
||||
def icon_classes
|
||||
"gl-alert-icon#{' gl-alert-icon-no-title' if @title.nil?}"
|
||||
"gl-alert-icon#{' gl-alert-icon-no-title' if @title.nil?}" # rubocop:disable Tailwind/StringInterpolation -- Not a CSS utility class
|
||||
end
|
||||
|
||||
def dismissible_button_options
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ module Pajamas
|
|||
private
|
||||
|
||||
def avatar_classes
|
||||
classes = ["gl-avatar", "gl-avatar-s#{@size}", @class]
|
||||
classes = ["gl-avatar", "gl-avatar-s#{@size}", @class] # rubocop:disable Tailwind/StringInterpolation -- Not a CSS utility class
|
||||
if @item.is_a?(User) || @item.is_a?(AvatarEmail)
|
||||
classes.push("gl-avatar-circle")
|
||||
else
|
||||
|
|
@ -36,7 +36,7 @@ module Pajamas
|
|||
|
||||
unless src
|
||||
classes.push("gl-avatar-identicon")
|
||||
classes.push("gl-avatar-identicon-bg#{((@item.id || 0) % 7) + 1}")
|
||||
classes.push("gl-avatar-identicon-bg#{((@item.id || 0) % 7) + 1}") # rubocop:disable Tailwind/StringInterpolation -- Not a CSS utility class
|
||||
end
|
||||
|
||||
classes.join(' ')
|
||||
|
|
|
|||
|
|
@ -20,7 +20,9 @@ module Pajamas
|
|||
private
|
||||
|
||||
def spinner_class
|
||||
# rubocop:disable Tailwind/StringInterpolation -- Not a CSS utility class
|
||||
["gl-spinner", "gl-spinner-#{@size}", "gl-spinner-#{@color} !gl-align-text-bottom"]
|
||||
# rubocop:enable Tailwind/StringInterpolation
|
||||
end
|
||||
|
||||
def html_options
|
||||
|
|
|
|||
|
|
@ -414,7 +414,7 @@ module ApplicationHelper
|
|||
end
|
||||
|
||||
def client_class_list
|
||||
"gl-browser-#{browser_id} gl-platform-#{platform_id}"
|
||||
"gl-browser-#{browser_id} gl-platform-#{platform_id}" # rubocop:disable Tailwind/StringInterpolation -- Not a CSS utility class
|
||||
end
|
||||
|
||||
def client_js_flags
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ module IconsHelper
|
|||
end
|
||||
|
||||
def external_snippet_icon(name)
|
||||
content_tag(:span, "", class: "gl-snippet-icon gl-snippet-icon-#{name}")
|
||||
content_tag(:span, "", class: "gl-snippet-icon gl-snippet-icon-#{name}") # rubocop:disable Tailwind/StringInterpolation -- Not a CSS utility class
|
||||
end
|
||||
|
||||
def audit_icon(name, css_class: nil)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Search
|
||||
class Cache
|
||||
DEFAULT_EXPIRES_IN = 1.minute
|
||||
|
||||
def self.lookup(...)
|
||||
new(...).lookup { yield }
|
||||
end
|
||||
|
||||
attr_reader :cache_key, :expires_in, :enabled
|
||||
|
||||
def initialize(resource:, action:, expires_in: DEFAULT_EXPIRES_IN, cache_key: nil, enabled: true)
|
||||
@cache_key = cache_key || generate_cache_key(resource, action)
|
||||
@expires_in = expires_in
|
||||
@enabled = enabled
|
||||
end
|
||||
|
||||
def lookup
|
||||
return yield unless enabled
|
||||
|
||||
Rails.cache.fetch(cache_key, expires_in: expires_in) { yield }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def generate_cache_key(resource, action)
|
||||
"search_#{resource.class.name.downcase}_#{resource.id}_#{action}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
---
|
||||
name: prompt_migration_merge_request_reader
|
||||
feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/499593
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/169942
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/modelops/applied-ml/code-suggestions/ai-assist/-/issues/595
|
||||
milestone: '17.6'
|
||||
group: group::custom models
|
||||
type: experiment
|
||||
default_enabled: false
|
||||
|
|
@ -1,8 +1,9 @@
|
|||
---
|
||||
name: search_index_partitioning_notes
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/112402
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/392376
|
||||
milestone: '15.10'
|
||||
type: development
|
||||
name: search_cache_authorizations
|
||||
feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/472011
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/169630
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/499721
|
||||
milestone: '17.6'
|
||||
group: group::global search
|
||||
type: gitlab_com_derisk
|
||||
default_enabled: false
|
||||
|
|
@ -4,6 +4,32 @@
|
|||
breaking_change: true
|
||||
reporter: hsutor
|
||||
body: | # Do not modify this line, instead modify the lines below.
|
||||
Whether your existing project access tokens have expiry dates automatically applied depends on what GitLab offering you have, and when you upgraded to GitLab 16.0 or later:
|
||||
|
||||
- On GitLab.com, during the 16.0 milestone, existing project access tokens without an expiry date were automatically given an expiry date of 365 days later than the current date.
|
||||
- On GitLab self-managed, if you upgraded from GitLab 15.11 or earlier to GitLab 16.0 or later:
|
||||
- On or before July 23, 2024, existing project access tokens without an expiry date were automatically given an expiry date of 365 days later than the current date.
|
||||
This change is a breaking change.
|
||||
- On or after July 24, 2024, existing project access tokens without an expiry date did not have an expiry date set.
|
||||
|
||||
On GitLab self-managed, if you do a new install of one of the following GitLab versions, your existing project access tokens do not have expiry dates automatically applied:
|
||||
|
||||
- 16.0.9
|
||||
- 16.1.7
|
||||
- 16.2.10
|
||||
- 16.3.8
|
||||
- 16.4.6
|
||||
- 16.5.9
|
||||
- 16.6.9
|
||||
- 16.7.9
|
||||
- 16.8.9
|
||||
- 16.9.10
|
||||
- 16.10.9
|
||||
- 16.11.7
|
||||
- 17.0.5
|
||||
- 17.1.3
|
||||
- 17.2.1
|
||||
|
||||
Access tokens that have no expiration date are valid indefinitely, which presents a security risk if the access token
|
||||
is divulged. Because access tokens that have an expiration date are better, from GitLab 15.3 we
|
||||
[populate a default expiration date](https://gitlab.com/gitlab-org/gitlab/-/issues/348660).
|
||||
|
|
|
|||
|
|
@ -9,4 +9,4 @@ description: Represents an Advanced Search index
|
|||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/113612/
|
||||
milestone: '15.11'
|
||||
gitlab_schema: gitlab_main_cell
|
||||
exempt_from_sharding: true # table scheduled for deletion https://gitlab.com/gitlab-org/gitlab/-/issues/473381
|
||||
exempt_from_sharding: true
|
||||
|
|
|
|||
|
|
@ -506,6 +506,7 @@ kanban
|
|||
kanbans
|
||||
kaniko
|
||||
Karma
|
||||
Kata
|
||||
KCachegrind
|
||||
Kerberos
|
||||
Keycloak
|
||||
|
|
@ -1009,6 +1010,7 @@ swimlane
|
|||
swimlanes
|
||||
syncable
|
||||
Sysbench
|
||||
Sysbox
|
||||
syscall
|
||||
syscalls
|
||||
syslog
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ DETAILS:
|
|||
**Offering:** Self-managed
|
||||
|
||||
Job logs are sent by a runner while it's processing a job. You can see
|
||||
logs in job pages, pipelines, email notifications, and so on.
|
||||
logs in places like job pages, pipelines, and email notifications.
|
||||
|
||||
## Data flow
|
||||
|
||||
|
|
@ -147,6 +147,12 @@ job logs are automatically migrated to it along with the other job artifacts.
|
|||
|
||||
See "Phase 3: uploading" in [Data flow](#data-flow) to learn about the process.
|
||||
|
||||
## Maximum log file size
|
||||
|
||||
The job log file size limit in GitLab is 100 megabytes by default.
|
||||
Any job that exceeds the limit is marked as failed, and dropped by the runner.
|
||||
For more details, see [Maximum file size for job logs](../../administration/instance_limits.md#maximum-file-size-for-job-logs).
|
||||
|
||||
## Prevent local disk usage
|
||||
|
||||
If you want to avoid any local disk usage for job logs,
|
||||
|
|
@ -158,11 +164,14 @@ you can do so using one of the following options:
|
|||
|
||||
## How to remove job logs
|
||||
|
||||
There isn't a way to automatically expire old job logs, but it's safe to remove
|
||||
There isn't a way to automatically expire old job logs. However, it's safe to remove
|
||||
them if they're taking up too much space. If you remove the logs manually, the
|
||||
job output in the UI is empty.
|
||||
|
||||
For example, to delete all job logs older than 60 days, run the following
|
||||
For details on how to delete job logs by using GitLab CLI,
|
||||
see [Delete job logs](../../user/storage_management_automation.md#delete-job-logs).
|
||||
|
||||
Alternatively, you can delete job logs with shell commands. For example, to delete all job logs older than 60 days, run the following
|
||||
command from a shell in your GitLab instance.
|
||||
|
||||
NOTE:
|
||||
|
|
|
|||
|
|
@ -13,7 +13,9 @@ a base64-encoded ASCII text file with a `.gitlab-license` extension.
|
|||
The first time you sign in to your GitLab instance, a note with a
|
||||
link to the **Add license** page should be displayed.
|
||||
|
||||
Otherwise, to add your license:
|
||||
Otherwise, add your license in the Admin area.
|
||||
|
||||
## Add license in the Admin area
|
||||
|
||||
1. Sign in to GitLab as an administrator.
|
||||
1. On the left sidebar, at the bottom, select **Admin**.
|
||||
|
|
|
|||
|
|
@ -26387,8 +26387,9 @@ Represents a member role.
|
|||
| <a id="memberroleeditpath"></a>`editPath` **{warning-solid}** | [`String!`](#string) | **Introduced** in GitLab 16.11. **Status**: Experiment. Web UI path to edit the custom role. |
|
||||
| <a id="memberroleenabledpermissions"></a>`enabledPermissions` **{warning-solid}** | [`CustomizablePermissionConnection!`](#customizablepermissionconnection) | **Introduced** in GitLab 16.5. **Status**: Experiment. Array of all permissions enabled for the custom role. |
|
||||
| <a id="memberroleid"></a>`id` | [`MemberRoleID!`](#memberroleid) | ID of the member role. |
|
||||
| <a id="memberrolememberscount"></a>`membersCount` **{warning-solid}** | [`Int!`](#int) | **Introduced** in GitLab 16.7. **Status**: Experiment. Total number of members with the custom role. |
|
||||
| <a id="memberrolememberscount"></a>`membersCount` **{warning-solid}** | [`Int`](#int) | **Introduced** in GitLab 16.7. **Status**: Experiment. Number of times the role has been directly assigned to a group or project member. |
|
||||
| <a id="memberrolename"></a>`name` | [`String!`](#string) | Name of the member role. |
|
||||
| <a id="memberroleuserscount"></a>`usersCount` **{warning-solid}** | [`Int`](#int) | **Introduced** in GitLab 17.5. **Status**: Experiment. Number of users who have been directly assigned the role in at least one group or project. |
|
||||
|
||||
### `MergeAccessLevel`
|
||||
|
||||
|
|
@ -33902,8 +33903,9 @@ Represents a standard role.
|
|||
| ---- | ---- | ----------- |
|
||||
| <a id="standardroleaccesslevel"></a>`accessLevel` | [`Int!`](#int) | Access level as a number. |
|
||||
| <a id="standardroledetailspath"></a>`detailsPath` **{warning-solid}** | [`String!`](#string) | **Introduced** in GitLab 17.4. **Status**: Experiment. URL path to the role details webpage. |
|
||||
| <a id="standardrolememberscount"></a>`membersCount` **{warning-solid}** | [`Int!`](#int) | **Introduced** in GitLab 17.3. **Status**: Experiment. Total number of members with the standard role. |
|
||||
| <a id="standardrolememberscount"></a>`membersCount` **{warning-solid}** | [`Int`](#int) | **Introduced** in GitLab 17.3. **Status**: Experiment. Number of times the role has been directly assigned to a group or project member. |
|
||||
| <a id="standardrolename"></a>`name` | [`String!`](#string) | Access level as a string. |
|
||||
| <a id="standardroleuserscount"></a>`usersCount` **{warning-solid}** | [`Int`](#int) | **Introduced** in GitLab 17.5. **Status**: Experiment. Number of users who have been directly assigned the role in at least one group or project. |
|
||||
|
||||
### `StandardsAdherenceChecksStatus`
|
||||
|
||||
|
|
|
|||
|
|
@ -310,6 +310,8 @@ You can view the contents of a job log in full screen mode by clicking **Show fu
|
|||
|
||||
To use full screen mode, your web browser must also support it. If your web browser does not support full screen mode, then the option is not available.
|
||||
|
||||
Job logs divided into sections can be collapsed or expanded. For more details, see [Expand and collapse job log sections](../yaml/script.md#expand-and-collapse-job-log-sections).
|
||||
|
||||
## Deployment jobs
|
||||
|
||||
Deployment jobs are a specific kind of CI job in that they deploy code to
|
||||
|
|
|
|||
|
|
@ -112,9 +112,9 @@ steps:
|
|||
|
||||
### The result
|
||||
|
||||
1. 
|
||||
1. 
|
||||
1. 
|
||||
1. 
|
||||
1. 
|
||||
1. 
|
||||
|
||||
### The commit step
|
||||
|
||||
|
|
|
|||
|
|
@ -163,18 +163,18 @@ A user is not counted as a billable user if:
|
|||
- They are a [blocked user](../../administration/moderate_users.md#block-a-user).
|
||||
- The account is a GitLab-created service account:
|
||||
- [Ghost User](../../user/profile/account/delete_account.md#associated-records).
|
||||
- Bots such as:
|
||||
- Bots:
|
||||
- [Support Bot](../../user/project/service_desk/configure.md#support-bot-user).
|
||||
- [Bot users for projects](../../user/project/settings/project_access_tokens.md#bot-users-for-projects).
|
||||
- [Bot users for groups](../../user/group/settings/group_access_tokens.md#bot-users-for-groups).
|
||||
|
||||
Seat usage is reviewed [quarterly or annually](../quarterly_reconciliation.md).
|
||||
|
||||
If a user goes to a different top-level group (one they have created themselves, for example)
|
||||
and that group does not have a paid subscription, they would not see any of the paid features.
|
||||
If a user views or selects a different top-level group (one they have created themselves, for example)
|
||||
and that group does not have a paid subscription, the user does not see any of the paid features.
|
||||
|
||||
It is also possible for users to belong to two different top-level groups with different subscriptions.
|
||||
In this case, they would see only the features available to that subscription.
|
||||
A user can belong to two different top-level groups with different subscriptions.
|
||||
In this case, the user sees only the features available to that subscription.
|
||||
|
||||
### Free Guest users
|
||||
|
||||
|
|
@ -185,11 +185,11 @@ DETAILS:
|
|||
In the **Ultimate** tier, users who are assigned the Guest role do not consume a seat.
|
||||
The user must not be assigned any other role, anywhere in the instance or in the namespace for GitLab.com.
|
||||
|
||||
- If your project is private or internal, a user with the Guest role has
|
||||
[a set of permissions](../../user/permissions.md#project-members-permissions).
|
||||
- If your project is public, all users, including those with the Guest role
|
||||
can access your project.
|
||||
- For GitLab.com, if a user with the Guest role creates a project in their personal namespace, it does not consume a seat. The project is under the user's personal namespace and does not relate to the group with the Ultimate subscription.
|
||||
- If your project is:
|
||||
- Private or internal, a user with the Guest role has [a set of permissions](../../user/permissions.md#project-members-permissions).
|
||||
- Public, all users, including those with the Guest role, can access your project.
|
||||
- For GitLab.com, if a user with the Guest role creates a project in their personal namespace, the user does not consume a seat.
|
||||
The project is under the user's personal namespace and does not relate to the group with the Ultimate subscription.
|
||||
|
||||
### Seats owed
|
||||
|
||||
|
|
@ -240,16 +240,18 @@ To view a list of seats being used:
|
|||
|
||||
1. On the left sidebar, select **Search or go to** and find your group.
|
||||
1. Select **Settings > Usage Quotas**.
|
||||
1. On the **Seats** tab, view usage information.
|
||||
1. Select the **Seats** tab.
|
||||
|
||||
For each user, a list shows groups and projects where the user is a direct member.
|
||||
|
||||
- **Group invite** indicates the user is a member of a [group shared with a group](../../user/project/members/sharing_projects_groups.md#invite-a-group-to-a-group).
|
||||
- **Project invite** indicates the user is a member of a [group shared with a project](../../user/project/members/sharing_projects_groups.md#invite-a-group-to-a-project).
|
||||
- **Group invite** indicates the user is a member of a [group invited to a group](../../user/project/members/sharing_projects_groups.md#invite-a-group-to-a-group).
|
||||
- **Project invite** indicates the user is a member of a [group invited to a project](../../user/project/members/sharing_projects_groups.md#invite-a-group-to-a-project).
|
||||
|
||||
The data in seat usage listing, **Seats in use**, and **Seats in subscription** are updated live.
|
||||
The counts for **Max seats used** and **Seats owed** are updated once per day.
|
||||
|
||||
### View billing information
|
||||
|
||||
To view your subscription information and a summary of seat counts:
|
||||
|
||||
1. On the left sidebar, select **Search or go to** and find your group.
|
||||
|
|
@ -262,14 +264,13 @@ To view your subscription information and a summary of seat counts:
|
|||
|
||||
## Search seat usage
|
||||
|
||||
To search billable users:
|
||||
To search seat usage:
|
||||
|
||||
1. On the left sidebar, select **Search or go to** and find your group.
|
||||
1. Select **Settings > Usage Quotas**.
|
||||
1. On the **Seats tab**, enter a string in the search field. A minimum of 3
|
||||
characters are required.
|
||||
1. On the **Seats tab**, enter a string in the search field. A minimum of 3 characters are required.
|
||||
|
||||
The search returns those users whose first name, last name, or username contain the search string.
|
||||
The search returns users whose first name, last name, or username contain the search string.
|
||||
|
||||
For example:
|
||||
|
||||
|
|
@ -318,10 +319,7 @@ To add seats to a subscription:
|
|||
1. Check the **I accept the Privacy Statement and Terms of Service** checkbox.
|
||||
1. Select **Purchase seats**.
|
||||
|
||||
The following is emailed to you:
|
||||
|
||||
- A payment receipt. You can also access this information in the Customers Portal under
|
||||
[**Invoices**](https://customers.gitlab.com/invoices).
|
||||
You receive the payment receipt by email. You can also access the receipt in the Customers Portal under [**Invoices**](https://customers.gitlab.com/invoices).
|
||||
|
||||
## Remove users from subscription
|
||||
|
||||
|
|
@ -335,8 +333,8 @@ To remove a billable user from your GitLab.com subscription:
|
|||
|
||||
If you add a member to a group by using the [share a group with another group](../../user/project/members/sharing_projects_groups.md#invite-a-group-to-a-group) feature, you can't remove the member by using this method. Instead, you can either:
|
||||
|
||||
- Remove the member from the shared group. You must be a group owner to do this.
|
||||
- From the group's membership page, remove access from the entire shared group.
|
||||
- [Remove the member from the shared group](../../user/group/index.md#remove-a-member-from-the-group).
|
||||
- [Remove the invited group](../../user/project/members/sharing_projects_groups.md#remove-an-invited-group).
|
||||
|
||||
To remove a user from your self-managed instance, see [Deactivate a user](../../administration/moderate_users.md#deactivate-a-user).
|
||||
|
||||
|
|
@ -413,7 +411,7 @@ You can [disable automatic subscription renewal](../self_managed/index.md#enable
|
|||
|
||||
1. Sign in to GitLab.com.
|
||||
1. From either your personal homepage or the group's page, go to **Settings > Usage Quotas**.
|
||||
1. Select **Storage** tab.
|
||||
1. Select the **Storage** tab.
|
||||
1. For each read-only project, total by how much its **Usage** exceeds the free quota and purchased
|
||||
storage. You must purchase the storage increment that exceeds this total.
|
||||
1. Select **Buy storage**. You are taken to the Customers Portal.
|
||||
|
|
@ -438,7 +436,7 @@ To purchase additional storage for your group on GitLab.com:
|
|||
|
||||
1. On the left sidebar, select **Search or go to** and find your group.
|
||||
1. Select **Settings > Usage Quotas**.
|
||||
1. Select **Storage** tab.
|
||||
1. Select the **Storage** tab.
|
||||
1. Select **Buy storage**. You are taken to the Customers Portal.
|
||||
1. In the **Subscription details** section, enter the desired quantity of storage packs.
|
||||
1. In the **Customer information** section, verify your address.
|
||||
|
|
@ -448,8 +446,7 @@ To purchase additional storage for your group on GitLab.com:
|
|||
|
||||
After your payment is processed, the extra storage is available for your group namespace.
|
||||
|
||||
To confirm the available storage, go to your group, and then select
|
||||
**Settings > Usage Quotas** and select the **Storage** tab.
|
||||
To confirm the available storage, follow the first three steps listed above.
|
||||
|
||||
The **Purchased storage available** total is incremented by the amount purchased. All locked
|
||||
projects are unlocked and their excess usage is deducted from the additional storage.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,76 @@
|
|||
---
|
||||
stage: Create
|
||||
group: Source Code
|
||||
info: "To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments"
|
||||
description: "Introduction to Git rebase and force push, methods to resolve merge conflicts through the command line."
|
||||
---
|
||||
|
||||
# Advanced Git operations
|
||||
|
||||
Advanced Git operations help you perform tasks to maintain and manage your code.
|
||||
They are more complex actions that go beyond [basic Git operations](basics.md).
|
||||
These operations enable you to:
|
||||
|
||||
- Rewrite commit history.
|
||||
- Revert and undo changes.
|
||||
- Manage remote repository connections.
|
||||
|
||||
They provide you with the following benefits:
|
||||
|
||||
- Code quality: Maintain a clean, linear project history.
|
||||
- Problem solving: Provide tools to fix mistakes or adjust your repository's state.
|
||||
- Workflow optimization: Streamline complex development processes.
|
||||
- Collaboration: Facilitate smoother teamwork in large or complex projects.
|
||||
|
||||
To use Git operations effectively, it's important to understand key concepts such as
|
||||
repositories, branches, commits, and merge requests.
|
||||
For more information, see [Get started learning Git](get_started.md).
|
||||
|
||||
## Best practices
|
||||
|
||||
When you use advanced Git operations, you should:
|
||||
|
||||
- Create a backup or work on a [separate branch](branch.md).
|
||||
- Communicate with your team before when you use operations that affect shared branch history.
|
||||
- Use descriptive [commit messages](../../tutorials/update_commit_messages/index.md)
|
||||
when you rewrite history.
|
||||
- Update your knowledge of Git to stay current with best practices and new features.
|
||||
For more information, see the [Git documentation](https://git-scm.com/docs).
|
||||
- Practice advanced operations in a test repository.
|
||||
|
||||
## Rebase and resolve conflicts
|
||||
|
||||
The `git rebase` command updates your branch with the contents of another branch.
|
||||
It confirms that changes in your branch don't conflict with changes in the target branch.
|
||||
If you have a [merge conflict](../../user/project/merge_requests/conflicts.md),
|
||||
you can rebase to fix it.
|
||||
|
||||
For more information, see [Rebase to address merge conflicts](git_rebase.md).
|
||||
|
||||
## Revert and undo changes
|
||||
|
||||
The following Git commands help you to revert and undo changes:
|
||||
|
||||
- `git revert`: Creates a new commit that undoes the changes made in a previous commit.
|
||||
This helps you to undo a mistake or a change that you no longer need.
|
||||
- `git reset`: Resets and undoes changes that are not yet committed.
|
||||
- `git restore`: Restores changes that are lost or deleted.
|
||||
|
||||
For more information, see [Revert changes](undo.md).
|
||||
|
||||
## Update Git remote URLs
|
||||
|
||||
The `git remote set-url` command updates the URL of the remote repository.
|
||||
Use this if:
|
||||
|
||||
- You imported an existing project from another Git repository host.
|
||||
- Your organization moved your projects to a new GitLab instance with a new domain name.
|
||||
- The project was renamed to a new path in the same GitLab instance.
|
||||
|
||||
For more information, see [Update Git remote URLs](../../tutorials/update_git_remote_url/index.md).
|
||||
|
||||
## Related topics
|
||||
|
||||
- [Getting started](get_started.md)
|
||||
- [Basic Git operations](basics.md)
|
||||
- [Common Git commands](commands.md)
|
||||
|
|
@ -4175,6 +4175,32 @@ However, since certificate-based integration with Kubernetes clusters is depreca
|
|||
|
||||
</div>
|
||||
|
||||
Whether your existing project access tokens have expiry dates automatically applied depends on what GitLab offering you have, and when you upgraded to GitLab 16.0 or later:
|
||||
|
||||
- On GitLab.com, during the 16.0 milestone, existing project access tokens without an expiry date were automatically given an expiry date of 365 days later than the current date.
|
||||
- On GitLab self-managed, if you upgraded from GitLab 15.11 or earlier to GitLab 16.0 or later:
|
||||
- On or before July 23, 2024, existing project access tokens without an expiry date were automatically given an expiry date of 365 days later than the current date.
|
||||
This change is a breaking change.
|
||||
- On or after July 24, 2024, existing project access tokens without an expiry date did not have an expiry date set.
|
||||
|
||||
On GitLab self-managed, if you do a new install of one of the following GitLab versions, your existing project access tokens do not have expiry dates automatically applied:
|
||||
|
||||
- 16.0.9
|
||||
- 16.1.7
|
||||
- 16.2.10
|
||||
- 16.3.8
|
||||
- 16.4.6
|
||||
- 16.5.9
|
||||
- 16.6.9
|
||||
- 16.7.9
|
||||
- 16.8.9
|
||||
- 16.9.10
|
||||
- 16.10.9
|
||||
- 16.11.7
|
||||
- 17.0.5
|
||||
- 17.1.3
|
||||
- 17.2.1
|
||||
|
||||
Access tokens that have no expiration date are valid indefinitely, which presents a security risk if the access token
|
||||
is divulged. Because access tokens that have an expiration date are better, from GitLab 15.3 we
|
||||
[populate a default expiration date](https://gitlab.com/gitlab-org/gitlab/-/issues/348660).
|
||||
|
|
|
|||
|
|
@ -10,11 +10,8 @@ DETAILS:
|
|||
**Tier:** Ultimate
|
||||
**Offering:** GitLab.com, Self-managed, GitLab Dedicated
|
||||
|
||||
> - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/13266) in GitLab 17.2 [with a flag](../../../administration/feature_flags.md) named `pipeline_execution_policy_type`. Enabled by default. [Feature flag removed in GitLab 17.3](https://gitlab.com/gitlab-org/gitlab/-/issues/454278).
|
||||
|
||||
FLAG:
|
||||
The availability of this feature is controlled by a feature flag.
|
||||
For more information, see the history.
|
||||
> - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/13266) in GitLab 17.2 [with a flag](../../../administration/feature_flags.md) named `pipeline_execution_policy_type`. Enabled by default.
|
||||
> - [Generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/454278) in GitLab 17.3. Feature flag `pipeline_execution_policy_type` removed.
|
||||
|
||||
Use Pipeline execution policies to enforce CI/CD jobs for all applicable projects.
|
||||
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ Before making changes, ensure either the SAML response includes the `groups` att
|
|||
and the `AttributeValue` value matches the **SAML Group Name** in GitLab,
|
||||
or that all groups are removed from GitLab to disable Group Sync.
|
||||
|
||||
SAML group sync allows users to be assigned to pre-existing GitLab groups with specific permissions based on the user's group assignment in the SAML identity provider (IdP). This feature allows you to create a many-to-many mapping between SAML IdP groups and GitLab groups. For example, if the user `@amelia` is assigned to the `security` group in the SAML IdP, SAML group sync allows you to assign `@amelia` to the `security-gitlab` and `vulnerability` GitLab groups with `maintainer` and `reporter` permissions, respectively. SAML group sync does not create groups. You [create groups separately](../index.md#create-a-group), and then create the mapping.
|
||||
|
||||
<i class="fa fa-youtube-play youtube" aria-hidden="true"></i>
|
||||
For a demo of Group Sync using Azure, see [Demo: SAML Group Sync](https://youtu.be/Iqvo2tJfXjg).
|
||||
|
||||
|
|
@ -34,8 +36,7 @@ Prerequisites:
|
|||
When SAML is enabled, users with the Owner role see a new menu
|
||||
item in group **Settings > SAML Group Links**.
|
||||
|
||||
- You can configure one or more **SAML Group Links** to map a SAML identity
|
||||
provider (IdP) group name to a GitLab role.
|
||||
- You can configure one or more **SAML Group Links** to map a SAML IdP group name to a GitLab role.
|
||||
- Members of the SAML IdP group are added as members of the GitLab
|
||||
group on their next SAML sign-in.
|
||||
- Group membership is evaluated each time a user signs in using SAML.
|
||||
|
|
|
|||
|
|
@ -33,7 +33,8 @@ To set up infrastructure for workspaces:
|
|||
1. Install an Ingress controller of your choice (for example, `ingress-nginx`).
|
||||
1. [Install](../clusters/agent/install/index.md) and [configure](gitlab_agent_configuration.md) the GitLab agent.
|
||||
1. Point [`dns_zone`](gitlab_agent_configuration.md#dns_zone) and `*.<dns_zone>`
|
||||
to the load balancer exposed by the Ingress controller. This load balancer must support WebSockets.
|
||||
to the load balancer exposed by the Ingress controller.
|
||||
This load balancer must support WebSockets.
|
||||
1. [Set up the GitLab workspaces proxy](set_up_workspaces_proxy.md).
|
||||
1. Optional. [Configure sudo access for a workspace](#configure-sudo-access-for-a-workspace).
|
||||
|
||||
|
|
@ -93,39 +94,40 @@ You can configure secure sudo access for a workspace with:
|
|||
|
||||
### With Sysbox
|
||||
|
||||
[Sysbox](https://github.com/nestybox/sysbox) is a container runtime that enhances containers to
|
||||
improves container isolation and enables containers to run same workloads as VMs.
|
||||
[Sysbox](https://github.com/nestybox/sysbox) is a container runtime that improves container isolation
|
||||
and enables containers to run the same workloads as virtual machines.
|
||||
|
||||
To configure your workspace to use Sysbox:
|
||||
To configure sudo access for a workspace with Sysbox:
|
||||
|
||||
1. [Install Sysbox in the Kubernetes cluster](https://github.com/nestybox/sysbox#installation).
|
||||
1. Configure the following settings in the GitLab agent for workspaces:
|
||||
- Set [`default_runtime_class`](gitlab_agent_configuration.md#default_runtime_class) to the runtime class set up by Sysbox. For example, `sysbox-runc`.
|
||||
1. In the Kubernetes cluster, [install Sysbox](https://github.com/nestybox/sysbox#installation).
|
||||
1. In the GitLab agent for workspaces:
|
||||
- Set [`default_runtime_class`](gitlab_agent_configuration.md#default_runtime_class) to the runtime class
|
||||
of Sysbox (for example, `sysbox-runc`).
|
||||
- Set [`allow_privilege_escalation`](gitlab_agent_configuration.md#allow_privilege_escalation) to `true`.
|
||||
|
||||
### With Kata Containers
|
||||
|
||||
[Kata Containers](https://github.com/kata-containers/kata-containers) is a standard implementation of lightweight virtual machines (VMs)
|
||||
that feel and perform like containers, but provide the workload isolation and security advantages of VMs.
|
||||
[Kata Containers](https://github.com/kata-containers/kata-containers) is a standard implementation of lightweight
|
||||
virtual machines that perform like containers but provide the workload isolation and security of virtual machines.
|
||||
|
||||
To configure your workspace to use Kata Containers:
|
||||
To configure sudo access for a workspace with Kata Containers:
|
||||
|
||||
1. [Install Kata Containers in the Kubernetes cluster](https://github.com/kata-containers/kata-containers/tree/main/docs/install).
|
||||
1. Configure the following settings in the GitLab agent for workspaces:
|
||||
- Set [`default_runtime_class`](gitlab_agent_configuration.md#default_runtime_class) to one of the runtime classes set up by Kata Containers. For example, `kata-qemu`.
|
||||
1. In the Kubernetes cluster, [install Kata Containers](https://github.com/kata-containers/kata-containers/tree/main/docs/install).
|
||||
1. In the GitLab agent for workspaces:
|
||||
- Set [`default_runtime_class`](gitlab_agent_configuration.md#default_runtime_class) to one of the runtime classes
|
||||
of Kata Containers (for example, `kata-qemu`).
|
||||
- Set [`allow_privilege_escalation`](gitlab_agent_configuration.md#allow_privilege_escalation) to `true`.
|
||||
|
||||
### With user namespaces
|
||||
|
||||
User namespaces isolate the user running inside the container from the user in the host.
|
||||
In Kubernetes 1.30, this feature is in beta.
|
||||
[User namespaces](https://kubernetes.io/docs/concepts/workloads/pods/user-namespaces/) isolate the user
|
||||
running inside the container from the user on the host.
|
||||
|
||||
To configure your workspace to use the user namespaces feature in Kubernetes:
|
||||
To configure sudo access for a workspace with user namespaces:
|
||||
|
||||
1. [Configure Kubernetes cluster with User namespaces](https://kubernetes.io/blog/2024/04/22/userns-beta/).
|
||||
1. Configure the following settings in GitLab agent for workspaces:
|
||||
- Set [`use_kubernetes_user_namespaces`](gitlab_agent_configuration.md#use_kubernetes_user_namespaces) to `true`.
|
||||
- Set [`allow_privilege_escalation`](gitlab_agent_configuration.md#allow_privilege_escalation) to `true`.
|
||||
1. In the Kubernetes cluster, [configure user namespaces](https://kubernetes.io/blog/2024/04/22/userns-beta/).
|
||||
1. In the GitLab agent for workspaces, set [`use_kubernetes_user_namespaces`](gitlab_agent_configuration.md#use_kubernetes_user_namespaces)
|
||||
and [`allow_privilege_escalation`](gitlab_agent_configuration.md#allow_privilege_escalation) to `true`.
|
||||
|
||||
## Connect to a workspace with SSH
|
||||
|
||||
|
|
|
|||
|
|
@ -337,8 +337,8 @@ remote_development:
|
|||
|
||||
Use this setting to specify whether to use the user namespaces feature in Kubernetes.
|
||||
|
||||
User namespaces isolate the user running inside the container from the user in the host.
|
||||
In Kubernetes 1.30, this feature is in beta.
|
||||
[User namespaces](https://kubernetes.io/docs/concepts/workloads/pods/user-namespaces/) isolate the user
|
||||
running inside the container from the user on the host.
|
||||
|
||||
The default value is `false`. Before you set the value to `true`, ensure your Kubernetes cluster supports user namespaces.
|
||||
|
||||
|
|
|
|||
|
|
@ -116,8 +116,8 @@ regardless of the machine or platform you use.
|
|||
- `parent`, `projects`, and `starterProjects` are not supported.
|
||||
- For `variables`, keys must not start with `gl-`, `gl_`, `GL-`, or `GL_`.
|
||||
- For `attributes`:
|
||||
- `pod-overrides` must not be set, either at the root level or within `components`.
|
||||
- `container-overrides` must not be set within `components`.
|
||||
- `pod-overrides` must not be set at the root level or in `components`.
|
||||
- `container-overrides` must not be set in `components`.
|
||||
|
||||
### `container` component type
|
||||
|
||||
|
|
|
|||
|
|
@ -105,6 +105,13 @@ module Gitlab
|
|||
Gitlab::Metrics.counter(name, comment)
|
||||
end
|
||||
|
||||
def self.duplicate_job_name_errors_counter
|
||||
name = :gitlab_ci_duplicate_job_name_errors_counter
|
||||
comment = 'Counter of duplicate job name errors'
|
||||
|
||||
Gitlab::Metrics.counter(name, comment)
|
||||
end
|
||||
|
||||
def ci_minutes_exceeded_builds_counter
|
||||
name = :ci_minutes_exceeded_builds_counter
|
||||
comment = 'Count of builds dropped due to compute minutes exceeded'
|
||||
|
|
|
|||
|
|
@ -13849,6 +13849,12 @@ msgstr ""
|
|||
msgid "Commit statistics for %{ref} %{start_time} - %{end_time}"
|
||||
msgstr ""
|
||||
|
||||
msgid "Commit to a new branch"
|
||||
msgstr ""
|
||||
|
||||
msgid "Commit to the current %{branchName} branch"
|
||||
msgstr ""
|
||||
|
||||
msgid "CommitBoxTitle|Commit"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -16204,6 +16210,9 @@ msgstr ""
|
|||
msgid "Create a merge request branch target."
|
||||
msgstr ""
|
||||
|
||||
msgid "Create a merge request for this change"
|
||||
msgstr ""
|
||||
|
||||
msgid "Create a merge request, or edit your criteria and try again."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -18231,9 +18240,6 @@ msgstr ""
|
|||
msgid "Delete epic"
|
||||
msgstr ""
|
||||
|
||||
msgid "Delete file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Delete group"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -23560,7 +23566,7 @@ msgstr ""
|
|||
msgid "Filter users"
|
||||
msgstr ""
|
||||
|
||||
msgid "Filter: %{filter_name})"
|
||||
msgid "Filter: %{filter_name}"
|
||||
msgstr ""
|
||||
|
||||
msgid "Finalizing"
|
||||
|
|
@ -62193,6 +62199,9 @@ msgstr ""
|
|||
msgid "WorkItem|Are you sure you want to cancel editing?"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Are you sure you want to close this %{workItemType}?"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Are you sure you want to close this blocked %{workItemType}?"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -62214,6 +62223,9 @@ msgstr ""
|
|||
msgid "WorkItem|Child items"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Child items of a confidential parent must be confidential. Turn off confidentiality on the parent item first."
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Child removal reverted"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -62574,6 +62586,9 @@ msgstr ""
|
|||
msgid "WorkItem|The current task"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|This %{workItemType} has open child items. If you close this %{workItemType}, they will remain open."
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|This %{workItemType} is confidential and should only be visible to team members with at least Reporter access"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -64976,6 +64991,9 @@ msgstr ""
|
|||
msgid "event"
|
||||
msgstr ""
|
||||
|
||||
msgid "example-branch-name"
|
||||
msgstr ""
|
||||
|
||||
msgid "example.com"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
source 'https://rubygems.org'
|
||||
|
||||
gem 'gitlab-qa', '~> 14', '>= 14.17.0', require: 'gitlab/qa'
|
||||
gem 'gitlab_quality-test_tooling', '~> 1.39.0', require: false
|
||||
gem 'gitlab_quality-test_tooling', '~> 2.0.0', require: false
|
||||
gem 'gitlab-utils', path: '../gems/gitlab-utils'
|
||||
gem 'activesupport', '~> 7.0.8.4' # This should stay in sync with the root's Gemfile
|
||||
gem 'allure-rspec', '~> 2.25.0'
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ GEM
|
|||
tins (~> 1.0)
|
||||
ast (2.4.2)
|
||||
base64 (0.2.0)
|
||||
bigdecimal (3.1.8)
|
||||
binding_of_caller (1.0.0)
|
||||
debug_inspector (>= 0.0.1)
|
||||
builder (3.3.0)
|
||||
|
|
@ -125,11 +126,11 @@ GEM
|
|||
rainbow (>= 3, < 4)
|
||||
table_print (= 1.5.7)
|
||||
zeitwerk (>= 2, < 3)
|
||||
gitlab_quality-test_tooling (1.39.0)
|
||||
gitlab_quality-test_tooling (2.0.0)
|
||||
activesupport (>= 7.0, < 7.2)
|
||||
amatch (~> 0.4.1)
|
||||
fog-google (~> 1.24, >= 1.24.1)
|
||||
gitlab (~> 4.19)
|
||||
gitlab (>= 4.19, < 6.0)
|
||||
http (~> 5.0)
|
||||
influxdb-client (~> 3.1)
|
||||
nokogiri (~> 1.10)
|
||||
|
|
@ -208,8 +209,7 @@ GEM
|
|||
mini_mime (1.1.5)
|
||||
mini_portile2 (2.8.7)
|
||||
minitest (5.23.1)
|
||||
mize (0.4.1)
|
||||
protocol (~> 2.0)
|
||||
mize (0.6.1)
|
||||
multi_json (1.15.0)
|
||||
multi_xml (0.6.0)
|
||||
net-http (0.4.1)
|
||||
|
|
@ -234,8 +234,6 @@ GEM
|
|||
coderay
|
||||
parser
|
||||
unparser
|
||||
protocol (2.0.0)
|
||||
ruby_parser (~> 3.0)
|
||||
pry (0.14.1)
|
||||
coderay (~> 1.1)
|
||||
method_source (~> 1.0)
|
||||
|
|
@ -296,9 +294,6 @@ GEM
|
|||
rspec-core (>= 2, < 4, != 2.12.0)
|
||||
ruby-debug-ide (0.7.3)
|
||||
rake (>= 0.8.1)
|
||||
ruby_parser (3.21.0)
|
||||
racc (~> 1.5)
|
||||
sexp_processor (~> 4.16)
|
||||
rubyzip (2.3.2)
|
||||
sawyer (0.9.2)
|
||||
addressable (>= 2.3.5)
|
||||
|
|
@ -309,7 +304,6 @@ GEM
|
|||
rexml (~> 3.2, >= 3.2.5)
|
||||
rubyzip (>= 1.2.2, < 3.0)
|
||||
websocket (~> 1.0)
|
||||
sexp_processor (4.17.1)
|
||||
signet (0.19.0)
|
||||
addressable (~> 2.8)
|
||||
faraday (>= 0.17.5, < 3.a)
|
||||
|
|
@ -321,7 +315,8 @@ GEM
|
|||
terminal-table (3.0.2)
|
||||
unicode-display_width (>= 1.1.1, < 3)
|
||||
thor (1.3.1)
|
||||
tins (1.32.1)
|
||||
tins (1.37.0)
|
||||
bigdecimal
|
||||
sync
|
||||
trailblazer-option (0.1.2)
|
||||
tty-color (0.6.0)
|
||||
|
|
@ -369,7 +364,7 @@ DEPENDENCIES
|
|||
gitlab-cng!
|
||||
gitlab-qa (~> 14, >= 14.17.0)
|
||||
gitlab-utils!
|
||||
gitlab_quality-test_tooling (~> 1.39.0)
|
||||
gitlab_quality-test_tooling (~> 2.0.0)
|
||||
googleauth (~> 1.9.0)
|
||||
influxdb-client (~> 3.1)
|
||||
junit_merge (~> 0.1.2)
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ module QA
|
|||
def self.included(base)
|
||||
super
|
||||
|
||||
base.view 'app/assets/javascripts/repository/components/delete_blob_modal.vue' do
|
||||
base.view 'app/assets/javascripts/repository/components/commit_changes_modal.vue' do
|
||||
element 'commit-message-field'
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -35,8 +35,8 @@ module QA
|
|||
click_element('question-icon')
|
||||
end
|
||||
|
||||
def click_delete_file
|
||||
click_on 'Delete file'
|
||||
def click_commit_changes
|
||||
click_on 'Commit changes'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ module QA
|
|||
end
|
||||
|
||||
context 'when imported via api' do
|
||||
it 'imports project', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347670' do
|
||||
it 'imports project', :blocking, testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347670' do
|
||||
expect_project_import_finished_successfully
|
||||
|
||||
aggregate_failures do
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ module QA
|
|||
Page::File::Show.perform do |file|
|
||||
file.click_delete
|
||||
file.add_commit_message(commit_message_for_delete)
|
||||
file.click_delete_file
|
||||
file.click_commit_changes
|
||||
end
|
||||
|
||||
Page::Project::Show.perform do |project|
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ RSpec.describe 'Projects > Files > User deletes files', :js, feature_category: :
|
|||
|
||||
click_on('Delete')
|
||||
fill_in(:commit_message, with: 'New commit message', visible: true)
|
||||
click_button('Delete file')
|
||||
click_button('Commit changes')
|
||||
|
||||
expect(page).to have_current_path(project_tree_path(project, 'master/'), ignore_query: true)
|
||||
expect(page).not_to have_content('.gitignore')
|
||||
|
|
@ -62,7 +62,7 @@ RSpec.describe 'Projects > Files > User deletes files', :js, feature_category: :
|
|||
|
||||
click_on('Delete')
|
||||
fill_in(:commit_message, with: 'New commit message', visible: true)
|
||||
click_button('Delete file')
|
||||
click_button('Commit changes')
|
||||
|
||||
fork = user.fork_of(project2.reload)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,11 @@
|
|||
import * as Sentry from '~/sentry/sentry_browser_wrapper';
|
||||
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
|
||||
import { sanitize } from '~/lib/dompurify';
|
||||
import { createAlert, VARIANT_WARNING } from '~/alert';
|
||||
|
||||
jest.mock('~/lib/dompurify', () => ({
|
||||
sanitize: jest.fn((val) => val),
|
||||
}));
|
||||
jest.mock('~/sentry/sentry_browser_wrapper');
|
||||
|
||||
describe('Flash', () => {
|
||||
|
|
@ -359,6 +363,36 @@ describe('Flash', () => {
|
|||
expect(messageLinks).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when rendered as HTML', () => {
|
||||
const findMessageHTML = () => document.querySelector('.gl-alert-body div').innerHTML;
|
||||
const message =
|
||||
'error: <a href="https://documentation.com/further-information">learn more</a>';
|
||||
|
||||
it('renders the given message as HTML', () => {
|
||||
alert = createAlert({
|
||||
message,
|
||||
renderMessageHTML: true,
|
||||
});
|
||||
|
||||
expect(findMessageHTML()).toBe(message);
|
||||
});
|
||||
|
||||
it('sanitizes the given message', () => {
|
||||
expect(sanitize).not.toHaveBeenCalled();
|
||||
|
||||
createAlert({
|
||||
message,
|
||||
renderMessageHTML: true,
|
||||
});
|
||||
|
||||
expect(sanitize).toHaveBeenCalledTimes(1);
|
||||
expect(sanitize).toHaveBeenCalledWith(message, {
|
||||
ALLOWED_TAGS: ['a'],
|
||||
ALLOWED_ATTR: ['href', 'rel', 'target'],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { GlButton } from '@gitlab/ui';
|
|||
import { mountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import { stubComponent } from 'helpers/stub_component';
|
||||
import BlobButtonGroup from '~/repository/components/blob_button_group.vue';
|
||||
import DeleteBlobModal from '~/repository/components/delete_blob_modal.vue';
|
||||
import CommitChangesModal from '~/repository/components/commit_changes_modal.vue';
|
||||
import UploadBlobModal from '~/repository/components/upload_blob_modal.vue';
|
||||
|
||||
const DEFAULT_PROPS = {
|
||||
|
|
@ -40,7 +40,7 @@ describe('BlobButtonGroup component', () => {
|
|||
show: showUploadBlobModalMock,
|
||||
},
|
||||
});
|
||||
const DeleteBlobModalStub = stubComponent(DeleteBlobModal, {
|
||||
const DeleteBlobModalStub = stubComponent(CommitChangesModal, {
|
||||
methods: {
|
||||
show: showDeleteBlobModalMock,
|
||||
},
|
||||
|
|
@ -56,12 +56,12 @@ describe('BlobButtonGroup component', () => {
|
|||
},
|
||||
stubs: {
|
||||
UploadBlobModal: UploadBlobModalStub,
|
||||
DeleteBlobModal: DeleteBlobModalStub,
|
||||
CommitChangesModal: DeleteBlobModalStub,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const findDeleteBlobModal = () => wrapper.findComponent(DeleteBlobModal);
|
||||
const findDeleteBlobModal = () => wrapper.findComponent(CommitChangesModal);
|
||||
const findUploadBlobModal = () => wrapper.findComponent(UploadBlobModal);
|
||||
const findDeleteButton = () => wrapper.findByTestId('delete');
|
||||
const findReplaceButton = () => wrapper.findByTestId('replace');
|
||||
|
|
@ -92,13 +92,13 @@ describe('BlobButtonGroup component', () => {
|
|||
});
|
||||
|
||||
it('triggers the UploadBlobModal from the replace button', () => {
|
||||
findReplaceButton().trigger('click');
|
||||
findReplaceButton().vm.$emit('click');
|
||||
|
||||
expect(showUploadBlobModalMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('triggers the DeleteBlobModal from the delete button', () => {
|
||||
findDeleteButton().trigger('click');
|
||||
it('triggers the CommitChangesModal from the delete button', () => {
|
||||
findDeleteButton().vm.$emit('click');
|
||||
|
||||
expect(showDeleteBlobModalMock).toHaveBeenCalled();
|
||||
});
|
||||
|
|
@ -109,14 +109,14 @@ describe('BlobButtonGroup component', () => {
|
|||
});
|
||||
|
||||
it('does not trigger the UploadBlobModal from the replace button', () => {
|
||||
findReplaceButton().trigger('click');
|
||||
findReplaceButton().vm.$emit('click');
|
||||
|
||||
expect(showUploadBlobModalMock).not.toHaveBeenCalled();
|
||||
expect(wrapper.emitted().fork).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('does not trigger the DeleteBlobModal from the delete button', () => {
|
||||
findDeleteButton().trigger('click');
|
||||
findDeleteButton().vm.$emit('click');
|
||||
|
||||
expect(showDeleteBlobModalMock).not.toHaveBeenCalled();
|
||||
expect(wrapper.emitted().fork).toHaveLength(1);
|
||||
|
|
@ -143,7 +143,7 @@ describe('BlobButtonGroup component', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('renders DeleteBlobModel', () => {
|
||||
it('renders CommitChangesModal for delete', () => {
|
||||
createComponent();
|
||||
|
||||
const { targetBranch, originalBranch } = DEFAULT_INJECT;
|
||||
|
|
@ -151,7 +151,6 @@ describe('BlobButtonGroup component', () => {
|
|||
const title = `Delete ${name}`;
|
||||
|
||||
expect(findDeleteBlobModal().props()).toMatchObject({
|
||||
modalTitle: title,
|
||||
commitMessage: title,
|
||||
targetBranch,
|
||||
originalBranch,
|
||||
|
|
|
|||
|
|
@ -1,16 +1,24 @@
|
|||
import { GlFormTextarea, GlModal, GlFormInput, GlToggle, GlForm, GlSprintf } from '@gitlab/ui';
|
||||
import {
|
||||
GlFormTextarea,
|
||||
GlModal,
|
||||
GlFormCheckbox,
|
||||
GlFormInput,
|
||||
GlFormRadioGroup,
|
||||
GlForm,
|
||||
GlSprintf,
|
||||
GlFormRadio,
|
||||
} from '@gitlab/ui';
|
||||
import { mount } from '@vue/test-utils';
|
||||
import { nextTick } from 'vue';
|
||||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import { RENDER_ALL_SLOTS_TEMPLATE, stubComponent } from 'helpers/stub_component';
|
||||
import DeleteBlobModal from '~/repository/components/delete_blob_modal.vue';
|
||||
import CommitChangesModal from '~/repository/components/commit_changes_modal.vue';
|
||||
import { sprintf } from '~/locale';
|
||||
|
||||
jest.mock('~/lib/utils/csrf', () => ({ token: 'mock-csrf-token' }));
|
||||
|
||||
const initialProps = {
|
||||
modalId: 'Delete-blob',
|
||||
modalTitle: 'Delete File',
|
||||
deletePath: 'some/path',
|
||||
commitMessage: 'Delete File',
|
||||
targetBranch: 'some-target-branch',
|
||||
|
|
@ -20,15 +28,15 @@ const initialProps = {
|
|||
emptyRepo: false,
|
||||
};
|
||||
|
||||
const { i18n } = DeleteBlobModal;
|
||||
const { i18n } = CommitChangesModal;
|
||||
|
||||
describe('DeleteBlobModal', () => {
|
||||
describe('CommitChangesModal', () => {
|
||||
let wrapper;
|
||||
|
||||
const createComponentFactory =
|
||||
(mountFn) =>
|
||||
(props = {}) => {
|
||||
wrapper = mountFn(DeleteBlobModal, {
|
||||
wrapper = mountFn(CommitChangesModal, {
|
||||
propsData: {
|
||||
...initialProps,
|
||||
...props,
|
||||
|
|
@ -50,6 +58,11 @@ describe('DeleteBlobModal', () => {
|
|||
const findModal = () => wrapper.findComponent(GlModal);
|
||||
const findForm = () => findModal().findComponent(GlForm);
|
||||
const findCommitTextarea = () => findForm().findComponent(GlFormTextarea);
|
||||
const findFormRadioGroup = () => findForm().findComponent(GlFormRadioGroup);
|
||||
const findRadioGroup = () => findForm().findAllComponents(GlFormRadio);
|
||||
const findCurrentBranchRadioOption = () => findRadioGroup().at(0);
|
||||
const findNewBranchRadioOption = () => findRadioGroup().at(1);
|
||||
const findCreateMrCheckbox = () => findForm().findComponent(GlFormCheckbox);
|
||||
const findTargetInput = () => findForm().findComponent(GlFormInput);
|
||||
const findCommitHint = () => wrapper.find('[data-testid="hint"]');
|
||||
|
||||
|
|
@ -92,13 +105,10 @@ describe('DeleteBlobModal', () => {
|
|||
it('renders Modal component', () => {
|
||||
createComponent();
|
||||
|
||||
const { modalTitle: title } = initialProps;
|
||||
|
||||
expect(findModal().props()).toMatchObject({
|
||||
title,
|
||||
size: 'md',
|
||||
actionPrimary: {
|
||||
text: 'Delete file',
|
||||
text: 'Commit changes',
|
||||
},
|
||||
actionCancel: {
|
||||
text: 'Cancel',
|
||||
|
|
@ -112,32 +122,53 @@ describe('DeleteBlobModal', () => {
|
|||
expect(findForm().attributes('action')).toBe(initialProps.deletePath);
|
||||
});
|
||||
|
||||
it.each`
|
||||
component | defaultValue | canPushCode | targetBranch | originalBranch | exist
|
||||
${GlFormTextarea} | ${initialProps.commitMessage} | ${true} | ${initialProps.targetBranch} | ${initialProps.originalBranch} | ${true}
|
||||
${GlFormInput} | ${initialProps.targetBranch} | ${true} | ${initialProps.targetBranch} | ${initialProps.originalBranch} | ${true}
|
||||
${GlFormInput} | ${undefined} | ${false} | ${initialProps.targetBranch} | ${initialProps.originalBranch} | ${false}
|
||||
${GlToggle} | ${'true'} | ${true} | ${initialProps.targetBranch} | ${initialProps.originalBranch} | ${true}
|
||||
${GlToggle} | ${undefined} | ${true} | ${'same-branch'} | ${'same-branch'} | ${false}
|
||||
`(
|
||||
'has the correct form fields',
|
||||
({ component, defaultValue, canPushCode, targetBranch, originalBranch, exist }) => {
|
||||
createComponent({
|
||||
canPushCode,
|
||||
targetBranch,
|
||||
originalBranch,
|
||||
});
|
||||
const formField = wrapper.findComponent(component);
|
||||
it('shows the correct form fields when commit to current branch', () => {
|
||||
createComponent();
|
||||
expect(findCommitTextarea().exists()).toBe(true);
|
||||
expect(findRadioGroup()).toHaveLength(2);
|
||||
expect(findCurrentBranchRadioOption().text()).toContain(initialProps.originalBranch);
|
||||
expect(findNewBranchRadioOption().text()).toBe('Commit to a new branch');
|
||||
});
|
||||
|
||||
if (!exist) {
|
||||
expect(formField.exists()).toBe(false);
|
||||
return;
|
||||
}
|
||||
it('shows the correct form fields when commit to new branch', async () => {
|
||||
createComponent();
|
||||
expect(findTargetInput().exists()).toBe(false);
|
||||
|
||||
expect(formField.exists()).toBe(true);
|
||||
expect(formField.attributes('value')).toBe(defaultValue);
|
||||
},
|
||||
);
|
||||
findFormRadioGroup().vm.$emit('input', true);
|
||||
await nextTick();
|
||||
|
||||
expect(findTargetInput().exists()).toBe(true);
|
||||
expect(findCreateMrCheckbox().text()).toBe('Create a merge request for this change');
|
||||
});
|
||||
|
||||
it('shows the correct form fields when `canPushToBranch` is `false`', () => {
|
||||
createComponent({ canPushToBranch: false, canPushCode: true });
|
||||
expect(wrapper.vm.$data.form.fields.branch_name.value).toBe('');
|
||||
expect(findCommitTextarea().exists()).toBe(true);
|
||||
expect(findRadioGroup().exists()).toBe(false);
|
||||
expect(findTargetInput().exists()).toBe(true);
|
||||
expect(findCreateMrCheckbox().text()).toBe('Create a merge request for this change');
|
||||
});
|
||||
|
||||
it('clear branch name when new branch option is selected', async () => {
|
||||
createComponent();
|
||||
expect(wrapper.vm.$data.form.fields.branch_name).toEqual({
|
||||
feedback: null,
|
||||
required: true,
|
||||
state: true,
|
||||
value: 'main',
|
||||
});
|
||||
|
||||
findFormRadioGroup().vm.$emit('input', true);
|
||||
await nextTick();
|
||||
|
||||
expect(wrapper.vm.$data.form.fields.branch_name).toEqual({
|
||||
feedback: null,
|
||||
required: true,
|
||||
state: true,
|
||||
value: '',
|
||||
});
|
||||
});
|
||||
|
||||
it.each`
|
||||
input | value | emptyRepo | canPushCode | canPushToBranch | exist
|
||||
|
|
@ -181,6 +212,7 @@ describe('DeleteBlobModal', () => {
|
|||
|
||||
beforeEach(async () => {
|
||||
createFullComponent();
|
||||
findFormRadioGroup().vm.$emit('input', true);
|
||||
await nextTick();
|
||||
});
|
||||
|
||||
|
|
@ -219,6 +251,9 @@ describe('DeleteBlobModal', () => {
|
|||
|
||||
describe('invalid form', () => {
|
||||
beforeEach(async () => {
|
||||
findFormRadioGroup().vm.$emit('input', true);
|
||||
await nextTick();
|
||||
|
||||
await fillForm({ targetText: '', commitText: '' });
|
||||
});
|
||||
|
||||
|
|
@ -236,6 +271,8 @@ describe('DeleteBlobModal', () => {
|
|||
|
||||
describe('valid form', () => {
|
||||
beforeEach(async () => {
|
||||
findFormRadioGroup().vm.$emit('input', true);
|
||||
await nextTick();
|
||||
await fillForm({
|
||||
targetText: 'some valid target branch',
|
||||
commitText: 'some valid commit message',
|
||||
|
|
@ -321,15 +321,17 @@ describe('WorkItemActions component', () => {
|
|||
expect(wrapper.emitted('toggleWorkItemConfidentiality')[0]).toEqual([true]);
|
||||
});
|
||||
|
||||
it.each`
|
||||
props | propName | value
|
||||
${{ isParentConfidential: true }} | ${'isParentConfidential'} | ${true}
|
||||
${{ canUpdate: false }} | ${'canUpdate'} | ${false}
|
||||
`('does not render when $propName is $value', ({ props }) => {
|
||||
createComponent(props);
|
||||
|
||||
it('does not render when canUpdate is false', () => {
|
||||
createComponent({ canUpdate: false });
|
||||
expect(findConfidentialityToggleButton().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('is disabled when item has confidential parent', () => {
|
||||
createComponent({ isParentConfidential: true });
|
||||
expect(findConfidentialityToggleButton().props('item')).toMatchObject({
|
||||
extraAttrs: { disabled: true },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('delete action', () => {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import { GlButton, GlModal, GlLink } from '@gitlab/ui';
|
||||
import { GlButton, GlLink } from '@gitlab/ui';
|
||||
import Vue from 'vue';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import { mockTracking } from 'helpers/tracking_helper';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import WorkItemStateToggle from '~/work_items/components/work_item_state_toggle.vue';
|
||||
import {
|
||||
STATE_OPEN,
|
||||
|
|
@ -16,11 +16,15 @@ import {
|
|||
import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
|
||||
import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql';
|
||||
import workItemLinkedItemsQuery from '~/work_items/graphql/work_item_linked_items.query.graphql';
|
||||
import workItemOpenChildCountQuery from '~/work_items/graphql/open_child_count.query.graphql';
|
||||
import {
|
||||
updateWorkItemMutationResponse,
|
||||
mockBlockedByLinkedItem,
|
||||
workItemByIidResponseFactory,
|
||||
workItemBlockedByLinkedItemsResponse,
|
||||
workItemNoBlockedByLinkedItemsResponse,
|
||||
mockOpenChildrenCount,
|
||||
mockNoOpenChildrenCount,
|
||||
} from '../mock_data';
|
||||
|
||||
describe('Work Item State toggle button component', () => {
|
||||
|
|
@ -34,27 +38,32 @@ describe('Work Item State toggle button component', () => {
|
|||
const querySuccessHander = jest.fn().mockResolvedValue(workItemQueryResponse);
|
||||
const workItemBlockedByItemsSuccessHandler = jest
|
||||
.fn()
|
||||
.mockResolvedValue(workItemBlockedByLinkedItemsResponse);
|
||||
.mockResolvedValue(workItemNoBlockedByLinkedItemsResponse);
|
||||
const openChildCountSuccessHandler = jest.fn().mockResolvedValue(mockNoOpenChildrenCount);
|
||||
|
||||
const findStateToggleButton = () => wrapper.findComponent(GlButton);
|
||||
const findModal = () => wrapper.findComponent(GlModal);
|
||||
const findModalLinkAt = (index) => findModal().findAllComponents(GlLink).at(index);
|
||||
const findBlockedByModal = () => wrapper.findByTestId('blocked-by-issues-modal');
|
||||
const findBlockedByModalLinkAt = (index) =>
|
||||
findBlockedByModal().findAllComponents(GlLink).at(index);
|
||||
const findOpenChildrenModal = () => wrapper.findByTestId('open-children-warning-modal');
|
||||
|
||||
const { id, iid } = workItemQueryResponse.data.workspace.workItem;
|
||||
|
||||
const createComponent = ({
|
||||
mutationHandler = mutationSuccessHandler,
|
||||
workItemLinkedItemsHandler = workItemBlockedByItemsSuccessHandler,
|
||||
workItemOpenChildCountHandler = openChildCountSuccessHandler,
|
||||
canUpdate = true,
|
||||
workItemState = STATE_OPEN,
|
||||
workItemType = 'Task',
|
||||
hasComment = false,
|
||||
} = {}) => {
|
||||
wrapper = shallowMount(WorkItemStateToggle, {
|
||||
wrapper = shallowMountExtended(WorkItemStateToggle, {
|
||||
apolloProvider: createMockApollo([
|
||||
[updateWorkItemMutation, mutationHandler],
|
||||
[workItemByIidQuery, querySuccessHander],
|
||||
[workItemLinkedItemsQuery, workItemLinkedItemsHandler],
|
||||
[workItemOpenChildCountQuery, workItemOpenChildCountHandler],
|
||||
]),
|
||||
propsData: {
|
||||
workItemId: id,
|
||||
|
|
@ -173,18 +182,22 @@ describe('Work Item State toggle button component', () => {
|
|||
const blockers = mockBlockedByLinkedItem.linkedItems.nodes;
|
||||
|
||||
beforeEach(async () => {
|
||||
createComponent();
|
||||
createComponent({
|
||||
workItemLinkedItemsHandler: jest
|
||||
.fn()
|
||||
.mockResolvedValue(workItemBlockedByLinkedItemsResponse),
|
||||
});
|
||||
await waitForPromises();
|
||||
});
|
||||
|
||||
it('has title text', () => {
|
||||
expect(findModal().attributes('title')).toBe(
|
||||
expect(findBlockedByModal().attributes('title')).toBe(
|
||||
'Are you sure you want to close this blocked task?',
|
||||
);
|
||||
});
|
||||
|
||||
it('has body text', () => {
|
||||
expect(findModal().text()).toContain(
|
||||
expect(findBlockedByModal().text()).toContain(
|
||||
'This task is currently blocked by the following items:',
|
||||
);
|
||||
});
|
||||
|
|
@ -195,12 +208,36 @@ describe('Work Item State toggle button component', () => {
|
|||
${'second'} | ${1}
|
||||
`('$ordinal blocked-by issue link', ({ index }) => {
|
||||
it('has link text', () => {
|
||||
expect(findModalLinkAt(index).text()).toBe(`#${blockers[index].workItem.iid}`);
|
||||
expect(findBlockedByModalLinkAt(index).text()).toBe(`#${blockers[index].workItem.iid}`);
|
||||
});
|
||||
|
||||
it('has url', () => {
|
||||
expect(findModalLinkAt(index).attributes('href')).toBe(blockers[index].workItem.webUrl);
|
||||
expect(findBlockedByModalLinkAt(index).attributes('href')).toBe(
|
||||
blockers[index].workItem.webUrl,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('with open child items', () => {
|
||||
beforeEach(async () => {
|
||||
createComponent({
|
||||
workItemOpenChildCountHandler: jest.fn().mockResolvedValue(mockOpenChildrenCount),
|
||||
workItemType: 'Epic',
|
||||
});
|
||||
await waitForPromises();
|
||||
});
|
||||
|
||||
it('has title text', () => {
|
||||
expect(findOpenChildrenModal().attributes('title')).toBe(
|
||||
'Are you sure you want to close this epic?',
|
||||
);
|
||||
});
|
||||
|
||||
it('has body text', () => {
|
||||
expect(findOpenChildrenModal().text()).toContain(
|
||||
'This epic has open child items. If you close this epic, they will remain open.',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -699,6 +699,13 @@ export const mockBlockedByLinkedItem = {
|
|||
__typename: 'WorkItemWidgetLinkedItems',
|
||||
};
|
||||
|
||||
export const mockNoLinkedItems = {
|
||||
type: WIDGET_TYPE_LINKED_ITEMS,
|
||||
linkedItems: {
|
||||
nodes: [],
|
||||
},
|
||||
};
|
||||
|
||||
export const mockLinkedItems = {
|
||||
type: WIDGET_TYPE_LINKED_ITEMS,
|
||||
linkedItems: {
|
||||
|
|
@ -886,6 +893,20 @@ export const workItemSingleLinkedItemResponse = {
|
|||
},
|
||||
};
|
||||
|
||||
export const workItemNoBlockedByLinkedItemsResponse = {
|
||||
data: {
|
||||
workspace: {
|
||||
__typename: 'Namespace',
|
||||
id: 'gid://gitlab/Group/1',
|
||||
workItem: {
|
||||
id: 'gid://gitlab/WorkItem/2',
|
||||
widgets: [mockNoLinkedItems],
|
||||
__typename: 'WorkItem',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const workItemBlockedByLinkedItemsResponse = {
|
||||
data: {
|
||||
workspace: {
|
||||
|
|
@ -2295,6 +2316,7 @@ export const mockDepthLimitReachedByType = [
|
|||
export const mockRolledUpCountsByType = [
|
||||
{
|
||||
countsByState: {
|
||||
opened: 0,
|
||||
all: 3,
|
||||
closed: 0,
|
||||
__typename: 'WorkItemStateCountsType',
|
||||
|
|
@ -2309,6 +2331,7 @@ export const mockRolledUpCountsByType = [
|
|||
},
|
||||
{
|
||||
countsByState: {
|
||||
opened: 0,
|
||||
all: 5,
|
||||
closed: 2,
|
||||
__typename: 'WorkItemStateCountsType',
|
||||
|
|
@ -2323,6 +2346,7 @@ export const mockRolledUpCountsByType = [
|
|||
},
|
||||
{
|
||||
countsByState: {
|
||||
opened: 0,
|
||||
all: 2,
|
||||
closed: 1,
|
||||
__typename: 'WorkItemStateCountsType',
|
||||
|
|
@ -2358,6 +2382,138 @@ export const mockHierarchyWidget = {
|
|||
__typename: 'WorkItemWidgetHierarchy',
|
||||
};
|
||||
|
||||
export const mockOpenChildrenCount = {
|
||||
data: {
|
||||
namespace: {
|
||||
id: 'gid://gitlab/Group/33',
|
||||
workItem: {
|
||||
id: 'gid://gitlab/WorkItem/843',
|
||||
widgets: [
|
||||
{
|
||||
type: 'HIERARCHY',
|
||||
rolledUpCountsByType: [
|
||||
{
|
||||
countsByState: {
|
||||
opened: 0,
|
||||
all: 0,
|
||||
closed: 0,
|
||||
__typename: 'WorkItemStateCountsType',
|
||||
},
|
||||
workItemType: {
|
||||
id: 'gid://gitlab/WorkItems::Type/8',
|
||||
name: 'Epic',
|
||||
iconName: 'issue-type-epic',
|
||||
__typename: 'WorkItemType',
|
||||
},
|
||||
__typename: 'WorkItemTypeCountsByState',
|
||||
},
|
||||
{
|
||||
countsByState: {
|
||||
opened: 1,
|
||||
all: 1,
|
||||
closed: 0,
|
||||
__typename: 'WorkItemStateCountsType',
|
||||
},
|
||||
workItemType: {
|
||||
id: 'gid://gitlab/WorkItems::Type/1',
|
||||
name: 'Issue',
|
||||
iconName: 'issue-type-issue',
|
||||
__typename: 'WorkItemType',
|
||||
},
|
||||
__typename: 'WorkItemTypeCountsByState',
|
||||
},
|
||||
{
|
||||
countsByState: {
|
||||
opened: 0,
|
||||
all: 0,
|
||||
closed: 0,
|
||||
__typename: 'WorkItemStateCountsType',
|
||||
},
|
||||
workItemType: {
|
||||
id: 'gid://gitlab/WorkItems::Type/5',
|
||||
name: 'Task',
|
||||
iconName: 'issue-type-task',
|
||||
__typename: 'WorkItemType',
|
||||
},
|
||||
__typename: 'WorkItemTypeCountsByState',
|
||||
},
|
||||
],
|
||||
__typename: 'WorkItemWidgetHierarchy',
|
||||
},
|
||||
],
|
||||
__typename: 'WorkItem',
|
||||
},
|
||||
__typename: 'Namespace',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const mockNoOpenChildrenCount = {
|
||||
data: {
|
||||
namespace: {
|
||||
id: 'gid://gitlab/Group/33',
|
||||
workItem: {
|
||||
id: 'gid://gitlab/WorkItem/843',
|
||||
widgets: [
|
||||
{
|
||||
type: 'HIERARCHY',
|
||||
rolledUpCountsByType: [
|
||||
{
|
||||
countsByState: {
|
||||
opened: 0,
|
||||
all: 0,
|
||||
closed: 0,
|
||||
__typename: 'WorkItemStateCountsType',
|
||||
},
|
||||
workItemType: {
|
||||
id: 'gid://gitlab/WorkItems::Type/8',
|
||||
name: 'Epic',
|
||||
iconName: 'issue-type-epic',
|
||||
__typename: 'WorkItemType',
|
||||
},
|
||||
__typename: 'WorkItemTypeCountsByState',
|
||||
},
|
||||
{
|
||||
countsByState: {
|
||||
opened: 0,
|
||||
all: 0,
|
||||
closed: 0,
|
||||
__typename: 'WorkItemStateCountsType',
|
||||
},
|
||||
workItemType: {
|
||||
id: 'gid://gitlab/WorkItems::Type/1',
|
||||
name: 'Issue',
|
||||
iconName: 'issue-type-issue',
|
||||
__typename: 'WorkItemType',
|
||||
},
|
||||
__typename: 'WorkItemTypeCountsByState',
|
||||
},
|
||||
{
|
||||
countsByState: {
|
||||
opened: 0,
|
||||
all: 0,
|
||||
closed: 0,
|
||||
__typename: 'WorkItemStateCountsType',
|
||||
},
|
||||
workItemType: {
|
||||
id: 'gid://gitlab/WorkItems::Type/5',
|
||||
name: 'Task',
|
||||
iconName: 'issue-type-task',
|
||||
__typename: 'WorkItemType',
|
||||
},
|
||||
__typename: 'WorkItemTypeCountsByState',
|
||||
},
|
||||
],
|
||||
__typename: 'WorkItemWidgetHierarchy',
|
||||
},
|
||||
],
|
||||
__typename: 'WorkItem',
|
||||
},
|
||||
__typename: 'Namespace',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const workItemHierarchyTreeResponse = {
|
||||
data: {
|
||||
workItem: {
|
||||
|
|
|
|||
|
|
@ -14,15 +14,15 @@ RSpec.describe Gitlab::Fp::Result, feature_category: :shared do
|
|||
context 'when checked with .ok? and .err?' do
|
||||
it 'works with ok result' do
|
||||
result = Gitlab::Fp::Result.ok(:success)
|
||||
expect(result.ok?).to eq(true)
|
||||
expect(result.err?).to eq(false)
|
||||
expect(result.ok?).to be(true)
|
||||
expect(result.err?).to be(false)
|
||||
expect(result.unwrap).to eq(:success)
|
||||
end
|
||||
|
||||
it 'works with error result' do
|
||||
result = Gitlab::Fp::Result.err(:failure)
|
||||
expect(result.err?).to eq(true)
|
||||
expect(result.ok?).to eq(false)
|
||||
expect(result.err?).to be(true)
|
||||
expect(result.ok?).to be(false)
|
||||
expect(result.unwrap_err).to eq(:failure)
|
||||
end
|
||||
end
|
||||
|
|
@ -97,7 +97,7 @@ RSpec.describe Gitlab::Fp::Result, feature_category: :shared do
|
|||
.and_then(->(value) { Gitlab::Fp::Result.ok(value + 1) })
|
||||
.and_then(->(value) { Gitlab::Fp::Result.ok(value + 1) })
|
||||
|
||||
expect(final_result.ok?).to eq(true)
|
||||
expect(final_result.ok?).to be(true)
|
||||
expect(final_result.unwrap).to eq(3)
|
||||
end
|
||||
|
||||
|
|
@ -108,7 +108,7 @@ RSpec.describe Gitlab::Fp::Result, feature_category: :shared do
|
|||
.and_then(->(value) { Gitlab::Fp::Result.err("invalid: #{value}") })
|
||||
.and_then(->(value) { Gitlab::Fp::Result.ok(value + 1) })
|
||||
|
||||
expect(final_result.err?).to eq(true)
|
||||
expect(final_result.err?).to be(true)
|
||||
expect(final_result.unwrap_err).to eq('invalid: 1')
|
||||
end
|
||||
end
|
||||
|
|
@ -137,7 +137,7 @@ RSpec.describe Gitlab::Fp::Result, feature_category: :shared do
|
|||
.and_then(::MyModuleUsingResult.method(:double))
|
||||
.and_then(::MyModuleUsingResult::MyClassUsingResult.method(:triple))
|
||||
|
||||
expect(final_result.ok?).to eq(true)
|
||||
expect(final_result.ok?).to be(true)
|
||||
expect(final_result.unwrap).to eq(6)
|
||||
end
|
||||
|
||||
|
|
@ -150,7 +150,7 @@ RSpec.describe Gitlab::Fp::Result, feature_category: :shared do
|
|||
.and_then(::MyModuleUsingResult.method(:return_err))
|
||||
.and_then(::MyModuleUsingResult.method(:double))
|
||||
|
||||
expect(final_result.err?).to eq(true)
|
||||
expect(final_result.err?).to be(true)
|
||||
expect(final_result.unwrap_err).to eq('invalid: 6')
|
||||
end
|
||||
end
|
||||
|
|
@ -195,7 +195,7 @@ RSpec.describe Gitlab::Fp::Result, feature_category: :shared do
|
|||
.map(->(value) { value + 1 })
|
||||
.map(->(value) { value + 1 })
|
||||
|
||||
expect(final_result.ok?).to eq(true)
|
||||
expect(final_result.ok?).to be(true)
|
||||
expect(final_result.unwrap).to eq(3)
|
||||
end
|
||||
|
||||
|
|
@ -206,7 +206,7 @@ RSpec.describe Gitlab::Fp::Result, feature_category: :shared do
|
|||
.and_then(->(value) { Gitlab::Fp::Result.err("invalid: #{value}") })
|
||||
.map(->(value) { value + 1 })
|
||||
|
||||
expect(final_result.err?).to eq(true)
|
||||
expect(final_result.err?).to be(true)
|
||||
expect(final_result.unwrap_err).to eq('invalid: 1')
|
||||
end
|
||||
end
|
||||
|
|
@ -231,7 +231,7 @@ RSpec.describe Gitlab::Fp::Result, feature_category: :shared do
|
|||
.map(::MyModuleNotUsingResult.method(:double))
|
||||
.map(::MyModuleNotUsingResult::MyClassNotUsingResult.method(:triple))
|
||||
|
||||
expect(final_result.ok?).to eq(true)
|
||||
expect(final_result.ok?).to be(true)
|
||||
expect(final_result.unwrap).to eq(6)
|
||||
end
|
||||
|
||||
|
|
@ -243,7 +243,7 @@ RSpec.describe Gitlab::Fp::Result, feature_category: :shared do
|
|||
.and_then(->(value) { Gitlab::Fp::Result.err("invalid: #{value}") })
|
||||
.map(::MyModuleUsingResult.method(:double))
|
||||
|
||||
expect(final_result.err?).to eq(true)
|
||||
expect(final_result.err?).to be(true)
|
||||
expect(final_result.unwrap_err).to eq('invalid: 2')
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,51 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Search::Cache, feature_category: :global_search do
|
||||
let_it_be(:resource) { create(:user) }
|
||||
let(:action) { 'test' }
|
||||
let(:cache_key) { "search_user_#{resource.id}_#{action}" }
|
||||
|
||||
describe '.lookup' do
|
||||
it 'uses Rails.cache to fetch the value' do
|
||||
expect(Rails.cache).to receive(:fetch)
|
||||
.with(cache_key, expires_in: described_class::DEFAULT_EXPIRES_IN)
|
||||
.and_call_original
|
||||
|
||||
described_class.lookup(resource: resource, action: action) { 'cached_value' }
|
||||
end
|
||||
|
||||
context 'when caching is disabled' do
|
||||
it 'does not use the cache' do
|
||||
expect(Rails.cache).not_to receive(:fetch)
|
||||
result = described_class.lookup(resource: resource, action: action, enabled: false) { 'uncached_value' }
|
||||
expect(result).to eq('uncached_value')
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a custom cache key' do
|
||||
let(:custom_cache_key) { 'my_cache_key' }
|
||||
|
||||
it 'uses the cache key' do
|
||||
expect(Rails.cache).to receive(:fetch)
|
||||
.with(custom_cache_key, expires_in: described_class::DEFAULT_EXPIRES_IN)
|
||||
.and_call_original
|
||||
|
||||
described_class.lookup(resource: resource, action: action, cache_key: custom_cache_key) { 'cached_value' }
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a custom expiration' do
|
||||
let(:custom_expiration) { 5.minutes }
|
||||
|
||||
it 'uses the expiration' do
|
||||
expect(Rails.cache).to receive(:fetch)
|
||||
.with(cache_key, expires_in: custom_expiration)
|
||||
.and_call_original
|
||||
|
||||
described_class.lookup(resource: resource, action: action, expires_in: custom_expiration) { 'cached_value' }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1363,7 +1363,6 @@
|
|||
- './ee/spec/models/namespaces/free_user_cap_spec.rb'
|
||||
- './ee/spec/models/namespaces/storage/root_excess_size_spec.rb'
|
||||
- './ee/spec/models/namespaces/storage/root_size_spec.rb'
|
||||
- './ee/spec/models/note_spec.rb'
|
||||
- './ee/spec/models/packages/package_file_spec.rb'
|
||||
- './ee/spec/models/path_lock_spec.rb'
|
||||
- './ee/spec/models/plan_spec.rb'
|
||||
|
|
|
|||
|
|
@ -422,6 +422,8 @@ RSpec.shared_examples 'work items notifications' do
|
|||
|
||||
click_button(class: 'gl-toggle')
|
||||
|
||||
wait_for_requests
|
||||
|
||||
expect(page).to have_button(class: 'gl-toggle is-checked')
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -46,3 +46,51 @@ RSpec.shared_examples 'merge request approval policy editor' do
|
|||
expect(page).to have_current_path(project_merge_request_path(policy_management_project, 1))
|
||||
end
|
||||
end
|
||||
|
||||
# Requires the url to the policy editor:
|
||||
# - path_to_merge_request_approval_policy_editor
|
||||
# - path_to_merge_request_approval_policy_editor
|
||||
RSpec.shared_examples 'merge request approval policy invalid policy properties' do
|
||||
let(:merge_request_approval_policy_with_exceeding_number_of_rules) do
|
||||
fixture_file('security_orchestration/merge_request_approval_policy_with_exceeding_number_of_rules.yml', dir: 'ee')
|
||||
end
|
||||
|
||||
it "fails to create a policy without name" do
|
||||
click_button _('Configure with a merge request')
|
||||
|
||||
expect(page).to have_content('Empty policy name')
|
||||
expect(page).to have_current_path(path_to_merge_request_approval_policy_editor)
|
||||
end
|
||||
|
||||
it "fails to create a policy without approvers" do
|
||||
fill_in _('Name'), with: 'Missing approvers'
|
||||
click_button _('Configure with a merge request')
|
||||
|
||||
expect(page).to have_content('Required approvals exceed eligible approvers.')
|
||||
expect(page).to have_current_path(path_to_merge_request_approval_policy_editor)
|
||||
end
|
||||
|
||||
it "fails to create a policy without rules" do
|
||||
fill_in _('Name'), with: 'Missing rules'
|
||||
|
||||
page.within(find_by_testid('actions-section')) do
|
||||
select_from_listbox 'Roles', from: 'Choose approver type'
|
||||
select_from_listbox 'Owner', from: 'Choose specific role'
|
||||
end
|
||||
|
||||
click_button _('Configure with a merge request')
|
||||
|
||||
expect(page).to have_content("Invalid policy YAML")
|
||||
expect(page).to have_current_path(path_to_merge_request_approval_policy_editor)
|
||||
end
|
||||
|
||||
it "fails to create policy with exceeding number of rules" do
|
||||
click_button _('.yaml mode')
|
||||
editor_set_value(merge_request_approval_policy_with_exceeding_number_of_rules.to_s)
|
||||
|
||||
click_button _('Configure with a merge request')
|
||||
|
||||
expect(page).to have_content("Invalid policy YAML")
|
||||
expect(page).to have_current_path(path_to_merge_request_approval_policy_editor)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
Loading…
Reference in New Issue