Commit fd87014e authored by Simon Knox's avatar Simon Knox

Merge branch '222763-use-project-iterations-query-for-project-iterations-report' into 'master'

Use top-level iterations query for project iterations report

See merge request gitlab-org/gitlab!38821
parents 1fd634ee aab9e61d
......@@ -13,7 +13,8 @@ import { __ } from '~/locale';
import IterationReportSummary from './iteration_report_summary.vue';
import IterationForm from './iteration_form.vue';
import IterationReportTabs from './iteration_report_tabs.vue';
import query from '../queries/group_iteration.query.graphql';
import query from '../queries/iteration.query.graphql';
import { Namespace } from '../constants';
const iterationStates = {
closed: 'closed',
......@@ -35,20 +36,19 @@ export default {
IterationReportTabs,
},
apollo: {
namespace: {
iteration: {
query,
variables() {
return {
groupPath: this.fullPath,
fullPath: this.fullPath,
id: `gid://gitlab/Iteration/${this.iterationId}`,
iid: this.iterationIid,
hasId: Boolean(this.iterationId),
hasIid: Boolean(this.iterationIid),
};
},
update(data) {
const iteration = data?.group?.iterations?.nodes[0] || {};
return {
iteration,
};
return data.group?.iterations?.nodes[0] || data.iteration || {};
},
error(err) {
this.error = err.message;
......@@ -60,15 +60,27 @@ export default {
type: String,
required: true,
},
iterationId: {
type: String,
required: false,
default: undefined,
},
iterationIid: {
type: String,
required: true,
required: false,
default: undefined,
},
canEdit: {
type: Boolean,
required: false,
default: false,
},
namespaceType: {
type: String,
required: false,
default: Namespace.Group,
validator: value => Object.values(Namespace).includes(value),
},
previewMarkdownPath: {
type: String,
required: false,
......@@ -79,17 +91,12 @@ export default {
return {
isEditing: false,
error: '',
namespace: {
iteration: {},
},
};
},
computed: {
iteration() {
return this.namespace.iteration;
},
hasIteration() {
return !this.$apollo.queries.namespace.loading && this.iteration?.title;
return !this.$apollo.queries.iteration.loading && this.iteration?.title;
},
status() {
switch (this.iteration.state) {
......@@ -120,7 +127,7 @@ export default {
<gl-alert v-if="error" variant="danger" @dismiss="error = ''">
{{ error }}
</gl-alert>
<gl-loading-icon v-if="$apollo.queries.namespace.loading" class="gl-py-5" size="lg" />
<gl-loading-icon v-if="$apollo.queries.iteration.loading" class="gl-py-5" size="lg" />
<gl-empty-state
v-else-if="!hasIteration"
:title="__('Could not find iteration')"
......@@ -164,8 +171,16 @@ export default {
</div>
<h3 ref="title" class="page-title">{{ iteration.title }}</h3>
<div ref="description" v-html="iteration.descriptionHtml"></div>
<iteration-report-summary :group-path="fullPath" :iteration-id="iteration.id" />
<iteration-report-tabs :group-path="fullPath" :iteration-id="iteration.id" />
<iteration-report-summary
:full-path="fullPath"
:iteration-id="iteration.id"
:namespace-type="namespaceType"
/>
<iteration-report-tabs
:full-path="fullPath"
:iteration-id="iteration.id"
:namespace-type="namespaceType"
/>
</template>
</div>
</template>
......@@ -3,6 +3,7 @@ import { GlCard, GlIcon } from '@gitlab/ui';
import { __ } from '~/locale';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import query from '../queries/iteration_issues_summary.query.graphql';
import { Namespace } from '../constants';
export default {
cardBodyClass: 'gl-text-center gl-py-3',
......@@ -19,9 +20,9 @@ export default {
},
update(data) {
return {
open: data?.group?.openIssues?.count || 0,
assigned: data?.group?.assignedIssues?.count || 0,
closed: data?.group?.closedIssues?.count || 0,
open: data[this.namespaceType]?.openIssues?.count || 0,
assigned: data[this.namespaceType]?.assignedIssues?.count || 0,
closed: data[this.namespaceType]?.closedIssues?.count || 0,
};
},
error() {
......@@ -30,7 +31,7 @@ export default {
},
},
props: {
groupPath: {
fullPath: {
type: String,
required: true,
},
......@@ -38,6 +39,12 @@ export default {
type: String,
required: true,
},
namespaceType: {
type: String,
required: false,
default: Namespace.Group,
validator: value => Object.values(Namespace).includes(value),
},
},
data() {
return {
......@@ -47,8 +54,9 @@ export default {
computed: {
queryVariables() {
return {
groupPath: this.groupPath,
fullPath: this.fullPath,
id: getIdFromGraphQLId(this.iterationId),
isGroup: this.namespaceType === Namespace.Group,
};
},
completedPercent() {
......
......@@ -14,6 +14,7 @@ import {
import { __, sprintf } from '~/locale';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import query from '../queries/iteration_issues.query.graphql';
import { Namespace } from '../constants';
const states = {
opened: 'opened',
......@@ -63,7 +64,7 @@ export default {
return this.queryVariables;
},
update(data) {
const { nodes: issues = [], count, pageInfo = {} } = data?.group?.issues || {};
const { nodes: issues = [], count, pageInfo = {} } = data[this.namespaceType]?.issues || {};
const list = issues.map(issue => ({
...issue,
......@@ -83,7 +84,7 @@ export default {
},
},
props: {
groupPath: {
fullPath: {
type: String,
required: true,
},
......@@ -91,6 +92,12 @@ export default {
type: String,
required: true,
},
namespaceType: {
type: String,
required: false,
default: Namespace.Group,
validator: value => Object.values(Namespace).includes(value),
},
},
data() {
return {
......@@ -110,8 +117,9 @@ export default {
computed: {
queryVariables() {
const vars = {
groupPath: this.groupPath,
fullPath: this.fullPath,
id: getIdFromGraphQLId(this.iterationId),
isGroup: this.namespaceType === Namespace.Group,
};
if (this.pagination.beforeCursor) {
......
......@@ -77,7 +77,6 @@ export default {
const vars = {
fullPath: this.fullPath,
isGroup: this.namespaceType === Namespace.Group,
isProject: this.namespaceType === Namespace.Project,
state: this.state,
};
......
......@@ -49,10 +49,16 @@ export function initIterationForm() {
});
}
export function initIterationReport() {
export function initIterationReport(namespaceType) {
const el = document.querySelector('.js-iteration');
const { fullPath, iterationIid, editIterationPath, previewMarkdownPath } = el.dataset;
const {
fullPath,
iterationId,
iterationIid,
editIterationPath,
previewMarkdownPath,
} = el.dataset;
const canEdit = parseBoolean(el.dataset.canEdit);
return new Vue({
......@@ -62,9 +68,11 @@ export function initIterationReport() {
return createElement(IterationReport, {
props: {
fullPath,
iterationId,
iterationIid,
canEdit,
editIterationPath,
namespaceType,
previewMarkdownPath,
},
});
......
query GroupIteration($groupPath: ID!, $iid: ID!) {
group(fullPath: $groupPath) {
iterations(iid: $iid, first: 1, includeAncestors: false) {
nodes {
title
state
iid
id
description
descriptionHtml
webPath
startDate
dueDate
}
}
}
}
#import "./iteration_report.fragment.graphql"
query Iteration(
$fullPath: ID!
$id: IterationID!
$iid: ID
$hasId: Boolean = false
$hasIid: Boolean = false
) {
iteration(id: $id) @include(if: $hasId) {
...IterationReport
}
group(fullPath: $fullPath) @include(if: $hasIid) {
iterations(iid: $iid, first: 1, includeAncestors: false) {
nodes {
...IterationReport
}
}
}
}
#import "~/graphql_shared/fragments/user.fragment.graphql"
#import "~/graphql_shared/fragments/pageInfo.fragment.graphql"
fragment IterationIssues on IssueConnection {
count
pageInfo {
...PageInfo
}
nodes {
iid
title
webUrl
state
assignees {
nodes {
...User
}
}
}
}
#import "~/graphql_shared/fragments/user.fragment.graphql"
#import "~/graphql_shared/fragments/pageInfo.fragment.graphql"
#import "./iteration_issues.fragment.graphql"
query GroupIteration(
$groupPath: ID!
query IterationIssues(
$fullPath: ID!
$id: ID!
$isGroup: Boolean = true
$beforeCursor: String = ""
$afterCursor: String = ""
$firstPageSize: Int
$lastPageSize: Int
) {
group(fullPath: $groupPath) {
group(fullPath: $fullPath) @include(if: $isGroup) {
issues(
iterationId: [$id]
before: $beforeCursor
......@@ -17,21 +17,18 @@ query GroupIteration(
first: $firstPageSize
last: $lastPageSize
) {
count
pageInfo {
...PageInfo
}
nodes {
iid
title
webUrl
state
assignees {
nodes {
...User
}
...IterationIssues
}
}
project(fullPath: $fullPath) @skip(if: $isGroup) {
issues(
iterationId: [$id]
before: $beforeCursor
after: $afterCursor
first: $firstPageSize
last: $lastPageSize
) {
...IterationIssues
}
}
}
query GroupIteration($groupPath: ID!, $id: ID!) {
group(fullPath: $groupPath) {
query IterationIssuesSummary($fullPath: ID!, $id: ID!, $isGroup: Boolean = true) {
group(fullPath: $fullPath) @include(if: $isGroup) {
openIssues: issues(iterationId: [$id], state: opened, assigneeId: "none") {
count
}
assignedIssues: issues(iterationId: [$id], state: opened, assigneeId: "any") {
count
}
closedIssues: issues(iterationId: [$id], state: closed) {
count
}
}
project(fullPath: $fullPath) @skip(if: $isGroup) {
openIssues: issues(iterationId: [$id], state: opened, assigneeId: "none") {
count
}
......
fragment IterationReport on Iteration {
description
descriptionHtml
dueDate
id
iid
startDate
state
title
webPath
}
#import "~/graphql_shared/fragments/pageInfo.fragment.graphql"
#import "./iteration.fragment.graphql"
#import "./iteration_list_item.fragment.graphql"
query Iterations(
$fullPath: ID!
$isGroup: Boolean = false
$isProject: Boolean = false
$isGroup: Boolean = true
$state: IterationState!
$beforeCursor: String = ""
$afterCursor: String = ""
......@@ -21,14 +20,14 @@ query Iterations(
last: $lastPageSize
) {
nodes {
...Iteration
...IterationListItem
}
pageInfo {
...PageInfo
}
}
}
project(fullPath: $fullPath) @include(if: $isProject) {
project(fullPath: $fullPath) @skip(if: $isGroup) {
iterations(
state: $state
before: $beforeCursor
......@@ -37,7 +36,7 @@ query Iterations(
last: $lastPageSize
) {
nodes {
...Iteration
...IterationListItem
}
pageInfo {
...PageInfo
......
import { initIterationReport } from 'ee/iterations';
import { Namespace } from 'ee/iterations/constants';
document.addEventListener('DOMContentLoaded', () => {
initIterationReport();
initIterationReport(Namespace.Group);
});
import { initIterationReport } from 'ee/iterations';
import { Namespace } from 'ee/iterations/constants';
document.addEventListener('DOMContentLoaded', () => {
initIterationReport(Namespace.Project);
});
import { initIterationReport } from 'ee/iterations';
document.addEventListener('DOMContentLoaded', initIterationReport);
......@@ -6,8 +6,6 @@ class Projects::IterationsController < Projects::ApplicationController
def index; end
def show; end
private
def check_iterations_available!
......
......@@ -3,7 +3,7 @@
- page_title _("Iteration")
- if Feature.enabled?(:project_iterations, @project.group)
.js-iteration{ data: { full_path: @project.group.full_path,
.js-iteration{ data: { full_path: @project.full_path,
can_edit: can?(current_user, :admin_iteration, @project).to_s,
iteration_id: params[:id],
preview_markdown_path: preview_markdown_path(@project) } }
- add_to_breadcrumbs _("Iterations"), project_iterations_path(@project)
- breadcrumb_title params[:id]
- page_title _("Iterations")
- if Feature.enabled?(:project_iterations, @project.group)
.js-iteration{ data: { full_path: @project.group.full_path,
can_edit: can?(current_user, :admin_iteration, @project).to_s,
iteration_iid: params[:id],
preview_markdown_path: preview_markdown_path(@project) } }
......@@ -108,7 +108,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
end
end
resources :iterations, only: [:index, :show], constraints: { id: /\d+/ }
resources :iterations, only: [:index]
namespace :iterations do
resources :inherited, only: [:show], constraints: { id: /\d+/ }
......
......@@ -6,11 +6,12 @@ RSpec.describe 'User views iteration' do
let_it_be(:now) { Time.now }
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, group: group) }
let_it_be(:user) { create(:group_member, :maintainer, user: create(:user), group: group ).user }
let_it_be(:project_2) { create(:project, group: group) }
let_it_be(:user) { create(:group_member, :maintainer, user: create(:user), group: group).user }
let_it_be(:iteration) { create(:iteration, :skip_future_date_validation, iid: 1, id: 2, group: group, title: 'Correct Iteration', start_date: now - 1.day, due_date: now) }
let_it_be(:other_iteration) { create(:iteration, :skip_future_date_validation, iid: 2, id: 1, group: group, title: 'Wrong Iteration', start_date: now - 4.days, due_date: now - 3.days) }
let_it_be(:issue) { create(:issue, project: project, iteration: iteration) }
let_it_be(:assigned_issue) { create(:issue, project: project, iteration: iteration, assignees: [user]) }
let_it_be(:assigned_issue) { create(:issue, project: project_2, iteration: iteration, assignees: [user]) }
let_it_be(:closed_issue) { create(:closed_issue, project: project, iteration: iteration) }
let_it_be(:other_issue) { create(:issue, project: project, iteration: other_iteration) }
......@@ -22,7 +23,7 @@ RSpec.describe 'User views iteration' do
context 'view an iteration', :js do
before do
visit group_iteration_path(iteration.group, iteration)
visit group_iteration_path(iteration.group, iteration.iid)
end
it 'shows iteration info and dates' do
......@@ -32,7 +33,14 @@ RSpec.describe 'User views iteration' do
expect(page).to have_content(iteration.due_date.strftime('%b %-d, %Y'))
end
it 'shows correct issues for issue' do
it 'shows correct summary information' do
expect(page).to have_content("Complete 33%")
expect(page).to have_content("Open 1")
expect(page).to have_content("In progress 1")
expect(page).to have_content("Completed 1")
end
it 'shows all issues within the group' do
expect(page).to have_content(issue.title)
expect(page).to have_content(assigned_issue.title)
expect(page).to have_content(closed_issue.title)
......@@ -48,7 +56,7 @@ RSpec.describe 'User views iteration' do
end
it 'shows page not found' do
visit group_iteration_path(iteration.group, iteration)
visit group_iteration_path(iteration.group, iteration.iid)
expect(page).to have_title('Not Found')
expect(page).to have_content('Page Not Found')
......
......@@ -6,11 +6,12 @@ RSpec.describe 'User views iteration' do
let_it_be(:now) { Time.now }
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, group: group) }
let_it_be(:project_2) { create(:project, group: group) }
let_it_be(:user) { create(:group_member, :maintainer, user: create(:user), group: group).user }
let_it_be(:iteration) { create(:iteration, :skip_future_date_validation, iid: 1, id: 2, group: group, title: 'Correct Iteration', start_date: now - 1.day, due_date: now) }
let_it_be(:other_iteration) { create(:iteration, :skip_future_date_validation, iid: 2, id: 1, group: group, title: 'Wrong Iteration', start_date: now - 4.days, due_date: now - 3.days) }
let_it_be(:issue) { create(:issue, project: project, iteration: iteration) }
let_it_be(:assigned_issue) { create(:issue, project: project, iteration: iteration, assignees: [user]) }
let_it_be(:assigned_issue) { create(:issue, project: project_2, iteration: iteration, assignees: [user]) }
let_it_be(:closed_issue) { create(:closed_issue, project: project, iteration: iteration) }
let_it_be(:other_issue) { create(:issue, project: project, iteration: other_iteration) }
......@@ -22,7 +23,7 @@ RSpec.describe 'User views iteration' do
context 'view an iteration', :js do
before do
visit project_iteration_path(project, iteration)
visit project_iterations_inherited_path(project, iteration.id)
end
it 'shows iteration info and dates' do
......@@ -32,9 +33,16 @@ RSpec.describe 'User views iteration' do
expect(page).to have_content(iteration.due_date.strftime('%b %-d, %Y'))
end
it 'shows correct issues for issue' do
it 'shows correct summary information' do
expect(page).to have_content("Complete 50%")
expect(page).to have_content("Open 1")
expect(page).to have_content("In progress 0")
expect(page).to have_content("Completed 1")
end
it 'shows only issues that are part of the project' do
expect(page).to have_content(issue.title)
expect(page).to have_content(assigned_issue.title)
expect(page).not_to have_content(assigned_issue.title)
expect(page).to have_content(closed_issue.title)
expect(page).not_to have_content(other_issue.title)
end
......@@ -48,7 +56,7 @@ RSpec.describe 'User views iteration' do
end
it 'shows page not found' do
visit project_iteration_path(project, iteration)
visit project_iterations_inherited_path(project, iteration.id)
expect(page).to have_title('Not Found')
expect(page).to have_content('Page Not Found')
......
import IterationReport from 'ee/iterations/components/iteration_report.vue';
import { shallowMount } from '@vue/test-utils';
import { GlEmptyState, GlLoadingIcon, GlTab, GlTabs } from '@gitlab/ui';
import IterationReport from 'ee/iterations/components/iteration_report.vue';
import IterationReportSummary from 'ee/iterations/components/iteration_report_summary.vue';
import IterationReportTabs from 'ee/iterations/components/iteration_report_tabs.vue';
import { Namespace } from 'ee/iterations/constants';
describe('Iterations tabs', () => {
describe('Iterations report', () => {
let wrapper;
const defaultProps = {
fullPath: 'gitlab-org',
......@@ -18,7 +21,7 @@ describe('Iterations tabs', () => {
propsData: props,
mocks: {
$apollo: {
queries: { namespace: { loading } },
queries: { iteration: { loading } },
},
},
stubs: {
......@@ -70,9 +73,7 @@ describe('Iterations tabs', () => {
});
wrapper.setData({
namespace: {
iteration,
},
});
});
......@@ -91,5 +92,21 @@ describe('Iterations tabs', () => {
expect(findTitle().text()).toContain(iteration.title);
expect(findDescription().text()).toContain(iteration.descriptionHtml);
});
it('passes correct props to IterationReportSummary', () => {
const iterationReportSummary = wrapper.find(IterationReportSummary);
expect(iterationReportSummary.props('fullPath')).toBe(defaultProps.fullPath);
expect(iterationReportSummary.props('iterationId')).toBe(iteration.id);
expect(iterationReportSummary.props('namespaceType')).toBe(Namespace.Group);
});
it('passes correct props to IterationReportTabs', () => {
const iterationReportTabs = wrapper.find(IterationReportTabs);
expect(iterationReportTabs.props('fullPath')).toBe(defaultProps.fullPath);
expect(iterationReportTabs.props('iterationId')).toBe(iteration.id);
expect(iterationReportTabs.props('namespaceType')).toBe(Namespace.Group);
});
});
});
import IterationReportSummary from 'ee/iterations/components/iteration_report_summary.vue';
import { mount } from '@vue/test-utils';
import { GlCard } from '@gitlab/ui';
import { Namespace } from 'ee/iterations/constants';
describe('Iterations report tabs', () => {
describe('Iterations report summary', () => {
let wrapper;
const id = 3;
const groupPath = 'gitlab-org';
const fullPath = 'gitlab-org';
const defaultProps = {
groupPath,
fullPath,
iterationId: `gid://gitlab/Iteration/${id}`,
};
......@@ -88,4 +89,43 @@ describe('Iterations report tabs', () => {
expect(findCompletedCard().text()).toContain('0');
});
});
describe('IterationIssuesSummary query variables', () => {
const expected = {
fullPath: defaultProps.fullPath,
id,
};
describe('when group', () => {
it('has expected query variable values', () => {
mountComponent({
props: {
...defaultProps,
namespaceType: Namespace.Group,
},
});
expect(wrapper.vm.queryVariables).toEqual({
...expected,
isGroup: true,
});
});
});
describe('when project', () => {
it('has expected query variable values', () => {
mountComponent({
props: {
...defaultProps,
namespaceType: Namespace.Project,
},
});
expect(wrapper.vm.queryVariables).toEqual({
...expected,
isGroup: false,
});
});
});
});
});
import IterationReportTabs from 'ee/iterations/components/iteration_report_tabs.vue';
import { mount } from '@vue/test-utils';
import { GlAlert, GlAvatar, GlLoadingIcon, GlPagination, GlTable, GlTab } from '@gitlab/ui';
import { Namespace } from 'ee/iterations/constants';
describe('Iterations report tabs', () => {
let wrapper;
const id = 3;
const groupPath = 'gitlab-org';
const fullPath = 'gitlab-org';
const defaultProps = {
groupPath,
fullPath,
iterationId: `gid://gitlab/Iteration/${id}`,
};
......@@ -145,9 +146,10 @@ describe('Iterations report tabs', () => {
return setPage(1).then(() => {
expect(wrapper.vm.queryVariables).toEqual({
beforeCursor: 'first-item',
groupPath,
fullPath,
id,
lastPageSize: 20,
isGroup: true,
});
});
});
......@@ -156,12 +158,54 @@ describe('Iterations report tabs', () => {
return setPage(2).then(() => {
expect(wrapper.vm.queryVariables).toEqual({
afterCursor: 'last-item',
groupPath,
fullPath,
id,
firstPageSize: 20,
isGroup: true,
});
});
});
});
});
describe('IterationReportTabs query variables', () => {
const expected = {
afterCursor: undefined,
firstPageSize: 20,
fullPath: defaultProps.fullPath,
id,
};
describe('when group', () => {
it('has expected query variable values', () => {
mountComponent({
props: {
...defaultProps,
namespaceType: Namespace.Group,
},
});
expect(wrapper.vm.queryVariables).toEqual({
...expected,
isGroup: true,
});
});
});
describe('when project', () => {
it('has expected query variable values', () => {
mountComponent({
props: {
...defaultProps,
namespaceType: Namespace.Project,
},
});
expect(wrapper.vm.queryVariables).toEqual({
...expected,
isGroup: false,
});
});
});
});
});
......@@ -4,7 +4,7 @@ import { shallowMount } from '@vue/test-utils';
import { GlAlert, GlLoadingIcon, GlPagination, GlTab, GlTabs } from '@gitlab/ui';
import { Namespace } from 'ee/iterations/constants';
describe('Iterations tabs', () => {
describe('Iterations', () => {
let wrapper;
const defaultProps = {
fullPath: 'gitlab-org',
......@@ -103,7 +103,6 @@ describe('Iterations tabs', () => {
expect(wrapper.vm.queryVariables).toEqual({
beforeCursor: 'first-item',
isGroup: true,
isProject: false,
lastPageSize: 20,
fullPath: defaultProps.fullPath,
state: 'opened',
......@@ -118,7 +117,6 @@ describe('Iterations tabs', () => {
firstPageSize: 20,
fullPath: defaultProps.fullPath,
isGroup: true,
isProject: false,
state: 'opened',
});
});
......@@ -161,7 +159,6 @@ describe('Iterations tabs', () => {
expect(wrapper.vm.queryVariables).toEqual({
...expected,
isGroup: true,
isProject: false,
});
});
});
......@@ -178,7 +175,6 @@ describe('Iterations tabs', () => {
expect(wrapper.vm.queryVariables).toEqual({
...expected,
isGroup: false,
isProject: true,
});
});
});
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment