Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
d2ce6b490c
commit
4315ea0bc4
|
|
@ -5,7 +5,7 @@ workflow:
|
|||
name: $PIPELINE_NAME
|
||||
|
||||
include:
|
||||
- component: "gitlab.com/gitlab-org/quality/pipeline-common/allure-report@8.15.1"
|
||||
- component: "gitlab.com/gitlab-org/quality/pipeline-common/allure-report@8.16.0"
|
||||
inputs:
|
||||
job_name: "e2e-test-report"
|
||||
job_stage: "report"
|
||||
|
|
@ -15,7 +15,7 @@ include:
|
|||
gitlab_auth_token_variable_name: "PROJECT_TOKEN_FOR_CI_SCRIPTS_API_USAGE"
|
||||
allure_job_name: "${QA_RUN_TYPE}"
|
||||
- project: gitlab-org/quality/pipeline-common
|
||||
ref: 8.15.1
|
||||
ref: 8.16.0
|
||||
file:
|
||||
- /ci/base.gitlab-ci.yml
|
||||
- /ci/knapsack-report.yml
|
||||
|
|
|
|||
|
|
@ -16,4 +16,4 @@ variables:
|
|||
QA_OMNIBUS_MR_TESTS: "only-smoke"
|
||||
# Retry failed specs in separate process
|
||||
QA_RETRY_FAILED_SPECS: "true"
|
||||
GITLAB_HELM_CHART_REF: "c7532b6e1ba98d5663b58012a879a781689db916" # helm chart ref used by test-on-cng pipeline
|
||||
GITLAB_HELM_CHART_REF: "4c21d49231b948fa31f502feee136f688e3abf76" # helm chart ref used by test-on-cng pipeline
|
||||
|
|
|
|||
|
|
@ -0,0 +1,119 @@
|
|||
<!--
|
||||
# README first!
|
||||
|
||||
This template covers steps required to do a Root Cause Analysis for unplanned upgrade stop.
|
||||
|
||||
Unplanned upgrade stop documentation: https://handbook.gitlab.com/handbook/engineering/unplanned-upgrade-stop/
|
||||
|
||||
Example RCA as a reference: https://gitlab.com/gitlab-org/gitlab/-/issues/423895
|
||||
-->
|
||||
|
||||
## Summary
|
||||
|
||||
A brief summary of what happened. Try to make it as executive-friendly as possible.
|
||||
|
||||
- Upgrade path(s) affected:
|
||||
- Upgrade type: downtime / zero downtime upgrade
|
||||
- [Type of degradation](https://handbook.gitlab.com/handbook/engineering/unplanned-upgrade-stop/#unplanned-upgrade-types): Database migration error / Configuration changes / Breaking functionality changes
|
||||
- Relevant bug issue:
|
||||
- Team attribution: ~group::
|
||||
|
||||
## Impact & Metrics
|
||||
|
||||
Start with the following:
|
||||
|
||||
| Question | Answer |
|
||||
| ----- | ----- |
|
||||
| Who was impacted? | (i.e. customers with specific environment configurations or data ...) |
|
||||
| How many customers affected? | |
|
||||
|
||||
Include any additional metrics that are of relevance.
|
||||
|
||||
## Detection & Response
|
||||
|
||||
Start with the following:
|
||||
|
||||
| Question | Answer |
|
||||
| ----- | ----- |
|
||||
| When was the issue detected? | YYYY-MM-DD |
|
||||
| How was the issue detected? | (i.e. Support request, Bug raised, ...) |
|
||||
| How long did it take from the start of the issue to its detection? | |
|
||||
| How long did it take from detection to remediation? | |
|
||||
| What steps were taken to remediate? | |
|
||||
| Was patch release created? | |
|
||||
| Was patch backported to older versions? | |
|
||||
|
||||
## Timeline
|
||||
|
||||
YYYY-MM-DD
|
||||
|
||||
- something happened
|
||||
- something else happened
|
||||
- ...
|
||||
|
||||
YYYY-MM-DD+1
|
||||
|
||||
- and then this happened
|
||||
- and more happened
|
||||
- ...
|
||||
|
||||
## Root Cause Analysis
|
||||
|
||||
The purpose of this document is to understand the reasons that caused an issue, and to create mechanisms to prevent it from recurring in the future. A root cause can **never be a person**, the way of writing has to refer to the system and the context rather than the specific actors.
|
||||
|
||||
### What is causing upgrade error
|
||||
|
||||
Start with the following:
|
||||
|
||||
- What is the cause of the upgrade error?
|
||||
- What steps could be done to reproduce the upgrade error?
|
||||
|
||||
### What can be improved
|
||||
|
||||
DRI: Corresponding Engineering team
|
||||
|
||||
- Using the root cause analysis, explain what can be improved to prevent this from happening again.
|
||||
- What changes to our tooling or review process would have prevented this issue?
|
||||
- Is there an existing issue that would have either prevented this issue or reduced the impact?
|
||||
- Did we have any indication or beforehand knowledge that this issue might take place?
|
||||
|
||||
DRI: Test Platform
|
||||
|
||||
- What is the cause of the test gap on integration and system level testing?
|
||||
- What can be done to increase test coverage?
|
||||
- Did relevant tests pass for this upgrade path?
|
||||
|
||||
## Corrective actions
|
||||
|
||||
- Link issues that have been created as corrective actions from this issue.
|
||||
- For each issue, include the following:
|
||||
- `<Issue link>` - Issue labeled as ~"corrective action" ~upgrades.
|
||||
- An estimated date of completion of the corrective action. Priority of the issue should correspond with RCA severity.
|
||||
|
||||
## Guidelines
|
||||
|
||||
- [Blameless RCA Guideline](https://about.gitlab.com/handbook/customer-success/professional-services-engineering/workflows/internal/root-cause-analysis.html)
|
||||
- [5 whys](https://en.wikipedia.org/wiki/5_Whys)
|
||||
|
||||
/confidential
|
||||
/label ~RCA ~upgrades ~"type::ignore"
|
||||
|
||||
<!--
|
||||
Workflow and other relevant labels
|
||||
|
||||
Select appropriate severity based on impact. For example:
|
||||
~"severity::1" when unplanned upgrade stop needed to be added,
|
||||
~"severity::2" when no unplanned stop was introduced but patch releases needed to be created
|
||||
https://handbook.gitlab.com/handbook/engineering/infrastructure/engineering-productivity/issue-triage/#severity
|
||||
-->
|
||||
/label ~severity::
|
||||
|
||||
<!--
|
||||
Specify Engineering group owning the bug related to this RCA
|
||||
-->
|
||||
/label ~group::
|
||||
|
||||
<!--
|
||||
Link existing upgrade bug to this RCA
|
||||
-->
|
||||
/relate
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
<script>
|
||||
import { GlLoadingIcon, GlPagination, GlSprintf, GlAlert } from '@gitlab/ui';
|
||||
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
|
||||
import { debounce } from 'lodash';
|
||||
import { debounce, throttle } from 'lodash';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { mapState, mapGetters, mapActions } from 'vuex';
|
||||
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||
|
|
@ -19,7 +19,7 @@ import { InternalEvents } from '~/tracking';
|
|||
import { isSingleViewStyle } from '~/helpers/diffs_helper';
|
||||
import { helpPagePath } from '~/helpers/help_page_helper';
|
||||
import { parseBoolean, handleLocationHash } from '~/lib/utils/common_utils';
|
||||
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
|
||||
import { BV_HIDE_TOOLTIP, DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
|
||||
import { Mousetrap } from '~/lib/mousetrap';
|
||||
import { updateHistory, getLocationHash } from '~/lib/utils/url_utility';
|
||||
import { __ } from '~/locale';
|
||||
|
|
@ -309,6 +309,13 @@ export default {
|
|||
renderFileTree() {
|
||||
return this.renderDiffFiles && this.showTreeList;
|
||||
},
|
||||
hideTooltips() {
|
||||
const hide = () => {
|
||||
if (!this.shouldShow) return;
|
||||
this.$root.$emit(BV_HIDE_TOOLTIP);
|
||||
};
|
||||
return throttle(hide, 100);
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
commit(newCommit, oldCommit) {
|
||||
|
|
@ -387,6 +394,7 @@ export default {
|
|||
|
||||
this.subscribeToVirtualScrollingEvents();
|
||||
window.addEventListener('hashchange', this.handleHashChange);
|
||||
window.addEventListener('scroll', this.hideTooltips);
|
||||
},
|
||||
beforeCreate() {
|
||||
diffsApp.instrument();
|
||||
|
|
@ -414,6 +422,7 @@ export default {
|
|||
this.removeEventListeners();
|
||||
|
||||
window.removeEventListener('hashchange', this.handleHashChange);
|
||||
window.removeEventListener('scroll', this.hideTooltips);
|
||||
|
||||
diffsEventHub.$off('scrollToFileHash', this.scrollVirtualScrollerToFileHash);
|
||||
diffsEventHub.$off('scrollToIndex', this.scrollVirtualScrollerToIndex);
|
||||
|
|
|
|||
|
|
@ -256,6 +256,8 @@ export default {
|
|||
this.idState.moreActionsShown = val;
|
||||
},
|
||||
toggleReview(newReviewedStatus) {
|
||||
// this is the easiest way to hide an already open tooltip that triggers on focus
|
||||
document.activeElement.blur();
|
||||
const autoCollapsed =
|
||||
this.isCollapsed && collapsedType(this.diffFile) === DIFF_FILE_AUTOMATIC_COLLAPSE;
|
||||
const open = this.expanded;
|
||||
|
|
|
|||
|
|
@ -26,7 +26,6 @@ const CREATE_BRANCH = 'create-branch';
|
|||
const VALIDATION_TYPE_BRANCH_UNAVAILABLE = 'branch_unavailable';
|
||||
const VALIDATION_TYPE_INVALID_CHARS = 'invalid_chars';
|
||||
|
||||
const INPUT_TARGET_PROJECT = 'project';
|
||||
const INPUT_TARGET_BRANCH = 'branch';
|
||||
const INPUT_TARGET_REF = 'ref';
|
||||
|
||||
|
|
@ -62,32 +61,36 @@ export default class CreateMergeRequestDropdown {
|
|||
constructor(wrapperEl) {
|
||||
this.wrapperEl = wrapperEl;
|
||||
this.availableButton = this.wrapperEl.querySelector('.available');
|
||||
this.branchNameInput = this.wrapperEl.querySelector('.js-branch-name');
|
||||
this.branchInput = this.wrapperEl.querySelector('.js-branch-name');
|
||||
this.branchMessage = this.wrapperEl.querySelector('.js-branch-message');
|
||||
this.targetProjectInput = this.wrapperEl.querySelector('.js-target-project');
|
||||
this.createMergeRequestButton = this.wrapperEl.querySelector('.js-create-merge-request');
|
||||
this.createMergeRequestLoading = this.createMergeRequestButton.querySelector('.js-spinner');
|
||||
this.createTargetButton = this.wrapperEl.querySelector('.js-create-target');
|
||||
this.dropdownList = this.wrapperEl.querySelector('.dropdown-menu');
|
||||
this.dropdownToggle = this.wrapperEl.querySelector('.js-dropdown-toggle');
|
||||
this.sourceRefInput = this.wrapperEl.querySelector('.js-ref');
|
||||
this.refInput = this.wrapperEl.querySelector('.js-ref');
|
||||
this.refMessage = this.wrapperEl.querySelector('.js-ref-message');
|
||||
this.unavailableButton = this.wrapperEl.querySelector('.unavailable');
|
||||
this.unavailableButtonSpinner = this.unavailableButton.querySelector('.js-create-mr-spinner');
|
||||
this.unavailableButtonText = this.unavailableButton.querySelector('.text');
|
||||
|
||||
this.isBranchCreationMode = false;
|
||||
this.branchCreated = false;
|
||||
this.branchIsValid = true;
|
||||
this.canCreatePath = this.wrapperEl.dataset.canCreatePath;
|
||||
this.createBranchPath = this.wrapperEl.dataset.createBranchPath;
|
||||
this.createMrPath = this.wrapperEl.dataset.createMrPath;
|
||||
this.droplabInitialized = false;
|
||||
this.isCreatingBranch = false;
|
||||
this.isCreatingMergeRequest = false;
|
||||
this.isGettingRef = false;
|
||||
this.refCancelToken = null;
|
||||
this.mergeRequestCreated = false;
|
||||
this.refDebounce = debounce((value, target) => this.getRef(value, target), 500);
|
||||
this.refIsValid = true;
|
||||
this.suggestedRef = this.refName;
|
||||
this.cancelSources = new Set();
|
||||
this.refsPath = this.wrapperEl.dataset.refsPath;
|
||||
this.suggestedRef = this.refInput.value;
|
||||
this.projectPath = this.wrapperEl.dataset.projectPath;
|
||||
this.projectId = this.wrapperEl.dataset.projectId;
|
||||
|
||||
// These regexps are used to replace
|
||||
// a backend generated new branch name and its source (ref)
|
||||
|
|
@ -111,53 +114,6 @@ export default class CreateMergeRequestDropdown {
|
|||
}
|
||||
}
|
||||
|
||||
get selectedProjectOption() {
|
||||
if (this.targetProjectInput) {
|
||||
return this.targetProjectInput.options[this.targetProjectInput.options.selectedIndex];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
get sourceProject() {
|
||||
return {
|
||||
refsPath: this.wrapperEl.dataset.refsPath,
|
||||
projectPath: this.wrapperEl.dataset.projectPath,
|
||||
projectId: this.wrapperEl.dataset.projectId,
|
||||
};
|
||||
}
|
||||
|
||||
get targetProject() {
|
||||
if (this.targetProjectInput) {
|
||||
const option = this.selectedProjectOption;
|
||||
return {
|
||||
refsPath: this.targetProjectInput.value,
|
||||
projectId: option.dataset.id,
|
||||
projectPath: option.dataset.fullPath,
|
||||
};
|
||||
}
|
||||
return this.sourceProject;
|
||||
}
|
||||
|
||||
get branchName() {
|
||||
return this.branchNameInput.value || this.branchNameInput.placeholder;
|
||||
}
|
||||
|
||||
get refName() {
|
||||
return this.sourceRefInput.value;
|
||||
}
|
||||
|
||||
get canCreatePath() {
|
||||
return this.wrapperEl.dataset.canCreatePath;
|
||||
}
|
||||
|
||||
get createBranchPath() {
|
||||
return this.wrapperEl.dataset.createBranchPath;
|
||||
}
|
||||
|
||||
get createMrPath() {
|
||||
return this.selectedProjectOption?.dataset?.createMrPath || this.wrapperEl.dataset.createMrPath;
|
||||
}
|
||||
|
||||
available() {
|
||||
this.availableButton.classList.remove('hidden');
|
||||
this.unavailableButton.classList.add('hidden');
|
||||
|
|
@ -172,36 +128,17 @@ export default class CreateMergeRequestDropdown {
|
|||
'click',
|
||||
this.onClickCreateMergeRequestButton.bind(this),
|
||||
);
|
||||
this.branchNameInput.addEventListener('input', this.onBranchChange.bind(this));
|
||||
this.branchNameInput.addEventListener('keyup', this.onBranchChange.bind(this));
|
||||
if (this.targetProjectInput) {
|
||||
this.targetProjectInput.addEventListener('change', this.onTargetProjectChange.bind(this));
|
||||
}
|
||||
this.branchInput.addEventListener('input', this.onChangeInput.bind(this));
|
||||
this.branchInput.addEventListener('keyup', this.onChangeInput.bind(this));
|
||||
this.dropdownToggle.addEventListener('click', this.onClickSetFocusOnBranchNameInput.bind(this));
|
||||
// Detect for example when user pastes ref using the mouse
|
||||
this.sourceRefInput.addEventListener('input', this.onRefChange.bind(this));
|
||||
this.refInput.addEventListener('input', this.onChangeInput.bind(this));
|
||||
// Detect for example when user presses right arrow to apply the suggested ref
|
||||
this.sourceRefInput.addEventListener('keyup', this.onRefChange.bind(this));
|
||||
this.refInput.addEventListener('keyup', this.onChangeInput.bind(this));
|
||||
// Detect when user clicks inside the input to apply the suggested ref
|
||||
this.sourceRefInput.addEventListener('click', this.onRefChange.bind(this));
|
||||
this.refInput.addEventListener('click', this.onChangeInput.bind(this));
|
||||
// Detect when user presses tab to apply the suggested ref
|
||||
this.sourceRefInput.addEventListener(
|
||||
'keydown',
|
||||
CreateMergeRequestDropdown.processTab.bind(this),
|
||||
);
|
||||
Array.from(this.wrapperEl.querySelectorAll('.js-type-toggle')).forEach((el) => {
|
||||
el.addEventListener('click', this.toggleBranchCreationMode.bind(this));
|
||||
});
|
||||
}
|
||||
|
||||
toggleBranchCreationMode(event) {
|
||||
if (event.currentTarget.dataset.value === 'create-branch') {
|
||||
this.disableProjectSelect();
|
||||
this.isBranchCreationMode = true;
|
||||
} else {
|
||||
this.enableProjectSelect();
|
||||
this.isBranchCreationMode = false;
|
||||
}
|
||||
this.refInput.addEventListener('keydown', CreateMergeRequestDropdown.processTab.bind(this));
|
||||
}
|
||||
|
||||
checkAbilityToCreateBranch() {
|
||||
|
|
@ -228,6 +165,8 @@ export default class CreateMergeRequestDropdown {
|
|||
}
|
||||
})
|
||||
.catch(() => {
|
||||
this.unavailable();
|
||||
this.disable();
|
||||
createAlert({
|
||||
message: __('Failed to check related branches.'),
|
||||
});
|
||||
|
|
@ -235,21 +174,19 @@ export default class CreateMergeRequestDropdown {
|
|||
}
|
||||
|
||||
createBranch(navigateToBranch = true) {
|
||||
// eslint-disable-next-line @gitlab/require-i18n-strings
|
||||
if (!this.refName) return Promise.reject(new Error('Missing ref name'));
|
||||
|
||||
this.isCreatingBranch = true;
|
||||
|
||||
const endpoint = createEndpoint(
|
||||
this.sourceProject.projectPath,
|
||||
mergeUrlParams({ ref: this.refName, branch_name: this.branchName }, this.createBranchPath),
|
||||
this.projectPath,
|
||||
mergeUrlParams(
|
||||
{ ref: this.refInput.value, branch_name: this.branchInput.value },
|
||||
this.createBranchPath,
|
||||
),
|
||||
);
|
||||
|
||||
return axios
|
||||
.post(endpoint, {
|
||||
confidential_issue_project_id: canCreateConfidentialMergeRequest()
|
||||
? this.sourceProject.projectId
|
||||
: null,
|
||||
confidential_issue_project_id: canCreateConfidentialMergeRequest() ? this.projectId : null,
|
||||
})
|
||||
.then(({ data }) => {
|
||||
this.branchCreated = true;
|
||||
|
|
@ -273,14 +210,14 @@ export default class CreateMergeRequestDropdown {
|
|||
.then(() => {
|
||||
let path = canCreateConfidentialMergeRequest()
|
||||
? this.createMrPath.replace(
|
||||
this.targetProject.projectPath,
|
||||
this.projectPath,
|
||||
confidentialMergeRequestState.selectedProject.pathWithNamespace,
|
||||
)
|
||||
: this.createMrPath;
|
||||
path = mergeUrlParams(
|
||||
{
|
||||
'merge_request[target_branch]': this.refName,
|
||||
'merge_request[source_branch]': this.branchName,
|
||||
'merge_request[target_branch]': this.refInput.value,
|
||||
'merge_request[source_branch]': this.branchInput.value,
|
||||
},
|
||||
path,
|
||||
);
|
||||
|
|
@ -315,18 +252,6 @@ export default class CreateMergeRequestDropdown {
|
|||
this.createTargetButton.removeAttribute('disabled');
|
||||
}
|
||||
|
||||
disableProjectSelect() {
|
||||
if (!this.targetProjectInput) return;
|
||||
this.targetProjectInput.classList.add('disabled');
|
||||
this.targetProjectInput.setAttribute('disabled', 'disabled');
|
||||
}
|
||||
|
||||
enableProjectSelect() {
|
||||
if (!this.targetProjectInput) return;
|
||||
this.targetProjectInput.classList.remove('disabled');
|
||||
this.targetProjectInput.removeAttribute('disabled');
|
||||
}
|
||||
|
||||
static findByValue(objects, ref, returnFirstMatch = false) {
|
||||
if (!objects || !objects.length) return false;
|
||||
if (objects.indexOf(ref) > -1) return ref;
|
||||
|
|
@ -370,17 +295,13 @@ export default class CreateMergeRequestDropdown {
|
|||
}
|
||||
|
||||
getRef(ref, target = 'all') {
|
||||
const source = axios.CancelToken.source();
|
||||
this.cancelSources.add(source);
|
||||
if (!ref) return false;
|
||||
|
||||
const project =
|
||||
target === INPUT_TARGET_REF && !this.isBranchCreationMode
|
||||
? this.targetProject
|
||||
: this.sourceProject;
|
||||
this.refCancelToken = axios.CancelToken.source();
|
||||
|
||||
return axios
|
||||
.get(`${createEndpoint(project.projectPath, project.refsPath)}${encodeURIComponent(ref)}`, {
|
||||
cancelToken: source.token,
|
||||
.get(`${createEndpoint(this.projectPath, this.refsPath)}${encodeURIComponent(ref)}`, {
|
||||
cancelToken: this.refCancelToken.token,
|
||||
})
|
||||
.then(({ data }) => {
|
||||
const branches = data[Object.keys(data)[0]];
|
||||
|
|
@ -396,22 +317,33 @@ export default class CreateMergeRequestDropdown {
|
|||
this.suggestedRef = result;
|
||||
}
|
||||
|
||||
this.updateInputState(target, ref, result);
|
||||
this.isGettingRef = false;
|
||||
|
||||
return this.updateInputState(target, ref, result);
|
||||
})
|
||||
.catch((thrown) => {
|
||||
if (axios.isCancel(thrown)) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
this.unavailable();
|
||||
this.disable();
|
||||
createAlert({
|
||||
message: __('Failed to get ref.'),
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
this.cancelSources.delete(source);
|
||||
|
||||
this.isGettingRef = false;
|
||||
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
getTargetData(target) {
|
||||
return {
|
||||
input: this[`${target}Input`],
|
||||
message: this[`${target}Message`],
|
||||
};
|
||||
}
|
||||
|
||||
hide() {
|
||||
this.wrapperEl.classList.add('hidden');
|
||||
}
|
||||
|
|
@ -440,53 +372,43 @@ export default class CreateMergeRequestDropdown {
|
|||
this.isCreatingMergeRequest ||
|
||||
this.mergeRequestCreated ||
|
||||
this.isCreatingBranch ||
|
||||
this.branchCreated
|
||||
this.branchCreated ||
|
||||
this.isGettingRef
|
||||
);
|
||||
}
|
||||
|
||||
beforeChange() {
|
||||
onChangeInput(event) {
|
||||
this.disable();
|
||||
let target;
|
||||
let value;
|
||||
|
||||
// User changed input, cancel to prevent previous request from interfering
|
||||
if (this.cancelSources.size !== 0) {
|
||||
this.cancelSources.forEach((token) => {
|
||||
token.cancel();
|
||||
this.cancelSources.delete(token);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onTargetProjectChange() {
|
||||
this.beforeChange();
|
||||
this.getRef(this.refName, INPUT_TARGET_REF);
|
||||
}
|
||||
|
||||
onBranchChange(event) {
|
||||
this.beforeChange();
|
||||
this.handleChange(event, INPUT_TARGET_BRANCH, this.branchName);
|
||||
}
|
||||
|
||||
onRefChange(event) {
|
||||
this.beforeChange();
|
||||
|
||||
const target = INPUT_TARGET_REF;
|
||||
|
||||
if (event.target !== document.activeElement) {
|
||||
this.handleChange(event, target, event.target.value);
|
||||
return;
|
||||
if (this.refCancelToken !== null) {
|
||||
this.refCancelToken.cancel();
|
||||
}
|
||||
|
||||
const value =
|
||||
event.target.value.slice(0, event.target.selectionStart) +
|
||||
event.target.value.slice(event.target.selectionEnd);
|
||||
if (event.target === this.branchInput) {
|
||||
target = INPUT_TARGET_BRANCH;
|
||||
({ value } = this.branchInput);
|
||||
} else if (event.target === this.refInput) {
|
||||
target = INPUT_TARGET_REF;
|
||||
if (event.target === document.activeElement) {
|
||||
value =
|
||||
event.target.value.slice(0, event.target.selectionStart) +
|
||||
event.target.value.slice(event.target.selectionEnd);
|
||||
} else {
|
||||
value = event.target.value;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.handleChange(event, target, value);
|
||||
}
|
||||
if (this.isGettingRef) return false;
|
||||
|
||||
handleChange(event, target, value) {
|
||||
// `ENTER` key submits the data.
|
||||
if (event.keyCode === 13 && this.inputsAreValid()) {
|
||||
event.preventDefault();
|
||||
this.createMergeRequestButton.click();
|
||||
return;
|
||||
return this.createMergeRequestButton.click();
|
||||
}
|
||||
|
||||
// If the input is empty, use the original value generated by the backend.
|
||||
|
|
@ -500,18 +422,19 @@ export default class CreateMergeRequestDropdown {
|
|||
this.enable();
|
||||
this.showAvailableMessage(target);
|
||||
this.refDebounce(value, target);
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (target !== INPUT_TARGET_PROJECT) this.showCheckingMessage(target);
|
||||
this.showCheckingMessage(target);
|
||||
this.refDebounce(value, target);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
onClickCreateMergeRequestButton(event) {
|
||||
let xhr = null;
|
||||
event.preventDefault();
|
||||
|
||||
if (this.cancelSources.size !== 0) return;
|
||||
|
||||
if (isConfidentialIssue() && !event.currentTarget.classList.contains('js-create-target')) {
|
||||
this.droplab.hooks.forEach((hook) => hook.list.toggle());
|
||||
|
||||
|
|
@ -522,7 +445,6 @@ export default class CreateMergeRequestDropdown {
|
|||
return;
|
||||
}
|
||||
|
||||
let xhr = null;
|
||||
if (event.currentTarget.dataset.action === CREATE_MERGE_REQUEST) {
|
||||
xhr = this.createMergeRequest();
|
||||
} else if (event.currentTarget.dataset.action === CREATE_BRANCH) {
|
||||
|
|
@ -542,12 +464,12 @@ export default class CreateMergeRequestDropdown {
|
|||
}
|
||||
|
||||
onClickSetFocusOnBranchNameInput() {
|
||||
this.branchNameInput.focus();
|
||||
this.branchInput.focus();
|
||||
}
|
||||
|
||||
// `TAB` autocompletes the source.
|
||||
static processTab(event) {
|
||||
if (event.keyCode !== 9) return;
|
||||
if (event.keyCode !== 9 || this.isGettingRef) return;
|
||||
|
||||
const selectedText = CreateMergeRequestDropdown.getInputSelectedText(this.refInput);
|
||||
|
||||
|
|
@ -560,22 +482,6 @@ export default class CreateMergeRequestDropdown {
|
|||
this.refInput.setSelectionRange(caretPositionEnd, caretPositionEnd);
|
||||
}
|
||||
|
||||
getTargetData(target) {
|
||||
if (target === INPUT_TARGET_BRANCH) {
|
||||
return {
|
||||
input: this.branchNameInput,
|
||||
message: this.branchMessage,
|
||||
};
|
||||
}
|
||||
if (target === INPUT_TARGET_REF) {
|
||||
return {
|
||||
input: this.sourceRefInput,
|
||||
message: this.refMessage,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
removeMessage(target) {
|
||||
const { input, message } = this.getTargetData(target);
|
||||
const inputClasses = ['gl-field-error-outline', 'gl-field-success-outline'];
|
||||
|
|
@ -628,8 +534,13 @@ export default class CreateMergeRequestDropdown {
|
|||
message.style.display = 'inline-block';
|
||||
}
|
||||
|
||||
unavailable() {
|
||||
this.availableButton.classList.add('hidden');
|
||||
this.unavailableButton.classList.remove('hidden');
|
||||
}
|
||||
|
||||
updateBranchName(suggestedBranchName) {
|
||||
this.branchNameInput.value = suggestedBranchName;
|
||||
this.branchInput.value = suggestedBranchName;
|
||||
this.updateInputState(INPUT_TARGET_BRANCH, suggestedBranchName, '');
|
||||
}
|
||||
|
||||
|
|
@ -648,20 +559,20 @@ export default class CreateMergeRequestDropdown {
|
|||
}
|
||||
|
||||
updateRefInput(ref, result) {
|
||||
this.sourceRefInput.dataset.value = ref;
|
||||
this.refInput.dataset.value = ref;
|
||||
if (ref === result) {
|
||||
this.refIsValid = true;
|
||||
this.showAvailableMessage(INPUT_TARGET_REF);
|
||||
} else {
|
||||
this.refIsValid = false;
|
||||
this.sourceRefInput.dataset.value = ref;
|
||||
this.refInput.dataset.value = ref;
|
||||
this.disableCreateAction();
|
||||
this.showNotAvailableMessage(INPUT_TARGET_REF);
|
||||
|
||||
// Show ref hint.
|
||||
if (result) {
|
||||
this.sourceRefInput.value = result;
|
||||
this.sourceRefInput.setSelectionRange(ref.length, result.length);
|
||||
this.refInput.value = result;
|
||||
this.refInput.setSelectionRange(ref.length, result.length);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ export default {
|
|||
i18n,
|
||||
DRAWER_Z_INDEX,
|
||||
removeBlobsHelpLink: helpPagePath('/user/project/repository/reducing_the_repo_size_using_git', {
|
||||
anchor: 'repository-cleanup',
|
||||
anchor: 'get-a-list-of-object-ids',
|
||||
}),
|
||||
modalCancel: { text: i18n.modalCancelText },
|
||||
components: { GlButton, GlDrawer, GlLink, GlFormTextarea, GlModal, GlFormInput },
|
||||
|
|
|
|||
|
|
@ -1,23 +1,18 @@
|
|||
import Api from '~/api';
|
||||
import { createAlert } from '~/alert';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import {
|
||||
visitUrl,
|
||||
setUrlParams,
|
||||
getBaseURL,
|
||||
queryToObject,
|
||||
objectToQuery,
|
||||
} from '~/lib/utils/url_utility';
|
||||
import { visitUrl, setUrlParams, getNormalizedURL } from '~/lib/utils/url_utility';
|
||||
import { logError } from '~/lib/logger';
|
||||
import { __ } from '~/locale';
|
||||
import { labelFilterData } from '~/search/sidebar/components/label_filter/data';
|
||||
import { SCOPE_BLOB } from '~/search/sidebar/constants';
|
||||
import {
|
||||
GROUPS_LOCAL_STORAGE_KEY,
|
||||
PROJECTS_LOCAL_STORAGE_KEY,
|
||||
SIDEBAR_PARAMS,
|
||||
PRESERVED_PARAMS,
|
||||
LOCAL_STORAGE_NAME_SPACE_EXTENSION,
|
||||
} from './constants';
|
||||
REGEX_PARAM,
|
||||
LS_REGEX_HANDLE,
|
||||
} from '~/search/store/constants';
|
||||
import * as types from './mutation_types';
|
||||
import {
|
||||
loadDataFromLS,
|
||||
|
|
@ -114,8 +109,8 @@ export const setQuery = ({ state, commit }, { key, value }) => {
|
|||
commit(types.SET_SIDEBAR_DIRTY, isSidebarDirty(state.query, state.urlQuery));
|
||||
}
|
||||
|
||||
if (PRESERVED_PARAMS.includes(key)) {
|
||||
setDataToLS(`${key}_${LOCAL_STORAGE_NAME_SPACE_EXTENSION}`, value);
|
||||
if (key === REGEX_PARAM) {
|
||||
setDataToLS(LS_REGEX_HANDLE, value);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -152,26 +147,24 @@ export const setLabelFilterSearch = ({ commit }, { value }) => {
|
|||
commit(types.SET_LABEL_SEARCH_STRING, value);
|
||||
};
|
||||
|
||||
const injectWildCardSearch = (state, link) => {
|
||||
const urlObject = new URL(`${getBaseURL()}${link}`);
|
||||
if (!state.urlQuery.search) {
|
||||
const queryObject = queryToObject(urlObject.search);
|
||||
urlObject.search = objectToQuery({ ...queryObject, search: '*' });
|
||||
}
|
||||
|
||||
return urlObject.href;
|
||||
};
|
||||
|
||||
export const fetchSidebarCount = ({ commit, state }) => {
|
||||
const items = Object.values(state.navigation)
|
||||
.filter((navigationItem) => !navigationItem.active && navigationItem.count_link)
|
||||
.map((navItem) => {
|
||||
const navigationItem = { ...navItem };
|
||||
const modifications = {
|
||||
search: state.query?.search || '*',
|
||||
};
|
||||
|
||||
if (navigationItem.count_link) {
|
||||
navigationItem.count_link = injectWildCardSearch(state, navigationItem.count_link);
|
||||
if (navigationItem.scope === SCOPE_BLOB && loadDataFromLS(LS_REGEX_HANDLE)) {
|
||||
modifications[REGEX_PARAM] = true;
|
||||
}
|
||||
|
||||
navigationItem.count_link = setUrlParams(
|
||||
modifications,
|
||||
getNormalizedURL(navigationItem.count_link),
|
||||
);
|
||||
|
||||
return navigationItem;
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -22,10 +22,6 @@ export const SIDEBAR_PARAMS = [
|
|||
|
||||
export const REGEX_PARAM = 'regex';
|
||||
|
||||
export const PRESERVED_PARAMS = [REGEX_PARAM];
|
||||
|
||||
export const LOCAL_STORAGE_NAME_SPACE_EXTENSION = 'advanced_search';
|
||||
|
||||
export const NUMBER_FORMATING_OPTIONS = { notation: 'compact', compactDisplay: 'short' };
|
||||
|
||||
export const ICON_MAP = {
|
||||
|
|
@ -49,3 +45,5 @@ export const BASIC_SEARCH_TYPE = 'basic';
|
|||
export const SEARCH_LEVEL_GLOBAL = 'global';
|
||||
export const SEARCH_LEVEL_PROJECT = 'project';
|
||||
export const SEARCH_LEVEL_GROUP = 'group';
|
||||
|
||||
export const LS_REGEX_HANDLE = `${REGEX_PARAM}_advanced_search`;
|
||||
|
|
|
|||
|
|
@ -1,9 +1,13 @@
|
|||
import { findKey, intersection } from 'lodash';
|
||||
import { languageFilterData } from '~/search/sidebar/components/language_filter/data';
|
||||
import { labelFilterData } from '~/search/sidebar/components/label_filter/data';
|
||||
import { formatSearchResultCount, addCountOverLimit } from '~/search/store/utils';
|
||||
import {
|
||||
formatSearchResultCount,
|
||||
addCountOverLimit,
|
||||
injectRegexSearch,
|
||||
} from '~/search/store/utils';
|
||||
|
||||
import { PROJECT_DATA } from '~/search/sidebar/constants';
|
||||
import { PROJECT_DATA, SCOPE_BLOB } from '~/search/sidebar/constants';
|
||||
import { GROUPS_LOCAL_STORAGE_KEY, PROJECTS_LOCAL_STORAGE_KEY, ICON_MAP } from './constants';
|
||||
|
||||
const queryLabelFilters = (state) => state?.query?.[labelFilterData.filterParam] || [];
|
||||
|
|
@ -81,7 +85,7 @@ export const navigationItems = (state) =>
|
|||
Object.values(state.navigation).map((item) => ({
|
||||
title: item.label,
|
||||
icon: ICON_MAP[item.scope] || '',
|
||||
link: item.link,
|
||||
link: item.scope === SCOPE_BLOB ? injectRegexSearch(item.link) : item.link,
|
||||
is_active: Boolean(item?.active),
|
||||
pill_count: `${formatSearchResultCount(item?.count)}${addCountOverLimit(item?.count)}` || '',
|
||||
items: [],
|
||||
|
|
|
|||
|
|
@ -1,13 +1,15 @@
|
|||
import { isEqual, orderBy } from 'lodash';
|
||||
import { isEqual, orderBy, isEmpty } from 'lodash';
|
||||
import AccessorUtilities from '~/lib/utils/accessor';
|
||||
import { formatNumber } from '~/locale';
|
||||
import { joinPaths } from '~/lib/utils/url_utility';
|
||||
import { joinPaths, queryToObject, objectToQuery, getBaseURL } from '~/lib/utils/url_utility';
|
||||
import { languageFilterData } from '~/search/sidebar/components/language_filter/data';
|
||||
import {
|
||||
MAX_FREQUENT_ITEMS,
|
||||
MAX_FREQUENCY,
|
||||
SIDEBAR_PARAMS,
|
||||
NUMBER_FORMATING_OPTIONS,
|
||||
REGEX_PARAM,
|
||||
LS_REGEX_HANDLE,
|
||||
} from './constants';
|
||||
|
||||
const LANGUAGE_AGGREGATION_NAME = languageFilterData.filterParam;
|
||||
|
|
@ -164,3 +166,16 @@ export const prepareSearchAggregations = (state, aggregationData) =>
|
|||
export const addCountOverLimit = (count = '') => {
|
||||
return count.includes('+') ? '+' : '';
|
||||
};
|
||||
|
||||
/** @param { string } link */
|
||||
export const injectRegexSearch = (link) => {
|
||||
const urlObject = new URL(link, getBaseURL());
|
||||
const queryObject = queryToObject(urlObject.search);
|
||||
if (loadDataFromLS(LS_REGEX_HANDLE) && (queryObject.project_id || queryObject.group_id)) {
|
||||
queryObject[REGEX_PARAM] = true;
|
||||
}
|
||||
if (isEmpty(queryObject)) {
|
||||
return urlObject.pathname;
|
||||
}
|
||||
return `${urlObject.pathname}?${objectToQuery(queryObject)}`;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -10,10 +10,10 @@ import {
|
|||
ZOEKT_SEARCH_TYPE,
|
||||
ADVANCED_SEARCH_TYPE,
|
||||
REGEX_PARAM,
|
||||
LOCAL_STORAGE_NAME_SPACE_EXTENSION,
|
||||
LS_REGEX_HANDLE,
|
||||
} from '~/search/store/constants';
|
||||
import { loadDataFromLS } from '~/search/store/utils';
|
||||
import { SYNTAX_OPTIONS_ADVANCED_DOCUMENT, SYNTAX_OPTIONS_ZOEKT_DOCUMENT } from '../constants';
|
||||
import { loadDataFromLS } from '../../store/utils';
|
||||
|
||||
import SearchTypeIndicator from './search_type_indicator.vue';
|
||||
import GlSearchBoxByType from './search_box_by_type.vue';
|
||||
|
|
@ -72,20 +72,10 @@ export default {
|
|||
this.glFeatures.zoektExactSearch
|
||||
);
|
||||
},
|
||||
localstorageReguralExpressionItem() {
|
||||
return `${REGEX_PARAM}_${LOCAL_STORAGE_NAME_SPACE_EXTENSION}`;
|
||||
},
|
||||
loadRegexStateFromLocalStorage() {
|
||||
return loadDataFromLS(this.localstorageReguralExpressionItem);
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.preloadStoredFrequentItems();
|
||||
this.regexEnabled = this.loadRegexStateFromLocalStorage;
|
||||
|
||||
if (!this.urlQuery?.[REGEX_PARAM] && this.regexEnabled) {
|
||||
this.addReguralExpressionToQuery(this.regexEnabled);
|
||||
}
|
||||
this.regexEnabled = loadDataFromLS(LS_REGEX_HANDLE);
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['applyQuery', 'setQuery', 'preloadStoredFrequentItems']),
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ import {
|
|||
import modalKeyboardNavigationMixin from '~/vue_shared/mixins/modal_keyboard_navigation_mixin';
|
||||
import { darkModeEnabled } from '~/lib/utils/color_utils';
|
||||
import ScrollScrim from '~/super_sidebar/components/scroll_scrim.vue';
|
||||
import { injectRegexSearch } from '~/search/store/utils';
|
||||
import {
|
||||
EVENT_PRESS_ENTER_TO_ADVANCED_SEARCH,
|
||||
EVENT_PRESS_ESCAPE_IN_COMMAND_PALETTE,
|
||||
|
|
@ -255,7 +256,8 @@ export default {
|
|||
if (this.search?.length <= SEARCH_SHORTCUTS_MIN_CHARACTERS) {
|
||||
return;
|
||||
}
|
||||
visitUrl(this.searchQuery);
|
||||
|
||||
visitUrl(injectRegexSearch(this.searchQuery));
|
||||
},
|
||||
runFirstCommand() {
|
||||
this.getFocusableOptions()[0]?.firstChild.click();
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import {
|
|||
EVENT_CLICK_GROUP_SCOPED_SEARCH_TO_ADVANCED_SEARCH,
|
||||
EVENT_CLICK_PROJECT_SCOPED_SEARCH_TO_ADVANCED_SEARCH,
|
||||
} from '~/super_sidebar/components/global_search/tracking_constants';
|
||||
import { injectRegexSearch } from '~/search/store/utils';
|
||||
import {
|
||||
OVERLAY_SEARCH,
|
||||
SCOPE_SEARCH_ALL,
|
||||
|
|
@ -40,6 +41,7 @@ export default {
|
|||
name: this.scopedSearchGroup.name,
|
||||
items: this.scopedSearchGroup.items.map((item) => ({
|
||||
...item,
|
||||
href: item.text === SCOPE_SEARCH_PROJECT ? injectRegexSearch(item.href) : item.href,
|
||||
scopeName: item.scope || item.description,
|
||||
extraAttrs: {
|
||||
class: 'show-hover-layover',
|
||||
|
|
|
|||
|
|
@ -24,7 +24,12 @@ import {
|
|||
COMMAND_PALETTE_FILES_CHAR,
|
||||
} from '~/vue_shared/global_search/constants';
|
||||
import { getFormattedItem } from '../utils';
|
||||
import { TRACKING_CLICK_COMMAND_PALETTE_ITEM } from '../command_palette/constants';
|
||||
import {
|
||||
TRACKING_CLICK_COMMAND_PALETTE_ITEM,
|
||||
SCOPE_SEARCH_PROJECT,
|
||||
SCOPE_SEARCH_GROUP,
|
||||
SCOPE_SEARCH_ALL,
|
||||
} from '../command_palette/constants';
|
||||
|
||||
import {
|
||||
ICON_GROUP,
|
||||
|
|
@ -178,7 +183,7 @@ export const scopedSearchOptions = (state, getters) => {
|
|||
|
||||
if (state.searchContext?.project) {
|
||||
items.push({
|
||||
text: 'scoped-in-project',
|
||||
text: SCOPE_SEARCH_PROJECT,
|
||||
scope: state.searchContext.project?.name || '',
|
||||
scopeCategory: PROJECTS_CATEGORY,
|
||||
icon: ICON_PROJECT,
|
||||
|
|
@ -192,7 +197,7 @@ export const scopedSearchOptions = (state, getters) => {
|
|||
|
||||
if (state.searchContext?.group) {
|
||||
items.push({
|
||||
text: 'scoped-in-group',
|
||||
text: SCOPE_SEARCH_GROUP,
|
||||
scope: state.searchContext.group?.name || '',
|
||||
scopeCategory: GROUPS_CATEGORY,
|
||||
icon: state.searchContext.group?.full_name?.includes('/') ? ICON_SUBGROUP : ICON_GROUP,
|
||||
|
|
@ -205,7 +210,7 @@ export const scopedSearchOptions = (state, getters) => {
|
|||
}
|
||||
|
||||
items.push({
|
||||
text: 'scoped-in-all',
|
||||
text: SCOPE_SEARCH_ALL,
|
||||
description: MSG_IN_ALL_GITLAB,
|
||||
href: getters.allUrl,
|
||||
extraAttrs: {
|
||||
|
|
|
|||
|
|
@ -9,9 +9,10 @@ import {
|
|||
GlLoadingIcon,
|
||||
GlSprintf,
|
||||
GlToggle,
|
||||
GlTooltipDirective,
|
||||
} from '@gitlab/ui';
|
||||
import { createAlert } from '~/alert';
|
||||
import { __, s__ } from '~/locale';
|
||||
import { __, s__, n__, sprintf } from '~/locale';
|
||||
import { helpPagePath } from '~/helpers/help_page_helper';
|
||||
import { TYPENAME_GROUP } from '~/graphql_shared/constants';
|
||||
import inboundAddGroupOrProjectCIJobTokenScope from '../graphql/mutations/inbound_add_group_or_project_ci_job_token_scope.mutation.graphql';
|
||||
|
|
@ -41,17 +42,6 @@ export default {
|
|||
projectsFetchError: __('There was a problem fetching the projects'),
|
||||
scopeFetchError: __('There was a problem fetching the job token scope value'),
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
key: 'fullPath',
|
||||
label: '',
|
||||
},
|
||||
{
|
||||
key: 'actions',
|
||||
label: '',
|
||||
tdClass: 'gl-text-right',
|
||||
},
|
||||
],
|
||||
components: {
|
||||
GlAlert,
|
||||
GlButton,
|
||||
|
|
@ -64,6 +54,9 @@ export default {
|
|||
GlToggle,
|
||||
TokenAccessTable,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
},
|
||||
inject: {
|
||||
fullPath: {
|
||||
default: '',
|
||||
|
|
@ -92,8 +85,13 @@ export default {
|
|||
};
|
||||
},
|
||||
update({ project }) {
|
||||
this.projects = project?.ciJobTokenScope?.inboundAllowlist?.nodes ?? [];
|
||||
this.groups = project?.ciJobTokenScope?.groupsAllowlist?.nodes ?? [];
|
||||
const projects = project?.ciJobTokenScope?.inboundAllowlist?.nodes ?? [];
|
||||
const groups = project?.ciJobTokenScope?.groupsAllowlist?.nodes ?? [];
|
||||
|
||||
this.projectCount = projects.length;
|
||||
this.groupCount = groups.length;
|
||||
|
||||
return [...groups, ...projects];
|
||||
},
|
||||
error() {
|
||||
createAlert({ message: this.$options.i18n.projectsFetchError });
|
||||
|
|
@ -103,19 +101,36 @@ export default {
|
|||
data() {
|
||||
return {
|
||||
inboundJobTokenScopeEnabled: null,
|
||||
targetPath: '',
|
||||
projects: [],
|
||||
groups: [],
|
||||
groupsAndProjectsWithAccess: [],
|
||||
groupOrProjectPath: '',
|
||||
projectCount: 0,
|
||||
groupCount: 0,
|
||||
isAddFormVisible: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
isTargetPathEmpty() {
|
||||
return this.targetPath === '';
|
||||
isGroupOrProjectPathEmpty() {
|
||||
return this.groupOrProjectPath === '';
|
||||
},
|
||||
ciJobTokenHelpPage() {
|
||||
return helpPagePath('ci/jobs/ci_job_token#control-job-token-access-to-your-project');
|
||||
},
|
||||
groupCountTooltip() {
|
||||
return sprintf(
|
||||
n__('%{count} group has access', '%{count} groups have access', this.groupCount),
|
||||
{
|
||||
count: this.groupCount,
|
||||
},
|
||||
);
|
||||
},
|
||||
projectCountTooltip() {
|
||||
return sprintf(
|
||||
n__('%{count} project has access', '%{count} projects have access', this.projectCount),
|
||||
{
|
||||
count: this.projectCount,
|
||||
},
|
||||
);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async updateCIJobTokenScope() {
|
||||
|
|
@ -152,7 +167,7 @@ export default {
|
|||
mutation: inboundAddGroupOrProjectCIJobTokenScope,
|
||||
variables: {
|
||||
projectPath: this.fullPath,
|
||||
targetPath: this.targetPath,
|
||||
targetPath: this.groupOrProjectPath,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -162,7 +177,7 @@ export default {
|
|||
} catch (error) {
|
||||
createAlert({ message: error.message });
|
||||
} finally {
|
||||
this.clearTargetPath();
|
||||
this.clearGroupOrProjectPath();
|
||||
this.getGroupsAndProjects();
|
||||
}
|
||||
},
|
||||
|
|
@ -204,8 +219,8 @@ export default {
|
|||
this.getGroupsAndProjects();
|
||||
}
|
||||
},
|
||||
clearTargetPath() {
|
||||
this.targetPath = '';
|
||||
clearGroupOrProjectPath() {
|
||||
this.groupOrProjectPath = '';
|
||||
this.isAddFormVisible = false;
|
||||
},
|
||||
getGroupsAndProjects() {
|
||||
|
|
@ -262,16 +277,28 @@ export default {
|
|||
body-class="gl-new-card-body gl-px-0"
|
||||
>
|
||||
<template #header>
|
||||
<div class="gl-new-card-title-wrapper">
|
||||
<h5 class="gl-new-card-title">{{ $options.i18n.cardHeaderTitle }}</h5>
|
||||
<span class="gl-new-card-count">
|
||||
<gl-icon name="group" class="gl-mr-2" />
|
||||
{{ groups.length }}
|
||||
</span>
|
||||
<span class="gl-new-card-count">
|
||||
<gl-icon name="project" class="gl-mr-2" />
|
||||
{{ projects.length }}
|
||||
</span>
|
||||
<div class="gl-new-card-title-wrapper gl-flex-direction-column gl-flex-wrap">
|
||||
<div class="gl-new-card-title gl-items-center">
|
||||
<h5 class="gl-my-0">{{ $options.i18n.cardHeaderTitle }}</h5>
|
||||
<span
|
||||
v-gl-tooltip
|
||||
:title="groupCountTooltip"
|
||||
class="gl-new-card-count"
|
||||
data-testid="group-count"
|
||||
>
|
||||
<gl-icon name="group" class="gl-mr-2" />
|
||||
{{ groupCount }}
|
||||
</span>
|
||||
<span
|
||||
v-gl-tooltip
|
||||
:title="projectCountTooltip"
|
||||
class="gl-new-card-count"
|
||||
data-testid="project-count"
|
||||
>
|
||||
<gl-icon name="project" class="gl-mr-2" />
|
||||
{{ projectCount }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gl-new-card-actions">
|
||||
<gl-button
|
||||
|
|
@ -287,35 +314,24 @@ export default {
|
|||
<div v-if="isAddFormVisible" class="gl-new-card-add-form gl-m-3">
|
||||
<h4 class="gl-mt-0">{{ $options.i18n.addGroupOrProject }}</h4>
|
||||
<gl-form-input
|
||||
v-model="targetPath"
|
||||
v-model="groupOrProjectPath"
|
||||
:placeholder="$options.i18n.addProjectPlaceholder"
|
||||
/>
|
||||
<div class="gl-display-flex gl-mt-5">
|
||||
<gl-button
|
||||
variant="confirm"
|
||||
:disabled="isTargetPathEmpty"
|
||||
:disabled="isGroupOrProjectPathEmpty"
|
||||
class="gl-mr-3"
|
||||
data-testid="add-project-btn"
|
||||
@click="addGroupOrProject"
|
||||
>
|
||||
{{ $options.i18n.add }}
|
||||
</gl-button>
|
||||
<gl-button @click="clearTargetPath">{{ $options.i18n.cancel }}</gl-button>
|
||||
<gl-button @click="clearGroupOrProjectPath">{{ $options.i18n.cancel }}</gl-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<token-access-table
|
||||
:is-group="true"
|
||||
:items="groups"
|
||||
:table-fields="$options.fields"
|
||||
@removeItem="removeItem"
|
||||
/>
|
||||
|
||||
<token-access-table
|
||||
:items="projects"
|
||||
:table-fields="$options.fields"
|
||||
@removeItem="removeItem"
|
||||
/>
|
||||
<token-access-table :items="groupsAndProjectsWithAccess" @removeItem="removeItem" />
|
||||
</gl-card>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -8,9 +8,10 @@ import {
|
|||
GlLoadingIcon,
|
||||
GlSprintf,
|
||||
GlToggle,
|
||||
GlTooltipDirective,
|
||||
} from '@gitlab/ui';
|
||||
import { createAlert } from '~/alert';
|
||||
import { __, s__ } from '~/locale';
|
||||
import { __, s__, n__, sprintf } from '~/locale';
|
||||
import { helpPagePath } from '~/helpers/help_page_helper';
|
||||
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||
import addProjectCIJobTokenScopeMutation from '../graphql/mutations/add_project_ci_job_token_scope.mutation.graphql';
|
||||
|
|
@ -46,17 +47,6 @@ export default {
|
|||
deprecationDocumentationLink: helpPagePath('ci/jobs/ci_job_token', {
|
||||
anchor: 'limit-your-projects-job-token-access',
|
||||
}),
|
||||
fields: [
|
||||
{
|
||||
key: 'fullPath',
|
||||
label: __('Project that can be accessed'),
|
||||
},
|
||||
{
|
||||
key: 'actions',
|
||||
label: '',
|
||||
tdClass: 'gl-text-right',
|
||||
},
|
||||
],
|
||||
components: {
|
||||
GlAlert,
|
||||
GlButton,
|
||||
|
|
@ -68,6 +58,9 @@ export default {
|
|||
GlToggle,
|
||||
TokenAccessTable,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
},
|
||||
mixins: [glFeatureFlagMixin()],
|
||||
inject: {
|
||||
fullPath: {
|
||||
|
|
@ -121,6 +114,14 @@ export default {
|
|||
disableTokenToggle() {
|
||||
return !this.jobTokenScopeEnabled;
|
||||
},
|
||||
projectCountTooltip() {
|
||||
return sprintf(
|
||||
n__('%{count} project has access', '%{count} projects have access', this.projects.length),
|
||||
{
|
||||
count: this.projects.length,
|
||||
},
|
||||
);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async updateCIJobTokenScope() {
|
||||
|
|
@ -265,13 +266,13 @@ export default {
|
|||
<div>
|
||||
<gl-card
|
||||
class="gl-new-card"
|
||||
header-class="gl-new-card-header"
|
||||
header-class="gl-new-card-header gl-border-b-0"
|
||||
body-class="gl-new-card-body gl-px-0"
|
||||
>
|
||||
<template #header>
|
||||
<div class="gl-new-card-title-wrapper">
|
||||
<h5 class="gl-new-card-title">{{ $options.i18n.cardHeaderTitle }}</h5>
|
||||
<span class="gl-new-card-count">
|
||||
<span v-gl-tooltip :title="projectCountTooltip" class="gl-new-card-count">
|
||||
<gl-icon name="project" class="gl-mr-2" />
|
||||
{{ projects.length }}
|
||||
</span>
|
||||
|
|
@ -280,11 +281,7 @@ export default {
|
|||
<gl-button size="small" disabled>{{ $options.i18n.addProject }}</gl-button>
|
||||
</div>
|
||||
</template>
|
||||
<token-access-table
|
||||
:items="projects"
|
||||
:table-fields="$options.fields"
|
||||
@removeItem="removeProject"
|
||||
/>
|
||||
<token-access-table :items="projects" @removeItem="removeProject" />
|
||||
</gl-card>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,11 +1,27 @@
|
|||
<script>
|
||||
import { GlButton, GlTableLite } from '@gitlab/ui';
|
||||
import { sprintf, s__ } from '~/locale';
|
||||
import { GlButton, GlIcon, GlLink, GlTableLite } from '@gitlab/ui';
|
||||
import { TYPENAME_GROUP } from '~/graphql_shared/constants';
|
||||
import ProjectAvatar from '~/vue_shared/components/project_avatar.vue';
|
||||
|
||||
export default {
|
||||
fields: [
|
||||
{
|
||||
key: 'fullPath',
|
||||
label: '',
|
||||
tdClass: 'gl-w-3/4',
|
||||
},
|
||||
{
|
||||
key: 'actions',
|
||||
label: '',
|
||||
tdClass: 'gl-w-1/4 gl-text-right',
|
||||
},
|
||||
],
|
||||
components: {
|
||||
GlButton,
|
||||
GlIcon,
|
||||
GlLink,
|
||||
GlTableLite,
|
||||
ProjectAvatar,
|
||||
},
|
||||
inject: {
|
||||
fullPath: {
|
||||
|
|
@ -22,19 +38,11 @@ export default {
|
|||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
tableFields: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
emptyText() {
|
||||
return sprintf(s__('CI/CD|No %{itemType}s have been added to the scope'), {
|
||||
itemType: this.itemType,
|
||||
});
|
||||
},
|
||||
itemType() {
|
||||
return this.isGroup ? 'group' : 'project';
|
||||
methods: {
|
||||
itemType(item) {
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
return item.__typename === TYPENAME_GROUP ? 'group' : 'project';
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
@ -42,16 +50,34 @@ export default {
|
|||
<template>
|
||||
<gl-table-lite
|
||||
:items="items"
|
||||
:fields="tableFields"
|
||||
:tbody-tr-attr="{ 'data-testid': `${itemType}s-token-table-row` }"
|
||||
:empty-text="emptyText"
|
||||
show-empty
|
||||
stacked="sm"
|
||||
:fields="$options.fields"
|
||||
:tbody-tr-attr="{ 'data-testid': 'token-access-table-row' }"
|
||||
thead-class="gl-display-none"
|
||||
class="gl-mb-0"
|
||||
fixed
|
||||
>
|
||||
<template #cell(fullPath)="{ item }">
|
||||
<span :data-testid="`token-access-${itemType}-name`">{{ item.fullPath }}</span>
|
||||
<div class="gl-inline-flex gl-items-center">
|
||||
<gl-icon
|
||||
:name="itemType(item)"
|
||||
class="gl-mr-3 gl-flex-shrink-0"
|
||||
:data-testid="`token-access-${itemType(item)}-icon`"
|
||||
/>
|
||||
<project-avatar
|
||||
:alt="item.name"
|
||||
:project-avatar-url="item.avatarUrl"
|
||||
:project-id="item.id"
|
||||
:project-name="item.name"
|
||||
class="gl-mr-3"
|
||||
:data-testid="`token-access-${itemType(item)}-avatar`"
|
||||
/>
|
||||
<gl-link
|
||||
class="gl-text-gray-900"
|
||||
:href="`/${item.fullPath}`"
|
||||
:data-testid="`token-access-${itemType(item)}-name`"
|
||||
>{{ item.fullPath }}</gl-link
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #cell(actions)="{ item }">
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ query inboundGetGroupsAndProjectsWithCIJobTokenScope($fullPath: ID!) {
|
|||
id
|
||||
name
|
||||
fullPath
|
||||
avatarUrl
|
||||
}
|
||||
}
|
||||
inboundAllowlist {
|
||||
|
|
@ -14,6 +15,7 @@ query inboundGetGroupsAndProjectsWithCIJobTokenScope($fullPath: ID!) {
|
|||
id
|
||||
name
|
||||
fullPath
|
||||
avatarUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -280,10 +280,8 @@ export default {
|
|||
>
|
||||
<template v-if="isLoading">{{ $options.FETCH_LOADING }}</template>
|
||||
<template v-else>
|
||||
<div class="gl-display-flex gl-flex-direction-column">
|
||||
<div
|
||||
class="gl-display-flex gl-flex-direction-column gl-sm-flex-direction-row gl-gap-3 gl-align-items-baseline"
|
||||
>
|
||||
<div class="gl-flex gl-flex-col">
|
||||
<div class="gl-flex gl-flex-col sm:gl-flex-row gl-gap-3 gl-items-baseline">
|
||||
<div v-if="requireSamlAuthToApprove && showApprove">
|
||||
<gl-form
|
||||
ref="form"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
.gl-new-card {
|
||||
margin-top: $gl-spacing-scale-5;
|
||||
background-color: $gray-10;
|
||||
background-color: var(--gl-background-color-subtle);
|
||||
border-width: $gl-border-size-1;
|
||||
border-style: solid;
|
||||
border-color: $gray-100;
|
||||
|
|
@ -13,7 +13,7 @@
|
|||
padding-bottom: $gl-spacing-scale-4;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
background-color: $white;
|
||||
background-color: var(--gl-background-color-default);
|
||||
border-bottom-width: $gl-border-size-1;
|
||||
border-bottom-style: solid;
|
||||
border-bottom-color: $gray-100;
|
||||
|
|
@ -88,14 +88,14 @@
|
|||
}
|
||||
|
||||
&-footer {
|
||||
background-color: $white;
|
||||
background-color: var(--gl-background-color-default);
|
||||
}
|
||||
|
||||
&-add-form {
|
||||
padding: $gl-spacing-scale-4;
|
||||
margin-top: $gl-spacing-scale-2;
|
||||
margin-bottom: $gl-spacing-scale-2;
|
||||
background-color: $white;
|
||||
background-color: var(--gl-background-color-default);
|
||||
border-width: $gl-border-size-1;
|
||||
border-style: solid;
|
||||
border-color: $gray-100;
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ module RendersNotes
|
|||
# rubocop:disable Gitlab/ModuleWithInstanceVariables
|
||||
def prepare_notes_for_rendering(notes)
|
||||
preload_noteable_for_regular_notes(notes)
|
||||
preload_note_namespace(notes)
|
||||
preload_max_access_for_authors(notes, @project)
|
||||
preload_author_status(notes)
|
||||
Notes::RenderService.new(current_user).execute(notes)
|
||||
|
|
@ -14,6 +15,10 @@ module RendersNotes
|
|||
|
||||
private
|
||||
|
||||
def preload_note_namespace(notes)
|
||||
ActiveRecord::Associations::Preloader.new(records: notes, associations: :namespace).call
|
||||
end
|
||||
|
||||
def preload_max_access_for_authors(notes, project)
|
||||
return unless project
|
||||
|
||||
|
|
|
|||
|
|
@ -2,8 +2,6 @@
|
|||
|
||||
module Projects
|
||||
module IssuesHelper
|
||||
MAX_FORK_CHAIN_LENGTH = 20
|
||||
|
||||
def create_mr_tracking_data(can_create_mr, can_create_confidential_mr)
|
||||
if can_create_confidential_mr
|
||||
{ event_tracking: 'click_create_confidential_mr_issues_list' }
|
||||
|
|
@ -13,52 +11,5 @@ module Projects
|
|||
{}
|
||||
end
|
||||
end
|
||||
|
||||
def default_target(project)
|
||||
target = project.default_merge_request_target
|
||||
target = project unless target.present? && can?(current_user, :create_merge_request_in, target)
|
||||
|
||||
target
|
||||
end
|
||||
|
||||
def target_projects(project)
|
||||
return [] unless project.forked?
|
||||
|
||||
target = default_target(project)
|
||||
|
||||
MergeRequestTargetProjectFinder
|
||||
.new(current_user: current_user, source_project: project)
|
||||
.execute(include_routes: true)
|
||||
.limit(MAX_FORK_CHAIN_LENGTH)
|
||||
.filter { |target_project| can?(current_user, :create_merge_request_in, target_project) }
|
||||
.sort { |target_project, _| target_project.id == target.id ? 0 : 1 }
|
||||
end
|
||||
|
||||
def merge_request_target_projects_options(issue, target, default_create_mr_path)
|
||||
default_project = default_target(target)
|
||||
target_projects(target).map do |project|
|
||||
value = refs_project_path(project, search: '')
|
||||
label = project.full_name
|
||||
project_create_mr_path = default_create_mr_path
|
||||
unless default_project.id == project.id
|
||||
project_create_mr_path = create_mr_path(
|
||||
from: issue.to_branch_name,
|
||||
source_project: target,
|
||||
target_project: project,
|
||||
to: project.default_branch,
|
||||
mr_params: { issue_iid: issue.iid }
|
||||
)
|
||||
end
|
||||
|
||||
[
|
||||
nil,
|
||||
value,
|
||||
{
|
||||
label: label,
|
||||
data: { id: project.id, full_path: project.full_path, create_mr_path: project_create_mr_path }
|
||||
}
|
||||
]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -560,7 +560,7 @@ class Issue < ApplicationRecord
|
|||
end
|
||||
|
||||
def can_be_worked_on?
|
||||
!self.closed?
|
||||
!self.closed? && !self.project.forked?
|
||||
end
|
||||
|
||||
# Returns `true` if the current issue can be viewed by either a logged in User
|
||||
|
|
@ -624,7 +624,10 @@ class Issue < ApplicationRecord
|
|||
end
|
||||
|
||||
def banzai_render_context(field)
|
||||
super.merge(label_url_method: :project_issues_url)
|
||||
additional_attributes = { label_url_method: :project_issues_url }
|
||||
additional_attributes[:group] = namespace if namespace.is_a?(Group)
|
||||
|
||||
super.merge(additional_attributes)
|
||||
end
|
||||
|
||||
def design_collection
|
||||
|
|
|
|||
|
|
@ -557,7 +557,10 @@ class Note < ApplicationRecord
|
|||
end
|
||||
|
||||
def banzai_render_context(field)
|
||||
super.merge(noteable: noteable, system_note: system?, label_url_method: noteable_label_url_method)
|
||||
additional_attributes = { noteable: noteable, system_note: system?, label_url_method: noteable_label_url_method }
|
||||
additional_attributes[:group] = namespace if namespace.is_a?(Group)
|
||||
|
||||
super.merge(additional_attributes)
|
||||
end
|
||||
|
||||
def retrieve_upload(_identifier, paths)
|
||||
|
|
|
|||
|
|
@ -256,7 +256,7 @@ module MergeRequests
|
|||
def append_closes_description
|
||||
return unless issue&.to_reference.present?
|
||||
|
||||
closes_issue = "#{target_project.autoclose_referenced_issues ? 'Closes' : 'Related to'} #{issue.to_reference(target_project)}"
|
||||
closes_issue = "#{target_project.autoclose_referenced_issues ? 'Closes' : 'Related to'} #{issue.to_reference}"
|
||||
|
||||
if description.present?
|
||||
descr_parts = [merge_request.description, closes_issue]
|
||||
|
|
@ -348,8 +348,7 @@ module MergeRequests
|
|||
|
||||
def issue
|
||||
strong_memoize(:issue) do
|
||||
issue_project = same_source_and_target_project? ? target_project : source_project
|
||||
issue_project.get_issue(issue_iid, current_user)
|
||||
target_project.get_issue(issue_iid, current_user)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -5,14 +5,12 @@
|
|||
- value = can_create_confidential_merge_request? ? _('Create confidential merge request') : value
|
||||
- create_mr_text = can_create_confidential_merge_request? ? _('Create confidential merge request') : _('Create merge request')
|
||||
|
||||
- default_project = default_target(@project)
|
||||
- can_create_path = can_create_branch_project_issue_path(@project, @issue)
|
||||
- create_branch_path = project_branches_path(@project, branch_name: @issue.to_branch_name, ref: @project.default_branch, issue_iid: @issue.iid, format: :json)
|
||||
- refs_path = refs_namespace_project_path(default_project.namespace, default_project, search: '')
|
||||
- refs_path = refs_namespace_project_path(@project.namespace, @project, search: '')
|
||||
- tracking_data = create_mr_tracking_data(can_create_merge_request, can_create_confidential_merge_request?)
|
||||
- default_create_mr_path = create_mr_path(from: @issue.to_branch_name, source_project: @project, to: default_project.default_branch, mr_params: { issue_iid: @issue.iid })
|
||||
|
||||
.create-mr-dropdown-wrap.gl-inline-block.full-width-mobile.js-create-mr{ data: { project_path: @project.full_path, project_id: @project.id, can_create_path: can_create_path, create_mr_path: default_create_mr_path, create_branch_path: create_branch_path, refs_path: refs_path, is_confidential: can_create_confidential_merge_request?.to_s } }
|
||||
.create-mr-dropdown-wrap.gl-inline-block.full-width-mobile.js-create-mr{ data: { project_path: @project.full_path, project_id: @project.id, can_create_path: can_create_path, create_mr_path: create_mr_path(from: @issue.to_branch_name, source_project: @project, to: @project.default_branch, mr_params: { issue_iid: @issue.iid }), create_branch_path: create_branch_path, refs_path: refs_path, is_confidential: can_create_confidential_merge_request?.to_s } }
|
||||
.btn-group.unavailable
|
||||
= render Pajamas::ButtonComponent.new(button_options: { disabled: 'disabled' }) do
|
||||
= gl_loading_icon(inline: true, css_class: 'js-create-mr-spinner gl-button-icon gl-hidden')
|
||||
|
|
@ -28,10 +26,10 @@
|
|||
|
||||
= render Pajamas::ButtonComponent.new(variant: :confirm, icon: 'chevron-down', button_options: { class: 'js-dropdown-toggle dropdown-toggle create-merge-request-dropdown-toggle', data: { 'dropdown-trigger': '#create-merge-request-dropdown', display: 'static' } })
|
||||
|
||||
%form.droplab-dropdown
|
||||
.droplab-dropdown
|
||||
%ul#create-merge-request-dropdown.create-merge-request-dropdown-menu.dropdown-menu.dropdown-menu-right.gl-show-field-errors{ class: ("create-confidential-merge-request-dropdown-menu" if can_create_confidential_merge_request?), data: { dropdown: true } }
|
||||
- if can_create_merge_request
|
||||
%li.js-type-toggle.droplab-item-selected{ role: 'button', data: { value: 'create-mr', text: create_mr_text } }
|
||||
%li.droplab-item-selected{ role: 'button', data: { value: 'create-mr', text: create_mr_text } }
|
||||
.menu-item.text-nowrap
|
||||
= sprite_icon('check', css_class: 'icon')
|
||||
- if can_create_confidential_merge_request?
|
||||
|
|
@ -39,7 +37,7 @@
|
|||
- else
|
||||
= _('Create merge request and branch')
|
||||
|
||||
%li.js-type-toggle{ class: [!can_create_merge_request && 'droplab-item-selected'], role: 'button', data: { value: 'create-branch', text: _('Create branch') } }
|
||||
%li{ class: [!can_create_merge_request && 'droplab-item-selected'], role: 'button', data: { value: 'create-branch', text: _('Create branch') } }
|
||||
.menu-item
|
||||
= sprite_icon('check', css_class: 'icon')
|
||||
= _('Create branch')
|
||||
|
|
@ -48,13 +46,6 @@
|
|||
%li.droplab-item-ignore.gl-ml-3.gl-mr-3.gl-mt-5
|
||||
- if can_create_confidential_merge_request?
|
||||
#js-forked-project{ data: { namespace_path: @project.namespace.full_path, project_path: @project.full_path, new_fork_path: new_project_fork_path(@project), help_page_path: help_page_path('user/project/merge_requests/index') } }
|
||||
|
||||
- if @project.forked? && can_create_merge_request
|
||||
.form-group
|
||||
%label{ for: 'target-project' }
|
||||
= _('Target project')
|
||||
= select nil, nil, options_for_select(merge_request_target_projects_options(@issue, @project, default_create_mr_path), refs_path), {}, id: 'target-project', class: 'js-target-project form-control gl-form-select custom-select'
|
||||
|
||||
.form-group
|
||||
%label{ for: 'new-branch-name' }
|
||||
= _('Branch name')
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
%h5.gl-heading-4= s_('ProjectMaintenance|Remove blobs')
|
||||
%p.gl-text-secondary
|
||||
= s_('ProjectMaintenance|Provide a list of blob object IDs to be removed.')
|
||||
= link_to s_('ProjectMaintenance|How do I get a list of object IDs?'), help_page_path('user/project/repository/reducing_the_repo_size_using_git', anchor: 'repository-cleanup')
|
||||
= link_to s_('ProjectMaintenance|How do I get a list of object IDs?'), help_page_path('user/project/repository/reducing_the_repo_size_using_git', anchor: 'get-a-list-of-object-ids')
|
||||
%p.gl-text-secondary
|
||||
= s_('ProjectMaintenance|Afterwards, run housekeeping manually to remove old versions of the file from Git history.')
|
||||
|
||||
|
|
|
|||
|
|
@ -5,5 +5,5 @@ introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/150407
|
|||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/444929
|
||||
milestone: '17.0'
|
||||
group: group::authentication
|
||||
type: gitlab_com_derisk
|
||||
default_enabled: false
|
||||
type: ops
|
||||
default_enabled: true
|
||||
|
|
@ -43,6 +43,10 @@ As part of the plan to switch to the new server, reindex all [affected indexes](
|
|||
|
||||
This approach was used for GitLab.com. To learn more about this process and how the different types of indexes were handled, see the blog post about [upgrading the operating system on our Postgres database clusters](https://about.gitlab.com/blog/2022/08/12/upgrading-database-os/).
|
||||
|
||||
After reindexing bad indexes, the collation must be refreshed.
|
||||
To update the system catalog to record the current collation version,
|
||||
run the query `ALTER COLLATION <collation_name> REFRESH VERSION`.
|
||||
|
||||
**Advantages**:
|
||||
|
||||
- Downtime is shorter: the time to perform the necessary reindexing, plus validation.
|
||||
|
|
|
|||
|
|
@ -82,8 +82,8 @@ Some of the scenarios where these `Security::Finding` records may be promoted to
|
|||
|
||||
If the pipeline ran on the default branch then the following steps, in addition to the steps in [Scan runs in a pipeline for a non-default branch](#scan-runs-in-a-pipeline-for-a-non-default-branch), are executed:
|
||||
|
||||
1. `Security::StoreScansService` gets called and schedules `StoreSecurityReportsWorker`.
|
||||
1. `StoreSecurityReportsWorker` executes `Security::Ingestion::IngestReportsService`.
|
||||
1. `Security::StoreScansService` gets called and schedules `StoreSecurityReportsByProjectWorker`.
|
||||
1. `StoreSecurityReportsByProjectWorker` executes `Security::Ingestion::IngestReportsService`.
|
||||
1. `Security::Ingestion::IngestReportsService` takes all reports from a given Pipeline and calls `Security::Ingestion::IngestReportService` and then calls `Security::Ingestion::MarkAsResolvedService`.
|
||||
1. `Security::Ingestion::IngestReportService` calls `Security::Ingestion::IngestReportSliceService` which executes a number of tasks for a report slice.
|
||||
|
||||
|
|
|
|||
|
|
@ -236,6 +236,67 @@ When using repository cleanup, note:
|
|||
[Clearing the instance cache](../../../administration/raketasks/maintenance.md#clear-redis-cache)
|
||||
may help to remove some of them, but it should not be depended on for security purposes!
|
||||
|
||||
## Remove blobs
|
||||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/450701) in GitLab 17.1 [with a flag](../../../administration/feature_flags.md) named `rewrite_history_ui`. Disabled by default.
|
||||
|
||||
FLAG:
|
||||
The availability of this feature is controlled by a feature flag.
|
||||
For more information, see the history.
|
||||
This feature is available for testing, but not ready for production use.
|
||||
|
||||
Permanently delete sensitive or confidential information that was accidentally committed, ensuring
|
||||
it's no longer accessible in your repository's history.
|
||||
|
||||
Prerequisites:
|
||||
|
||||
- You must have the Owner role for the instance.
|
||||
- You must have [a list of object IDs](#get-a-list-of-object-ids) to remove.
|
||||
|
||||
To remove blobs from your repository:
|
||||
|
||||
1. On the left sidebar, select **Search or go to** and find your project.
|
||||
1. Select **Settings > Repository**.
|
||||
1. Expand **Repository maintenance**.
|
||||
1. Select **Remove blobs**.
|
||||
1. On the drawer, enter a list of blob IDs to remove, each ID on its own line.
|
||||
1. Select **Remove blobs**.
|
||||
1. On the confirmation dialog, enter your project path.
|
||||
1. Select **Yes, remove blobs**.
|
||||
1. On the left sidebar, select **Settings > General**.
|
||||
1. Expand the section labeled **Advanced**.
|
||||
1. Select **Run housekeeping**.
|
||||
|
||||
### Get a list of object IDs
|
||||
|
||||
To remove blobs, you need a list of objects to remove.
|
||||
To get these IDs, use the Git `ls-tree command`.
|
||||
|
||||
Prerequisites:
|
||||
|
||||
- You must have the repository cloned to your local machine.
|
||||
|
||||
For example, to get a list of files at a given commit or branch sorted by size:
|
||||
|
||||
1. Open a terminal and go to your repository directory.
|
||||
1. Run the following command:
|
||||
|
||||
```shell
|
||||
git ls-tree -r -t --long --full-name <COMMIT/BRANCH> | sort -nk 4
|
||||
```
|
||||
|
||||
Example output:
|
||||
|
||||
```plaintext
|
||||
100644 blob 8150ee86f923548d376459b29afecbe8495514e9 133508 doc/howto/img/remote-development-new-workspace-button.png
|
||||
100644 blob cde4360b3d3ee4f4c04c998d43cfaaf586f09740 214231 doc/howto/img/dependency_proxy_macos_config_new.png
|
||||
100644 blob 2ad0e839a709e73a6174e78321e87021b20be445 216452 doc/howto/img/gdk-in-gitpod.jpg
|
||||
100644 blob 115dd03fc0828a9011f012abbc58746f7c587a05 242304 doc/howto/img/gitpod-button-repository.jpg
|
||||
100644 blob c41ebb321a6a99f68ee6c353dd0ed29f52c1dc80 491158 doc/howto/img/dependency_proxy_macos_config.png
|
||||
```
|
||||
|
||||
The third column in the output is the object ID of the blob.
|
||||
|
||||
## Storage limits
|
||||
|
||||
Repository size limits:
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ module Banzai
|
|||
[
|
||||
Filter::References::UserReferenceFilter,
|
||||
Filter::References::IssueReferenceFilter,
|
||||
Filter::References::WorkItemReferenceFilter,
|
||||
Filter::References::ExternalIssueReferenceFilter,
|
||||
Filter::References::MergeRequestReferenceFilter,
|
||||
Filter::References::SnippetReferenceFilter,
|
||||
|
|
|
|||
|
|
@ -675,6 +675,11 @@ msgid_plural "%{count} groups"
|
|||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
msgid "%{count} group has access"
|
||||
msgid_plural "%{count} groups have access"
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
msgid "%{count} issue"
|
||||
msgid_plural "%{count} issues"
|
||||
msgstr[0] ""
|
||||
|
|
@ -718,6 +723,11 @@ msgid_plural "%{count} projects"
|
|||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
msgid "%{count} project has access"
|
||||
msgid_plural "%{count} projects have access"
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
msgid "%{count} related %{pluralized_subject}: %{links}"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -9971,9 +9981,6 @@ msgstr ""
|
|||
msgid "CI/CD limits"
|
||||
msgstr ""
|
||||
|
||||
msgid "CI/CD|No %{itemType}s have been added to the scope"
|
||||
msgstr ""
|
||||
|
||||
msgid "CICDAnalytics|%{percent}%{percentSymbol}"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -40767,9 +40774,6 @@ msgstr ""
|
|||
msgid "Project slug"
|
||||
msgstr ""
|
||||
|
||||
msgid "Project that can be accessed"
|
||||
msgstr ""
|
||||
|
||||
msgid "Project uploads"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -52184,9 +52188,6 @@ msgstr ""
|
|||
msgid "Target branches"
|
||||
msgstr ""
|
||||
|
||||
msgid "Target project"
|
||||
msgstr ""
|
||||
|
||||
msgid "Target project cannot be equal to source project"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -138,6 +138,11 @@ FactoryBot.define do
|
|||
noteable { association(:work_item, project: project) }
|
||||
end
|
||||
|
||||
trait :on_group_work_item do
|
||||
project { nil }
|
||||
noteable { association(:work_item, :group_level) }
|
||||
end
|
||||
|
||||
trait :on_merge_request do
|
||||
noteable { association(:merge_request, source_project: project) }
|
||||
end
|
||||
|
|
|
|||
|
|
@ -3,284 +3,240 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'User creates branch and merge request on issue page', :js, feature_category: :team_planning do
|
||||
include ProjectForksHelper
|
||||
|
||||
let(:user) { create(:user) }
|
||||
let(:membership_level) { :developer }
|
||||
let(:user) { create(:user) }
|
||||
let!(:project) { create(:project, :repository, :public) }
|
||||
let(:issue) { create(:issue, project: project, title: 'Cherry-Coloured Funk') }
|
||||
|
||||
shared_examples "user creates branch and merge request from issue" do
|
||||
context 'when signed out' do
|
||||
context 'when signed out' do
|
||||
before do
|
||||
visit project_issue_path(project, issue)
|
||||
end
|
||||
|
||||
it "doesn't show 'Create merge request' button" do
|
||||
expect(page).not_to have_selector('.create-mr-dropdown-wrap')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when signed in' do
|
||||
before do
|
||||
project.add_member(user, membership_level)
|
||||
|
||||
sign_in(user)
|
||||
end
|
||||
|
||||
context 'when interacting with the dropdown' do
|
||||
before do
|
||||
visit project_issue_path(project, issue)
|
||||
end
|
||||
|
||||
it "doesn't show 'Create merge request' button" do
|
||||
expect(page).not_to have_selector('.create-mr-dropdown-wrap')
|
||||
end
|
||||
end
|
||||
# In order to improve tests performance, all UI checks are placed in this test.
|
||||
it 'shows elements' do
|
||||
button_create_merge_request = find('.js-create-merge-request')
|
||||
button_toggle_dropdown = find('.create-mr-dropdown-wrap .dropdown-toggle')
|
||||
|
||||
context 'when signed in' do
|
||||
before do
|
||||
project.add_member(user, membership_level)
|
||||
button_toggle_dropdown.click
|
||||
|
||||
sign_in(user)
|
||||
end
|
||||
dropdown = find('.create-merge-request-dropdown-menu')
|
||||
|
||||
context 'when interacting with the dropdown' do
|
||||
before do
|
||||
visit project_issue_path(project, issue)
|
||||
end
|
||||
page.within(dropdown) do
|
||||
button_create_target = find('.js-create-target')
|
||||
input_branch_name = find('.js-branch-name')
|
||||
input_source = find('.js-ref')
|
||||
li_create_branch = find("li[data-value='create-branch']")
|
||||
li_create_merge_request = find("li[data-value='create-mr']")
|
||||
|
||||
# In order to improve tests performance, all UI checks are placed in this test.
|
||||
it 'shows elements' do
|
||||
button_create_merge_request = find('.js-create-merge-request')
|
||||
button_toggle_dropdown = find('.create-mr-dropdown-wrap .dropdown-toggle')
|
||||
# Test that all elements are presented.
|
||||
expect(page).to have_content('Create merge request and branch')
|
||||
expect(page).to have_content('Create branch')
|
||||
expect(page).to have_content('Branch name')
|
||||
expect(page).to have_content('Source (branch or tag)')
|
||||
expect(page).to have_button('Create merge request')
|
||||
expect(page).to have_selector('.js-branch-name:focus')
|
||||
|
||||
button_toggle_dropdown.click
|
||||
test_selection_mark(li_create_branch, li_create_merge_request, button_create_target, button_create_merge_request)
|
||||
test_branch_name_checking(input_branch_name)
|
||||
test_source_checking(input_source)
|
||||
|
||||
dropdown = find('.create-merge-request-dropdown-menu')
|
||||
|
||||
page.within(dropdown) do
|
||||
button_create_target = find('.js-create-target')
|
||||
input_branch_name = find('.js-branch-name')
|
||||
input_source = find('.js-ref')
|
||||
li_create_branch = find("li[data-value='create-branch']")
|
||||
li_create_merge_request = find("li[data-value='create-mr']")
|
||||
|
||||
# Test that all elements are presented.
|
||||
expect(page).to have_content('Create merge request and branch')
|
||||
expect(page).to have_content('Create branch')
|
||||
expect(page).to have_content('Branch name')
|
||||
expect(page).to have_content('Source (branch or tag)')
|
||||
expect(page).to have_button('Create merge request')
|
||||
expect(page).to have_selector('.js-branch-name:focus')
|
||||
|
||||
test_selection_mark(li_create_branch, li_create_merge_request, button_create_target, button_create_merge_request)
|
||||
test_branch_name_checking(input_branch_name)
|
||||
test_source_checking(input_source)
|
||||
|
||||
# The button inside dropdown should be disabled if any errors occurred.
|
||||
expect(page).to have_button('Create branch', disabled: true)
|
||||
end
|
||||
|
||||
# The top level button should be disabled if any errors occurred.
|
||||
# The button inside dropdown should be disabled if any errors occurred.
|
||||
expect(page).to have_button('Create branch', disabled: true)
|
||||
end
|
||||
|
||||
context 'when branch name is auto-generated' do
|
||||
it 'creates a merge request', :sidekiq_might_not_need_inline do
|
||||
perform_enqueued_jobs do
|
||||
select_dropdown_option('create-mr')
|
||||
# The top level button should be disabled if any errors occurred.
|
||||
expect(page).to have_button('Create branch', disabled: true)
|
||||
end
|
||||
|
||||
expect(page).to have_content('New merge request')
|
||||
expect(page).to have_content(/From .*#{Regexp.escape(issue.to_branch_name)} into .*#{Regexp.escape(project.default_branch)}/)
|
||||
expect(page).to have_field("Title", with: "Draft: Resolve \"Cherry-Coloured Funk\"")
|
||||
expect(page).to have_field("Description", with: "Closes #{issue.to_reference(upstream_project)}")
|
||||
expect(page).to have_current_path(project_new_merge_request_path(project, merge_request: { source_branch: issue.to_branch_name, target_branch: project.default_branch, issue_iid: issue.iid }))
|
||||
end
|
||||
end
|
||||
context 'when branch name is auto-generated' do
|
||||
it 'creates a merge request', :sidekiq_might_not_need_inline do
|
||||
perform_enqueued_jobs do
|
||||
select_dropdown_option('create-mr')
|
||||
|
||||
it 'creates a branch' do
|
||||
select_dropdown_option('create-branch')
|
||||
|
||||
wait_for_requests
|
||||
|
||||
expect(page).to have_selector('.ref-selector ', text: '1-cherry-coloured-funk')
|
||||
expect(page).to have_current_path project_tree_path(project, '1-cherry-coloured-funk'), ignore_query: true
|
||||
expect(page).to have_content('New merge request')
|
||||
expect(page).to have_content("From #{issue.to_branch_name} into #{project.default_branch}")
|
||||
expect(page).to have_field("Title", with: "Draft: Resolve \"Cherry-Coloured Funk\"")
|
||||
expect(page).to have_field("Description", with: "Closes ##{issue.iid}")
|
||||
expect(page).to have_current_path(project_new_merge_request_path(project, merge_request: { source_branch: issue.to_branch_name, target_branch: project.default_branch, issue_iid: issue.iid }))
|
||||
end
|
||||
end
|
||||
|
||||
context 'when branch name is custom' do
|
||||
let(:branch_name) { 'custom-branch-name' }
|
||||
it 'creates a branch' do
|
||||
select_dropdown_option('create-branch')
|
||||
|
||||
it 'creates a merge request', :sidekiq_might_not_need_inline do
|
||||
perform_enqueued_jobs do
|
||||
select_dropdown_option('create-mr', branch_name)
|
||||
wait_for_requests
|
||||
|
||||
expect(page).to have_content('New merge request')
|
||||
expect(page).to have_content(/From .*#{Regexp.escape(branch_name)} into .*#{Regexp.escape(project.default_branch)}/)
|
||||
expect(page).to have_field("Title", with: "Draft: Resolve \"Cherry-Coloured Funk\"")
|
||||
expect(page).to have_field("Description", with: "Closes #{issue.to_reference(upstream_project)}")
|
||||
expect(page).to have_current_path(project_new_merge_request_path(project, merge_request: { source_branch: branch_name, target_branch: project.default_branch, issue_iid: issue.iid }))
|
||||
end
|
||||
expect(page).to have_selector('.ref-selector ', text: '1-cherry-coloured-funk')
|
||||
expect(page).to have_current_path project_tree_path(project, '1-cherry-coloured-funk'), ignore_query: true
|
||||
end
|
||||
end
|
||||
|
||||
context 'when branch name is custom' do
|
||||
let(:branch_name) { 'custom-branch-name' }
|
||||
|
||||
it 'creates a merge request', :sidekiq_might_not_need_inline do
|
||||
perform_enqueued_jobs do
|
||||
select_dropdown_option('create-mr', branch_name)
|
||||
|
||||
expect(page).to have_content('New merge request')
|
||||
expect(page).to have_content("From #{branch_name} into #{project.default_branch}")
|
||||
expect(page).to have_field("Title", with: "Draft: Resolve \"Cherry-Coloured Funk\"")
|
||||
expect(page).to have_field("Description", with: "Closes ##{issue.iid}")
|
||||
expect(page).to have_current_path(project_new_merge_request_path(project, merge_request: { source_branch: branch_name, target_branch: project.default_branch, issue_iid: issue.iid }))
|
||||
end
|
||||
end
|
||||
|
||||
it 'creates a branch' do
|
||||
select_dropdown_option('create-branch', branch_name)
|
||||
|
||||
wait_for_requests
|
||||
|
||||
expect(page).to have_selector('.ref-selector', text: branch_name)
|
||||
expect(page).to have_current_path project_tree_path(project, branch_name), ignore_query: true
|
||||
end
|
||||
|
||||
context 'when source branch is non-default' do
|
||||
let(:source_branch) { 'feature' }
|
||||
|
||||
it 'creates a branch' do
|
||||
select_dropdown_option('create-branch', branch_name)
|
||||
|
||||
select_dropdown_option('create-branch', branch_name, source_branch)
|
||||
wait_for_requests
|
||||
|
||||
expect(page).to have_selector('.ref-selector', text: branch_name)
|
||||
expect(page).to have_current_path project_tree_path(project, branch_name), ignore_query: true
|
||||
end
|
||||
|
||||
context 'when source branch is non-default' do
|
||||
let(:source_branch) { 'feature' }
|
||||
|
||||
it 'creates a branch' do
|
||||
select_dropdown_option('create-branch', branch_name, source_branch)
|
||||
wait_for_requests
|
||||
|
||||
expect(page).to have_selector('.ref-selector', text: branch_name)
|
||||
expect(page).to have_current_path project_tree_path(project, branch_name), ignore_query: true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when branch name is invalid' do
|
||||
shared_examples 'has error message' do |dropdown|
|
||||
it 'has error message' do
|
||||
select_dropdown_option(dropdown, 'custom-branch-name w~th ^bad chars?')
|
||||
|
||||
wait_for_requests
|
||||
|
||||
expect(page).to have_text("Can't contain spaces, ~, ^, ?")
|
||||
end
|
||||
end
|
||||
|
||||
context 'when creating a merge request', :sidekiq_might_not_need_inline do
|
||||
it_behaves_like 'has error message', 'create-mr'
|
||||
end
|
||||
|
||||
context 'when creating a branch', :sidekiq_might_not_need_inline do
|
||||
it_behaves_like 'has error message', 'create-branch'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when there is a referenced merge request" do
|
||||
let!(:note) do
|
||||
create(
|
||||
:note,
|
||||
:on_issue,
|
||||
:system,
|
||||
project: project,
|
||||
noteable: issue,
|
||||
note: "mentioned in #{referenced_mr.to_reference}"
|
||||
)
|
||||
end
|
||||
|
||||
let(:referenced_mr) do
|
||||
create(
|
||||
:merge_request,
|
||||
:simple,
|
||||
source_project: project,
|
||||
target_project: project,
|
||||
description: "Fixes #{issue.to_reference}",
|
||||
author: user
|
||||
)
|
||||
end
|
||||
|
||||
before do
|
||||
referenced_mr.cache_merge_request_closes_issues!(user)
|
||||
|
||||
visit project_issue_path(project, issue)
|
||||
end
|
||||
|
||||
it 'disables the create branch button', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/27985' do
|
||||
expect(page).to have_css('.create-mr-dropdown-wrap .unavailable:not(.hidden)')
|
||||
expect(page).to have_css('.create-mr-dropdown-wrap .available.hidden', visible: false)
|
||||
expect(page).to have_content(/Related merge requests/)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when merge requests are disabled' do
|
||||
before do
|
||||
project.project_feature.update!(merge_requests_access_level: 0)
|
||||
|
||||
visit project_issue_path(project, issue)
|
||||
end
|
||||
|
||||
it 'shows only create branch button' do
|
||||
expect(page).not_to have_button('Create merge request')
|
||||
expect(page).to have_button('Create branch')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when issue is confidential' do
|
||||
let(:issue) { create(:issue, :confidential, project: project) }
|
||||
|
||||
it 'enables the create branch button' do
|
||||
visit project_issue_path(project, issue)
|
||||
|
||||
expect(page).to have_css('.create-mr-dropdown-wrap')
|
||||
expect(page).to have_button('Create confidential merge request')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when related branch exists' do
|
||||
let!(:project) { create(:project, :repository, :private) }
|
||||
let(:branch_name) { "#{issue.iid}-foo" }
|
||||
|
||||
before do
|
||||
project.repository.create_branch(branch_name)
|
||||
|
||||
visit project_issue_path(project, issue)
|
||||
end
|
||||
|
||||
context 'when user is developer' do
|
||||
it 'shows related branches' do
|
||||
expect(page).to have_css('#related-branches')
|
||||
context 'when branch name is invalid' do
|
||||
shared_examples 'has error message' do |dropdown|
|
||||
it 'has error message' do
|
||||
select_dropdown_option(dropdown, 'custom-branch-name w~th ^bad chars?')
|
||||
|
||||
wait_for_requests
|
||||
|
||||
expect(page).to have_content(branch_name)
|
||||
expect(page).to have_text("Can't contain spaces, ~, ^, ?")
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user is guest' do
|
||||
let(:membership_level) { :guest }
|
||||
context 'when creating a merge request', :sidekiq_might_not_need_inline do
|
||||
it_behaves_like 'has error message', 'create-mr'
|
||||
end
|
||||
|
||||
it 'does not show related branches' do
|
||||
expect(page).not_to have_css('#related-branches')
|
||||
|
||||
wait_for_requests
|
||||
|
||||
expect(page).not_to have_content(branch_name)
|
||||
end
|
||||
context 'when creating a branch', :sidekiq_might_not_need_inline do
|
||||
it_behaves_like 'has error message', 'create-branch'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with source project' do
|
||||
let!(:project) { create(:project, :repository, :public) }
|
||||
let!(:upstream_project) { project }
|
||||
let(:issue) { create(:issue, project: project, title: 'Cherry-Coloured Funk') }
|
||||
context "when there is a referenced merge request" do
|
||||
let!(:note) do
|
||||
create(
|
||||
:note,
|
||||
:on_issue,
|
||||
:system,
|
||||
project: project,
|
||||
noteable: issue,
|
||||
note: "mentioned in #{referenced_mr.to_reference}"
|
||||
)
|
||||
end
|
||||
|
||||
include_examples 'user creates branch and merge request from issue'
|
||||
end
|
||||
let(:referenced_mr) do
|
||||
create(
|
||||
:merge_request,
|
||||
:simple,
|
||||
source_project: project,
|
||||
target_project: project,
|
||||
description: "Fixes #{issue.to_reference}",
|
||||
author: user
|
||||
)
|
||||
end
|
||||
|
||||
context 'with forked project' do
|
||||
let!(:upstream_project) { create(:project, :repository, :public) }
|
||||
let!(:project) { fork_project(upstream_project, user, repository: true) }
|
||||
let(:issue) { create(:issue, project: project, title: 'Cherry-Coloured Funk') }
|
||||
|
||||
before do
|
||||
upstream_project.add_member(user, :guest)
|
||||
end
|
||||
|
||||
include_examples 'user creates branch and merge request from issue'
|
||||
|
||||
context 'when logged in' do
|
||||
before do
|
||||
project.add_member(user, membership_level)
|
||||
sign_in(user)
|
||||
referenced_mr.cache_merge_request_closes_issues!(user)
|
||||
|
||||
visit project_issue_path(project, issue)
|
||||
wait_for_requests
|
||||
end
|
||||
|
||||
it 'creates MR in an upstream project' do
|
||||
find('.js-create-merge-request').click
|
||||
expect(page).to have_current_path(project_new_merge_request_path(project, merge_request: { source_branch: issue.to_branch_name, target_branch: upstream_project.default_branch, issue_iid: issue.iid }))
|
||||
it 'disables the create branch button', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/27985' do
|
||||
expect(page).to have_css('.create-mr-dropdown-wrap .unavailable:not(.hidden)')
|
||||
expect(page).to have_css('.create-mr-dropdown-wrap .available.hidden', visible: false)
|
||||
expect(page).to have_content(/Related merge requests/)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when merge requests are disabled' do
|
||||
before do
|
||||
project.project_feature.update!(merge_requests_access_level: 0)
|
||||
|
||||
visit project_issue_path(project, issue)
|
||||
end
|
||||
|
||||
it 'creates MR in a forked project' do
|
||||
find('.create-mr-dropdown-wrap .dropdown-toggle').click
|
||||
find("#target-project option[label='#{project.full_name}']").select_option
|
||||
wait_for_requests
|
||||
find('.js-create-merge-request').click
|
||||
expect(page).to have_current_path(project_new_merge_request_path(project, merge_request: { source_branch: issue.to_branch_name, target_branch: project.default_branch, issue_iid: issue.iid, target_project_id: project.id }))
|
||||
it 'shows only create branch button' do
|
||||
expect(page).not_to have_button('Create merge request')
|
||||
expect(page).to have_button('Create branch')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when issue is confidential' do
|
||||
let(:issue) { create(:issue, :confidential, project: project) }
|
||||
|
||||
it 'enables the create branch button' do
|
||||
visit project_issue_path(project, issue)
|
||||
|
||||
expect(page).to have_css('.create-mr-dropdown-wrap')
|
||||
expect(page).to have_button('Create confidential merge request')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when related branch exists' do
|
||||
let!(:project) { create(:project, :repository, :private) }
|
||||
let(:branch_name) { "#{issue.iid}-foo" }
|
||||
|
||||
before do
|
||||
project.repository.create_branch(branch_name)
|
||||
|
||||
visit project_issue_path(project, issue)
|
||||
end
|
||||
|
||||
context 'when user is developer' do
|
||||
it 'shows related branches' do
|
||||
expect(page).to have_css('#related-branches')
|
||||
|
||||
wait_for_requests
|
||||
|
||||
expect(page).to have_content(branch_name)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user is guest' do
|
||||
let(:membership_level) { :guest }
|
||||
|
||||
it 'does not show related branches' do
|
||||
expect(page).not_to have_css('#related-branches')
|
||||
|
||||
wait_for_requests
|
||||
|
||||
expect(page).not_to have_content(branch_name)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -81,7 +81,6 @@ export default (
|
|||
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
const validateResults = () => {
|
||||
expect({
|
||||
mutations,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { GlLoadingIcon, GlPagination } from '@gitlab/ui';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import { createWrapper, shallowMount } from '@vue/test-utils';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import Vue, { nextTick } from 'vue';
|
||||
import VueApollo from 'vue-apollo';
|
||||
|
|
@ -31,7 +31,7 @@ import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
|
|||
import { Mousetrap } from '~/lib/mousetrap';
|
||||
import * as urlUtils from '~/lib/utils/url_utility';
|
||||
import * as commonUtils from '~/lib/utils/common_utils';
|
||||
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
|
||||
import { BV_HIDE_TOOLTIP, DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
|
||||
import { stubPerformanceWebAPI } from 'helpers/performance';
|
||||
import { getDiffFileMock } from 'jest/diffs/mock_data/diff_file';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
|
|
@ -1049,4 +1049,26 @@ describe('diffs/components/app', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('tooltips', () => {
|
||||
const scroll = () => {
|
||||
const scrollEvent = document.createEvent('Event');
|
||||
scrollEvent.initEvent('scroll', true, true, window, 1);
|
||||
window.dispatchEvent(scrollEvent);
|
||||
};
|
||||
|
||||
it('hides tooltips on scroll', () => {
|
||||
createComponent({ props: { shouldShow: true } });
|
||||
const rootWrapper = createWrapper(wrapper.vm.$root);
|
||||
scroll();
|
||||
expect(rootWrapper.emitted(BV_HIDE_TOOLTIP)).toStrictEqual([[]]);
|
||||
});
|
||||
|
||||
it('does not hide tooltips on scroll when invisible', () => {
|
||||
createComponent({ props: { shouldShow: false } });
|
||||
const rootWrapper = createWrapper(wrapper.vm.$root);
|
||||
scroll();
|
||||
expect(rootWrapper.emitted(BV_HIDE_TOOLTIP)).toStrictEqual(undefined);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -545,6 +545,7 @@ describe('DiffFileHeader component', () => {
|
|||
|
||||
describe('file reviews', () => {
|
||||
it('calls the action to set the new review', () => {
|
||||
jest.spyOn(document.activeElement, 'blur');
|
||||
createComponent({
|
||||
props: {
|
||||
diffFile: {
|
||||
|
|
@ -564,6 +565,8 @@ describe('DiffFileHeader component', () => {
|
|||
|
||||
findReviewFileCheckbox().vm.$emit('change', true);
|
||||
|
||||
expect(document.activeElement.blur).toHaveBeenCalled();
|
||||
|
||||
return testAction(
|
||||
reviewFile,
|
||||
{ file, reviewed: true },
|
||||
|
|
|
|||
|
|
@ -4,8 +4,6 @@ import confidentialState from '~/confidential_merge_request/state';
|
|||
import CreateMergeRequestDropdown from '~/issues/create_merge_request_dropdown';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
|
||||
const REFS_PATH = `${TEST_HOST}/dummy/refs?search=`;
|
||||
|
||||
describe('CreateMergeRequestDropdown', () => {
|
||||
let axiosMock;
|
||||
let dropdown;
|
||||
|
|
@ -14,7 +12,7 @@ describe('CreateMergeRequestDropdown', () => {
|
|||
axiosMock = new MockAdapter(axios);
|
||||
|
||||
document.body.innerHTML = `
|
||||
<div id="dummy-wrapper-element" data-refs-path="${REFS_PATH}">
|
||||
<div id="dummy-wrapper-element">
|
||||
<div class="available"></div>
|
||||
<div class="unavailable">
|
||||
<div class="js-create-mr-spinner"></div>
|
||||
|
|
@ -32,6 +30,7 @@ describe('CreateMergeRequestDropdown', () => {
|
|||
|
||||
const dummyElement = document.getElementById('dummy-wrapper-element');
|
||||
dropdown = new CreateMergeRequestDropdown(dummyElement);
|
||||
dropdown.refsPath = `${TEST_HOST}/dummy/refs?search=`;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
|
@ -40,7 +39,7 @@ describe('CreateMergeRequestDropdown', () => {
|
|||
|
||||
describe('getRef', () => {
|
||||
it('escapes branch names correctly', async () => {
|
||||
const endpoint = `${REFS_PATH}contains%23hash`;
|
||||
const endpoint = `${dropdown.refsPath}contains%23hash`;
|
||||
jest.spyOn(axios, 'get');
|
||||
axiosMock.onGet(endpoint).replyOnce({});
|
||||
|
||||
|
|
|
|||
|
|
@ -122,6 +122,12 @@ export const MOCK_NAVIGATION = {
|
|||
blobs: {
|
||||
label: 'Code',
|
||||
scope: 'blobs',
|
||||
link: '/search?scope=blobs&search=et&group_id=123',
|
||||
count_link: '/search/count?scope=blobs&group_id=123&search=et',
|
||||
},
|
||||
blobs2: {
|
||||
label: 'Code2',
|
||||
scope: 'blobs',
|
||||
link: '/search?scope=blobs&search=et',
|
||||
count_link: '/search/count?scope=blobs&search=et',
|
||||
},
|
||||
|
|
@ -489,6 +495,14 @@ export const MOCK_NAVIGATION_ITEMS = [
|
|||
{
|
||||
title: 'Code',
|
||||
icon: 'code',
|
||||
link: '/search?scope=blobs&search=et&group_id=123®ex=true',
|
||||
is_active: false,
|
||||
pill_count: '0',
|
||||
items: [],
|
||||
},
|
||||
{
|
||||
title: 'Code2',
|
||||
icon: 'code',
|
||||
link: '/search?scope=blobs&search=et',
|
||||
is_active: false,
|
||||
pill_count: '0',
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import Api from '~/api';
|
|||
import { createAlert } from '~/alert';
|
||||
import * as logger from '~/lib/logger';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import setWindowLocation from 'helpers/set_window_location_helper';
|
||||
import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
|
||||
import * as urlUtils from '~/lib/utils/url_utility';
|
||||
import * as actions from '~/search/store/actions';
|
||||
|
|
@ -13,7 +14,7 @@ import {
|
|||
PROJECTS_LOCAL_STORAGE_KEY,
|
||||
SIDEBAR_PARAMS,
|
||||
REGEX_PARAM,
|
||||
LOCAL_STORAGE_NAME_SPACE_EXTENSION,
|
||||
LS_REGEX_HANDLE,
|
||||
} from '~/search/store/constants';
|
||||
import * as types from '~/search/store/mutation_types';
|
||||
import createState from '~/search/store/state';
|
||||
|
|
@ -29,6 +30,7 @@ import {
|
|||
PRELOAD_EXPECTED_MUTATIONS,
|
||||
PROMISE_ALL_EXPECTED_MUTATIONS,
|
||||
MOCK_NAVIGATION_DATA,
|
||||
MOCK_NAVIGATION,
|
||||
MOCK_NAVIGATION_ACTION_MUTATION,
|
||||
MOCK_ENDPOINT_RESPONSE,
|
||||
MOCK_RECEIVE_AGGREGATIONS_SUCCESS_MUTATION,
|
||||
|
|
@ -38,18 +40,6 @@ import {
|
|||
} from '../mock_data';
|
||||
|
||||
jest.mock('~/alert');
|
||||
jest.mock('~/lib/utils/url_utility', () => ({
|
||||
setUrlParams: jest.fn(),
|
||||
joinPaths: jest.fn().mockReturnValue(''),
|
||||
visitUrl: jest.fn(),
|
||||
queryToObject: jest.fn().mockReturnValue({ scope: 'projects', search: '' }),
|
||||
objectToQuery: jest.fn((params) =>
|
||||
Object.keys(params)
|
||||
.map((k) => `${encodeURIComponent(k)}=${encodeURIComponent(params[k])}`)
|
||||
.join('&'),
|
||||
),
|
||||
getBaseURL: jest.fn().mockReturnValue('http://gdk.test:3000'),
|
||||
}));
|
||||
|
||||
jest.mock('~/lib/logger', () => ({
|
||||
logError: jest.fn(),
|
||||
|
|
@ -203,28 +193,32 @@ describe('Global Search Store Actions', () => {
|
|||
});
|
||||
|
||||
it(`setsItem in local storage`, () => {
|
||||
expect(storeUtils.setDataToLS).toHaveBeenCalledWith(
|
||||
`${payload.key}_${LOCAL_STORAGE_NAME_SPACE_EXTENSION}`,
|
||||
expect.anything(),
|
||||
);
|
||||
expect(storeUtils.setDataToLS).toHaveBeenCalledWith(LS_REGEX_HANDLE, expect.anything());
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('applyQuery', () => {
|
||||
beforeEach(() => {
|
||||
setWindowLocation('https://test/');
|
||||
jest.spyOn(urlUtils, 'visitUrl').mockReturnValue({});
|
||||
});
|
||||
|
||||
it('calls visitUrl and setParams with the state.query', async () => {
|
||||
await testAction(actions.applyQuery, null, state, [], []);
|
||||
expect(urlUtils.setUrlParams).toHaveBeenCalledWith(
|
||||
{ ...state.query, page: null },
|
||||
'http://test.host/',
|
||||
false,
|
||||
true,
|
||||
expect(urlUtils.visitUrl).toHaveBeenCalledWith(
|
||||
'https://test/?scope=issues&state=all&group_id=1&language%5B%5D=C&language%5B%5D=JavaScript&labels%5B%5D=60&labels%5B%5D=37&search=*',
|
||||
);
|
||||
expect(urlUtils.visitUrl).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('resetQuery', () => {
|
||||
beforeEach(() => {
|
||||
setWindowLocation('https://test/');
|
||||
jest.spyOn(urlUtils, 'visitUrl').mockReturnValue({});
|
||||
jest.spyOn(urlUtils, 'setUrlParams').mockReturnValue({});
|
||||
});
|
||||
|
||||
it('calls visitUrl and setParams with empty values', async () => {
|
||||
await testAction(actions.resetQuery, null, state, [], []);
|
||||
const resetParams = SIDEBAR_PARAMS.reduce((acc, param) => {
|
||||
|
|
@ -327,6 +321,9 @@ describe('Global Search Store Actions', () => {
|
|||
state.urlQuery = {
|
||||
scope,
|
||||
};
|
||||
state.query = {
|
||||
search: 'et',
|
||||
};
|
||||
|
||||
if (axiosMock.method) {
|
||||
mock[axiosMock.method]().reply(axiosMock.code, MOCK_ENDPOINT_RESPONSE);
|
||||
|
|
@ -360,17 +357,15 @@ describe('Global Search Store Actions', () => {
|
|||
|
||||
describe('fetchSidebarCount uses wild card seach', () => {
|
||||
beforeEach(() => {
|
||||
state.navigation = mapValues(MOCK_NAVIGATION_DATA, (navItem) => ({
|
||||
...navItem,
|
||||
count_link: '/search/count?scope=projects&search=',
|
||||
}));
|
||||
state.navigation = MOCK_NAVIGATION;
|
||||
state.urlQuery.search = '';
|
||||
});
|
||||
|
||||
it('should use wild card', async () => {
|
||||
await testAction({ action: actions.fetchSidebarCount, state, expectedMutations: [] });
|
||||
expect(mock.history.get[0].url).toBe(
|
||||
'http://gdk.test:3000/search/count?scope=projects&search=*',
|
||||
expect(mock.history.get[0].url).toBe('http://test.host/search/count?scope=projects&search=*');
|
||||
expect(mock.history.get[3].url).toBe(
|
||||
'http://test.host/search/count?scope=merge_requests&search=*',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,5 +1,9 @@
|
|||
import { cloneDeep } from 'lodash';
|
||||
import { GROUPS_LOCAL_STORAGE_KEY, PROJECTS_LOCAL_STORAGE_KEY } from '~/search/store/constants';
|
||||
import {
|
||||
GROUPS_LOCAL_STORAGE_KEY,
|
||||
PROJECTS_LOCAL_STORAGE_KEY,
|
||||
LS_REGEX_HANDLE,
|
||||
} from '~/search/store/constants';
|
||||
import * as getters from '~/search/store/getters';
|
||||
import createState from '~/search/store/state';
|
||||
import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
|
||||
|
|
@ -71,6 +75,8 @@ describe('Global Search Store Getters', () => {
|
|||
|
||||
describe('navigationItems', () => {
|
||||
it('returns the re-mapped navigation data', () => {
|
||||
localStorage.setItem(LS_REGEX_HANDLE, JSON.stringify(true));
|
||||
|
||||
state.navigation = MOCK_NAVIGATION;
|
||||
expect(getters.navigationItems(state)).toStrictEqual(MOCK_NAVIGATION_ITEMS);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
|
||||
import { MAX_FREQUENCY, SIDEBAR_PARAMS } from '~/search/store/constants';
|
||||
import { MAX_FREQUENCY, SIDEBAR_PARAMS, LS_REGEX_HANDLE } from '~/search/store/constants';
|
||||
import {
|
||||
loadDataFromLS,
|
||||
setFrequentItemToLS,
|
||||
|
|
@ -9,8 +9,10 @@ import {
|
|||
getAggregationsUrl,
|
||||
prepareSearchAggregations,
|
||||
addCountOverLimit,
|
||||
injectRegexSearch,
|
||||
} from '~/search/store/utils';
|
||||
import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
|
||||
import { TEST_HOST } from 'helpers/test_constants';
|
||||
import {
|
||||
MOCK_LS_KEY,
|
||||
MOCK_GROUPS,
|
||||
|
|
@ -303,4 +305,55 @@ describe('Global Search Store Utils', () => {
|
|||
expect(addCountOverLimit()).toEqual('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('injectRegexSearch', () => {
|
||||
describe.each`
|
||||
urlIn | urlOut
|
||||
${`${TEST_HOST}/search?search=test&group_id=123`} | ${'/search?search=test&group_id=123'}
|
||||
${'/search?search=test&group_id=123'} | ${'/search?search=test&group_id=123'}
|
||||
${`${TEST_HOST}/search?search=test&project_id=123`} | ${'/search?search=test&project_id=123'}
|
||||
${'/search?search=test&project_id=123'} | ${'/search?search=test&project_id=123'}
|
||||
${`${TEST_HOST}/search?search=test&project_id=123&group_id=123`} | ${'/search?search=test&project_id=123&group_id=123'}
|
||||
${'/search?search=test&project_id=123&group_id=123'} | ${'/search?search=test&project_id=123&group_id=123'}
|
||||
`('modifies urls and links', ({ urlIn, urlOut }) => {
|
||||
it(`should add regex=true to ${urlIn}`, () => {
|
||||
localStorage.setItem(LS_REGEX_HANDLE, JSON.stringify(true));
|
||||
expect(injectRegexSearch(urlIn)).toEqual(`${urlOut}®ex=true`);
|
||||
});
|
||||
|
||||
it(`should NOT add regex=true to ${urlIn}`, () => {
|
||||
localStorage.setItem(LS_REGEX_HANDLE, JSON.stringify(false));
|
||||
expect(injectRegexSearch(urlIn)).toEqual(urlOut);
|
||||
});
|
||||
});
|
||||
|
||||
describe.each`
|
||||
urlIn | urlOut
|
||||
${'/search?search=test'} | ${'/search?search=test'}
|
||||
${`${TEST_HOST}/search?search=test`} | ${'/search?search=test'}
|
||||
${'/'} | ${'/'}
|
||||
`('does not modify urls and links', ({ urlIn, urlOut }) => {
|
||||
it('should return link with regex equals true', () => {
|
||||
localStorage.setItem(LS_REGEX_HANDLE, JSON.stringify(true));
|
||||
expect(injectRegexSearch(urlIn)).toEqual(urlOut);
|
||||
});
|
||||
});
|
||||
|
||||
describe.each`
|
||||
urlIn | urlOut
|
||||
${'/search?search=test&group_id=123®ex=true'} | ${'/search?search=test&group_id=123®ex=true'}
|
||||
${'/search?search=test&project_id=123®ex=true'} | ${'/search?search=test&project_id=123®ex=true'}
|
||||
${'/search?search=test&project_id=123&group_id=123®ex=true'} | ${'/search?search=test&project_id=123&group_id=123®ex=true'}
|
||||
`('does not double params', ({ urlIn, urlOut }) => {
|
||||
it(`should not add additional regex=true to ${urlIn} if enabled`, () => {
|
||||
localStorage.setItem(LS_REGEX_HANDLE, JSON.stringify(true));
|
||||
expect(injectRegexSearch(urlIn)).toEqual(`${urlOut}`);
|
||||
});
|
||||
|
||||
it(`should NOT remove regex=true from ${urlIn} if disabled`, () => {
|
||||
localStorage.setItem(LS_REGEX_HANDLE, JSON.stringify(false));
|
||||
expect(injectRegexSearch(urlIn)).toEqual(urlOut);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ Vue.use(Vuex);
|
|||
|
||||
jest.mock('~/search/store/utils', () => ({
|
||||
loadDataFromLS: jest.fn(() => true),
|
||||
LS_REGEX_HANDLE: jest.fn(() => 'test'),
|
||||
}));
|
||||
|
||||
describe('GlobalSearchTopbar', () => {
|
||||
|
|
@ -184,7 +185,7 @@ describe('GlobalSearchTopbar', () => {
|
|||
describe.each`
|
||||
search | reload
|
||||
${''} | ${0}
|
||||
${'test'} | ${2}
|
||||
${'test'} | ${1}
|
||||
`('clicking regular expression button', ({ search, reload }) => {
|
||||
beforeEach(() => {
|
||||
createComponent({ query: { search }, searchType: 'zoekt' }, '', { GlSearchBoxByType });
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import CommandPaletteItems from '~/super_sidebar/components/global_search/comman
|
|||
import CommandsOverviewDropdown from '~/super_sidebar/components/global_search/command_palette/command_overview_dropdown.vue';
|
||||
import ScrollScrim from '~/super_sidebar/components/scroll_scrim.vue';
|
||||
import { useMockInternalEventsTracking } from 'helpers/tracking_internal_events_helper';
|
||||
import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
|
||||
import {
|
||||
SEARCH_OR_COMMAND_MODE_PLACEHOLDER,
|
||||
COMMON_HANDLES,
|
||||
|
|
@ -52,6 +53,14 @@ Vue.use(Vuex);
|
|||
|
||||
jest.mock('~/lib/utils/url_utility', () => ({
|
||||
visitUrl: jest.fn(),
|
||||
queryToObject: jest.fn(),
|
||||
objectToQuery: jest.fn(() => 'search=test'),
|
||||
isRootRelative: jest.fn(),
|
||||
getBaseURL: jest.fn(() => 'https://gdk.test:3000'),
|
||||
}));
|
||||
|
||||
jest.mock('~/search/store/utils.js', () => ({
|
||||
injectRegexSearch: jest.fn(() => '/search?search=test'),
|
||||
}));
|
||||
|
||||
const triggerKeydownEvent = (target, code, metaKey = false) => {
|
||||
|
|
@ -136,6 +145,9 @@ describe('GlobalSearchModal', () => {
|
|||
const findCommandPaletteDropdown = () => wrapper.findComponent(CommandsOverviewDropdown);
|
||||
|
||||
describe('template', () => {
|
||||
beforeEach(() => {
|
||||
useMockLocationHelper();
|
||||
});
|
||||
describe('always renders', () => {
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
|
|
@ -379,6 +391,7 @@ describe('GlobalSearchModal', () => {
|
|||
it('will submit a search with the sufficient number of characters', () => {
|
||||
createComponent();
|
||||
findGlobalSearchInput().vm.$emit('input', MOCK_SEARCH);
|
||||
|
||||
submitSearch();
|
||||
expect(visitUrl).toHaveBeenCalledWith(MOCK_SEARCH_QUERY);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -15,6 +15,12 @@ import {
|
|||
MSG_IN_ALL_GITLAB,
|
||||
} from '~/vue_shared/global_search/constants';
|
||||
|
||||
import {
|
||||
SCOPE_SEARCH_PROJECT,
|
||||
SCOPE_SEARCH_GROUP,
|
||||
SCOPE_SEARCH_ALL,
|
||||
} from '~/super_sidebar/components/global_search/command_palette/constants';
|
||||
|
||||
export const MOCK_USERNAME = 'anyone';
|
||||
|
||||
export const MOCK_SEARCH_PATH = '/search';
|
||||
|
|
@ -51,7 +57,7 @@ export const MOCK_SUBGROUP = {
|
|||
path: `${MOCK_GROUP}/mock-subgroup`,
|
||||
};
|
||||
|
||||
export const MOCK_SEARCH_QUERY = 'http://gitlab.com/search?search=test';
|
||||
export const MOCK_SEARCH_QUERY = '/search?search=test';
|
||||
|
||||
export const MOCK_SEARCH = 'test';
|
||||
|
||||
|
|
@ -104,7 +110,7 @@ export const MOCK_DEFAULT_SEARCH_OPTIONS = [
|
|||
];
|
||||
export const MOCK_SCOPED_SEARCH_OPTIONS_DEF = [
|
||||
{
|
||||
text: 'scoped-in-project',
|
||||
text: SCOPE_SEARCH_PROJECT,
|
||||
scope: MOCK_PROJECT.name,
|
||||
scopeCategory: PROJECTS_CATEGORY,
|
||||
icon: ICON_PROJECT,
|
||||
|
|
@ -115,7 +121,7 @@ export const MOCK_SCOPED_SEARCH_OPTIONS_DEF = [
|
|||
},
|
||||
},
|
||||
{
|
||||
text: 'scoped-in-group',
|
||||
text: SCOPE_SEARCH_GROUP,
|
||||
scope: MOCK_GROUP.name,
|
||||
scopeCategory: GROUPS_CATEGORY,
|
||||
icon: ICON_GROUP,
|
||||
|
|
@ -126,7 +132,7 @@ export const MOCK_SCOPED_SEARCH_OPTIONS_DEF = [
|
|||
},
|
||||
},
|
||||
{
|
||||
text: 'scoped-in-all',
|
||||
text: SCOPE_SEARCH_ALL,
|
||||
description: MSG_IN_ALL_GITLAB,
|
||||
href: MOCK_ALL_PATH,
|
||||
extraAttrs: {
|
||||
|
|
@ -137,7 +143,7 @@ export const MOCK_SCOPED_SEARCH_OPTIONS_DEF = [
|
|||
];
|
||||
export const MOCK_SCOPED_SEARCH_OPTIONS = [
|
||||
{
|
||||
text: 'scoped-in-project',
|
||||
text: SCOPE_SEARCH_PROJECT,
|
||||
scope: MOCK_PROJECT.name,
|
||||
scopeCategory: PROJECTS_CATEGORY,
|
||||
icon: ICON_PROJECT,
|
||||
|
|
@ -151,7 +157,7 @@ export const MOCK_SCOPED_SEARCH_OPTIONS = [
|
|||
url: MOCK_PROJECT_LONG.path,
|
||||
},
|
||||
{
|
||||
text: 'scoped-in-group',
|
||||
text: SCOPE_SEARCH_GROUP,
|
||||
scope: MOCK_GROUP.name,
|
||||
scopeCategory: GROUPS_CATEGORY,
|
||||
icon: ICON_GROUP,
|
||||
|
|
@ -165,7 +171,7 @@ export const MOCK_SCOPED_SEARCH_OPTIONS = [
|
|||
url: MOCK_SUBGROUP.path,
|
||||
},
|
||||
{
|
||||
text: 'scoped-in-all',
|
||||
text: SCOPE_SEARCH_ALL,
|
||||
description: MSG_IN_ALL_GITLAB,
|
||||
url: MOCK_ALL_PATH,
|
||||
},
|
||||
|
|
@ -174,21 +180,21 @@ export const MOCK_SCOPED_SEARCH_OPTIONS = [
|
|||
export const MOCK_SCOPED_SEARCH_GROUP = {
|
||||
items: [
|
||||
{
|
||||
text: 'scoped-in-project',
|
||||
text: SCOPE_SEARCH_PROJECT,
|
||||
scope: MOCK_PROJECT.name,
|
||||
scopeCategory: PROJECTS_CATEGORY,
|
||||
icon: ICON_PROJECT,
|
||||
href: MOCK_PROJECT.path,
|
||||
},
|
||||
{
|
||||
text: 'scoped-in-group',
|
||||
text: SCOPE_SEARCH_GROUP,
|
||||
scope: MOCK_GROUP.name,
|
||||
scopeCategory: GROUPS_CATEGORY,
|
||||
icon: ICON_GROUP,
|
||||
href: MOCK_GROUP.path,
|
||||
},
|
||||
{
|
||||
text: 'scoped-in-all',
|
||||
text: SCOPE_SEARCH_ALL,
|
||||
description: MSG_IN_ALL_GITLAB,
|
||||
href: MOCK_ALL_PATH,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import {
|
|||
inboundJobTokenScopeEnabledResponse,
|
||||
inboundJobTokenScopeDisabledResponse,
|
||||
inboundGroupsAndProjectsWithScopeResponse,
|
||||
inboundGroupsAndProjectsWithScopeResponseWithAddedItem,
|
||||
inboundAddGroupOrProjectSuccessResponse,
|
||||
inboundRemoveGroupSuccess,
|
||||
inboundRemoveProjectSuccess,
|
||||
|
|
@ -232,7 +233,7 @@ describe('TokenAccess component', () => {
|
|||
type | testPath
|
||||
${'group'} | ${testGroupPath}
|
||||
${'project'} | ${testProjectPath}
|
||||
`('add $type', ({ testPath }) => {
|
||||
`('add $type', ({ type, testPath }) => {
|
||||
it(`calls add group or project mutation`, async () => {
|
||||
createComponent(
|
||||
[
|
||||
|
|
@ -261,6 +262,44 @@ describe('TokenAccess component', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it(`increments the ${type} count`, async () => {
|
||||
createComponent(
|
||||
[
|
||||
[inboundGetCIJobTokenScopeQuery, inboundJobTokenScopeEnabledResponseHandler],
|
||||
[
|
||||
inboundGetGroupsAndProjectsWithCIJobTokenScopeQuery,
|
||||
jest
|
||||
.fn()
|
||||
.mockResolvedValueOnce(inboundGroupsAndProjectsWithScopeResponse)
|
||||
.mockResolvedValueOnce(inboundGroupsAndProjectsWithScopeResponseWithAddedItem),
|
||||
],
|
||||
[
|
||||
inboundAddGroupOrProjectCIJobTokenScopeMutation,
|
||||
inboundAddGroupOrProjectSuccessResponseHandler,
|
||||
],
|
||||
],
|
||||
mountExtended,
|
||||
);
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
expect(wrapper.findByTestId(`${type}-count`).text()).toBe('1');
|
||||
expect(wrapper.findByTestId(`${type}-count`).attributes('title')).toBe(
|
||||
`1 ${type} has access`,
|
||||
);
|
||||
|
||||
await findToggleFormBtn().trigger('click');
|
||||
await findProjectInput().vm.$emit('input', testPath);
|
||||
findAddProjectBtn().trigger('click');
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
expect(wrapper.findByTestId(`${type}-count`).text()).toBe('2');
|
||||
expect(wrapper.findByTestId(`${type}-count`).attributes('title')).toBe(
|
||||
`2 ${type}s have access`,
|
||||
);
|
||||
});
|
||||
|
||||
it('add group or project handles error correctly', async () => {
|
||||
createComponent(
|
||||
[
|
||||
|
|
@ -343,6 +382,39 @@ describe('TokenAccess component', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it(`decrements the ${type} count`, async () => {
|
||||
createComponent(
|
||||
[
|
||||
[inboundGetCIJobTokenScopeQuery, inboundJobTokenScopeEnabledResponseHandler],
|
||||
[
|
||||
inboundGetGroupsAndProjectsWithCIJobTokenScopeQuery,
|
||||
jest
|
||||
.fn()
|
||||
.mockResolvedValueOnce(inboundGroupsAndProjectsWithScopeResponseWithAddedItem)
|
||||
.mockResolvedValueOnce(inboundGroupsAndProjectsWithScopeResponse),
|
||||
],
|
||||
[mutation, handler],
|
||||
],
|
||||
mountExtended,
|
||||
);
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
expect(wrapper.findByTestId(`${type}-count`).text()).toBe('2');
|
||||
expect(wrapper.findByTestId(`${type}-count`).attributes('title')).toBe(
|
||||
`2 ${type}s have access`,
|
||||
);
|
||||
|
||||
findRemoveProjectBtnAt(index).trigger('click');
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
expect(wrapper.findByTestId(`${type}-count`).text()).toBe('1');
|
||||
expect(wrapper.findByTestId(`${type}-count`).attributes('title')).toBe(
|
||||
`1 ${type} has access`,
|
||||
);
|
||||
});
|
||||
|
||||
it(`remove ${type} handles error correctly`, async () => {
|
||||
createComponent(
|
||||
[
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
export const enabledJobTokenScope = {
|
||||
data: {
|
||||
project: {
|
||||
id: '1',
|
||||
id: 1,
|
||||
ciCdSettings: {
|
||||
jobTokenScopeEnabled: true,
|
||||
__typename: 'ProjectCiCdSetting',
|
||||
|
|
@ -14,7 +14,7 @@ export const enabledJobTokenScope = {
|
|||
export const disabledJobTokenScope = {
|
||||
data: {
|
||||
project: {
|
||||
id: '1',
|
||||
id: 1,
|
||||
ciCdSettings: {
|
||||
jobTokenScopeEnabled: false,
|
||||
__typename: 'ProjectCiCdSetting',
|
||||
|
|
@ -28,14 +28,14 @@ export const projectsWithScope = {
|
|||
data: {
|
||||
project: {
|
||||
__typename: 'Project',
|
||||
id: '1',
|
||||
id: 1,
|
||||
ciJobTokenScope: {
|
||||
__typename: 'CiJobTokenScopeType',
|
||||
projects: {
|
||||
__typename: 'ProjectConnection',
|
||||
nodes: [
|
||||
{
|
||||
id: '2',
|
||||
id: 2,
|
||||
fullPath: 'root/332268-test',
|
||||
name: 'root/332268-test',
|
||||
namespace: {
|
||||
|
|
@ -141,7 +141,7 @@ export const mockFields = [
|
|||
export const inboundJobTokenScopeEnabledResponse = {
|
||||
data: {
|
||||
project: {
|
||||
id: '1',
|
||||
id: 1,
|
||||
ciCdSettings: {
|
||||
inboundJobTokenScopeEnabled: true,
|
||||
__typename: 'ProjectCiCdSetting',
|
||||
|
|
@ -154,7 +154,7 @@ export const inboundJobTokenScopeEnabledResponse = {
|
|||
export const inboundJobTokenScopeDisabledResponse = {
|
||||
data: {
|
||||
project: {
|
||||
id: '1',
|
||||
id: 1,
|
||||
ciCdSettings: {
|
||||
inboundJobTokenScopeEnabled: false,
|
||||
__typename: 'ProjectCiCdSetting',
|
||||
|
|
@ -168,7 +168,7 @@ export const inboundGroupsAndProjectsWithScopeResponse = {
|
|||
data: {
|
||||
project: {
|
||||
__typename: 'Project',
|
||||
id: '1',
|
||||
id: 1,
|
||||
ciJobTokenScope: {
|
||||
__typename: 'CiJobTokenScopeType',
|
||||
inboundAllowlist: {
|
||||
|
|
@ -179,7 +179,7 @@ export const inboundGroupsAndProjectsWithScopeResponse = {
|
|||
fullPath: 'root/ci-project',
|
||||
id: 'gid://gitlab/Project/23',
|
||||
name: 'ci-project',
|
||||
namespace: { id: 'gid://gitlab/Namespaces::UserNamespace/1', fullPath: 'root' },
|
||||
avatarUrl: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
@ -191,6 +191,43 @@ export const inboundGroupsAndProjectsWithScopeResponse = {
|
|||
fullPath: 'root/ci-group',
|
||||
id: 'gid://gitlab/Group/45',
|
||||
name: 'ci-group',
|
||||
avatarUrl: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const inboundGroupsAndProjectsWithScopeResponseWithAddedItem = {
|
||||
data: {
|
||||
project: {
|
||||
...inboundGroupsAndProjectsWithScopeResponse.data.project,
|
||||
ciJobTokenScope: {
|
||||
inboundAllowlist: {
|
||||
nodes: [
|
||||
...inboundGroupsAndProjectsWithScopeResponse.data.project.ciJobTokenScope
|
||||
.inboundAllowlist.nodes,
|
||||
{
|
||||
__typename: 'Project',
|
||||
fullPath: 'root/test',
|
||||
id: 'gid://gitlab/Project/25',
|
||||
name: 'test',
|
||||
avatarUrl: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
groupsAllowlist: {
|
||||
nodes: [
|
||||
...inboundGroupsAndProjectsWithScopeResponse.data.project.ciJobTokenScope
|
||||
.groupsAllowlist.nodes,
|
||||
{
|
||||
__typename: 'Group',
|
||||
fullPath: 'gitlab-org',
|
||||
id: 'gid://gitlab/Group/49',
|
||||
name: 'gitlab-org',
|
||||
avatarUrl: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
|
|||
|
|
@ -19,7 +19,9 @@ describe('Token access table', () => {
|
|||
|
||||
const findTable = () => wrapper.findComponent(GlTableLite);
|
||||
const findDeleteButton = () => wrapper.findComponent(GlButton);
|
||||
const findAllTableRows = (type) => wrapper.findAllByTestId(`${type}s-token-table-row`);
|
||||
const findAllTableRows = () => wrapper.findAllByTestId('token-access-table-row');
|
||||
const findIcon = (type) => wrapper.findByTestId(`token-access-${type}-icon`);
|
||||
const findProjectAvatar = (type) => wrapper.findByTestId(`token-access-${type}-avatar`);
|
||||
const findName = (type) => wrapper.findByTestId(`token-access-${type}-name`);
|
||||
|
||||
describe.each`
|
||||
|
|
@ -49,8 +51,14 @@ describe('Token access table', () => {
|
|||
expect(wrapper.emitted('removeItem')).toEqual([[items[0]]]);
|
||||
});
|
||||
|
||||
it('displays fullpath', () => {
|
||||
it('displays icon and avatar', () => {
|
||||
expect(findIcon(type).props('name')).toBe(type);
|
||||
expect(findProjectAvatar(type).props('projectName')).toBe(items[0].name);
|
||||
});
|
||||
|
||||
it('displays fullpath as a link to the project', () => {
|
||||
expect(findName(type).text()).toBe(items[0].fullPath);
|
||||
expect(findName(type).attributes('href')).toBe(`/${items[0].fullPath}`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,15 +3,6 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Projects::IssuesHelper, feature_category: :team_planning do
|
||||
include ProjectForksHelper
|
||||
|
||||
let_it_be(:current_user) { build(:user) }
|
||||
let_it_be(:project) { build(:project, :public) }
|
||||
# rubocop:disable RSpec/FactoryBot/AvoidCreate -- forked projects require persistence
|
||||
let_it_be(:source_project) { create(:project, :public) }
|
||||
# rubocop:enable RSpec/FactoryBot/AvoidCreate
|
||||
let_it_be(:forked_project) { fork_project(source_project, current_user, repository: true) }
|
||||
|
||||
describe '#create_mr_tracking_data' do
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
|
||||
|
|
@ -27,24 +18,4 @@ RSpec.describe Projects::IssuesHelper, feature_category: :team_planning do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#default_target' do
|
||||
context 'when a project has no forks' do
|
||||
it 'returns the same project' do
|
||||
expect(default_target(project)).to be project
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a project has forks' do
|
||||
it 'returns the source project' do
|
||||
expect(default_target(forked_project)).to eq source_project
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#target_projects' do
|
||||
it 'returns all the forks and the source project' do
|
||||
expect(target_projects(forked_project)).to eq [source_project, forked_project]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1140,7 +1140,7 @@ RSpec.describe Issue, feature_category: :team_planning do
|
|||
allow(project).to receive(:forked?).and_return(true)
|
||||
end
|
||||
|
||||
it { is_expected.to be_can_be_worked_on }
|
||||
it { is_expected.not_to be_can_be_worked_on }
|
||||
end
|
||||
|
||||
it { is_expected.to be_can_be_worked_on }
|
||||
|
|
@ -2292,4 +2292,100 @@ RSpec.describe Issue, feature_category: :team_planning do
|
|||
it { is_expected.to be_truthy }
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'a markdown field that parses work item references' do
|
||||
shared_examples 'a html field with work item information' do
|
||||
it 'parses the work item reference' do
|
||||
html_link = Nokogiri::HTML.fragment(issue[:"#{field}_html"]).css('a').first
|
||||
|
||||
expect(html_link.text).to eq(expected_link_text)
|
||||
expect(html_link[:href]).to eq(work_item_path)
|
||||
end
|
||||
end
|
||||
|
||||
let_it_be(:group) { create(:group) }
|
||||
|
||||
context 'when it is a group level issue', :aggregate_failures do
|
||||
let(:issue) { create(:issue, :group_level, namespace: group, field => work_item_reference) }
|
||||
let(:work_item_path) { Gitlab::UrlBuilder.build(group_work_item, only_path: true) }
|
||||
let(:expected_link_text) { group_work_item.to_reference }
|
||||
|
||||
context 'when field contains a work item reference (URL)' do
|
||||
let(:work_item_path) { Gitlab::UrlBuilder.build(group_work_item) }
|
||||
let(:work_item_reference) { work_item_path }
|
||||
|
||||
it_behaves_like 'a html field with work item information'
|
||||
end
|
||||
|
||||
context 'when field contains a work item reference (short)' do
|
||||
let(:work_item_reference) { group_work_item.to_reference }
|
||||
|
||||
it_behaves_like 'a html field with work item information'
|
||||
end
|
||||
|
||||
context 'when field contains a work item reference (full)' do
|
||||
let(:work_item_reference) { group_work_item.to_reference(full: true) }
|
||||
|
||||
it_behaves_like 'a html field with work item information'
|
||||
end
|
||||
|
||||
context 'when field contains a project level work item reference (URL)' do
|
||||
let(:work_item_path) { Gitlab::UrlBuilder.build(project_work_item) }
|
||||
let(:work_item_reference) { work_item_path }
|
||||
let(:expected_link_text) { "#{reusable_project.full_path}##{project_work_item.iid}" }
|
||||
|
||||
it_behaves_like 'a html field with work item information'
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is a project level issue', :aggregate_failures do
|
||||
let(:issue) { create(:issue, :task, project: reusable_project, field => work_item_reference) }
|
||||
let(:work_item_path) { Gitlab::UrlBuilder.build(project_work_item, only_path: true) }
|
||||
let(:expected_link_text) { group_work_item.to_reference }
|
||||
|
||||
context 'when field contains a work item reference (URL)' do
|
||||
let(:work_item_path) { Gitlab::UrlBuilder.build(project_work_item) }
|
||||
let(:work_item_reference) { work_item_path }
|
||||
let(:expected_link_text) { project_work_item.to_reference }
|
||||
|
||||
it_behaves_like 'a html field with work item information'
|
||||
end
|
||||
|
||||
context 'when field contains a work item reference (short)' do
|
||||
let(:work_item_reference) { project_work_item.to_reference }
|
||||
|
||||
it_behaves_like 'a html field with work item information'
|
||||
end
|
||||
|
||||
context 'when field contains a work item reference (full)' do
|
||||
let(:work_item_reference) { project_work_item.to_reference(full: true) }
|
||||
|
||||
it_behaves_like 'a html field with work item information'
|
||||
end
|
||||
|
||||
context 'when field contains a group level work item reference (URL)' do
|
||||
let(:work_item_path) { Gitlab::UrlBuilder.build(group_work_item) }
|
||||
let(:work_item_reference) { work_item_path }
|
||||
let(:expected_link_text) { "#{group.full_path}##{group_work_item.iid}" }
|
||||
|
||||
it_behaves_like 'a html field with work item information'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#title_html' do
|
||||
it_behaves_like 'a markdown field that parses work item references' do
|
||||
let_it_be(:group_work_item) { create(:work_item, :group_level, namespace: group) }
|
||||
let_it_be(:project_work_item) { create(:work_item, :task, project: reusable_project) }
|
||||
let(:field) { :title }
|
||||
end
|
||||
end
|
||||
|
||||
describe '#description_html' do
|
||||
it_behaves_like 'a markdown field that parses work item references' do
|
||||
let_it_be(:group_work_item) { create(:work_item, :group_level, namespace: group) }
|
||||
let_it_be(:project_work_item) { create(:work_item, :task, project: reusable_project) }
|
||||
let(:field) { :description }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -518,6 +518,55 @@ RSpec.describe Note, feature_category: :team_planning do
|
|||
let(:set_mentionable_text) { ->(txt) { subject.note = txt } }
|
||||
end
|
||||
|
||||
describe '#note_html' do
|
||||
shared_examples 'note that parses work item references' do
|
||||
it 'parses the work item reference' do
|
||||
html_link = Nokogiri::HTML.fragment(note.note_html).css('a').first
|
||||
|
||||
expect(html_link.text).to eq(expected_link_text)
|
||||
expect(html_link[:href]).to eq(work_item_path)
|
||||
end
|
||||
end
|
||||
|
||||
let_it_be(:group) { create(:group) }
|
||||
let_it_be(:project) { create(:project, group: group) }
|
||||
let_it_be(:group_work_item) { create(:work_item, :group_level, namespace: group) }
|
||||
let_it_be(:project_work_item) { create(:work_item, :task, project: project) }
|
||||
|
||||
context 'when noteable is a group level work item', :aggregate_failures do
|
||||
let(:work_item_path) { Gitlab::UrlBuilder.build(group_work_item, only_path: true) }
|
||||
let(:expected_link_text) { group_work_item.to_reference }
|
||||
let(:note) { create(:note, :on_group_work_item, noteable: group_work_item, note: note_text) }
|
||||
|
||||
context 'when note text contains a group reference (URL)' do
|
||||
let(:work_item_path) { Gitlab::UrlBuilder.build(group_work_item) }
|
||||
let(:note_text) { work_item_path }
|
||||
|
||||
it_behaves_like 'note that parses work item references'
|
||||
end
|
||||
|
||||
context 'when note text contains a group reference (short)' do
|
||||
let(:note_text) { group_work_item.to_reference }
|
||||
|
||||
it_behaves_like 'note that parses work item references'
|
||||
end
|
||||
|
||||
context 'when note text contains a group reference (full)' do
|
||||
let(:note_text) { group_work_item.to_reference(full: true) }
|
||||
|
||||
it_behaves_like 'note that parses work item references'
|
||||
end
|
||||
|
||||
context 'when note text contains a project reference (URL)' do
|
||||
let(:work_item_path) { Gitlab::UrlBuilder.build(project_work_item) }
|
||||
let(:note_text) { work_item_path }
|
||||
let(:expected_link_text) { "#{project.path}##{project_work_item.iid}" }
|
||||
|
||||
it_behaves_like 'note that parses work item references'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#all_references" do
|
||||
let!(:note1) { create(:note_on_issue) }
|
||||
let!(:note2) { create(:note_on_issue) }
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ RSpec.describe MergeRequests::BuildService, feature_category: :code_review_workf
|
|||
let(:user) { create(:user, name: "John Doe", email: "jdoe@gitlab.com") }
|
||||
let(:issue_confidential) { false }
|
||||
let(:issue) { create(:issue, project: project, title: 'A bug', confidential: issue_confidential) }
|
||||
let(:issue_iid) { nil }
|
||||
let(:description) { nil }
|
||||
let(:source_branch) { 'feature' }
|
||||
let(:target_branch) { 'master' }
|
||||
|
|
@ -63,8 +62,7 @@ RSpec.describe MergeRequests::BuildService, feature_category: :code_review_workf
|
|||
source_project: source_project,
|
||||
target_project: target_project,
|
||||
milestone_id: milestone_id,
|
||||
label_ids: label_ids,
|
||||
issue_iid: issue_iid
|
||||
label_ids: label_ids
|
||||
}
|
||||
end
|
||||
|
||||
|
|
@ -535,17 +533,6 @@ RSpec.describe MergeRequests::BuildService, feature_category: :code_review_workf
|
|||
expect(merge_request.description).to eq('Create the app')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the issue is created in the fork' do
|
||||
let(:source_project) { create(:project, :repository, forked_from_project: project) }
|
||||
let(:issue) { create(:issue, project: source_project, title: 'A bug') }
|
||||
let(:issue_iid) { issue.iid }
|
||||
|
||||
it 'uses a full reference to the issue' do
|
||||
allow(project).to receive(:get_issue).and_return issue
|
||||
expect(merge_request.description).to include("Closes #{source_project.full_path}##{issue_iid}")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'source branch does not exist' do
|
||||
|
|
|
|||
|
|
@ -2728,7 +2728,6 @@
|
|||
- './ee/spec/services/security/security_orchestration_policies/validate_policy_service_spec.rb'
|
||||
- './ee/spec/services/security/store_grouped_scans_service_spec.rb'
|
||||
- './ee/spec/services/security/store_scan_service_spec.rb'
|
||||
- './ee/spec/services/security/store_scans_service_spec.rb'
|
||||
- './ee/spec/services/security/token_revocation_service_spec.rb'
|
||||
- './ee/spec/services/security/track_scan_service_spec.rb'
|
||||
- './ee/spec/services/security/update_training_service_spec.rb'
|
||||
|
|
@ -2950,7 +2949,6 @@
|
|||
- './ee/spec/workers/security/create_orchestration_policy_worker_spec.rb'
|
||||
- './ee/spec/workers/security/orchestration_policy_rule_schedule_namespace_worker_spec.rb'
|
||||
- './ee/spec/workers/security/orchestration_policy_rule_schedule_worker_spec.rb'
|
||||
- './ee/spec/workers/security/store_scans_worker_spec.rb'
|
||||
- './ee/spec/workers/security/sync_scan_policies_worker_spec.rb'
|
||||
- './ee/spec/workers/security/track_secure_scans_worker_spec.rb'
|
||||
- './ee/spec/workers/set_user_status_based_on_user_cap_setting_worker_spec.rb'
|
||||
|
|
|
|||
|
|
@ -449,6 +449,7 @@ RSpec.describe 'Every Sidekiq worker', feature_category: :shared do
|
|||
'StageUpdateWorker' => 3,
|
||||
'StatusPage::PublishWorker' => 5,
|
||||
'StoreSecurityReportsWorker' => 3,
|
||||
'Security::StoreSecurityReportsByProjectWorker' => 3,
|
||||
'SyncSeatLinkRequestWorker' => 20,
|
||||
'SyncSeatLinkWorker' => 12,
|
||||
'SystemHookPushWorker' => 3,
|
||||
|
|
|
|||
Loading…
Reference in New Issue