Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
775b2961fe
commit
b0a5a92e83
|
@ -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)
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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);
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
import initBlobBundle from '~/blob_edit/blob_bundle';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', initBlobBundle);
|
||||
initBlobBundle();
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
|
@ -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
|
||||
|
|
|
@ -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|
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
||||
.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}
|
|
@ -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
|
||||
|
||||
|
||||
.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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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' }
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add tracking to merge request assignees/reviewers changes
|
||||
merge_request: 55486
|
||||
author:
|
||||
type: other
|
|
@ -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
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Refactor blame view
|
||||
merge_request: 55488
|
||||
author:
|
||||
type: performance
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Move from btn-success to btn-confirm in shared/groups directory
|
||||
merge_request: 55302
|
||||
author: Yogi (@yo)
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Move from btn-success to btn-confirm in shared/wikis directory
|
||||
merge_request: 55316
|
||||
author: Yogi (@yo)
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Move from btn-success to btn-confirm in shared directory
|
||||
merge_request: 55317
|
||||
author: Yogi (@yo)
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Remove unneeded transitions on MR for mark_as_unchecked event
|
||||
merge_request: 53537
|
||||
author:
|
||||
type: performance
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Update validate_url gem
|
||||
merge_request: 55706
|
||||
author:
|
||||
type: fixed
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
14
lefthook.yml
14
lefthook.yml
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 ""
|
||||
|
||||
|
|
|
@ -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([[]]);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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();
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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) }
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) }
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue