Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-03-04 09:10:52 +00:00
parent 775b2961fe
commit b0a5a92e83
53 changed files with 1309 additions and 362 deletions

View File

@ -1277,7 +1277,7 @@ GEM
validate_email (0.1.6)
activemodel (>= 3.0)
mail (>= 2.2.5)
validate_url (1.0.8)
validate_url (1.0.13)
activemodel (>= 3.0.0)
public_suffix
validates_hostname (1.0.11)

View File

@ -1,38 +1,23 @@
<script>
import {
GlButton,
GlFormGroup,
GlFormRadio,
GlFormRadioGroup,
GlLabel,
GlSearchBoxByType,
GlSkeletonLoader,
GlTooltipDirective as GlTooltip,
} from '@gitlab/ui';
import { mapActions, mapGetters, mapState } from 'vuex';
import BoardAddNewColumnForm from '~/boards/components/board_add_new_column_form.vue';
import { ListType } from '~/boards/constants';
import boardsStore from '~/boards/stores/boards_store';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { isScopedLabel } from '~/lib/utils/common_utils';
import { __ } from '~/locale';
import boardsStore from '../stores/boards_store';
export default {
i18n: {
add: __('Add'),
cancel: __('Cancel'),
formDescription: __('A label list displays all issues with the selected label.'),
newLabelList: __('New label list'),
noLabelSelected: __('No label selected'),
searchPlaceholder: __('Search labels'),
selectLabel: __('Select label'),
selected: __('Selected'),
},
components: {
GlButton,
GlFormGroup,
BoardAddNewColumnForm,
GlFormRadio,
GlFormRadioGroup,
GlLabel,
GlSearchBoxByType,
GlSkeletonLoader,
},
directives: {
GlTooltip,
@ -40,31 +25,27 @@ export default {
inject: ['scopedLabelsAvailable'],
data() {
return {
searchTerm: '',
selectedLabelId: null,
selectedId: null,
};
},
computed: {
...mapState(['labels', 'labelsLoading', 'isEpicBoard']),
...mapGetters(['getListByLabelId', 'shouldUseGraphQL']),
selectedLabel() {
return this.labels.find(({ id }) => id === this.selectedLabelId);
if (!this.selectedId) {
return null;
}
return this.labels.find(({ id }) => id === this.selectedId);
},
columnForSelected() {
return this.getListByLabelId(this.selectedId);
},
},
created() {
this.filterLabels();
this.filterItems();
},
methods: {
...mapActions(['createList', 'fetchLabels', 'highlightList', 'setAddColumnFormVisibility']),
getListByLabel(label) {
if (this.shouldUseGraphQL || this.isEpicBoard) {
return this.getListByLabelId(label);
}
return boardsStore.findListByLabelId(label.id);
},
columnExists(label) {
return Boolean(this.getListByLabel(label));
},
highlight(listId) {
if (this.shouldUseGraphQL || this.isEpicBoard) {
this.highlightList(listId);
@ -77,44 +58,35 @@ export default {
}
},
addList() {
if (!this.selectedLabelId) {
return;
}
const label = this.selectedLabel;
if (!label) {
if (!this.selectedLabel) {
return;
}
this.setAddColumnFormVisibility(false);
if (this.columnExists({ id: this.selectedLabelId })) {
const listId = this.getListByLabel(label).id;
if (this.columnForSelected) {
const listId = this.columnForSelected.id;
this.highlight(listId);
return;
}
if (this.shouldUseGraphQL || this.isEpicBoard) {
this.createList({ labelId: this.selectedLabelId });
this.createList({ labelId: this.selectedId });
} else {
boardsStore.new({
title: label.title,
const listObj = {
labelId: getIdFromGraphQLId(this.selectedId),
title: this.selectedLabel.title,
position: boardsStore.state.lists.length - 2,
list_type: 'label',
label: {
id: label.id,
title: label.title,
color: label.color,
},
});
list_type: ListType.label,
label: this.selectedLabel,
};
this.highlight(boardsStore.findListByLabelId(label.id).id);
boardsStore.new(listObj);
}
},
filterLabels() {
this.fetchLabels(this.searchTerm);
filterItems(searchTerm) {
this.fetchLabels(searchTerm);
},
showScopedLabels(label) {
@ -125,103 +97,43 @@ export default {
</script>
<template>
<div
class="board-add-new-list board gl-display-inline-block gl-h-full gl-px-3 gl-vertical-align-top gl-white-space-normal gl-flex-shrink-0"
data-testid="board-add-new-column"
data-qa-selector="board_add_new_list"
<board-add-new-column-form
:loading="labelsLoading"
:form-description="__('A label list displays issues with the selected label.')"
:search-label="__('Select label')"
:search-placeholder="__('Search labels')"
:selected-id="selectedId"
@filter-items="filterItems"
@add-list="addList"
>
<div
class="board-inner gl-display-flex gl-flex-direction-column gl-relative gl-h-full gl-rounded-base gl-bg-white"
>
<h3
class="gl-font-base gl-px-5 gl-py-5 gl-m-0 gl-border-b-1 gl-border-b-solid gl-border-b-gray-100"
data-testid="board-add-column-form-title"
>
{{ $options.i18n.newLabelList }}
</h3>
<template slot="selected">
<gl-label
v-if="selectedLabel"
v-gl-tooltip
:title="selectedLabel.title"
:description="selectedLabel.description"
:background-color="selectedLabel.color"
:scoped="showScopedLabels(selectedLabel)"
/>
</template>
<div class="gl-display-flex gl-flex-direction-column gl-h-full gl-overflow-hidden">
<!-- selectbox is here in EE -->
<p class="gl-m-5">{{ $options.i18n.formDescription }}</p>
<div class="gl-px-5 gl-pb-4">
<label class="gl-mb-2">{{ $options.i18n.selected }}</label>
<div>
<gl-label
v-if="selectedLabel"
v-gl-tooltip
:title="selectedLabel.title"
:description="selectedLabel.description"
:background-color="selectedLabel.color"
:scoped="showScopedLabels(selectedLabel)"
/>
<div v-else class="gl-text-gray-500">{{ $options.i18n.noLabelSelected }}</div>
</div>
</div>
<gl-form-group
class="gl-mx-5 gl-mb-3"
:label="$options.i18n.selectLabel"
label-for="board-available-labels"
<template slot="items">
<gl-form-radio-group v-model="selectedId" class="gl-overflow-y-auto gl-px-5 gl-pt-3">
<label
v-for="label in labels"
:key="label.id"
class="gl-display-flex gl-flex-align-items-center gl-mb-5 gl-font-weight-normal"
>
<gl-search-box-by-type
id="board-available-labels"
v-model.trim="searchTerm"
debounce="250"
:placeholder="$options.i18n.searchPlaceholder"
@input="filterLabels"
/>
</gl-form-group>
<div v-if="labelsLoading" class="gl-m-5">
<gl-skeleton-loader :width="500" :height="172">
<rect width="480" height="20" x="10" y="15" rx="4" />
<rect width="380" height="20" x="10" y="50" rx="4" />
<rect width="430" height="20" x="10" y="85" rx="4" />
</gl-skeleton-loader>
</div>
<gl-form-radio-group
v-else
v-model="selectedLabelId"
class="gl-overflow-y-auto gl-px-5 gl-pt-3"
>
<label
v-for="label in labels"
:key="label.id"
class="gl-display-flex gl-flex-align-items-center gl-mb-5 gl-font-weight-normal"
>
<gl-form-radio :value="label.id" class="gl-mb-0 gl-mr-3" />
<span
class="dropdown-label-box gl-top-0"
:style="{
backgroundColor: label.color,
}"
></span>
<span>{{ label.title }}</span>
</label>
</gl-form-radio-group>
</div>
<div
class="gl-display-flex gl-p-3 gl-border-t-1 gl-border-t-solid gl-border-gray-100 gl-bg-gray-10"
>
<gl-button
data-testid="cancelAddNewColumn"
class="gl-ml-auto gl-mr-3"
@click="setAddColumnFormVisibility(false)"
>{{ $options.i18n.cancel }}</gl-button
>
<gl-button
data-testid="addNewColumnButton"
:disabled="!selectedLabelId"
variant="success"
class="gl-mr-4"
@click="addList"
>{{ $options.i18n.add }}</gl-button
>
</div>
</div>
</div>
<gl-form-radio :value="label.id" class="gl-mb-0 gl-mr-3" />
<span
class="dropdown-label-box gl-top-0"
:style="{
backgroundColor: label.color,
}"
></span>
<span>{{ label.title }}</span>
</label>
</gl-form-radio-group>
</template>
</board-add-new-column-form>
</template>

View File

@ -0,0 +1,122 @@
<script>
import { GlButton, GlFormGroup, GlSearchBoxByType, GlSkeletonLoader } from '@gitlab/ui';
import { mapActions } from 'vuex';
import { __ } from '~/locale';
export default {
i18n: {
add: __('Add'),
cancel: __('Cancel'),
newList: __('New list'),
noneSelected: __('None'),
selected: __('Selected'),
},
components: {
GlButton,
GlFormGroup,
GlSearchBoxByType,
GlSkeletonLoader,
},
props: {
loading: {
type: Boolean,
required: true,
},
formDescription: {
type: String,
required: true,
},
searchLabel: {
type: String,
required: true,
},
searchPlaceholder: {
type: String,
required: true,
},
selectedId: {
type: [Number, String],
required: false,
default: null,
},
},
methods: {
...mapActions(['setAddColumnFormVisibility']),
},
};
</script>
<template>
<div
class="board-add-new-list board gl-display-inline-block gl-h-full gl-px-3 gl-vertical-align-top gl-white-space-normal gl-flex-shrink-0"
data-testid="board-add-new-column"
data-qa-selector="board_add_new_list"
>
<div
class="board-inner gl-display-flex gl-flex-direction-column gl-relative gl-h-full gl-rounded-base gl-bg-white"
>
<h3
class="gl-font-base gl-px-5 gl-py-5 gl-m-0 gl-border-b-1 gl-border-b-solid gl-border-b-gray-100"
data-testid="board-add-column-form-title"
>
{{ $options.i18n.newList }}
</h3>
<div class="gl-display-flex gl-flex-direction-column gl-h-full gl-overflow-hidden">
<slot name="select-list-type">
<div class="gl-mb-5"></div>
</slot>
<p class="gl-px-5">{{ formDescription }}</p>
<div class="gl-px-5 gl-pb-4">
<label class="gl-mb-2">{{ $options.i18n.selected }}</label>
<slot name="selected">
<div class="gl-text-gray-500">{{ $options.i18n.noneSelected }}</div>
</slot>
</div>
<gl-form-group
class="gl-mx-5 gl-mb-3"
:label="searchLabel"
label-for="board-available-column-entities"
>
<gl-search-box-by-type
id="board-available-column-entities"
debounce="250"
:placeholder="searchPlaceholder"
@input="$emit('filter-items', $event)"
/>
</gl-form-group>
<div v-if="loading" class="gl-px-5">
<gl-skeleton-loader :width="500" :height="172">
<rect width="480" height="20" x="10" y="15" rx="4" />
<rect width="380" height="20" x="10" y="50" rx="4" />
<rect width="430" height="20" x="10" y="85" rx="4" />
</gl-skeleton-loader>
</div>
<slot v-else name="items"></slot>
</div>
<div
class="gl-display-flex gl-p-3 gl-border-t-1 gl-border-t-solid gl-border-gray-100 gl-bg-gray-10"
>
<gl-button
data-testid="cancelAddNewColumn"
class="gl-ml-auto gl-mr-3"
@click="setAddColumnFormVisibility(false)"
>{{ $options.i18n.cancel }}</gl-button
>
<gl-button
data-testid="addNewColumnButton"
:disabled="!selectedId"
variant="success"
class="gl-mr-4"
@click="$emit('add-list')"
>{{ $options.i18n.add }}</gl-button
>
</div>
</div>
</div>
</template>

View File

@ -157,8 +157,8 @@ export default {
},
})
.then(({ data }) => {
if (data?.boardListCreate?.errors.length) {
commit(types.CREATE_LIST_FAILURE);
if (data.boardListCreate?.errors.length) {
commit(types.CREATE_LIST_FAILURE, data.boardListCreate.errors[0]);
} else {
const list = data.boardListCreate?.list;
dispatch('addList', list);

View File

@ -60,8 +60,11 @@ export default {
state.filterParams = filterParams;
},
[mutationTypes.CREATE_LIST_FAILURE]: (state) => {
state.error = s__('Boards|An error occurred while creating the list. Please try again.');
[mutationTypes.CREATE_LIST_FAILURE]: (
state,
error = s__('Boards|An error occurred while creating the list. Please try again.'),
) => {
state.error = error;
},
[mutationTypes.RECEIVE_LABELS_REQUEST]: (state) => {

View File

@ -4,13 +4,11 @@ import { FILTERED_SEARCH } from '~/pages/constants';
import initFilteredSearch from '~/pages/search/init_filtered_search';
import projectSelect from '~/project_select';
document.addEventListener('DOMContentLoaded', () => {
initFilteredSearch({
page: FILTERED_SEARCH.ISSUES,
filteredSearchTokenKeys: IssuableFilteredSearchTokenKeys,
useDefaultState: true,
});
projectSelect();
initManualOrdering();
initFilteredSearch({
page: FILTERED_SEARCH.ISSUES,
filteredSearchTokenKeys: IssuableFilteredSearchTokenKeys,
useDefaultState: true,
});
projectSelect();
initManualOrdering();

View File

@ -1,13 +1,11 @@
import IntegrationSettingsForm from '~/integrations/integration_settings_form';
import PrometheusMetrics from '~/prometheus_metrics/prometheus_metrics';
document.addEventListener('DOMContentLoaded', () => {
const prometheusSettingsWrapper = document.querySelector('.js-prometheus-metrics-monitoring');
const integrationSettingsForm = new IntegrationSettingsForm('.js-integration-settings-form');
integrationSettingsForm.init();
const prometheusSettingsWrapper = document.querySelector('.js-prometheus-metrics-monitoring');
const integrationSettingsForm = new IntegrationSettingsForm('.js-integration-settings-form');
integrationSettingsForm.init();
if (prometheusSettingsWrapper) {
const prometheusMetrics = new PrometheusMetrics('.js-prometheus-metrics-monitoring');
prometheusMetrics.loadActiveMetrics();
}
});
if (prometheusSettingsWrapper) {
const prometheusMetrics = new PrometheusMetrics('.js-prometheus-metrics-monitoring');
prometheusMetrics.loadActiveMetrics();
}

View File

@ -1,3 +1,3 @@
import initBlobBundle from '~/blob_edit/blob_bundle';
document.addEventListener('DOMContentLoaded', initBlobBundle);
initBlobBundle();

View File

@ -7,61 +7,59 @@ import initWebIdeLink from '~/pages/projects/shared/web_ide_link';
import commitPipelineStatus from '~/projects/tree/components/commit_pipeline_status_component.vue';
import '~/sourcegraph/load';
document.addEventListener('DOMContentLoaded', () => {
new BlobViewer(); // eslint-disable-line no-new
initBlob();
new BlobViewer(); // eslint-disable-line no-new
initBlob();
const CommitPipelineStatusEl = document.querySelector('.js-commit-pipeline-status');
const statusLink = document.querySelector('.commit-actions .ci-status-link');
if (statusLink) {
statusLink.remove();
// eslint-disable-next-line no-new
new Vue({
el: CommitPipelineStatusEl,
components: {
commitPipelineStatus,
},
render(createElement) {
return createElement('commit-pipeline-status', {
props: {
endpoint: CommitPipelineStatusEl.dataset.endpoint,
},
});
},
});
}
const CommitPipelineStatusEl = document.querySelector('.js-commit-pipeline-status');
const statusLink = document.querySelector('.commit-actions .ci-status-link');
if (statusLink) {
statusLink.remove();
// eslint-disable-next-line no-new
new Vue({
el: CommitPipelineStatusEl,
components: {
commitPipelineStatus,
},
render(createElement) {
return createElement('commit-pipeline-status', {
props: {
endpoint: CommitPipelineStatusEl.dataset.endpoint,
},
});
},
});
}
initWebIdeLink({ el: document.getElementById('js-blob-web-ide-link') });
initWebIdeLink({ el: document.getElementById('js-blob-web-ide-link') });
GpgBadges.fetch();
GpgBadges.fetch();
const codeNavEl = document.getElementById('js-code-navigation');
const codeNavEl = document.getElementById('js-code-navigation');
if (codeNavEl) {
const { codeNavigationPath, blobPath, definitionPathPrefix } = codeNavEl.dataset;
if (codeNavEl) {
const { codeNavigationPath, blobPath, definitionPathPrefix } = codeNavEl.dataset;
// eslint-disable-next-line promise/catch-or-return
import('~/code_navigation').then((m) =>
m.default({
blobs: [{ path: blobPath, codeNavigationPath }],
definitionPathPrefix,
}),
);
}
// eslint-disable-next-line promise/catch-or-return
import('~/code_navigation').then((m) =>
m.default({
blobs: [{ path: blobPath, codeNavigationPath }],
definitionPathPrefix,
}),
);
}
const successPipelineEl = document.querySelector('.js-success-pipeline-modal');
const successPipelineEl = document.querySelector('.js-success-pipeline-modal');
if (successPipelineEl) {
// eslint-disable-next-line no-new
new Vue({
el: successPipelineEl,
render(createElement) {
return createElement(PipelineTourSuccessModal, {
props: {
...successPipelineEl.dataset,
},
});
},
});
}
});
if (successPipelineEl) {
// eslint-disable-next-line no-new
new Vue({
el: successPipelineEl,
render(createElement) {
return createElement(PipelineTourSuccessModal, {
props: {
...successPipelineEl.dataset,
},
});
},
});
}

View File

@ -15,9 +15,7 @@ function initUserProfile(action) {
});
}
document.addEventListener('DOMContentLoaded', () => {
const page = $('body').attr('data-page');
const action = page.split(':')[1];
initUserProfile(action);
new UserCallout(); // eslint-disable-line no-new
});
const page = $('body').attr('data-page');
const action = page.split(':')[1];
initUserProfile(action);
new UserCallout(); // eslint-disable-line no-new

View File

@ -0,0 +1,218 @@
<script>
import {
GlModal,
GlForm,
GlFormGroup,
GlFormInput,
GlFormTextarea,
GlToggle,
GlButton,
GlAlert,
} from '@gitlab/ui';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { ContentTypeMultipartFormData } from '~/lib/utils/headers';
import { numberToHumanSize } from '~/lib/utils/number_utils';
import { visitUrl, joinPaths } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
import UploadDropzone from '~/vue_shared/components/upload_dropzone/upload_dropzone.vue';
const PRIMARY_OPTIONS_TEXT = __('Upload file');
const SECONDARY_OPTIONS_TEXT = __('Cancel');
const MODAL_TITLE = __('Upload New File');
const COMMIT_LABEL = __('Commit message');
const TARGET_BRANCH_LABEL = __('Target branch');
const TOGGLE_CREATE_MR_LABEL = __('Start a new merge request with these changes');
const REMOVE_FILE_TEXT = __('Remove file');
const NEW_BRANCH_IN_FORK = __(
'A new branch will be created in your fork and a new merge request will be started.',
);
const ERROR_MESSAGE = __('Error uploading file. Please try again.');
export default {
components: {
GlModal,
GlForm,
GlFormGroup,
GlFormInput,
GlFormTextarea,
GlToggle,
GlButton,
UploadDropzone,
GlAlert,
},
i18n: {
MODAL_TITLE,
COMMIT_LABEL,
TARGET_BRANCH_LABEL,
TOGGLE_CREATE_MR_LABEL,
REMOVE_FILE_TEXT,
NEW_BRANCH_IN_FORK,
},
props: {
modalId: {
type: String,
required: true,
},
commitMessage: {
type: String,
required: true,
},
targetBranch: {
type: String,
required: true,
},
origionalBranch: {
type: String,
required: true,
},
canPushCode: {
type: Boolean,
required: true,
},
path: {
type: String,
required: true,
},
},
data() {
return {
commit: this.commitMessage,
target: this.targetBranch,
createNewMr: true,
file: null,
filePreviewURL: null,
fileBinary: null,
loading: false,
};
},
computed: {
primaryOptions() {
return {
text: PRIMARY_OPTIONS_TEXT,
attributes: [
{
variant: 'success',
loading: this.loading,
disabled: !this.formCompleted || this.loading,
},
],
};
},
cancelOptions() {
return {
text: SECONDARY_OPTIONS_TEXT,
attributes: [
{
disabled: this.loading,
},
],
};
},
formattedFileSize() {
return numberToHumanSize(this.file.size);
},
showCreateNewMrToggle() {
return this.canPushCode && this.target !== this.origionalBranch;
},
formCompleted() {
return this.file && this.commit && this.target;
},
},
methods: {
setFile(file) {
this.file = file;
const fileUurlReader = new FileReader();
fileUurlReader.readAsDataURL(this.file);
fileUurlReader.onload = (e) => {
this.filePreviewURL = e.target?.result;
};
},
removeFile() {
this.file = null;
this.filePreviewURL = null;
},
uploadFile() {
this.loading = true;
const {
$route: {
params: { path },
},
} = this;
const uploadPath = joinPaths(this.path, path);
const formData = new FormData();
formData.append('branch_name', this.target);
formData.append('create_merge_request', this.createNewMr);
formData.append('commit_message', this.commit);
formData.append('file', this.file);
return axios
.post(uploadPath, formData, {
headers: {
...ContentTypeMultipartFormData,
},
})
.then((response) => {
visitUrl(response.data.filePath);
})
.catch(() => {
this.loading = false;
createFlash(ERROR_MESSAGE);
});
},
},
};
</script>
<template>
<gl-form>
<gl-modal
:modal-id="modalId"
:title="$options.i18n.MODAL_TITLE"
:action-primary="primaryOptions"
:action-cancel="cancelOptions"
@primary.prevent="uploadFile"
>
<upload-dropzone class="gl-h-200! gl-mb-4" single-file-selection @change="setFile">
<div
v-if="file"
class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-3"
>
<img v-if="filePreviewURL" :src="filePreviewURL" class="gl-h-11" />
<div>{{ formattedFileSize }}</div>
<div>{{ file.name }}</div>
<gl-button
category="tertiary"
variant="confirm"
:disabled="loading"
@click="removeFile"
>{{ $options.i18n.REMOVE_FILE_TEXT }}</gl-button
>
</div>
</upload-dropzone>
<gl-form-group :label="$options.i18n.COMMIT_LABEL" label-for="commit_message">
<gl-form-textarea v-model="commit" name="commit_message" :disabled="loading" />
</gl-form-group>
<gl-form-group
v-if="canPushCode"
:label="$options.i18n.TARGET_BRANCH_LABEL"
label-for="branch_name"
>
<gl-form-input v-model="target" :disabled="loading" name="branch_name" />
</gl-form-group>
<gl-toggle
v-if="showCreateNewMrToggle"
v-model="createNewMr"
:disabled="loading"
:label="$options.i18n.TOGGLE_CREATE_MR_LABEL"
/>
<gl-alert v-if="!canPushCode" variant="info" :dismissible="false" class="gl-mt-3">
{{ $options.i18n.NEW_BRANCH_IN_FORK }}
</gl-alert>
</gl-modal>
</gl-form>
</template>

View File

@ -196,8 +196,8 @@ class MergeRequest < ApplicationRecord
end
event :mark_as_unchecked do
transition [:preparing, :can_be_merged, :checking, :unchecked] => :unchecked
transition [:cannot_be_merged, :cannot_be_merged_rechecking, :cannot_be_merged_recheck] => :cannot_be_merged_recheck
transition [:preparing, :can_be_merged, :checking] => :unchecked
transition [:cannot_be_merged, :cannot_be_merged_rechecking] => :cannot_be_merged_recheck
end
event :mark_as_checking do
@ -326,7 +326,7 @@ class MergeRequest < ApplicationRecord
scope :preload_approved_by_users, -> { preload(:approved_by_users) }
scope :preload_metrics, -> (relation) { preload(metrics: relation) }
scope :preload_project_and_latest_diff, -> { preload(:source_project, :latest_merge_request_diff) }
scope :preload_latest_diff_comment, -> { preload(latest_merge_request_diff: :merge_request_diff_commits) }
scope :preload_latest_diff_commit, -> { preload(latest_merge_request_diff: :merge_request_diff_commits) }
scope :with_web_entity_associations, -> { preload(:author, :target_project) }
scope :with_auto_merge_enabled, -> do

View File

@ -75,7 +75,7 @@ module MergeRequests
commit_ids = @commits.map(&:id)
merge_requests = @project.merge_requests.opened
.preload_project_and_latest_diff
.preload_latest_diff_comment
.preload_latest_diff_commit
.where(target_branch: @push.branch_name).to_a
.select(&:diff_head_commit)
.select do |merge_request|

View File

@ -205,6 +205,7 @@ module MergeRequests
new_assignees = merge_request.assignees - old_assignees
merge_request_activity_counter.track_users_assigned_to_mr(users: new_assignees)
merge_request_activity_counter.track_assignees_changed_action(user: current_user)
end
def handle_reviewers_change(merge_request, old_reviewers)
@ -216,6 +217,7 @@ module MergeRequests
new_reviewers = merge_request.reviewers - old_reviewers
merge_request_activity_counter.track_users_review_requested(users: new_reviewers)
merge_request_activity_counter.track_reviewers_changed_action(user: current_user)
end
def create_branch_change_note(issuable, branch_type, event_type, old_branch, new_branch)

View File

@ -1,12 +0,0 @@
%span.left-label Newer
%span.legend-box.legend-box-0
%span.legend-box.legend-box-1
%span.legend-box.legend-box-2
%span.legend-box.legend-box-3
%span.legend-box.legend-box-4
%span.legend-box.legend-box-5
%span.legend-box.legend-box-6
%span.legend-box.legend-box-7
%span.legend-box.legend-box-8
%span.legend-box.legend-box-9
%span.right-label Older

View File

@ -1,26 +0,0 @@
%tr
%td.blame-commit{ class: commit_data.age_map_class }
.commit
= commit_data.author_avatar
.commit-row-title
%span.item-title.str-truncated-100
= commit_data.commit_link
%span
= commit_data.project_blame_link
&nbsp;
.light
= commit_data.commit_author_link
= _('committed')
#{commit_data.time_ago_tooltip}
%td.line-numbers
- line_count = blame_group[:lines].count
- (current_line...(current_line + line_count)).each do |i|
%a.diff-line-num{ href: "#L#{i}", id: "L#{i}", 'data-line-number' => i }
= link_icon
= i
\
%td.lines
%pre.code.highlight
%code
- blame_group[:lines].each do |line|
#{line}

View File

@ -6,18 +6,56 @@
.file-holder
= render "projects/blob/header", blob: @blob, blame: true
.file-blame-legend
= render 'age_map_legend'
%span.left-label Newer
%span.legend-box.legend-box-0
%span.legend-box.legend-box-1
%span.legend-box.legend-box-2
%span.legend-box.legend-box-3
%span.legend-box.legend-box-4
%span.legend-box.legend-box-5
%span.legend-box.legend-box-6
%span.legend-box.legend-box-7
%span.legend-box.legend-box-8
%span.legend-box.legend-box-9
%span.right-label Older
.table-responsive.file-content.blame.code.js-syntax-highlight
%table
- current_line = 1
- @blame.groups.each do |blame_group|
- commit_data = @blame.commit_data(blame_group[:commit])
- line_count = blame_group[:lines].count
= render 'blame_group',
blame_group: blame_group,
current_line: current_line,
link_icon: link_icon,
commit_data: commit_data
%tr
%td.blame-commit{ class: commit_data.age_map_class }
.commit
= commit_data.author_avatar
- current_line += blame_group[:lines].count
.commit-row-title
%span.item-title.str-truncated-100
= commit_data.commit_link
%span
= commit_data.project_blame_link
&nbsp;
.light
= commit_data.commit_author_link
= _('committed')
#{commit_data.time_ago_tooltip}
%td.line-numbers
- (current_line...(current_line + line_count)).each do |i|
%a.diff-line-num{ href: "#L#{i}", id: "L#{i}", 'data-line-number' => i }
= link_icon
= i
\
%td.lines
%pre.code.highlight
%code
- blame_group[:lines].each do |line|
#{line}
- current_line += line_count

View File

@ -8,5 +8,5 @@
.modal-body.p-3
%p= _("You're not allowed to %{tag_start}edit%{tag_end} files in this project directly. Please fork this project, make your changes there, and submit a merge request.") % { tag_start: '', tag_end: ''}
.modal-footer
= link_to _('Cancel'), '#', class: "btn btn-cancel", "data-dismiss" => "modal"
= link_to _('Fork project'), fork_path, class: 'btn btn-success', data: { qa_selector: 'fork_project_button' }, method: :post
= link_to _('Cancel'), '#', class: "gl-button btn btn-default btn-cancel", "data-dismiss" => "modal"
= link_to _('Fork project'), fork_path, class: 'gl-button btn btn-confirm', data: { qa_selector: 'fork_project_button' }, method: :post

View File

@ -1,7 +1,7 @@
- if any_projects?(@projects)
.project-item-select-holder.btn-group.gl-ml-auto.gl-mr-auto.gl-py-3.gl-relative.gl-display-flex.gl-overflow-hidden
%a.btn.gl-button.btn-success.new-project-item-link.block-truncated.qa-new-project-item-link{ href: '', data: { label: local_assigns[:label], type: local_assigns[:type] }, class: "gl-m-0!" }
%a.btn.gl-button.btn-confirm.new-project-item-link.block-truncated.qa-new-project-item-link{ href: '', data: { label: local_assigns[:label], type: local_assigns[:type] }, class: "gl-m-0!" }
= loading_icon(color: 'light')
= project_select_tag :project_path, class: "project-item-select gl-absolute! gl-visibility-hidden", data: { include_groups: local_assigns[:include_groups], order_by: 'last_activity_at', relative_path: local_assigns[:path], with_shared: local_assigns[:with_shared], include_projects_in_subgroups: local_assigns[:include_projects_in_subgroups] }, with_feature_enabled: local_assigns[:with_feature_enabled]
%button.btn.dropdown-toggle.btn-success.btn-md.gl-button.gl-dropdown-toggle.dropdown-toggle-split.new-project-item-select-button.qa-new-project-item-select-button.gl-p-0.gl-w-100{ class: "gl-m-0!", 'aria-label': _('Toggle project select') }
%button.btn.dropdown-toggle.btn-confirm.btn-md.gl-button.gl-dropdown-toggle.dropdown-toggle-split.new-project-item-select-button.qa-new-project-item-select-button.gl-p-0.gl-w-100{ class: "gl-m-0!", 'aria-label': _('Toggle project select') }
= sprite_icon('chevron-down')

View File

@ -20,4 +20,4 @@
- if has_submit
.row-content-block.footer-block
= f.submit _("Submit %{humanized_resource_name}") % { humanized_resource_name: humanized_resource_name }, class: 'btn btn-success'
= f.submit _("Submit %{humanized_resource_name}") % { humanized_resource_name: humanized_resource_name }, class: 'gl-button btn btn-confirm'

View File

@ -9,5 +9,5 @@
- if invite_group_members?(@group)
= link_to _('Invite your team'),
group_group_members_path(@group),
class: 'gl-button btn btn-success-secondary',
class: 'gl-button btn btn-confirm-secondary',
data: { track_event: 'click_invite_team_group_empty_state', track_label: 'invite_team_group_empty_state' }

View File

@ -4,7 +4,6 @@
- stars = true unless local_assigns[:stars] == false
- forks = true unless local_assigns[:forks] == false
- merge_requests = true unless local_assigns[:merge_requests] == false
- issues = true unless local_assigns[:issues] == false
- pipeline_status = true unless local_assigns[:pipeline_status] == false
- skip_namespace = false unless local_assigns[:skip_namespace] == true
- user = local_assigns[:user]
@ -41,7 +40,7 @@
= render "shared/projects/project", project: project, skip_namespace: skip_namespace,
avatar: avatar, stars: stars, css_class: css_class, use_creator_avatar: use_creator_avatar,
forks: forks, show_last_commit_as_description: show_last_commit_as_description, user: user, merge_requests: merge_requests,
issues: issues, pipeline_status: pipeline_status, compact_mode: compact_mode
issues: project.issues_enabled?, pipeline_status: pipeline_status, compact_mode: compact_mode
= paginate_collection(projects, remote: remote) unless skip_pagination
- else
- if @contributed_projects

View File

@ -70,10 +70,10 @@
.form-actions
- if @page && @page.persisted?
= f.submit _("Save changes"), class: 'btn gl-button btn-success qa-save-changes-button js-wiki-btn-submit', disabled: 'true'
= f.submit _("Save changes"), class: 'btn gl-button btn-confirm qa-save-changes-button js-wiki-btn-submit', disabled: 'true'
.float-right
= link_to _("Cancel"), wiki_page_path(@wiki, @page), class: 'btn gl-button btn-cancel btn-default'
- else
= f.submit s_("Wiki|Create page"), class: 'btn-success gl-button btn qa-create-page-button rspec-create-page-button js-wiki-btn-submit', disabled: 'true'
= f.submit s_("Wiki|Create page"), class: 'btn-confirm gl-button btn qa-create-page-button rspec-create-page-button js-wiki-btn-submit', disabled: 'true'
.float-right
= link_to _("Cancel"), wiki_path(@wiki), class: 'btn gl-button btn-cancel btn-default'

View File

@ -2,5 +2,5 @@
= link_to wiki_page_path(@wiki, @page, action: :history), class: "btn gl-button", role: "button", data: { qa_selector: 'page_history_button' } do
= s_("Wiki|Page history")
- if can?(current_user, :create_wiki, @wiki.container)
= link_to wiki_path(@wiki, action: :new), class: "btn gl-button btn-success btn-inverted", role: "button", data: { qa_selector: 'new_page_button' } do
= link_to wiki_path(@wiki, action: :new), class: "btn gl-button btn-confirm-secondary", role: "button", data: { qa_selector: 'new_page_button' } do
= s_("Wiki|New page")

View File

@ -0,0 +1,5 @@
---
title: Add tracking to merge request assignees/reviewers changes
merge_request: 55486
author:
type: other

View File

@ -0,0 +1,5 @@
---
title: Hide issue count and link in project list for projects with disabled issues
merge_request: 54275
author: Simon Stieger @sim0
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Refactor blame view
merge_request: 55488
author:
type: performance

View File

@ -0,0 +1,5 @@
---
title: Move from btn-success to btn-confirm in shared/groups directory
merge_request: 55302
author: Yogi (@yo)
type: changed

View File

@ -0,0 +1,5 @@
---
title: Move from btn-success to btn-confirm in shared/wikis directory
merge_request: 55316
author: Yogi (@yo)
type: changed

View File

@ -0,0 +1,5 @@
---
title: Move from btn-success to btn-confirm in shared directory
merge_request: 55317
author: Yogi (@yo)
type: changed

View File

@ -0,0 +1,5 @@
---
title: Remove unneeded transitions on MR for mark_as_unchecked event
merge_request: 53537
author:
type: performance

View File

@ -0,0 +1,5 @@
---
title: Update validate_url gem
merge_request: 55706
author:
type: fixed

View File

@ -0,0 +1,8 @@
---
name: usage_data_i_code_review_user_assignees_changed
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/55486
rollout_issue_url:
milestone: '13.10'
type: development
group: group::code review
default_enabled: true

View File

@ -0,0 +1,8 @@
---
name: usage_data_i_code_review_user_reviewers_changed
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/55486
rollout_issue_url:
milestone: '13.10'
type: development
group: group::code review
default_enabled: true

View File

@ -0,0 +1,20 @@
---
key_path: redis_hll_counters.code_review.i_code_review_user_assignees_changed_monthly
description: Count of unique users per month who changed assignees of a MR
product_section: dev
product_stage: create
product_group: group::code review
product_category: code_review
value_type: number
status: implemented
milestone: "13.10"
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/55486
time_frame: 28d
data_source: redis_hll
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate

View File

@ -0,0 +1,20 @@
---
key_path: redis_hll_counters.code_review.i_code_review_user_reviewers_changed_monthly
description: Count of unique users per month who changed reviewers of a MR
product_section: dev
product_stage: create
product_group: group::code review
product_category: code_review
value_type: number
status: implemented
milestone: "13.10"
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/55486
time_frame: 28d
data_source: redis_hll
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate

View File

@ -0,0 +1,20 @@
---
key_path: redis_hll_counters.code_review.i_code_review_user_assignees_changed_weekly
description: Count of unique users per week who changed assignees of a MR
product_section: dev
product_stage: create
product_group: group::code review
product_category: code_review
value_type: number
status: implemented
milestone: "13.10"
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/55486
time_frame: 7d
data_source: redis_hll
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate

View File

@ -0,0 +1,20 @@
---
key_path: redis_hll_counters.code_review.i_code_review_user_reviewers_changed_weekly
description: Count of unique users per week who changed reviewers of a MR
product_section: dev
product_stage: create
product_group: group::code review
product_category: code_review
value_type: number
status: implemented
milestone: "13.10"
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/55486
time_frame: 7d
data_source: redis_hll
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate

View File

@ -13004,6 +13004,46 @@ Missing description
| `tier` | |
| `skip_validation` | true |
## `redis_hll_counters.code_review.i_code_review_user_assignees_changed_monthly`
Count of unique users per month who changed assignees of a MR
| field | value |
| --- | --- |
| `key_path` | **`redis_hll_counters.code_review.i_code_review_user_assignees_changed_monthly`** |
| `product_section` | dev |
| `product_stage` | create |
| `product_group` | `group::code review` |
| `product_category` | `code_review` |
| `value_type` | number |
| `status` | implemented |
| `milestone` | 13.10 |
| `introduced_by_url` | [Introduced by](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/55486) |
| `time_frame` | 28d |
| `data_source` | Redis_hll |
| `distribution` | ce, ee |
| `tier` | free, premium, ultimate |
## `redis_hll_counters.code_review.i_code_review_user_assignees_changed_weekly`
Count of unique users per week who changed assignees of a MR
| field | value |
| --- | --- |
| `key_path` | **`redis_hll_counters.code_review.i_code_review_user_assignees_changed_weekly`** |
| `product_section` | dev |
| `product_stage` | create |
| `product_group` | `group::code review` |
| `product_category` | `code_review` |
| `value_type` | number |
| `status` | implemented |
| `milestone` | 13.10 |
| `introduced_by_url` | [Introduced by](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/55486) |
| `time_frame` | 7d |
| `data_source` | Redis_hll |
| `distribution` | ce, ee |
| `tier` | free, premium, ultimate |
## `redis_hll_counters.code_review.i_code_review_user_close_mr_monthly`
Count of unique users per week|month who closed a MR
@ -13692,6 +13732,46 @@ Missing description
| `tier` | |
| `skip_validation` | true |
## `redis_hll_counters.code_review.i_code_review_user_reviewers_changed_monthly`
Count of unique users per month who changed reviewers of a MR
| field | value |
| --- | --- |
| `key_path` | **`redis_hll_counters.code_review.i_code_review_user_reviewers_changed_monthly`** |
| `product_section` | dev |
| `product_stage` | create |
| `product_group` | `group::code review` |
| `product_category` | `code_review` |
| `value_type` | number |
| `status` | implemented |
| `milestone` | 13.10 |
| `introduced_by_url` | [Introduced by](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/55486) |
| `time_frame` | 28d |
| `data_source` | Redis_hll |
| `distribution` | ce, ee |
| `tier` | free, premium, ultimate |
## `redis_hll_counters.code_review.i_code_review_user_reviewers_changed_weekly`
Count of unique users per week who changed reviewers of a MR
| field | value |
| --- | --- |
| `key_path` | **`redis_hll_counters.code_review.i_code_review_user_reviewers_changed_weekly`** |
| `product_section` | dev |
| `product_stage` | create |
| `product_group` | `group::code review` |
| `product_category` | `code_review` |
| `value_type` | number |
| `status` | implemented |
| `milestone` | 13.10 |
| `introduced_by_url` | [Introduced by](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/55486) |
| `time_frame` | 7d |
| `data_source` | Redis_hll |
| `distribution` | ce, ee |
| `tier` | free, premium, ultimate |
## `redis_hll_counters.code_review.i_code_review_user_single_file_diffs_monthly`
Count of unique users per week|month with diffs viewed file by file

View File

@ -5,36 +5,36 @@ pre-push:
run: bundle exec danger dry_run
eslint:
tags: frontend style
files: git diff --name-only $(git merge-base origin/master HEAD)..HEAD
files: git diff --name-only --diff-filter=d $(git merge-base origin/master HEAD)..HEAD
glob: "*.{js,vue}"
run: yarn run lint:eslint {files}
haml-lint:
tags: view haml style
files: git diff --name-only $(git merge-base origin/master HEAD)..HEAD
files: git diff --name-only --diff-filter=d $(git merge-base origin/master HEAD)..HEAD
glob: "*.html.haml"
run: bundle exec haml-lint --config .haml-lint.yml {files}
markdownlint:
tags: documentation style
files: git diff --name-only $(git merge-base origin/master HEAD)..HEAD
files: git diff --name-only --diff-filter=d $(git merge-base origin/master HEAD)..HEAD
glob: "doc/*.md"
run: yarn markdownlint {files}
stylelint:
tags: stylesheet css style
files: git diff --name-only $(git merge-base origin/master HEAD)..HEAD
files: git diff --name-only --diff-filter=d $(git merge-base origin/master HEAD)..HEAD
glob: "*.scss{,.css}"
run: yarn stylelint -q {files}
prettier:
tags: frontend style
files: git diff --name-only $(git merge-base origin/master HEAD)..HEAD
files: git diff --name-only --diff-filter=d $(git merge-base origin/master HEAD)..HEAD
glob: "*.{js,vue,graphql}"
run: yarn run prettier --check {files}
rubocop:
tags: backend style
files: git diff --name-only $(git merge-base origin/master HEAD)..HEAD
files: git diff --name-only --diff-filter=d $(git merge-base origin/master HEAD)..HEAD
glob: "*.rb"
run: bundle exec rubocop --parallel --force-exclusion {files}
vale: # Requires Vale: https://docs.gitlab.com/ee/development/documentation/#install-linters
tags: documentation style
files: git diff --name-only $(git merge-base origin/master HEAD)..HEAD
files: git diff --name-only --diff-filter=d $(git merge-base origin/master HEAD)..HEAD
glob: "doc/*.md"
run: if command -v vale 2> /dev/null; then vale --config .vale.ini --minAlertLevel error {files}; else echo "Vale not found. Install Vale"; fi

View File

@ -46,7 +46,9 @@
'i_code_review_user_mr_discussion_locked',
'i_code_review_user_mr_discussion_unlocked',
'i_code_review_user_time_estimate_changed',
'i_code_review_user_time_spent_changed'
'i_code_review_user_time_spent_changed',
'i_code_review_user_assignees_changed',
'i_code_review_user_reviewers_changed'
]
- name: code_review_category_monthly_active_users
operator: OR
@ -86,7 +88,9 @@
'i_code_review_user_mr_discussion_locked',
'i_code_review_user_mr_discussion_unlocked',
'i_code_review_user_time_estimate_changed',
'i_code_review_user_time_spent_changed'
'i_code_review_user_time_spent_changed',
'i_code_review_user_assignees_changed',
'i_code_review_user_reviewers_changed'
]
- name: code_review_extension_category_monthly_active_users
operator: OR

View File

@ -184,3 +184,13 @@
category: code_review
aggregation: weekly
feature_flag: usage_data_i_code_review_user_time_spent_changed
- name: i_code_review_user_assignees_changed
redis_slot: code_review
category: code_review
aggregation: weekly
feature_flag: usage_data_i_code_review_user_assignees_changed
- name: i_code_review_user_reviewers_changed
redis_slot: code_review
category: code_review
aggregation: weekly
feature_flag: usage_data_i_code_review_user_reviewers_changed

View File

@ -39,6 +39,8 @@ module Gitlab
MR_DISCUSSION_UNLOCKED_ACTION = 'i_code_review_user_mr_discussion_unlocked'
MR_TIME_ESTIMATE_CHANGED_ACTION = 'i_code_review_user_time_estimate_changed'
MR_TIME_SPENT_CHANGED_ACTION = 'i_code_review_user_time_spent_changed'
MR_ASSIGNEES_CHANGED_ACTION = 'i_code_review_user_assignees_changed'
MR_REVIEWERS_CHANGED_ACTION = 'i_code_review_user_reviewers_changed'
class << self
def track_mr_diffs_action(merge_request:)
@ -173,6 +175,14 @@ module Gitlab
track_unique_action_by_user(MR_TIME_SPENT_CHANGED_ACTION, user)
end
def track_assignees_changed_action(user:)
track_unique_action_by_user(MR_ASSIGNEES_CHANGED_ACTION, user)
end
def track_reviewers_changed_action(user:)
track_unique_action_by_user(MR_REVIEWERS_CHANGED_ACTION, user)
end
private
def track_unique_action_by_merge_request(action, merge_request)

View File

@ -1330,7 +1330,7 @@ msgstr ""
msgid "A job artifact is an archive of files and directories saved by a job when it finishes."
msgstr ""
msgid "A label list displays all issues with the selected label."
msgid "A label list displays issues with the selected label."
msgstr ""
msgid "A limit of %{ci_project_subscriptions_limit} subscriptions to or from a project applies."
@ -12053,6 +12053,9 @@ msgstr ""
msgid "Error uploading file"
msgstr ""
msgid "Error uploading file. Please try again."
msgstr ""
msgid "Error uploading file: %{stripped}"
msgstr ""
@ -20298,7 +20301,7 @@ msgstr ""
msgid "New label"
msgstr ""
msgid "New label list"
msgid "New list"
msgstr ""
msgid "New merge request"
@ -20520,9 +20523,6 @@ msgstr ""
msgid "No label"
msgstr ""
msgid "No label selected"
msgstr ""
msgid "No labels with such name or description"
msgstr ""
@ -25023,6 +25023,9 @@ msgstr ""
msgid "Remove due date"
msgstr ""
msgid "Remove file"
msgstr ""
msgid "Remove fork relationship"
msgstr ""
@ -28386,6 +28389,9 @@ msgstr ""
msgid "Start a new merge request"
msgstr ""
msgid "Start a new merge request with these changes"
msgstr ""
msgid "Start a review"
msgstr ""

View File

@ -0,0 +1,166 @@
import { GlFormGroup, GlSearchBoxByType, GlSkeletonLoader } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import BoardAddNewColumnForm from '~/boards/components/board_add_new_column_form.vue';
import defaultState from '~/boards/stores/state';
import { mockLabelList } from '../mock_data';
Vue.use(Vuex);
describe('Board card layout', () => {
let wrapper;
const createStore = ({ actions = {}, getters = {}, state = {} } = {}) => {
return new Vuex.Store({
state: {
...defaultState,
...state,
},
actions,
getters,
});
};
const mountComponent = ({
loading = false,
formDescription = '',
searchLabel = '',
searchPlaceholder = '',
selectedId,
actions,
slots,
} = {}) => {
wrapper = extendedWrapper(
shallowMount(BoardAddNewColumnForm, {
stubs: {
GlFormGroup: true,
},
propsData: {
loading,
formDescription,
searchLabel,
searchPlaceholder,
selectedId,
},
slots,
store: createStore({
actions: {
setAddColumnFormVisibility: jest.fn(),
...actions,
},
}),
}),
);
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
const formTitle = () => wrapper.findByTestId('board-add-column-form-title').text();
const findSearchInput = () => wrapper.find(GlSearchBoxByType);
const findSearchLabel = () => wrapper.find(GlFormGroup);
const cancelButton = () => wrapper.findByTestId('cancelAddNewColumn');
const submitButton = () => wrapper.findByTestId('addNewColumnButton');
it('shows form title & search input', () => {
mountComponent();
expect(formTitle()).toEqual(BoardAddNewColumnForm.i18n.newList);
expect(findSearchInput().exists()).toBe(true);
});
it('clicking cancel hides the form', () => {
const setAddColumnFormVisibility = jest.fn();
mountComponent({
actions: {
setAddColumnFormVisibility,
},
});
cancelButton().vm.$emit('click');
expect(setAddColumnFormVisibility).toHaveBeenCalledWith(expect.anything(), false);
});
it('sets placeholder and description from props', () => {
const props = {
formDescription: 'Some description of a list',
};
mountComponent(props);
expect(wrapper.html()).toHaveText(props.formDescription);
});
describe('items', () => {
const mountWithItems = (loading) =>
mountComponent({
loading,
slots: {
items: '<div class="item-slot">Some kind of list</div>',
},
});
it('hides items slot and shows skeleton while loading', () => {
mountWithItems(true);
expect(wrapper.findComponent(GlSkeletonLoader).exists()).toBe(true);
expect(wrapper.find('.item-slot').exists()).toBe(false);
});
it('shows items slot and hides skeleton while not loading', () => {
mountWithItems(false);
expect(wrapper.findComponent(GlSkeletonLoader).exists()).toBe(false);
expect(wrapper.find('.item-slot').exists()).toBe(true);
});
});
describe('search box', () => {
it('sets label and placeholder text from props', () => {
const props = {
searchLabel: 'Some items',
searchPlaceholder: 'Search for an item',
};
mountComponent(props);
expect(findSearchLabel().attributes('label')).toEqual(props.searchLabel);
expect(findSearchInput().attributes('placeholder')).toEqual(props.searchPlaceholder);
});
it('emits filter event on input', () => {
mountComponent();
const searchText = 'some text';
findSearchInput().vm.$emit('input', searchText);
expect(wrapper.emitted('filter-items')).toEqual([[searchText]]);
});
});
describe('Add list button', () => {
it('is disabled if no item is selected', () => {
mountComponent();
expect(submitButton().props('disabled')).toBe(true);
});
it('emits add-list event on click', async () => {
mountComponent({
selectedId: mockLabelList.label.id,
});
await nextTick();
submitButton().vm.$emit('click');
expect(wrapper.emitted('add-list')).toEqual([[]]);
});
});
});

View File

@ -1,9 +1,9 @@
import { GlSearchBoxByType } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import BoardAddNewColumn from '~/boards/components/board_add_new_column.vue';
import BoardAddNewColumnForm from '~/boards/components/board_add_new_column_form.vue';
import defaultState from '~/boards/stores/state';
import { mockLabelList } from '../mock_data';
@ -11,7 +11,6 @@ Vue.use(Vuex);
describe('Board card layout', () => {
let wrapper;
let shouldUseGraphQL;
const createStore = ({ actions = {}, getters = {}, state = {} } = {}) => {
return new Vuex.Store({
@ -25,19 +24,16 @@ describe('Board card layout', () => {
};
const mountComponent = ({
selectedLabelId,
selectedId,
labels = [],
getListByLabelId = jest.fn(),
actions = {},
} = {}) => {
wrapper = extendedWrapper(
shallowMount(BoardAddNewColumn, {
stubs: {
GlFormGroup: true,
},
data() {
return {
selectedLabelId,
selectedId,
};
},
store: createStore({
@ -47,12 +43,13 @@ describe('Board card layout', () => {
...actions,
},
getters: {
shouldUseGraphQL: () => shouldUseGraphQL,
shouldUseGraphQL: () => true,
getListByLabelId: () => getListByLabelId,
},
state: {
labels,
labelsLoading: false,
isEpicBoard: false,
},
}),
provide: {
@ -64,65 +61,32 @@ describe('Board card layout', () => {
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
const formTitle = () => wrapper.findByTestId('board-add-column-form-title').text();
const findSearchInput = () => wrapper.find(GlSearchBoxByType);
const cancelButton = () => wrapper.findByTestId('cancelAddNewColumn');
const submitButton = () => wrapper.findByTestId('addNewColumnButton');
beforeEach(() => {
shouldUseGraphQL = true;
});
it('shows form title & search input', () => {
mountComponent();
expect(formTitle()).toEqual(BoardAddNewColumn.i18n.newLabelList);
expect(findSearchInput().exists()).toBe(true);
});
it('clicking cancel hides the form', () => {
const setAddColumnFormVisibility = jest.fn();
mountComponent({
actions: {
setAddColumnFormVisibility,
},
});
cancelButton().vm.$emit('click');
expect(setAddColumnFormVisibility).toHaveBeenCalledWith(expect.anything(), false);
});
describe('Add list button', () => {
it('is disabled if no item is selected', () => {
mountComponent();
expect(submitButton().props('disabled')).toBe(true);
});
it('adds a new list on click', async () => {
const labelId = mockLabelList.label.id;
it('calls addList', async () => {
const getListByLabelId = jest.fn().mockReturnValue(null);
const highlightList = jest.fn();
const createList = jest.fn();
mountComponent({
labels: [mockLabelList.label],
selectedLabelId: labelId,
selectedId: mockLabelList.label.id,
getListByLabelId,
actions: {
createList,
highlightList,
},
});
wrapper.findComponent(BoardAddNewColumnForm).vm.$emit('add-list');
await nextTick();
submitButton().vm.$emit('click');
expect(highlightList).not.toHaveBeenCalled();
expect(createList).toHaveBeenCalledWith(expect.anything(), { labelId });
expect(createList).toHaveBeenCalledWith(expect.anything(), {
labelId: mockLabelList.label.id,
});
});
it('highlights existing list if trying to re-add', async () => {
@ -132,7 +96,7 @@ describe('Board card layout', () => {
mountComponent({
labels: [mockLabelList.label],
selectedLabelId: mockLabelList.label.id,
selectedId: mockLabelList.label.id,
getListByLabelId,
actions: {
createList,
@ -140,9 +104,9 @@ describe('Board card layout', () => {
},
});
await nextTick();
wrapper.findComponent(BoardAddNewColumnForm).vm.$emit('add-list');
submitButton().vm.$emit('click');
await nextTick();
expect(highlightList).toHaveBeenCalledWith(expect.anything(), mockLabelList.id);
expect(createList).not.toHaveBeenCalled();

View File

@ -293,7 +293,7 @@ describe('createIssueList', () => {
data: {
boardListCreate: {
list: {},
errors: [{ foo: 'bar' }],
errors: ['foo'],
},
},
}),
@ -301,7 +301,7 @@ describe('createIssueList', () => {
await actions.createIssueList({ getters, state, commit, dispatch }, { backlog: true });
expect(commit).toHaveBeenCalledWith(types.CREATE_LIST_FAILURE);
expect(commit).toHaveBeenCalledWith(types.CREATE_LIST_FAILURE, 'foo');
});
it('highlights list and does not re-query if it already exists', async () => {

View File

@ -0,0 +1,193 @@
import { GlModal, GlFormInput, GlFormTextarea, GlToggle, GlAlert } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import waitForPromises from 'helpers/wait_for_promises';
import createFlash from '~/flash';
import httpStatusCodes from '~/lib/utils/http_status';
import { visitUrl } from '~/lib/utils/url_utility';
import UploadBlobModal from '~/repository/components/upload_blob_modal.vue';
import UploadDropzone from '~/vue_shared/components/upload_dropzone/upload_dropzone.vue';
jest.mock('~/flash');
jest.mock('~/lib/utils/url_utility', () => ({
visitUrl: jest.fn(),
joinPaths: () => '/new_upload',
}));
const initialProps = {
modalId: 'upload-blob',
commitMessage: 'Upload New File',
targetBranch: 'master',
origionalBranch: 'master',
canPushCode: true,
path: 'new_upload',
};
describe('UploadBlobModal', () => {
let wrapper;
let mock;
const mockEvent = { preventDefault: jest.fn() };
const createComponent = (props) => {
wrapper = shallowMount(UploadBlobModal, {
propsData: {
...initialProps,
...props,
},
mocks: {
$route: {
params: {
path: '',
},
},
},
});
};
const findModal = () => wrapper.find(GlModal);
const findAlert = () => wrapper.find(GlAlert);
const findCommitMessage = () => wrapper.find(GlFormTextarea);
const findBranchName = () => wrapper.find(GlFormInput);
const findMrToggle = () => wrapper.find(GlToggle);
const findUploadDropzone = () => wrapper.find(UploadDropzone);
const actionButtonDisabledState = () => findModal().props('actionPrimary').attributes[0].disabled;
const cancelButtonDisabledState = () => findModal().props('actionCancel').attributes[0].disabled;
const actionButtonLoadingState = () => findModal().props('actionPrimary').attributes[0].loading;
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
describe.each`
canPushCode | displayBranchName | displayForkedBranchMessage
${true} | ${true} | ${false}
${false} | ${false} | ${true}
`(
'canPushCode = $canPushCode',
({ canPushCode, displayBranchName, displayForkedBranchMessage }) => {
beforeEach(() => {
createComponent({ canPushCode });
});
it('displays the modal', () => {
expect(findModal().exists()).toBe(true);
});
it('includes the upload dropzone', () => {
expect(findUploadDropzone().exists()).toBe(true);
});
it('includes the commit message', () => {
expect(findCommitMessage().exists()).toBe(true);
});
it('displays the disabled upload button', () => {
expect(actionButtonDisabledState()).toBe(true);
});
it('displays the enabled cancel button', () => {
expect(cancelButtonDisabledState()).toBe(false);
});
it('does not display the MR toggle', () => {
expect(findMrToggle().exists()).toBe(false);
});
it(`${
displayForkedBranchMessage ? 'displays' : 'does not display'
} the forked branch message`, () => {
expect(findAlert().exists()).toBe(displayForkedBranchMessage);
});
it(`${displayBranchName ? 'displays' : 'does not display'} the branch name`, () => {
expect(findBranchName().exists()).toBe(displayBranchName);
});
if (canPushCode) {
describe('when changing the branch name', () => {
it('displays the MR toggle', async () => {
wrapper.setData({ target: 'Not master' });
await wrapper.vm.$nextTick();
expect(findMrToggle().exists()).toBe(true);
});
});
}
describe('completed form', () => {
beforeEach(() => {
wrapper.setData({
file: { type: 'jpg' },
filePreviewURL: 'http://file.com?format=jpg',
});
});
it('enables the upload button when the form is completed', () => {
expect(actionButtonDisabledState()).toBe(false);
});
describe('form submission', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
findModal().vm.$emit('primary', mockEvent);
});
afterEach(() => {
mock.restore();
});
it('disables the upload button', () => {
expect(actionButtonDisabledState()).toBe(true);
});
it('sets the upload button to loading', () => {
expect(actionButtonLoadingState()).toBe(true);
});
});
describe('successful response', () => {
beforeEach(async () => {
mock = new MockAdapter(axios);
mock.onPost(initialProps.path).reply(httpStatusCodes.OK, { filePath: 'blah' });
findModal().vm.$emit('primary', mockEvent);
await waitForPromises();
});
it('redirects to the uploaded file', () => {
expect(visitUrl).toHaveBeenCalled();
});
afterEach(() => {
mock.restore();
});
});
describe('error response', () => {
beforeEach(async () => {
mock = new MockAdapter(axios);
mock.onPost(initialProps.path).timeout();
findModal().vm.$emit('primary', mockEvent);
await waitForPromises();
});
it('creates a flash error', () => {
expect(createFlash).toHaveBeenCalledWith('Error uploading file. Please try again.');
});
afterEach(() => {
mock.restore();
});
});
});
},
);
});

View File

@ -87,7 +87,7 @@ RSpec.describe Gitlab::Git::Push do
it { is_expected.to be_force_push }
end
context 'when called muiltiple times' do
context 'when called mulitiple times' do
it 'does not make make multiple calls to the force push check' do
expect(Gitlab::Checks::ForcePush).to receive(:force_push?).once

View File

@ -6,7 +6,7 @@ RSpec.describe Gitlab::Kroki do
describe '.formats' do
def default_formats
%w[bytefield c4plantuml ditaa erd graphviz nomnoml plantuml svgbob umlet vega vegalite wavedrow].freeze
%w[bytefield c4plantuml ditaa erd graphviz nomnoml plantuml svgbob umlet vega vegalite wavedrom].freeze
end
subject { described_class.formats(Gitlab::CurrentSettings) }

View File

@ -316,4 +316,20 @@ RSpec.describe Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter, :cl
let(:action) { described_class::MR_TIME_SPENT_CHANGED_ACTION }
end
end
describe '.track_assignees_changed_action' do
subject { described_class.track_assignees_changed_action(user: user) }
it_behaves_like 'a tracked merge request unique event' do
let(:action) { described_class::MR_ASSIGNEES_CHANGED_ACTION }
end
end
describe '.track_reviewers_changed_action' do
subject { described_class.track_reviewers_changed_action(user: user) }
it_behaves_like 'a tracked merge request unique event' do
let(:action) { described_class::MR_REVIEWERS_CHANGED_ACTION }
end
end
end

View File

@ -4105,6 +4105,72 @@ RSpec.describe MergeRequest, factory_default: :keep do
end
end
describe '#mark_as_unchecked' do
subject { create(:merge_request, source_project: project, merge_status: merge_status) }
shared_examples 'for an invalid state transition' do
it 'is not a valid state transition' do
expect { subject.mark_as_unchecked! }.to raise_error(StateMachines::InvalidTransition)
end
end
shared_examples 'for an valid state transition' do
it 'is a valid state transition' do
expect { subject.mark_as_unchecked! }
.to change { subject.merge_status }
.from(merge_status.to_s)
.to(expected_merge_status)
end
end
context 'when the status is unchecked' do
let(:merge_status) { :unchecked }
include_examples 'for an invalid state transition'
end
context 'when the status is checking' do
let(:merge_status) { :checking }
let(:expected_merge_status) { 'unchecked' }
include_examples 'for an valid state transition'
end
context 'when the status is preparing' do
let(:merge_status) { :preparing }
let(:expected_merge_status) { 'unchecked' }
include_examples 'for an valid state transition'
end
context 'when the status is can_be_merged' do
let(:merge_status) { :can_be_merged }
let(:expected_merge_status) { 'unchecked' }
include_examples 'for an valid state transition'
end
context 'when the status is cannot_be_merged_recheck' do
let(:merge_status) { :cannot_be_merged_recheck }
include_examples 'for an invalid state transition'
end
context 'when the status is cannot_be_merged' do
let(:merge_status) { :cannot_be_merged }
let(:expected_merge_status) { 'cannot_be_merged_recheck' }
include_examples 'for an valid state transition'
end
context 'when the status is cannot_be_merged' do
let(:merge_status) { :cannot_be_merged }
let(:expected_merge_status) { 'cannot_be_merged_recheck' }
include_examples 'for an valid state transition'
end
end
describe 'transition to cannot_be_merged' do
let(:notification_service) { double(:notification_service) }
let(:todo_service) { double(:todo_service) }

View File

@ -186,6 +186,54 @@ RSpec.describe MergeRequests::UpdateService, :mailer do
MergeRequests::UpdateService.new(project, user, opts).execute(merge_request)
end
context 'assignees' do
context 'when assignees changed' do
it 'tracks assignees changed event' do
expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter)
.to receive(:track_assignees_changed_action).once.with(user: user)
opts[:assignees] = [user2]
MergeRequests::UpdateService.new(project, user, opts).execute(merge_request)
end
end
context 'when assignees did not change' do
it 'does not track assignees changed event' do
expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter)
.not_to receive(:track_assignees_changed_action)
opts[:assignees] = merge_request.assignees
MergeRequests::UpdateService.new(project, user, opts).execute(merge_request)
end
end
end
context 'reviewers' do
context 'when reviewers changed' do
it 'tracks reviewers changed event' do
expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter)
.to receive(:track_reviewers_changed_action).once.with(user: user)
opts[:reviewers] = [user2]
MergeRequests::UpdateService.new(project, user, opts).execute(merge_request)
end
end
context 'when reviewers did not change' do
it 'does not track reviewers changed event' do
expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter)
.not_to receive(:track_reviewers_changed_action)
opts[:reviewers] = merge_request.reviewers
MergeRequests::UpdateService.new(project, user, opts).execute(merge_request)
end
end
end
end
context 'updating milestone' do