Commit e94200ed authored by Simon Knox's avatar Simon Knox

Add project iteration cadences

parent 5a0d8403
...@@ -83,7 +83,7 @@ export default { ...@@ -83,7 +83,7 @@ export default {
GlFormSelect, GlFormSelect,
GlFormTextarea, GlFormTextarea,
}, },
inject: ['groupPath', 'cadencesListPath'], inject: ['fullPath', 'cadencesListPath'],
data() { data() {
return { return {
group: { group: {
...@@ -133,7 +133,7 @@ export default { ...@@ -133,7 +133,7 @@ export default {
const id = this.isEdit const id = this.isEdit
? convertToGraphQLId(TYPE_ITERATIONS_CADENCE, this.cadenceId) ? convertToGraphQLId(TYPE_ITERATIONS_CADENCE, this.cadenceId)
: undefined; : undefined;
const groupPath = this.isEdit ? undefined : this.groupPath; const groupPath = this.isEdit ? undefined : this.fullPath;
const vars = { const vars = {
input: { input: {
...@@ -160,7 +160,7 @@ export default { ...@@ -160,7 +160,7 @@ export default {
query: readCadence, query: readCadence,
variables() { variables() {
return { return {
fullPath: this.groupPath, fullPath: this.fullPath,
id: this.cadenceId, id: this.cadenceId,
}; };
}, },
......
...@@ -12,7 +12,9 @@ import { ...@@ -12,7 +12,9 @@ import {
} from '@gitlab/ui'; } from '@gitlab/ui';
import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { __, s__ } from '~/locale'; import { __, s__ } from '~/locale';
import query from '../queries/iterations_in_cadence.query.graphql'; import { Namespace } from '../constants';
import groupQuery from '../queries/group_iterations_in_cadence.query.graphql';
import projectQuery from '../queries/project_iterations_in_cadence.query.graphql';
const pageSize = 20; const pageSize = 20;
...@@ -43,11 +45,13 @@ export default { ...@@ -43,11 +45,13 @@ export default {
GlSkeletonLoader, GlSkeletonLoader,
}, },
apollo: { apollo: {
group: { workspace: {
skip() { skip() {
return !this.expanded; return !this.expanded;
}, },
query, query() {
return this.query;
},
variables() { variables() {
return this.queryVariables; return this.queryVariables;
}, },
...@@ -56,7 +60,7 @@ export default { ...@@ -56,7 +60,7 @@ export default {
}, },
}, },
}, },
inject: ['groupPath', 'canEditCadence'], inject: ['fullPath', 'canEditCadence', 'namespaceType'],
props: { props: {
title: { title: {
type: String, type: String,
...@@ -86,7 +90,7 @@ export default { ...@@ -86,7 +90,7 @@ export default {
i18n, i18n,
expanded: false, expanded: false,
// query response // query response
group: { workspace: {
iterations: { iterations: {
nodes: [], nodes: [],
pageInfo: { pageInfo: {
...@@ -100,25 +104,34 @@ export default { ...@@ -100,25 +104,34 @@ export default {
}; };
}, },
computed: { computed: {
query() {
if (this.namespaceType === Namespace.Group) {
return groupQuery;
}
if (this.namespaceType === Namespace.Project) {
return projectQuery;
}
throw new Error('Must provide a namespaceType');
},
queryVariables() { queryVariables() {
return { return {
fullPath: this.groupPath, fullPath: this.fullPath,
iterationCadenceId: this.cadenceId, iterationCadenceId: this.cadenceId,
firstPageSize: pageSize, firstPageSize: pageSize,
state: this.iterationState, state: this.iterationState,
}; };
}, },
pageInfo() { pageInfo() {
return this.group.iterations?.pageInfo || {}; return this.workspace.iterations?.pageInfo || {};
}, },
hasNextPage() { hasNextPage() {
return this.pageInfo.hasNextPage; return this.pageInfo.hasNextPage;
}, },
iterations() { iterations() {
return this.group?.iterations?.nodes || []; return this.workspace?.iterations?.nodes || [];
}, },
loading() { loading() {
return this.$apollo.queries.group.loading; return this.$apollo.queries.workspace.loading;
}, },
editCadence() { editCadence() {
return { return {
...@@ -144,24 +157,24 @@ export default { ...@@ -144,24 +157,24 @@ export default {
} }
// Fetch more data and transform the original result // Fetch more data and transform the original result
this.$apollo.queries.group.fetchMore({ this.$apollo.queries.workspace.fetchMore({
variables: { variables: {
...this.queryVariables, ...this.queryVariables,
afterCursor: this.pageInfo.endCursor, afterCursor: this.pageInfo.endCursor,
}, },
// Transform the previous result with new data // Transform the previous result with new data
updateQuery: (previousResult, { fetchMoreResult }) => { updateQuery: (previousResult, { fetchMoreResult }) => {
const newIterations = fetchMoreResult.group?.iterations.nodes || []; const newIterations = fetchMoreResult.workspace?.iterations.nodes || [];
return { return {
group: { workspace: {
// eslint-disable-next-line @gitlab/require-i18n-strings id: fetchMoreResult.workspace.id,
__typename: 'Group', __typename: this.namespaceType,
iterations: { iterations: {
__typename: 'IterationConnection', __typename: 'IterationConnection',
// Merging the list // Merging the list
nodes: [...previousResult.group.iterations.nodes, ...newIterations], nodes: [...previousResult.workspace.iterations.nodes, ...newIterations],
pageInfo: fetchMoreResult.group?.iterations.pageInfo || {}, pageInfo: fetchMoreResult.workspace?.iterations.pageInfo || {},
}, },
}, },
}; };
......
...@@ -2,8 +2,10 @@ ...@@ -2,8 +2,10 @@
import { GlAlert, GlButton, GlLoadingIcon, GlKeysetPagination, GlTab, GlTabs } from '@gitlab/ui'; import { GlAlert, GlButton, GlLoadingIcon, GlKeysetPagination, GlTab, GlTabs } from '@gitlab/ui';
import produce from 'immer'; import produce from 'immer';
import { __, s__ } from '~/locale'; import { __, s__ } from '~/locale';
import { Namespace } from '../constants';
import destroyIterationCadence from '../queries/destroy_cadence.mutation.graphql'; import destroyIterationCadence from '../queries/destroy_cadence.mutation.graphql';
import query from '../queries/iteration_cadences_list.query.graphql'; import groupQuery from '../queries/group_iteration_cadences_list.query.graphql';
import projectQuery from '../queries/project_iteration_cadences_list.query.graphql';
import IterationCadenceListItem from './iteration_cadence_list_item.vue'; import IterationCadenceListItem from './iteration_cadence_list_item.vue';
const pageSize = 20; const pageSize = 20;
...@@ -20,8 +22,10 @@ export default { ...@@ -20,8 +22,10 @@ export default {
GlTabs, GlTabs,
}, },
apollo: { apollo: {
group: { workspace: {
query, query() {
return this.query;
},
variables() { variables() {
return this.queryVariables; return this.queryVariables;
}, },
...@@ -30,10 +34,10 @@ export default { ...@@ -30,10 +34,10 @@ export default {
}, },
}, },
}, },
inject: ['groupPath', 'cadencesListPath', 'canCreateCadence'], inject: ['fullPath', 'cadencesListPath', 'canCreateCadence', 'namespaceType'],
data() { data() {
return { return {
group: { workspace: {
iterationCadences: { iterationCadences: {
nodes: [], nodes: [],
pageInfo: { pageInfo: {
...@@ -48,9 +52,18 @@ export default { ...@@ -48,9 +52,18 @@ export default {
}; };
}, },
computed: { computed: {
query() {
if (this.namespaceType === Namespace.Group) {
return groupQuery;
}
if (this.namespaceType === Namespace.Project) {
return projectQuery;
}
throw new Error('Must provide a namespaceType');
},
queryVariables() { queryVariables() {
const vars = { const vars = {
fullPath: this.groupPath, fullPath: this.fullPath,
}; };
if (this.pagination.beforeCursor) { if (this.pagination.beforeCursor) {
...@@ -64,13 +77,13 @@ export default { ...@@ -64,13 +77,13 @@ export default {
return vars; return vars;
}, },
cadences() { cadences() {
return this.group?.iterationCadences?.nodes || []; return this.workspace?.iterationCadences?.nodes || [];
}, },
pageInfo() { pageInfo() {
return this.group?.iterationCadences?.pageInfo || {}; return this.workspace?.iterationCadences?.pageInfo || {};
}, },
loading() { loading() {
return this.$apollo.queries.group.loading; return this.$apollo.queries.workspace.loading;
}, },
state() { state() {
switch (this.tabIndex) { switch (this.tabIndex) {
...@@ -111,18 +124,18 @@ export default { ...@@ -111,18 +124,18 @@ export default {
} }
const sourceData = store.readQuery({ const sourceData = store.readQuery({
query, query: this.query,
variables: this.queryVariables, variables: this.queryVariables,
}); });
const data = produce(sourceData, (draftData) => { const data = produce(sourceData, (draftData) => {
draftData.group.iterationCadences.nodes = draftData.group.iterationCadences.nodes.filter( draftData.workspace.iterationCadences.nodes = draftData.workspace.iterationCadences.nodes.filter(
({ id }) => id !== cadenceId, ({ id }) => id !== cadenceId,
); );
}); });
store.writeQuery({ store.writeQuery({
query, query: this.query,
variables: this.queryVariables, variables: this.queryVariables,
data, data,
}); });
......
...@@ -130,7 +130,7 @@ export function initCadenceApp({ namespaceType }) { ...@@ -130,7 +130,7 @@ export function initCadenceApp({ namespaceType }) {
} }
const { const {
groupFullPath: groupPath, fullPath,
cadencesListPath, cadencesListPath,
canCreateCadence, canCreateCadence,
canEditCadence, canEditCadence,
...@@ -158,8 +158,7 @@ export function initCadenceApp({ namespaceType }) { ...@@ -158,8 +158,7 @@ export function initCadenceApp({ namespaceType }) {
router, router,
apolloProvider, apolloProvider,
provide: { provide: {
fullPath: groupPath, fullPath,
groupPath,
cadencesListPath, cadencesListPath,
canCreateCadence: parseBoolean(canCreateCadence), canCreateCadence: parseBoolean(canCreateCadence),
canEditCadence: parseBoolean(canEditCadence), canEditCadence: parseBoolean(canEditCadence),
...@@ -178,3 +177,4 @@ export function initCadenceApp({ namespaceType }) { ...@@ -178,3 +177,4 @@ export function initCadenceApp({ namespaceType }) {
} }
export const initGroupCadenceApp = () => initCadenceApp({ namespaceType: Namespace.Group }); export const initGroupCadenceApp = () => initCadenceApp({ namespaceType: Namespace.Group });
export const initProjectCadenceApp = () => initCadenceApp({ namespaceType: Namespace.Project });
...@@ -7,7 +7,8 @@ query IterationCadences( ...@@ -7,7 +7,8 @@ query IterationCadences(
$firstPageSize: Int $firstPageSize: Int
$lastPageSize: Int $lastPageSize: Int
) { ) {
group(fullPath: $fullPath) { workspace: group(fullPath: $fullPath) {
id
iterationCadences( iterationCadences(
includeAncestorGroups: true includeAncestorGroups: true
before: $beforeCursor before: $beforeCursor
......
...@@ -10,7 +10,8 @@ query Iterations( ...@@ -10,7 +10,8 @@ query Iterations(
$firstPageSize: Int $firstPageSize: Int
$lastPageSize: Int $lastPageSize: Int
) { ) {
group(fullPath: $fullPath) { workspace: group(fullPath: $fullPath) {
id
iterations( iterations(
iterationCadenceIds: [$iterationCadenceId] iterationCadenceIds: [$iterationCadenceId]
state: $state state: $state
......
#import "~/graphql_shared/fragments/pageInfo.fragment.graphql"
query IterationCadences(
$fullPath: ID!
$beforeCursor: String = ""
$afterCursor: String = ""
$firstPageSize: Int
$lastPageSize: Int
) {
workspace: project(fullPath: $fullPath) {
id
iterationCadences(
includeAncestorGroups: true
before: $beforeCursor
after: $afterCursor
first: $firstPageSize
last: $lastPageSize
) {
nodes {
id
title
durationInWeeks
}
pageInfo {
...PageInfo
}
}
}
}
#import "~/graphql_shared/fragments/pageInfo.fragment.graphql"
#import "./iteration_list_item.fragment.graphql"
query Iterations(
$fullPath: ID!
$iterationCadenceId: ID!
$state: IterationState!
$beforeCursor: String
$afterCursor: String
$firstPageSize: Int
$lastPageSize: Int
) {
workspace: project(fullPath: $fullPath) {
id
iterations(
iterationCadenceIds: [$iterationCadenceId]
state: $state
before: $beforeCursor
after: $afterCursor
first: $firstPageSize
last: $lastPageSize
) {
nodes {
...IterationListItem
}
pageInfo {
...PageInfo
}
}
}
}
import { initProjectCadenceApp } from 'ee/iterations';
initProjectCadenceApp();
# frozen_string_literal: true
module IterationCadencesActions
extend ActiveSupport::Concern
included do
before_action :check_cadences_available!
before_action :authorize_show_cadence!, only: [:index]
feature_category :issue_tracking
end
def index; end
private
def check_cadences_available!
render_404 unless group&.iteration_cadences_feature_flag_enabled?
end
def authorize_show_cadence!
render_404 unless can?(current_user, :read_iteration_cadence, group)
end
end
# frozen_string_literal: true # frozen_string_literal: true
class Groups::IterationCadencesController < Groups::ApplicationController class Groups::IterationCadencesController < Groups::ApplicationController
before_action :check_cadences_available! include IterationCadencesActions
before_action :authorize_show_cadence!, only: [:index]
feature_category :issue_tracking
def index; end
private
def check_cadences_available!
render_404 unless group.iteration_cadences_feature_flag_enabled?
end
def authorize_show_cadence!
render_404 unless can?(current_user, :read_iteration_cadence, group)
end
end end
# frozen_string_literal: true
class Projects::IterationCadencesController < Projects::ApplicationController
include IterationCadencesActions
private
def group
project.group
end
end
- page_title s_('Iterations|Iteration cadences') - page_title s_('Iterations|Iteration cadences')
.js-iteration-cadence-app{ data: { group_full_path: @group.full_path, .js-iteration-cadence-app{ data: { full_path: @group.full_path,
cadences_list_path: group_iteration_cadences_path(@group), cadences_list_path: group_iteration_cadences_path(@group),
can_create_cadence: can?(current_user, :create_iteration_cadence, @group).to_s, can_create_cadence: can?(current_user, :create_iteration_cadence, @group).to_s,
can_edit_cadence: can?(current_user, :admin_iteration_cadence, @group).to_s, can_edit_cadence: can?(current_user, :admin_iteration_cadence, @group).to_s,
......
- page_title _('Iterations|Iteration cadences')
.js-iteration-cadence-app{ data: { full_path: @project.full_path,
cadences_list_path: project_iteration_cadences_path(@project),
has_scoped_labels_feature: @project.licensed_feature_available?(:scoped_labels).to_s,
labels_fetch_path: project_labels_path(@project, format: :json, include_ancestor_groups: true),
preview_markdown_path: preview_markdown_path(@project),
no_issues_svg_path: image_path('illustrations/issues.svg') } }
...@@ -131,6 +131,10 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do ...@@ -131,6 +131,10 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
resources :iterations, only: [:index, :show], constraints: { id: /\d+/ } resources :iterations, only: [:index, :show], constraints: { id: /\d+/ }
resources :iteration_cadences, path: 'cadences(/*vueroute)', action: :index do
resources :iterations, only: [:index, :show], constraints: { id: /\d+/ }, controller: :iteration_cadences, action: :index
end
namespace :incident_management, path: '' do namespace :incident_management, path: '' do
resources :oncall_schedules, only: [:index], path: 'oncall_schedules' resources :oncall_schedules, only: [:index], path: 'oncall_schedules'
resources :escalation_policies, only: [:index], path: 'escalation_policies' resources :escalation_policies, only: [:index], path: 'escalation_policies'
......
...@@ -24,10 +24,13 @@ module EE ...@@ -24,10 +24,13 @@ module EE
return ::Sidebars::NilMenuItem.new(item_id: :iterations) return ::Sidebars::NilMenuItem.new(item_id: :iterations)
end end
link = context.project.group&.iteration_cadences_feature_flag_enabled? ? project_iteration_cadences_path(context.project) : project_iterations_path(context.project)
controller = context.project.group&.iteration_cadences_feature_flag_enabled? ? :iteration_cadences : :iterations
::Sidebars::MenuItem.new( ::Sidebars::MenuItem.new(
title: _('Iterations'), title: _('Iterations'),
link: project_iterations_path(context.project), link: link,
active_routes: { controller: :iterations }, active_routes: { controller: controller },
item_id: :iterations item_id: :iterations
) )
end end
......
...@@ -3,30 +3,9 @@ ...@@ -3,30 +3,9 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe Groups::IterationCadencesController do RSpec.describe Groups::IterationCadencesController do
using RSpec::Parameterized::TableSyntax
let_it_be(:group) { create(:group, :private) } let_it_be(:group) { create(:group, :private) }
let_it_be(:cadence) { create(:iterations_cadence, group: group) }
let_it_be(:user) { create(:user) }
before do
stub_feature_flags(iteration_cadences: feature_flag_available)
group.add_user(user, role) unless role == :none
sign_in(user)
end
describe 'index' do it_behaves_like 'accessing iteration cadences' do
subject { get :index, params: { group_id: group } } subject { get :index, params: { group_id: group } }
where(:feature_flag_available, :role, :status) do
false | :developer | :not_found
true | :none | :not_found
true | :guest | :success
true | :developer | :success
end
with_them do
it_behaves_like 'returning response status', params[:status]
end
end end
end end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Projects::IterationCadencesController do
let_it_be(:group) { create(:group, :private) }
let_it_be(:project) { create(:project, namespace: group) }
it_behaves_like 'accessing iteration cadences' do
subject { get :index, params: { namespace_id: project.namespace, project_id: project } }
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'User views project iteration cadences', :js do
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, group: group) }
let_it_be(:cadence) { create(:iterations_cadence, group: group) }
let_it_be(:other_cadence) { create(:iterations_cadence, group: group) }
let_it_be(:iteration_in_cadence) { create(:iteration, group: group, iterations_cadence: cadence) }
let_it_be(:closed_iteration_in_cadence) { create(:iteration, group: group, iterations_cadence: cadence, start_date: 2.weeks.ago, due_date: 1.week.ago) }
let_it_be(:iteration_in_other_cadence) { create(:iteration, group: group, iterations_cadence: other_cadence) }
before do
stub_licensed_features(iterations: true)
end
context 'as authorized user' do
before do
group.add_developer(user)
sign_in(user)
visit project_iteration_cadences_path(project)
end
it 'shows read-only iteration cadences', :aggregate_failures do
expect(page).to have_title('Iteration cadences')
expect(page).to have_content(cadence.title)
expect(page).to have_content(other_cadence.title)
expect(page).not_to have_content(iteration_in_cadence.title)
expect(page).not_to have_content(iteration_in_other_cadence.title)
click_button cadence.title
expect(page).to have_content(iteration_in_cadence.title)
expect(page).not_to have_content(iteration_in_other_cadence.title)
expect(page).not_to have_content(closed_iteration_in_cadence.title)
expect(page).not_to have_link('New iteration cadence')
end
end
end
...@@ -67,7 +67,7 @@ describe('Iteration cadence form', () => { ...@@ -67,7 +67,7 @@ describe('Iteration cadence form', () => {
$router, $router,
}, },
provide: { provide: {
groupPath, fullPath: groupPath,
cadencesListPath: TEST_HOST, cadencesListPath: TEST_HOST,
}, },
}), }),
......
...@@ -3,7 +3,9 @@ import { createLocalVue, RouterLinkStub } from '@vue/test-utils'; ...@@ -3,7 +3,9 @@ import { createLocalVue, RouterLinkStub } from '@vue/test-utils';
import { nextTick } from 'vue'; import { nextTick } from 'vue';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import IterationCadenceListItem from 'ee/iterations/components/iteration_cadence_list_item.vue'; import IterationCadenceListItem from 'ee/iterations/components/iteration_cadence_list_item.vue';
import iterationsInCadenceQuery from 'ee/iterations/queries/iterations_in_cadence.query.graphql'; import { Namespace } from 'ee/iterations/constants';
import groupIterationsInCadenceQuery from 'ee/iterations/queries/group_iterations_in_cadence.query.graphql';
import projectIterationsInCadenceQuery from 'ee/iterations/queries/project_iterations_in_cadence.query.graphql';
import createMockApollo from 'helpers/mock_apollo_helper'; import createMockApollo from 'helpers/mock_apollo_helper';
import { mountExtended as mount } from 'helpers/vue_test_utils_helper'; import { mountExtended as mount } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises'; import waitForPromises from 'helpers/wait_for_promises';
...@@ -25,7 +27,7 @@ describe('Iteration cadence list item', () => { ...@@ -25,7 +27,7 @@ describe('Iteration cadence list item', () => {
let wrapper; let wrapper;
let apolloProvider; let apolloProvider;
const groupPath = 'gitlab-org'; const fullPath = 'gitlab-org';
const iterations = [ const iterations = [
{ {
dueDate: '2021-08-14', dueDate: '2021-08-14',
...@@ -49,7 +51,8 @@ describe('Iteration cadence list item', () => { ...@@ -49,7 +51,8 @@ describe('Iteration cadence list item', () => {
const endCursor = 'MjA'; const endCursor = 'MjA';
const querySuccessResponse = { const querySuccessResponse = {
data: { data: {
group: { workspace: {
id: '1',
iterations: { iterations: {
nodes: iterations, nodes: iterations,
pageInfo: { pageInfo: {
...@@ -65,7 +68,8 @@ describe('Iteration cadence list item', () => { ...@@ -65,7 +68,8 @@ describe('Iteration cadence list item', () => {
const queryEmptyResponse = { const queryEmptyResponse = {
data: { data: {
group: { workspace: {
id: '1',
iterations: { iterations: {
nodes: [], nodes: [],
pageInfo: { pageInfo: {
...@@ -83,9 +87,11 @@ describe('Iteration cadence list item', () => { ...@@ -83,9 +87,11 @@ describe('Iteration cadence list item', () => {
props = {}, props = {},
canCreateCadence, canCreateCadence,
canEditCadence, canEditCadence,
namespaceType = Namespace.Group,
query = groupIterationsInCadenceQuery,
resolverMock = jest.fn().mockResolvedValue(querySuccessResponse), resolverMock = jest.fn().mockResolvedValue(querySuccessResponse),
} = {}) { } = {}) {
apolloProvider = createMockApolloProvider([[iterationsInCadenceQuery, resolverMock]]); apolloProvider = createMockApolloProvider([[query, resolverMock]]);
wrapper = mount(IterationCadenceListItem, { wrapper = mount(IterationCadenceListItem, {
localVue, localVue,
...@@ -97,9 +103,10 @@ describe('Iteration cadence list item', () => { ...@@ -97,9 +103,10 @@ describe('Iteration cadence list item', () => {
RouterLink: RouterLinkStub, RouterLink: RouterLinkStub,
}, },
provide: { provide: {
groupPath, fullPath,
canCreateCadence, canCreateCadence,
canEditCadence, canEditCadence,
namespaceType,
}, },
propsData: { propsData: {
title: cadence.title, title: cadence.title,
...@@ -155,6 +162,21 @@ describe('Iteration cadence list item', () => { ...@@ -155,6 +162,21 @@ describe('Iteration cadence list item', () => {
}); });
}); });
it('loads project iterations for Project namespaceType', async () => {
await createComponent({
namespaceType: Namespace.Project,
query: projectIterationsInCadenceQuery,
});
expand();
await waitForPromises();
iterations.forEach(({ title }) => {
expect(wrapper.text()).toContain(title);
});
});
it('shows alert on query error', async () => { it('shows alert on query error', async () => {
await createComponent({ await createComponent({
resolverMock: jest.fn().mockRejectedValue(queryEmptyResponse), resolverMock: jest.fn().mockRejectedValue(queryEmptyResponse),
...@@ -171,7 +193,7 @@ describe('Iteration cadence list item', () => { ...@@ -171,7 +193,7 @@ describe('Iteration cadence list item', () => {
it('calls fetchMore after scrolling down', async () => { it('calls fetchMore after scrolling down', async () => {
await createComponent(); await createComponent();
jest.spyOn(wrapper.vm.$apollo.queries.group, 'fetchMore').mockResolvedValue({}); jest.spyOn(wrapper.vm.$apollo.queries.workspace, 'fetchMore').mockResolvedValue({});
expand(); expand();
...@@ -179,7 +201,7 @@ describe('Iteration cadence list item', () => { ...@@ -179,7 +201,7 @@ describe('Iteration cadence list item', () => {
wrapper.findComponent(GlInfiniteScroll).vm.$emit('bottomReached'); wrapper.findComponent(GlInfiniteScroll).vm.$emit('bottomReached');
expect(wrapper.vm.$apollo.queries.group.fetchMore).toHaveBeenCalledWith( expect(wrapper.vm.$apollo.queries.workspace.fetchMore).toHaveBeenCalledWith(
expect.objectContaining({ expect.objectContaining({
variables: expect.objectContaining({ variables: expect.objectContaining({
afterCursor: endCursor, afterCursor: endCursor,
......
...@@ -4,8 +4,10 @@ import { nextTick } from 'vue'; ...@@ -4,8 +4,10 @@ import { nextTick } from 'vue';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import IterationCadenceListItem from 'ee/iterations/components/iteration_cadence_list_item.vue'; import IterationCadenceListItem from 'ee/iterations/components/iteration_cadence_list_item.vue';
import IterationCadencesList from 'ee/iterations/components/iteration_cadences_list.vue'; import IterationCadencesList from 'ee/iterations/components/iteration_cadences_list.vue';
import { Namespace } from 'ee/iterations/constants';
import destroyIterationCadence from 'ee/iterations/queries/destroy_cadence.mutation.graphql'; import destroyIterationCadence from 'ee/iterations/queries/destroy_cadence.mutation.graphql';
import cadencesListQuery from 'ee/iterations/queries/iteration_cadences_list.query.graphql'; import cadencesListQuery from 'ee/iterations/queries/group_iteration_cadences_list.query.graphql';
import projectCadencesListQuery from 'ee/iterations/queries/project_iteration_cadences_list.query.graphql';
import createRouter from 'ee/iterations/router'; import createRouter from 'ee/iterations/router';
import createMockApollo from 'helpers/mock_apollo_helper'; import createMockApollo from 'helpers/mock_apollo_helper';
import { TEST_HOST } from 'helpers/test_constants'; import { TEST_HOST } from 'helpers/test_constants';
...@@ -28,7 +30,7 @@ describe('Iteration cadences list', () => { ...@@ -28,7 +30,7 @@ describe('Iteration cadences list', () => {
let apolloProvider; let apolloProvider;
const cadencesListPath = TEST_HOST; const cadencesListPath = TEST_HOST;
const groupPath = 'gitlab-org'; const fullPath = 'gitlab-org';
const cadences = [ const cadences = [
{ {
id: 'gid://gitlab/Iterations::Cadence/561', id: 'gid://gitlab/Iterations::Cadence/561',
...@@ -54,7 +56,8 @@ describe('Iteration cadences list', () => { ...@@ -54,7 +56,8 @@ describe('Iteration cadences list', () => {
const endCursor = 'MjA'; const endCursor = 'MjA';
const querySuccessResponse = { const querySuccessResponse = {
data: { data: {
group: { workspace: {
id: 'id',
iterationCadences: { iterationCadences: {
nodes: cadences, nodes: cadences,
pageInfo: { pageInfo: {
...@@ -70,7 +73,8 @@ describe('Iteration cadences list', () => { ...@@ -70,7 +73,8 @@ describe('Iteration cadences list', () => {
const queryEmptyResponse = { const queryEmptyResponse = {
data: { data: {
group: { workspace: {
id: '234',
iterationCadences: { iterationCadences: {
nodes: [], nodes: [],
pageInfo: { pageInfo: {
...@@ -87,13 +91,15 @@ describe('Iteration cadences list', () => { ...@@ -87,13 +91,15 @@ describe('Iteration cadences list', () => {
function createComponent({ function createComponent({
canCreateCadence, canCreateCadence,
canEditCadence, canEditCadence,
namespaceType = Namespace.Group,
query = cadencesListQuery,
resolverMock = jest.fn().mockResolvedValue(querySuccessResponse), resolverMock = jest.fn().mockResolvedValue(querySuccessResponse),
destroyMutationMock = jest destroyMutationMock = jest
.fn() .fn()
.mockResolvedValue({ data: { iterationCadenceDestroy: { errors: [] } } }), .mockResolvedValue({ data: { iterationCadenceDestroy: { errors: [] } } }),
} = {}) { } = {}) {
apolloProvider = createMockApolloProvider([ apolloProvider = createMockApolloProvider([
[cadencesListQuery, resolverMock], [query, resolverMock],
[destroyIterationCadence, destroyMutationMock], [destroyIterationCadence, destroyMutationMock],
]); ]);
...@@ -102,7 +108,8 @@ describe('Iteration cadences list', () => { ...@@ -102,7 +108,8 @@ describe('Iteration cadences list', () => {
apolloProvider, apolloProvider,
router, router,
provide: { provide: {
groupPath, fullPath,
namespaceType,
cadencesListPath, cadencesListPath,
canCreateCadence, canCreateCadence,
canEditCadence, canEditCadence,
...@@ -169,6 +176,19 @@ describe('Iteration cadences list', () => { ...@@ -169,6 +176,19 @@ describe('Iteration cadences list', () => {
}); });
}); });
it('loads project iterations for Project namespaceType', async () => {
await createComponent({
namespaceType: Namespace.Project,
query: projectCadencesListQuery,
});
await waitForPromises();
cadences.forEach(({ title }) => {
expect(wrapper.text()).toContain(title);
});
});
it('shows alert on query error', async () => { it('shows alert on query error', async () => {
await createComponent({ await createComponent({
resolverMock: jest.fn().mockRejectedValue(queryEmptyResponse), resolverMock: jest.fn().mockRejectedValue(queryEmptyResponse),
......
# frozen_string_literal: true
RSpec.shared_examples 'accessing iteration cadences' do
using RSpec::Parameterized::TableSyntax
let_it_be(:cadence) { create(:iterations_cadence, group: group) }
let_it_be(:user) { create(:user) }
before do
stub_feature_flags(iteration_cadences: feature_flag_available)
group.add_user(user, role) unless role == :none
sign_in(user)
end
describe 'index' do
where(:feature_flag_available, :role, :status) do
false | :developer | :not_found
true | :none | :not_found
true | :guest | :success
true | :developer | :success
end
with_them do
it_behaves_like 'returning response status', params[:status]
end
end
end
...@@ -28,13 +28,36 @@ RSpec.describe 'layouts/nav/sidebar/_project' do ...@@ -28,13 +28,36 @@ RSpec.describe 'layouts/nav/sidebar/_project' do
describe 'Issues' do describe 'Issues' do
describe 'Iterations' do describe 'Iterations' do
it 'has a link to the issue iterations path' do let_it_be(:user) { create(:user) }
allow(view).to receive(:current_user).and_return(user) let_it_be(:group) { create(:group) }
stub_licensed_features(iterations: true) let_it_be_with_refind(:project) { create(:project, group: group) }
render before do
group.add_reporter(user)
end
describe 'iteration_cadences flag enabled' do
it 'has a link to the iteration cadences path' do
stub_feature_flags(iteration_cadences: true)
allow(view).to receive(:current_user).and_return(user)
stub_licensed_features(iterations: true)
render
expect(rendered).to have_link('Iterations', href: project_iterations_path(project)) expect(rendered).to have_link('Iterations', href: project_iteration_cadences_path(project))
end
end
describe 'iteration_cadences flag enabled' do
it 'has a link to the issue iterations path' do
stub_feature_flags(iteration_cadences: false)
allow(view).to receive(:current_user).and_return(user)
stub_licensed_features(iterations: true)
render
expect(rendered).to have_link('Iterations', href: project_iterations_path(project))
end
end end
end end
end end
......
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