Skip to content
Snippets Groups Projects
Commit 3a27f35e authored by Nicolò Mezzopera's avatar Nicolò Mezzopera
Browse files

Merge branch '322755-refactor-group-issues-page-to-vue' into 'master'

Refactor group issues page from Haml to Vue

See merge request gitlab-org/gitlab!68978
parents 2aed4648 50111d1d
No related branches found
No related tags found
No related merge requests found
Loading
Loading
@@ -13,8 +13,8 @@
issues_path: project_issues_path(@project),
project_path: @project.full_path } }
 
- if Feature.enabled?(:vue_issues_list, @project)
.js-issues-list{ data: issues_list_data(@project, current_user, finder) }
- if Feature.enabled?(:vue_issues_list, @project&.group, default_enabled: :yaml)
.js-issues-list{ data: project_issues_list_data(@project, current_user, finder) }
- if @can_bulk_update
= render 'shared/issuable/bulk_update_sidebar', type: :issues
- elsif project_issues(@project).exists?
Loading
Loading
Loading
Loading
@@ -2,6 +2,7 @@
#import "~/issues_list/queries/issue.fragment.graphql"
 
query getIssues(
$isProject: Boolean = false
$isSignedIn: Boolean = false
$fullPath: ID!
$search: String
Loading
Loading
@@ -24,7 +25,42 @@ query getIssues(
$firstPageSize: Int
$lastPageSize: Int
) {
project(fullPath: $fullPath) {
group(fullPath: $fullPath) @skip(if: $isProject) {
issues(
includeSubgroups: true
search: $search
sort: $sort
state: $state
assigneeId: $assigneeId
assigneeUsernames: $assigneeUsernames
authorUsername: $authorUsername
labelName: $labelName
milestoneTitle: $milestoneTitle
milestoneWildcardId: $milestoneWildcardId
types: $types
epicId: $epicId
iterationId: $iterationId
iterationWildcardId: $iterationWildcardId
weight: $weight
not: $not
before: $beforeCursor
after: $afterCursor
first: $firstPageSize
last: $lastPageSize
) {
pageInfo {
...PageInfo
}
nodes {
...IssueFragment
reference(full: true)
blockingCount
healthStatus
weight
}
}
}
project(fullPath: $fullPath) @include(if: $isProject) {
issues(
search: $search
sort: $sort
Loading
Loading
query getIssuesCount(
$isProject: Boolean = false
$fullPath: ID!
$search: String
$assigneeId: String
Loading
Loading
@@ -14,7 +15,66 @@ query getIssuesCount(
$weight: String
$not: NegatedIssueFilterInput
) {
project(fullPath: $fullPath) {
group(fullPath: $fullPath) @skip(if: $isProject) {
openedIssues: issues(
includeSubgroups: true
state: opened
search: $search
assigneeId: $assigneeId
assigneeUsernames: $assigneeUsernames
authorUsername: $authorUsername
labelName: $labelName
milestoneTitle: $milestoneTitle
milestoneWildcardId: $milestoneWildcardId
types: $types
epicId: $epicId
iterationId: $iterationId
iterationWildcardId: $iterationWildcardId
weight: $weight
not: $not
) {
count
}
closedIssues: issues(
includeSubgroups: true
state: closed
search: $search
assigneeId: $assigneeId
assigneeUsernames: $assigneeUsernames
authorUsername: $authorUsername
labelName: $labelName
milestoneTitle: $milestoneTitle
milestoneWildcardId: $milestoneWildcardId
types: $types
epicId: $epicId
iterationId: $iterationId
iterationWildcardId: $iterationWildcardId
weight: $weight
not: $not
) {
count
}
allIssues: issues(
includeSubgroups: true
state: all
search: $search
assigneeId: $assigneeId
assigneeUsernames: $assigneeUsernames
authorUsername: $authorUsername
labelName: $labelName
milestoneTitle: $milestoneTitle
milestoneWildcardId: $milestoneWildcardId
types: $types
epicId: $epicId
iterationId: $iterationId
iterationWildcardId: $iterationWildcardId
weight: $weight
not: $not
) {
count
}
}
project(fullPath: $fullPath) @include(if: $isProject) {
openedIssues: issues(
state: opened
search: $search
Loading
Loading
Loading
Loading
@@ -42,21 +42,35 @@ def issue_header_actions_data(project, issuable, current_user)
actions
end
 
override :issues_list_data
def issues_list_data(project, current_user, finder)
data = super.merge!(
has_blocked_issues_feature: project.feature_available?(:blocked_issues).to_s,
has_issuable_health_status_feature: project.feature_available?(:issuable_health_status).to_s,
has_issue_weights_feature: project.feature_available?(:issue_weights).to_s,
has_iterations_feature: project.feature_available?(:iterations).to_s,
has_multiple_issue_assignees_feature: project.feature_available?(:multiple_issue_assignees).to_s
override :common_issues_list_data
def common_issues_list_data(namespace, current_user)
super.merge(
has_blocked_issues_feature: namespace.feature_available?(:blocked_issues).to_s,
has_issuable_health_status_feature: namespace.feature_available?(:issuable_health_status).to_s,
has_issue_weights_feature: namespace.feature_available?(:issue_weights).to_s,
has_iterations_feature: namespace.feature_available?(:iterations).to_s,
has_multiple_issue_assignees_feature: namespace.feature_available?(:multiple_issue_assignees).to_s
)
end
 
if project.feature_available?(:epics) && project.group
data[:group_epics_path] = group_epics_path(project.group, format: :json)
override :project_issues_list_data
def project_issues_list_data(project, current_user, finder)
super.tap do |data|
if project.feature_available?(:epics) && project.group
data[:group_epics_path] = group_epics_path(project.group, format: :json)
end
end
end
 
data
override :group_issues_list_data
def group_issues_list_data(group, current_user, issues)
super.tap do |data|
data[:can_bulk_update] = (can?(current_user, :admin_issue, group) && group.feature_available?(:group_bulk_edit)).to_s
if group.feature_available?(:epics)
data[:group_epics_path] = group_epics_path(group, format: :json)
end
end
end
end
end
Loading
Loading
@@ -124,7 +124,7 @@
end
end
 
describe '#issues_list_data' do
describe '#project_issues_list_data' do
let(:current_user) { double.as_null_object }
let(:finder) { double.as_null_object }
 
Loading
Loading
@@ -150,14 +150,14 @@
group_epics_path: group_epics_path(project.group, format: :json)
}
 
expect(helper.issues_list_data(project, current_user, finder)).to include(expected)
expect(helper.project_issues_list_data(project, current_user, finder)).to include(expected)
end
 
context 'when project does not have group' do
let(:project_with_no_group) { create :project }
 
it 'does not return group_epics_path' do
expect(helper.issues_list_data(project_with_no_group, current_user, finder)).not_to include(:group_epics_path)
expect(helper.project_issues_list_data(project_with_no_group, current_user, finder)).not_to include(:group_epics_path)
end
end
end
Loading
Loading
@@ -176,7 +176,60 @@
has_multiple_issue_assignees_feature: 'false'
}
 
result = helper.issues_list_data(project, current_user, finder)
result = helper.project_issues_list_data(project, current_user, finder)
expect(result).to include(expected)
expect(result).not_to include(:group_epics_path)
end
end
end
describe '#group_issues_list_data' do
let(:current_user) { double.as_null_object }
let(:issues) { [] }
before do
allow(helper).to receive(:current_user).and_return(current_user)
allow(helper).to receive(:can?).and_return(true)
allow(helper).to receive(:url_for).and_return('#')
end
context 'when features are enabled' do
before do
stub_licensed_features(blocked_issues: true, epics: true, group_bulk_edit: true, issuable_health_status: true, issue_weights: true, iterations: true, multiple_issue_assignees: true)
end
it 'returns data with licensed features enabled' do
expected = {
can_bulk_update: 'true',
has_blocked_issues_feature: 'true',
has_issuable_health_status_feature: 'true',
has_issue_weights_feature: 'true',
has_iterations_feature: 'true',
has_multiple_issue_assignees_feature: 'true',
group_epics_path: group_epics_path(project.group, format: :json)
}
expect(helper.group_issues_list_data(group, current_user, issues)).to include(expected)
end
end
context 'when features are disabled' do
before do
stub_licensed_features(blocked_issues: false, epics: false, group_bulk_edit: false, issuable_health_status: false, issue_weights: false, iterations: false, multiple_issue_assignees: false)
end
it 'returns data with licensed features disabled' do
expected = {
can_bulk_update: 'false',
has_blocked_issues_feature: 'false',
has_issuable_health_status_feature: 'false',
has_issue_weights_feature: 'false',
has_iterations_feature: 'false',
has_multiple_issue_assignees_feature: 'false'
}
result = helper.group_issues_list_data(group, current_user, issues)
 
expect(result).to include(expected)
expect(result).not_to include(:group_epics_path)
Loading
Loading
Loading
Loading
@@ -68,8 +68,8 @@ describe('IssuesListApp component', () => {
hasBlockedIssuesFeature: true,
hasIssueWeightsFeature: true,
hasIterationsFeature: true,
isProject: true,
isSignedIn: true,
issuesPath: 'path/to/issues',
jiraIntegrationPath: 'jira/integration/path',
newIssuePath: 'new/issue/path',
rssPath: 'rss/path',
Loading
Loading
@@ -191,7 +191,7 @@ describe('IssuesListApp component', () => {
setWindowLocation(search);
 
wrapper = mountComponent({
provide: { ...defaultProvide, isSignedIn: true },
provide: { isSignedIn: true },
mountFn: mount,
});
 
Loading
Loading
@@ -208,7 +208,15 @@ describe('IssuesListApp component', () => {
 
describe('when user is not signed in', () => {
it('does not render', () => {
wrapper = mountComponent({ provide: { ...defaultProvide, isSignedIn: false } });
wrapper = mountComponent({ provide: { isSignedIn: false } });
expect(findCsvImportExportButtons().exists()).toBe(false);
});
});
describe('when in a group context', () => {
it('does not render', () => {
wrapper = mountComponent({ provide: { isProject: false } });
 
expect(findCsvImportExportButtons().exists()).toBe(false);
});
Loading
Loading
@@ -625,72 +633,89 @@ describe('IssuesListApp component', () => {
...defaultQueryResponse.data.project.issues.nodes[0],
id: 'gid://gitlab/Issue/1',
iid: '101',
title: 'Issue one',
reference: 'group/project#1',
webPath: '/group/project/-/issues/1',
};
const issueTwo = {
...defaultQueryResponse.data.project.issues.nodes[0],
id: 'gid://gitlab/Issue/2',
iid: '102',
title: 'Issue two',
reference: 'group/project#2',
webPath: '/group/project/-/issues/2',
};
const issueThree = {
...defaultQueryResponse.data.project.issues.nodes[0],
id: 'gid://gitlab/Issue/3',
iid: '103',
title: 'Issue three',
reference: 'group/project#3',
webPath: '/group/project/-/issues/3',
};
const issueFour = {
...defaultQueryResponse.data.project.issues.nodes[0],
id: 'gid://gitlab/Issue/4',
iid: '104',
title: 'Issue four',
reference: 'group/project#4',
webPath: '/group/project/-/issues/4',
};
const response = {
const response = (isProject = true) => ({
data: {
project: {
[isProject ? 'project' : 'group']: {
issues: {
...defaultQueryResponse.data.project.issues,
nodes: [issueOne, issueTwo, issueThree, issueFour],
},
},
},
};
beforeEach(() => {
wrapper = mountComponent({ issuesQueryResponse: jest.fn().mockResolvedValue(response) });
jest.runOnlyPendingTimers();
});
 
describe('when successful', () => {
describe.each`
description | issueToMove | oldIndex | newIndex | moveBeforeId | moveAfterId
${'to the beginning of the list'} | ${issueThree} | ${2} | ${0} | ${null} | ${issueOne.id}
${'down the list'} | ${issueOne} | ${0} | ${1} | ${issueTwo.id} | ${issueThree.id}
${'up the list'} | ${issueThree} | ${2} | ${1} | ${issueOne.id} | ${issueTwo.id}
${'to the end of the list'} | ${issueTwo} | ${1} | ${3} | ${issueFour.id} | ${null}
`(
'when moving issue $description',
({ issueToMove, oldIndex, newIndex, moveBeforeId, moveAfterId }) => {
it('makes API call to reorder the issue', async () => {
findIssuableList().vm.$emit('reorder', { oldIndex, newIndex });
await waitForPromises();
expect(axiosMock.history.put[0]).toMatchObject({
url: joinPaths(defaultProvide.issuesPath, issueToMove.iid, 'reorder'),
data: JSON.stringify({
move_before_id: getIdFromGraphQLId(moveBeforeId),
move_after_id: getIdFromGraphQLId(moveAfterId),
}),
describe.each([true, false])('when isProject=%s', (isProject) => {
describe.each`
description | issueToMove | oldIndex | newIndex | moveBeforeId | moveAfterId
${'to the beginning of the list'} | ${issueThree} | ${2} | ${0} | ${null} | ${issueOne.id}
${'down the list'} | ${issueOne} | ${0} | ${1} | ${issueTwo.id} | ${issueThree.id}
${'up the list'} | ${issueThree} | ${2} | ${1} | ${issueOne.id} | ${issueTwo.id}
${'to the end of the list'} | ${issueTwo} | ${1} | ${3} | ${issueFour.id} | ${null}
`(
'when moving issue $description',
({ issueToMove, oldIndex, newIndex, moveBeforeId, moveAfterId }) => {
beforeEach(() => {
wrapper = mountComponent({
provide: { isProject },
issuesQueryResponse: jest.fn().mockResolvedValue(response(isProject)),
});
jest.runOnlyPendingTimers();
});
});
},
);
it('makes API call to reorder the issue', async () => {
findIssuableList().vm.$emit('reorder', { oldIndex, newIndex });
await waitForPromises();
expect(axiosMock.history.put[0]).toMatchObject({
url: joinPaths(issueToMove.webPath, 'reorder'),
data: JSON.stringify({
move_before_id: getIdFromGraphQLId(moveBeforeId),
move_after_id: getIdFromGraphQLId(moveAfterId),
group_full_path: isProject ? undefined : defaultProvide.fullPath,
}),
});
});
},
);
});
});
 
describe('when unsuccessful', () => {
beforeEach(() => {
wrapper = mountComponent({
issuesQueryResponse: jest.fn().mockResolvedValue(response()),
});
jest.runOnlyPendingTimers();
});
it('displays an error message', async () => {
axiosMock.onPut(joinPaths(defaultProvide.issuesPath, issueOne.iid, 'reorder')).reply(500);
axiosMock.onPut(joinPaths(issueOne.webPath, 'reorder')).reply(500);
 
findIssuableList().vm.$emit('reorder', { oldIndex: 0, newIndex: 1 });
 
Loading
Loading
Loading
Loading
@@ -29,6 +29,7 @@ export const getIssuesQueryResponse = {
updatedAt: '2021-05-22T04:08:01Z',
upvotes: 3,
userDiscussionsCount: 4,
webPath: 'project/-/issues/789',
webUrl: 'project/-/issues/789',
assignees: {
nodes: [
Loading
Loading
Loading
Loading
@@ -318,8 +318,8 @@
has_any_issues: project_issues(project).exists?.to_s,
import_csv_issues_path: '#',
initial_email: project.new_issuable_address(current_user, 'issue'),
is_project: 'true',
is_signed_in: current_user.present?.to_s,
issues_path: project_issues_path(project),
jira_integration_path: help_page_url('integration/jira/issues', anchor: 'view-jira-issues'),
markdown_help_path: help_page_path('user/markdown'),
max_attachment_size: number_to_human_size(Gitlab::CurrentSettings.max_attachment_size.megabytes),
Loading
Loading
@@ -332,11 +332,11 @@
sign_in_path: new_user_session_path
}
 
expect(helper.issues_list_data(project, current_user, finder)).to include(expected)
expect(helper.project_issues_list_data(project, current_user, finder)).to include(expected)
end
end
 
describe '#issues_list_data' do
describe '#project_issues_list_data' do
context 'when user is signed in' do
it_behaves_like 'issues list data' do
let(:current_user) { double.as_null_object }
Loading
Loading
@@ -350,6 +350,33 @@
end
end
 
describe '#group_issues_list_data' do
let(:group) { create(:group) }
let(:current_user) { double.as_null_object }
let(:issues) { [] }
it 'returns expected result' do
allow(helper).to receive(:current_user).and_return(current_user)
allow(helper).to receive(:can?).and_return(true)
allow(helper).to receive(:image_path).and_return('#')
allow(helper).to receive(:url_for).and_return('#')
expected = {
autocomplete_award_emojis_path: autocomplete_award_emojis_path,
calendar_path: '#',
empty_state_svg_path: '#',
full_path: group.full_path,
has_any_issues: issues.to_a.any?.to_s,
is_signed_in: current_user.present?.to_s,
jira_integration_path: help_page_url('integration/jira/issues', anchor: 'view-jira-issues'),
rss_path: '#',
sign_in_path: new_user_session_path
}
expect(helper.group_issues_list_data(group, current_user, issues)).to include(expected)
end
end
describe '#issue_manual_ordering_class' do
context 'when sorting by relative position' do
before do
Loading
Loading
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment