Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
7d4b2ed7bf
commit
80088b7f88
14 changed files with 242 additions and 84 deletions
|
@ -1,10 +1,12 @@
|
|||
<script>
|
||||
import { GlLabel } from '@gitlab/ui';
|
||||
import { mapGetters, mapActions } from 'vuex';
|
||||
import Api from '~/api';
|
||||
import BoardEditableItem from '~/boards/components/sidebar/board_editable_item.vue';
|
||||
import createFlash from '~/flash';
|
||||
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
|
||||
import { isScopedLabel } from '~/lib/utils/common_utils';
|
||||
import { mergeUrlParams } from '~/lib/utils/url_utility';
|
||||
import { __ } from '~/locale';
|
||||
import LabelsSelect from '~/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue';
|
||||
|
||||
|
@ -14,7 +16,13 @@ export default {
|
|||
LabelsSelect,
|
||||
GlLabel,
|
||||
},
|
||||
inject: ['labelsFetchPath', 'labelsManagePath', 'labelsFilterBasePath'],
|
||||
inject: {
|
||||
labelsFetchPath: {
|
||||
default: null,
|
||||
},
|
||||
labelsManagePath: {},
|
||||
labelsFilterBasePath: {},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
|
@ -38,6 +46,32 @@ export default {
|
|||
scoped: isScopedLabel(label),
|
||||
}));
|
||||
},
|
||||
fetchPath() {
|
||||
/*
|
||||
Labels fetched in epic boards are always group-level labels
|
||||
and the correct path are passed from the backend (injected through labelsFetchPath)
|
||||
|
||||
For issue boards, we should always include project-level labels and use a different endpoint.
|
||||
(it requires knowing the project path of a selected issue.)
|
||||
|
||||
Note 1. that we will be using GraphQL to fetch labels when we create a labels select widget.
|
||||
And this component will be removed _wholesale_ https://gitlab.com/gitlab-org/gitlab/-/issues/300653.
|
||||
|
||||
Note 2. Moreover, 'fetchPath' needs to be used as a key for 'labels-select' component to force updates.
|
||||
'labels-select' has its own vuex store and initializes the passed props as states
|
||||
and these states aren't reactively bound to the passed props.
|
||||
*/
|
||||
|
||||
const projectLabelsFetchPath = mergeUrlParams(
|
||||
{ include_ancestor_groups: true },
|
||||
Api.buildUrl(Api.projectLabelsPath).replace(
|
||||
':namespace_path/:project_path',
|
||||
this.projectPathForActiveIssue,
|
||||
),
|
||||
);
|
||||
|
||||
return this.labelsFetchPath || projectLabelsFetchPath;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['setActiveBoardItemLabels']),
|
||||
|
@ -100,12 +134,13 @@ export default {
|
|||
<template #default="{ edit }">
|
||||
<labels-select
|
||||
ref="labelsSelect"
|
||||
:key="fetchPath"
|
||||
:allow-label-edit="false"
|
||||
:allow-label-create="false"
|
||||
:allow-multiselect="true"
|
||||
:allow-scoped-labels="true"
|
||||
:selected-labels="selectedLabels"
|
||||
:labels-fetch-path="labelsFetchPath"
|
||||
:labels-fetch-path="fetchPath"
|
||||
:labels-manage-path="labelsManagePath"
|
||||
:labels-filter-base-path="labelsFilterBasePath"
|
||||
:labels-list-title="__('Select label')"
|
||||
|
|
|
@ -97,7 +97,6 @@ export default () => {
|
|||
currentUserId: gon.current_user_id || null,
|
||||
canUpdate: parseBoolean($boardApp.dataset.canUpdate),
|
||||
canAdminList: parseBoolean($boardApp.dataset.canAdminList),
|
||||
labelsFetchPath: $boardApp.dataset.labelsFetchPath,
|
||||
labelsManagePath: $boardApp.dataset.labelsManagePath,
|
||||
labelsFilterBasePath: $boardApp.dataset.labelsFilterBasePath,
|
||||
timeTrackingLimitToHours: parseBoolean($boardApp.dataset.timeTrackingLimitToHours),
|
||||
|
|
|
@ -7,7 +7,7 @@ import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
|||
import { DEFAULT, DRAW_FAILURE, LOAD_FAILURE } from '../../constants';
|
||||
import DismissPipelineGraphCallout from '../../graphql/mutations/dismiss_pipeline_notification.graphql';
|
||||
import getUserCallouts from '../../graphql/queries/get_user_callouts.query.graphql';
|
||||
import { reportToSentry } from '../../utils';
|
||||
import { reportToSentry, reportMessageToSentry } from '../../utils';
|
||||
import { listByLayers } from '../parsing_utils';
|
||||
import { IID_FAILURE, LAYER_VIEW, STAGE_VIEW, VIEW_TYPE_KEY } from './constants';
|
||||
import PipelineGraph from './graph_component.vue';
|
||||
|
@ -109,14 +109,16 @@ export default {
|
|||
},
|
||||
error(err) {
|
||||
this.reportFailure({ type: LOAD_FAILURE, skipSentry: true });
|
||||
reportToSentry(
|
||||
|
||||
reportMessageToSentry(
|
||||
this.$options.name,
|
||||
`| type: ${LOAD_FAILURE} |
|
||||
| rawError: ${JSON.stringify(err)} |
|
||||
| info: ${serializeLoadErrors(err)} |
|
||||
| graphqlResourceEtag: ${this.graphqlResourceEtag} |
|
||||
| projectPath: ${this.projectPath} |
|
||||
| iid: ${this.pipelineIid} |`,
|
||||
`| type: ${LOAD_FAILURE} , info: ${serializeLoadErrors(err)}`,
|
||||
{
|
||||
projectPath: this.projectPath,
|
||||
pipelineIid: this.pipelineIid,
|
||||
pipelineStages: this.pipeline?.stages?.length || 0,
|
||||
nbOfDownstreams: this.pipeline?.downstream?.length || 0,
|
||||
},
|
||||
);
|
||||
},
|
||||
result({ error }) {
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { isEmpty } from 'lodash';
|
||||
import Visibility from 'visibilityjs';
|
||||
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
|
||||
import { unwrapStagesWithNeedsAndLookup } from '../unwrapping_utils';
|
||||
|
@ -39,15 +40,15 @@ const serializeGqlErr = (gqlError) => {
|
|||
const serializeLoadErrors = (errors) => {
|
||||
const { gqlError, graphQLErrors, networkError, message } = errors;
|
||||
|
||||
if (graphQLErrors) {
|
||||
if (!isEmpty(graphQLErrors)) {
|
||||
return graphQLErrors.map((err) => serializeGqlErr(err)).join('; ');
|
||||
}
|
||||
|
||||
if (gqlError) {
|
||||
if (!isEmpty(gqlError)) {
|
||||
return serializeGqlErr(gqlError);
|
||||
}
|
||||
|
||||
if (networkError) {
|
||||
if (!isEmpty(networkError)) {
|
||||
return `Network error: ${networkError.message}`;
|
||||
}
|
||||
|
||||
|
|
|
@ -73,3 +73,12 @@ export const reportToSentry = (component, failureType) => {
|
|||
Sentry.captureException(failureType);
|
||||
});
|
||||
};
|
||||
|
||||
export const reportMessageToSentry = (component, message, context) => {
|
||||
Sentry.withScope((scope) => {
|
||||
// eslint-disable-next-line @gitlab/require-i18n-strings
|
||||
scope.setContext('Vue data', context);
|
||||
scope.setTag('component', component);
|
||||
Sentry.captureMessage(message);
|
||||
});
|
||||
};
|
||||
|
|
|
@ -22,19 +22,19 @@ The GitLab Code Owners feature defines who owns specific
|
|||
files or paths in a repository, allowing other users to understand
|
||||
who is responsible for each file or path.
|
||||
|
||||
As an alternative to using Code Owners for approvals, you can instead
|
||||
[configure rules](merge_requests/approvals/rules.md).
|
||||
|
||||
## Why is this useful?
|
||||
|
||||
Code Owners allows for a version controlled, single source of
|
||||
truth file outlining the exact GitLab users or groups that
|
||||
own certain files or paths in a repository. Code Owners can be
|
||||
used in the merge request approval process which can streamline
|
||||
the process of finding the right reviewers and approvers for a given
|
||||
merge request.
|
||||
|
||||
In larger organizations or popular open source projects, Code Owners
|
||||
can help you understand who to contact if you have
|
||||
a question that may not be related to code review or a merge request
|
||||
approval.
|
||||
own certain files or paths in a repository. In larger organizations
|
||||
or popular open source projects, Code Owners can help you understand
|
||||
who to contact if you have a question about a specific portion of
|
||||
the codebase. Code Owners can also streamline the merge request approval
|
||||
process, identifying the most relevant reviewers and approvers for a
|
||||
given change.
|
||||
|
||||
## How to set up Code Owners
|
||||
|
||||
|
|
|
@ -10,14 +10,15 @@ disqus_identifier: 'https://docs.gitlab.com/ee/user/project/merge_requests/merge
|
|||
|
||||
> Redesign [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/1979) in [GitLab Premium](https://about.gitlab.com/pricing/) 11.8 and [feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/issues/10685) in 12.0.
|
||||
|
||||
Successful projects depend on code reviews. Merge request approvals clearly communicate
|
||||
someone's ability to merge proposed changes. Approvals [are optional](#optional-approvals)
|
||||
in GitLab Free, but you can require them for your project in higher tiers.
|
||||
You can configure your merge requests so that they must be approved before
|
||||
they can be merged. You can do this by creating [rules](rules.md) or by specifying
|
||||
a list of users who act as [code owners](../../code_owners.md) for specific files.
|
||||
|
||||
Merge request approvals are configured at the project level. Administrator users
|
||||
of self-managed GitLab installations can also configure
|
||||
[instance-level approval rules](../../../admin_area/merge_requests_approvals.md)
|
||||
that cannot be overridden on a project-level basis.
|
||||
You can configure merge request approvals for each project. In higher GitLab tiers,
|
||||
Administrators of self-managed GitLab instances can configure approvals
|
||||
[for the entire instance](../../../admin_area/merge_requests_approvals.md).
|
||||
|
||||
## How approvals work
|
||||
|
||||
With [merge request approval rules](rules.md), you can set the minimum number of
|
||||
required approvals before work can merge into your project. You can also extend these
|
||||
|
|
34
spec/features/boards/sidebar_labels_in_namespaces_spec.rb
Normal file
34
spec/features/boards/sidebar_labels_in_namespaces_spec.rb
Normal file
|
@ -0,0 +1,34 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'Issue boards sidebar labels select', :js do
|
||||
include BoardHelpers
|
||||
|
||||
include_context 'labels from nested groups and projects'
|
||||
|
||||
let(:card) { find('.board:nth-child(1)').first('[data-testid="board_card"]') }
|
||||
|
||||
context 'group boards' do
|
||||
context 'in the top-level group board' do
|
||||
let_it_be(:group_board) { create(:board, group: group) }
|
||||
let_it_be(:board_list) { create(:backlog_list, board: group_board) }
|
||||
|
||||
before do
|
||||
load_board group_board_path(group, group_board)
|
||||
end
|
||||
|
||||
context 'selecting an issue from a direct descendant project' do
|
||||
let_it_be(:project_issue) { create(:issue, project: project) }
|
||||
|
||||
include_examples 'an issue from a direct descendant project is selected'
|
||||
end
|
||||
|
||||
context "selecting an issue from a subgroup's project" do
|
||||
let_it_be(:subproject_issue) { create(:issue, project: subproject) }
|
||||
|
||||
include_examples "an issue from a subgroup's project is selected"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,46 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'Sub-group project issue boards', :js do
|
||||
let(:group) { create(:group) }
|
||||
let(:nested_group_1) { create(:group, parent: group) }
|
||||
let(:project) { create(:project, group: nested_group_1) }
|
||||
let(:board) { create(:board, project: project) }
|
||||
let(:label) { create(:label, project: project) }
|
||||
let(:user) { create(:user) }
|
||||
let!(:list1) { create(:list, board: board, label: label, position: 0) }
|
||||
let!(:issue) { create(:labeled_issue, project: project, labels: [label]) }
|
||||
|
||||
before do
|
||||
project.add_maintainer(user)
|
||||
|
||||
sign_in(user)
|
||||
|
||||
visit project_board_path(project, board)
|
||||
wait_for_requests
|
||||
end
|
||||
|
||||
# TODO https://gitlab.com/gitlab-org/gitlab/-/issues/324290
|
||||
xit 'creates new label from sidebar' do
|
||||
find('.board-card').click
|
||||
|
||||
page.within '.labels' do
|
||||
click_link 'Edit'
|
||||
click_link 'Create project label'
|
||||
end
|
||||
|
||||
page.within '.dropdown-new-label' do
|
||||
fill_in 'new_label_name', with: 'test label'
|
||||
first('.suggest-colors-dropdown a').click
|
||||
|
||||
click_button 'Create'
|
||||
|
||||
wait_for_requests
|
||||
end
|
||||
|
||||
page.within '.labels' do
|
||||
expect(page).to have_link 'test label'
|
||||
end
|
||||
end
|
||||
end
|
|
@ -12,6 +12,14 @@ RSpec.describe 'User visits the notifications tab', :js do
|
|||
visit(profile_notifications_path)
|
||||
end
|
||||
|
||||
it 'turns on the receive product marketing emails setting' do
|
||||
expect(page).to have_content('Notifications')
|
||||
|
||||
expect do
|
||||
check 'Receive product marketing emails'
|
||||
end.to change { user.reload.email_opted_in }.to(true)
|
||||
end
|
||||
|
||||
it 'changes the project notifications setting' do
|
||||
expect(page).to have_content('Notifications')
|
||||
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
import { GlLabel } from '@gitlab/ui';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import { TEST_HOST } from 'helpers/test_constants';
|
||||
import { labels as TEST_LABELS, mockIssue as TEST_ISSUE } from 'jest/boards/mock_data';
|
||||
import {
|
||||
labels as TEST_LABELS,
|
||||
mockIssue as TEST_ISSUE,
|
||||
mockIssueFullPath as TEST_ISSUE_FULLPATH,
|
||||
} from 'jest/boards/mock_data';
|
||||
import BoardEditableItem from '~/boards/components/sidebar/board_editable_item.vue';
|
||||
import BoardSidebarLabelsSelect from '~/boards/components/sidebar/board_sidebar_labels_select.vue';
|
||||
import { createStore } from '~/boards/stores';
|
||||
|
@ -23,7 +27,7 @@ describe('~/boards/components/sidebar/board_sidebar_labels_select.vue', () => {
|
|||
wrapper = null;
|
||||
});
|
||||
|
||||
const createWrapper = ({ labels = [] } = {}) => {
|
||||
const createWrapper = ({ labels = [], providedValues = {} } = {}) => {
|
||||
store = createStore();
|
||||
store.state.boardItems = { [TEST_ISSUE.id]: { ...TEST_ISSUE, labels } };
|
||||
store.state.activeId = TEST_ISSUE.id;
|
||||
|
@ -32,9 +36,9 @@ describe('~/boards/components/sidebar/board_sidebar_labels_select.vue', () => {
|
|||
store,
|
||||
provide: {
|
||||
canUpdate: true,
|
||||
labelsFetchPath: TEST_HOST,
|
||||
labelsManagePath: TEST_HOST,
|
||||
labelsFilterBasePath: TEST_HOST,
|
||||
...providedValues,
|
||||
},
|
||||
stubs: {
|
||||
BoardEditableItem,
|
||||
|
@ -48,6 +52,22 @@ describe('~/boards/components/sidebar/board_sidebar_labels_select.vue', () => {
|
|||
wrapper.findAll(GlLabel).wrappers.map((item) => item.props('title'));
|
||||
const findCollapsed = () => wrapper.find('[data-testid="collapsed-content"]');
|
||||
|
||||
describe('when labelsFetchPath is provided', () => {
|
||||
it('uses injected labels fetch path', () => {
|
||||
createWrapper({ providedValues: { labelsFetchPath: 'foobar' } });
|
||||
|
||||
expect(findLabelsSelect().props('labelsFetchPath')).toEqual('foobar');
|
||||
});
|
||||
});
|
||||
|
||||
it('uses the default project label endpoint', () => {
|
||||
createWrapper();
|
||||
|
||||
expect(findLabelsSelect().props('labelsFetchPath')).toEqual(
|
||||
`/${TEST_ISSUE_FULLPATH}/-/labels?include_ancestor_groups=true`,
|
||||
);
|
||||
});
|
||||
|
||||
it('renders "None" when no labels are selected', () => {
|
||||
createWrapper();
|
||||
|
||||
|
@ -78,7 +98,7 @@ describe('~/boards/components/sidebar/board_sidebar_labels_select.vue', () => {
|
|||
it('commits change to the server', () => {
|
||||
expect(wrapper.vm.setActiveBoardItemLabels).toHaveBeenCalledWith({
|
||||
addLabelIds: TEST_LABELS.map((label) => label.id),
|
||||
projectPath: 'gitlab-org/test-subgroup/gitlab-test',
|
||||
projectPath: TEST_ISSUE_FULLPATH,
|
||||
removeLabelIds: [],
|
||||
});
|
||||
});
|
||||
|
@ -103,7 +123,7 @@ describe('~/boards/components/sidebar/board_sidebar_labels_select.vue', () => {
|
|||
expect(wrapper.vm.setActiveBoardItemLabels).toHaveBeenCalledWith({
|
||||
addLabelIds: [5, 7],
|
||||
removeLabelIds: [6],
|
||||
projectPath: 'gitlab-org/test-subgroup/gitlab-test',
|
||||
projectPath: TEST_ISSUE_FULLPATH,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -122,7 +142,7 @@ describe('~/boards/components/sidebar/board_sidebar_labels_select.vue', () => {
|
|||
|
||||
expect(wrapper.vm.setActiveBoardItemLabels).toHaveBeenCalledWith({
|
||||
removeLabelIds: [getIdFromGraphQLId(testLabel.id)],
|
||||
projectPath: 'gitlab-org/test-subgroup/gitlab-test',
|
||||
projectPath: TEST_ISSUE_FULLPATH,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -151,6 +151,8 @@ export const rawIssue = {
|
|||
},
|
||||
};
|
||||
|
||||
export const mockIssueFullPath = 'gitlab-org/test-subgroup/gitlab-test';
|
||||
|
||||
export const mockIssue = {
|
||||
id: 'gid://gitlab/Issue/436',
|
||||
iid: '27',
|
||||
|
@ -159,8 +161,8 @@ export const mockIssue = {
|
|||
timeEstimate: 0,
|
||||
weight: null,
|
||||
confidential: false,
|
||||
referencePath: 'gitlab-org/test-subgroup/gitlab-test#27',
|
||||
path: '/gitlab-org/test-subgroup/gitlab-test/-/issues/27',
|
||||
referencePath: `${mockIssueFullPath}#27`,
|
||||
path: `/${mockIssueFullPath}/-/issues/27`,
|
||||
assignees,
|
||||
labels: [
|
||||
{
|
||||
|
|
|
@ -7,4 +7,20 @@ module BoardHelpers
|
|||
wait_for_requests
|
||||
end
|
||||
end
|
||||
|
||||
def load_board(board_path)
|
||||
visit board_path
|
||||
|
||||
wait_for_requests
|
||||
end
|
||||
|
||||
def click_card_and_edit_label
|
||||
click_card(card)
|
||||
|
||||
page.within(labels_select) do
|
||||
click_button 'Edit'
|
||||
|
||||
wait_for_requests
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,77 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
RSpec.shared_context 'labels from nested groups and projects' do
|
||||
let_it_be(:group) { create(:group) }
|
||||
let_it_be(:group_label) { create(:group_label, group: group, name: 'Group label') }
|
||||
let_it_be(:project) { create(:project, group: group) }
|
||||
let_it_be(:project_label) { create(:label, project: project, name: 'Project label') }
|
||||
|
||||
let_it_be(:subgroup) { create(:group, parent: group) }
|
||||
let_it_be(:subgroup_label) { create(:group_label, group: subgroup, name: 'Subgroup label') }
|
||||
let_it_be(:subproject) { create(:project, group: subgroup) }
|
||||
let_it_be(:subproject_label) { create(:label, project: subproject, name: 'Subproject label') }
|
||||
|
||||
let_it_be(:subgroup2) { create(:group, parent: group) }
|
||||
let_it_be(:subgroup2_label) { create(:group_label, group: subgroup2, name: 'Subgroup2 label') }
|
||||
|
||||
let_it_be(:maintainer) { create(:user) }
|
||||
|
||||
let(:labels_select) { find("[data-testid='sidebar-labels']") }
|
||||
let(:labels_dropdown) { labels_select.find('[data-testid="dropdown-content"]')}
|
||||
|
||||
before do
|
||||
group.add_maintainer(maintainer)
|
||||
|
||||
sign_in(maintainer)
|
||||
end
|
||||
end
|
||||
|
||||
RSpec.shared_examples "an issue from a subgroup's project is selected" do
|
||||
context 'when editing labels' do
|
||||
before do
|
||||
click_card_and_edit_label
|
||||
end
|
||||
|
||||
it 'displays the label from the top-level group' do
|
||||
expect(labels_dropdown).to have_content(group_label.name)
|
||||
end
|
||||
|
||||
it 'displays the label from the subgroup' do
|
||||
expect(labels_dropdown).to have_content(subgroup_label.name)
|
||||
end
|
||||
|
||||
it 'displays the label from the project' do
|
||||
expect(labels_dropdown).to have_content(subproject_label.name)
|
||||
end
|
||||
|
||||
it "does not display labels from the subgroup's siblings (project or group)" do
|
||||
aggregate_failures do
|
||||
expect(labels_dropdown).not_to have_content(project_label.name)
|
||||
expect(labels_dropdown).not_to have_content(subgroup2_label.name)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
RSpec.shared_examples 'an issue from a direct descendant project is selected' do
|
||||
context 'when editing labels' do
|
||||
before do
|
||||
click_card_and_edit_label
|
||||
end
|
||||
|
||||
it 'displays the label from the top-level group' do
|
||||
expect(labels_dropdown).to have_content(group_label.name)
|
||||
end
|
||||
|
||||
it 'displays the label from the project' do
|
||||
expect(labels_dropdown).to have_content(project_label.name)
|
||||
end
|
||||
|
||||
it "does not display labels from the project's siblings or their descendents" do
|
||||
aggregate_failures do
|
||||
expect(labels_dropdown).not_to have_content(subgroup_label.name)
|
||||
expect(labels_dropdown).not_to have_content(subproject_label.name)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in a new issue