Commit 52eff031 authored by Peter Leitzen's avatar Peter Leitzen

Merge branch 'psi-iteration-report-init' into 'master'

Add basic iteration report view

See merge request gitlab-org/gitlab!34359
parents ec56e779 05090671
......@@ -4819,6 +4819,11 @@ type Group {
"""
first: Int
"""
The ID of the Iteration to look up
"""
id: ID
"""
Returns the last _n_ elements from the list.
"""
......
......@@ -13275,6 +13275,16 @@
},
"defaultValue": null
},
{
"name": "id",
"description": "The ID of the Iteration to look up",
"type": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
},
"defaultValue": null
},
{
"name": "after",
"description": "Returns the elements in the list that come after the specified cursor.",
<script>
import { GlAlert, GlBadge, GlLoadingIcon, GlEmptyState } from '@gitlab/ui';
import { formatDate } from '~/lib/utils/datetime_utility';
import { __ } from '~/locale';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import query from '../queries/group_iteration.query.graphql';
const iterationStates = {
closed: 'closed',
upcoming: 'upcoming',
expired: 'expired',
};
export default {
components: {
GlAlert,
GlBadge,
GlLoadingIcon,
GlEmptyState,
},
apollo: {
group: {
query,
variables() {
return {
groupPath: this.groupPath,
id: getIdFromGraphQLId(this.iterationId),
};
},
update(data) {
const iteration = data?.group?.iterations?.nodes[0] || {};
return {
iteration,
};
},
error(err) {
this.error = err.message;
},
},
},
props: {
groupPath: {
type: String,
required: true,
},
iterationId: {
type: String,
required: true,
},
canEdit: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
error: '',
group: {
iteration: {},
},
};
},
computed: {
iteration() {
return this.group.iteration;
},
hasIteration() {
return !this.$apollo.queries.group.loading && this.iteration?.title;
},
status() {
switch (this.iteration.state) {
case iterationStates.closed:
return {
text: __('Closed'),
variant: 'danger',
};
case iterationStates.expired:
return { text: __('Past due'), variant: 'warning' };
case iterationStates.upcoming:
return { text: __('Upcoming'), variant: 'neutral' };
default:
return { text: __('Open'), variant: 'success' };
}
},
},
methods: {
formatDate(date) {
return formatDate(date, 'mmm d, yyyy');
},
},
};
</script>
<template>
<div>
<gl-alert v-if="error" variant="danger" @dismiss="error = ''">
{{ error }}
</gl-alert>
<gl-loading-icon v-if="$apollo.queries.group.loading" class="gl-py-5" size="lg" />
<gl-empty-state
v-else-if="!hasIteration"
:title="__('Could not find iteration')"
:compact="false"
/>
<template v-else>
<div
ref="topbar"
class="gl-display-flex gl-justify-items-center gl-align-items-center gl-py-3 gl-border-1 gl-border-b-solid gl-border-gray-200"
>
<gl-badge :variant="status.variant">
{{ status.text }}
</gl-badge>
<span class="gl-ml-4"
>{{ formatDate(iteration.startDate) }}{{ formatDate(iteration.dueDate) }}</span
>
</div>
<h3 ref="title" class="page-title">{{ iteration.title }}</h3>
<div ref="description" v-html="iteration.description"></div>
</template>
</div>
</template>
<script>
import { GlLink } from '@gitlab/ui';
import dateFormat from 'dateformat';
import { formatDate } from '~/lib/utils/datetime_utility';
export default {
components: {
GlLink,
},
filters: {
date: value => {
const date = new Date(value);
return dateFormat(date, 'mmm d, yyyy', true);
},
},
props: {
iterations: {
type: Array,
......@@ -19,6 +13,11 @@ export default {
default: () => [],
},
},
methods: {
formatDate(date) {
return formatDate(date, 'mmm d, yyyy');
},
},
};
</script>
......@@ -32,7 +31,7 @@ export default {
>
</div>
<div class="text-secondary gl-mb-3">
{{ iteration.startDate | date }}{{ iteration.dueDate | date }}
{{ formatDate(iteration.startDate) }}{{ formatDate(iteration.dueDate) }}
</div>
</li>
</ul>
......
......@@ -4,6 +4,7 @@ import createDefaultClient from '~/lib/graphql';
import { parseBoolean } from '~/lib/utils/common_utils';
import Iterations from './components/iterations.vue';
import IterationForm from './components/iteration_form.vue';
import IterationReport from './components/iteration_report.vue';
Vue.use(VueApollo);
......@@ -47,4 +48,26 @@ export function initIterationForm() {
});
}
export function initIterationReport() {
const el = document.querySelector('.js-iteration');
const { groupPath, iterationId, editIterationPath } = el.dataset;
const canEdit = parseBoolean(el.dataset.canEdit);
return new Vue({
el,
apolloProvider,
render(createElement) {
return createElement(IterationReport, {
props: {
groupPath,
iterationId,
canEdit,
editIterationPath,
},
});
},
});
}
export default {};
query GroupIteration($groupPath: ID!, $id: ID!) {
group(fullPath: $groupPath) {
iterations(id: $id, first: 1) {
nodes {
title
state
id
description
webPath
startDate
dueDate
}
}
}
}
import { initIterationReport } from 'ee/iterations';
document.addEventListener('DOMContentLoaded', () => {
initIterationReport();
});
......@@ -21,6 +21,7 @@ class IterationsFinder
def execute
items = Iteration.all
items = by_id(items)
items = by_groups_and_projects(items)
items = by_title(items)
items = by_search_title(items)
......@@ -36,6 +37,14 @@ class IterationsFinder
items.for_projects_and_groups(params[:project_ids], params[:group_ids])
end
def by_id(items)
if params[:id]
items.id_in(params[:id])
else
items
end
end
def by_title(items)
if params[:title]
items.with_title(params[:title])
......
......@@ -11,6 +11,9 @@ module Resolvers
argument :title, GraphQL::STRING_TYPE,
required: false,
description: 'Fuzzy search by title'
argument :id, GraphQL::ID_TYPE,
required: false,
description: 'The ID of the Iteration to look up'
type Types::IterationType, null: true
......@@ -28,6 +31,7 @@ module Resolvers
def iterations_finder_params(args)
{
id: args[:id],
state: args[:state] || 'all',
start_date: args[:start_date],
end_date: args[:end_date],
......
......@@ -2,5 +2,5 @@
- breadcrumb_title params[:id]
- page_title _("Iterations")
.js-iteration{ data: { group_full_path: @group.full_path, iteration_id: params[:iteration_id] } }
- if Feature.enabled?(:group_iterations, @group)
.js-iteration{ data: { group_path: @group.full_path, iteration_id: params[:id], can_edit: can?(current_user, :admin_iteration, @group).to_s, preview_markdown_path: preview_markdown_path(@group) } }
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'User views iteration' do
let_it_be(:now) { Time.now }
let_it_be(:group) { create(: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, group: group, start_date: now - 1.day, due_date: now) }
around do |example|
Timecop.freeze { example.run }
end
before do
sign_in(user)
end
context 'view an iteration', :js do
before do
visit group_iteration_path(iteration.group, iteration)
end
it 'shows iteration info and dates' do
expect(page).to have_content(iteration.title)
expect(page).to have_content(iteration.description)
end
end
end
......@@ -91,6 +91,12 @@ RSpec.describe IterationsFinder do
expect(subject.to_a).to contain_exactly(started_group_iteration)
end
it 'filters by ID' do
params[:id] = iteration_from_project_1.id
expect(subject).to contain_exactly(iteration_from_project_1)
end
context 'by timeframe' do
it 'returns iterations with start_date and due_date between timeframe' do
params.merge!(start_date: now - 1.day, end_date: 3.days.from_now)
......
import IterationReport from 'ee/iterations/components/iteration_report.vue';
import { shallowMount } from '@vue/test-utils';
import { GlEmptyState, GlLoadingIcon, GlTab, GlTabs } from '@gitlab/ui';
describe('Iterations tabs', () => {
let wrapper;
const defaultProps = {
groupPath: 'gitlab-org',
iterationId: '3',
};
const findTopbar = () => wrapper.find({ ref: 'topbar' });
const findTitle = () => wrapper.find({ ref: 'title' });
const findDescription = () => wrapper.find({ ref: 'description' });
const mountComponent = ({ props = defaultProps, loading = false } = {}) => {
wrapper = shallowMount(IterationReport, {
propsData: props,
mocks: {
$apollo: {
queries: { group: { loading } },
},
},
stubs: {
GlLoadingIcon,
GlTab,
GlTabs,
},
});
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
it('shows spinner while loading', () => {
mountComponent({
loading: true,
});
expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
});
describe('empty state', () => {
it('shows empty state if no item loaded', () => {
mountComponent({
loading: false,
});
expect(wrapper.find(GlEmptyState).exists()).toBe(true);
expect(wrapper.find(GlEmptyState).props('title')).toEqual('Could not find iteration');
expect(findTitle().exists()).toBe(false);
expect(findDescription().exists()).toBe(false);
});
});
describe('item loaded', () => {
const iteration = {
title: 'June week 1',
description: 'The first week of June',
startDate: '2020-06-02',
dueDate: '2020-06-08',
};
beforeEach(() => {
mountComponent({
loading: false,
});
wrapper.setData({
group: {
iteration,
},
});
});
it('shows status and date in header', () => {
expect(findTopbar().text()).toContain('Open');
expect(findTopbar().text()).toContain('Jun 2, 2020');
expect(findTopbar().text()).toContain('Jun 8, 2020');
});
it('hides empty region and loading spinner', () => {
expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
expect(wrapper.find(GlEmptyState).exists()).toBe(false);
});
it('shows title and description', () => {
expect(findTitle().text()).toContain(iteration.title);
expect(findDescription().text()).toContain(iteration.description);
});
});
});
......@@ -31,7 +31,7 @@ RSpec.describe Resolvers::IterationsResolver do
context 'without parameters' do
it 'calls IterationsFinder to retrieve all iterations' do
expect(IterationsFinder).to receive(:new)
.with(group_ids: group.id, state: 'all', start_date: nil, end_date: nil, search_title: nil)
.with(group_ids: group.id, state: 'all', start_date: nil, end_date: nil, search_title: nil, id: nil)
.and_call_original
resolve_group_iterations
......@@ -43,12 +43,13 @@ RSpec.describe Resolvers::IterationsResolver do
start_date = now
end_date = start_date + 1.hour
search = 'wow'
id = 1
expect(IterationsFinder).to receive(:new)
.with(group_ids: group.id, state: 'closed', start_date: start_date, end_date: end_date, search_title: search)
.with(group_ids: group.id, state: 'closed', start_date: start_date, end_date: end_date, search_title: search, id: id)
.and_call_original
resolve_group_iterations(start_date: start_date, end_date: end_date, state: 'closed', title: search)
resolve_group_iterations(start_date: start_date, end_date: end_date, state: 'closed', title: search, id: id)
end
end
......
......@@ -6465,6 +6465,9 @@ msgstr ""
msgid "Could not find design."
msgstr ""
msgid "Could not find iteration"
msgstr ""
msgid "Could not remove the trigger."
msgstr ""
......
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