Commit 6f505360 authored by Brandon Labuschagne's avatar Brandon Labuschagne

Merge branch '322755-get-issue-counts' into 'master'

Add tab counts to issues list refactor [RUN ALL RSPEC]

See merge request gitlab-org/gitlab!64954
parents b9771818 e991c439
...@@ -20,6 +20,7 @@ import { ...@@ -20,6 +20,7 @@ import {
CREATED_DESC, CREATED_DESC,
i18n, i18n,
initialPageParams, initialPageParams,
issuesCountSmartQueryBase,
MAX_LIST_SIZE, MAX_LIST_SIZE,
PAGE_SIZE, PAGE_SIZE,
PARAM_DUE_DATE, PARAM_DUE_DATE,
...@@ -29,11 +30,11 @@ import { ...@@ -29,11 +30,11 @@ import {
TOKEN_TYPE_ASSIGNEE, TOKEN_TYPE_ASSIGNEE,
TOKEN_TYPE_AUTHOR, TOKEN_TYPE_AUTHOR,
TOKEN_TYPE_CONFIDENTIAL, TOKEN_TYPE_CONFIDENTIAL,
TOKEN_TYPE_MY_REACTION,
TOKEN_TYPE_EPIC, TOKEN_TYPE_EPIC,
TOKEN_TYPE_ITERATION, TOKEN_TYPE_ITERATION,
TOKEN_TYPE_LABEL, TOKEN_TYPE_LABEL,
TOKEN_TYPE_MILESTONE, TOKEN_TYPE_MILESTONE,
TOKEN_TYPE_MY_REACTION,
TOKEN_TYPE_WEIGHT, TOKEN_TYPE_WEIGHT,
UPDATED_DESC, UPDATED_DESC,
urlSortParams, urlSortParams,
...@@ -171,26 +172,17 @@ export default { ...@@ -171,26 +172,17 @@ export default {
showBulkEditSidebar: false, showBulkEditSidebar: false,
sortKey: getSortKey(getParameterByName(PARAM_SORT)) || defaultSortKey, sortKey: getSortKey(getParameterByName(PARAM_SORT)) || defaultSortKey,
state: state || IssuableStates.Opened, state: state || IssuableStates.Opened,
totalIssues: 0,
}; };
}, },
apollo: { apollo: {
issues: { issues: {
query: getIssuesQuery, query: getIssuesQuery,
variables() { variables() {
return { return this.queryVariables;
projectPath: this.projectPath,
search: this.searchQuery,
sort: this.sortKey,
state: this.state,
...this.pageParams,
...this.apiFilterParams,
};
}, },
update: ({ project }) => project?.issues.nodes ?? [], update: ({ project }) => project?.issues.nodes ?? [],
result({ data }) { result({ data }) {
this.pageInfo = data.project?.issues.pageInfo ?? {}; this.pageInfo = data.project?.issues.pageInfo ?? {};
this.totalIssues = data.project?.issues.count ?? 0;
this.exportCsvPathWithQuery = this.getExportCsvPathWithQuery(); this.exportCsvPathWithQuery = this.getExportCsvPathWithQuery();
}, },
error(error) { error(error) {
...@@ -201,8 +193,55 @@ export default { ...@@ -201,8 +193,55 @@ export default {
}, },
debounce: 200, debounce: 200,
}, },
countOpened: {
...issuesCountSmartQueryBase,
variables() {
return {
...this.queryVariables,
state: IssuableStates.Opened,
};
},
skip() {
return !this.hasProjectIssues;
},
},
countClosed: {
...issuesCountSmartQueryBase,
variables() {
return {
...this.queryVariables,
state: IssuableStates.Closed,
};
},
skip() {
return !this.hasProjectIssues;
},
},
countAll: {
...issuesCountSmartQueryBase,
variables() {
return {
...this.queryVariables,
state: IssuableStates.All,
};
},
skip() {
return !this.hasProjectIssues;
},
},
}, },
computed: { computed: {
queryVariables() {
return {
isSignedIn: this.isSignedIn,
projectPath: this.projectPath,
search: this.searchQuery,
sort: this.sortKey,
state: this.state,
...this.pageParams,
...this.apiFilterParams,
};
},
hasSearch() { hasSearch() {
return this.searchQuery || Object.keys(this.urlFilterParams).length; return this.searchQuery || Object.keys(this.urlFilterParams).length;
}, },
...@@ -347,13 +386,14 @@ export default { ...@@ -347,13 +386,14 @@ export default {
return getSortOptions(this.hasIssueWeightsFeature, this.hasBlockedIssuesFeature); return getSortOptions(this.hasIssueWeightsFeature, this.hasBlockedIssuesFeature);
}, },
tabCounts() { tabCounts() {
return Object.values(IssuableStates).reduce( return {
(acc, state) => ({ [IssuableStates.Opened]: this.countOpened,
...acc, [IssuableStates.Closed]: this.countClosed,
[state]: this.state === state ? this.totalIssues : undefined, [IssuableStates.All]: this.countAll,
}), };
{}, },
); currentTabCount() {
return this.tabCounts[this.state] ?? 0;
}, },
urlParams() { urlParams() {
return { return {
...@@ -595,7 +635,7 @@ export default { ...@@ -595,7 +635,7 @@ export default {
v-if="isSignedIn" v-if="isSignedIn"
class="gl-md-mr-3" class="gl-md-mr-3"
:export-csv-path="exportCsvPathWithQuery" :export-csv-path="exportCsvPathWithQuery"
:issuable-count="totalIssues" :issuable-count="currentTabCount"
/> />
<gl-button <gl-button
v-if="canBulkUpdate" v-if="canBulkUpdate"
...@@ -706,7 +746,7 @@ export default { ...@@ -706,7 +746,7 @@ export default {
<csv-import-export-buttons <csv-import-export-buttons
class="gl-mr-3" class="gl-mr-3"
:export-csv-path="exportCsvPathWithQuery" :export-csv-path="exportCsvPathWithQuery"
:issuable-count="totalIssues" :issuable-count="currentTabCount"
/> />
</template> </template>
</gl-empty-state> </gl-empty-state>
......
import getIssuesCountQuery from 'ee_else_ce/issues_list/queries/get_issues_count.query.graphql';
import createFlash from '~/flash';
import { __, s__ } from '~/locale'; import { __, s__ } from '~/locale';
import { import {
FILTER_ANY, FILTER_ANY,
...@@ -68,6 +70,7 @@ export const i18n = { ...@@ -68,6 +70,7 @@ export const i18n = {
confidentialYes: __('Yes'), confidentialYes: __('Yes'),
downvotes: __('Downvotes'), downvotes: __('Downvotes'),
editIssues: __('Edit issues'), editIssues: __('Edit issues'),
errorFetchingCounts: __('An error occurred while getting issue counts'),
errorFetchingIssues: __('An error occurred while loading issues'), errorFetchingIssues: __('An error occurred while loading issues'),
jiraIntegrationMessage: s__( jiraIntegrationMessage: s__(
'JiraService|%{jiraDocsLinkStart}Enable the Jira integration%{jiraDocsLinkEnd} to view your Jira issues in GitLab.', 'JiraService|%{jiraDocsLinkStart}Enable the Jira integration%{jiraDocsLinkEnd} to view your Jira issues in GitLab.',
...@@ -321,3 +324,15 @@ export const filters = { ...@@ -321,3 +324,15 @@ export const filters = {
}, },
}, },
}; };
export const issuesCountSmartQueryBase = {
query: getIssuesCountQuery,
context: {
isSingleRequest: true,
},
update: ({ project }) => project?.issues.count,
error(error) {
createFlash({ message: i18n.errorFetchingCounts, captureError: true, error });
},
debounce: 200,
};
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
#import "./issue.fragment.graphql" #import "./issue.fragment.graphql"
query getProjectIssues( query getProjectIssues(
$isSignedIn: Boolean = false
$projectPath: ID! $projectPath: ID!
$search: String $search: String
$sort: IssueSort $sort: IssueSort
...@@ -33,7 +34,6 @@ query getProjectIssues( ...@@ -33,7 +34,6 @@ query getProjectIssues(
first: $firstPageSize first: $firstPageSize
last: $lastPageSize last: $lastPageSize
) { ) {
count
pageInfo { pageInfo {
...PageInfo ...PageInfo
} }
......
query getProjectIssuesCount(
$projectPath: ID!
$search: String
$state: IssuableState
$assigneeId: String
$assigneeUsernames: [String!]
$authorUsername: String
$labelName: [String]
$milestoneTitle: [String]
$not: NegatedIssueFilterInput
) {
project(fullPath: $projectPath) {
issues(
search: $search
state: $state
assigneeId: $assigneeId
assigneeUsernames: $assigneeUsernames
authorUsername: $authorUsername
labelName: $labelName
milestoneTitle: $milestoneTitle
not: $not
) {
count
}
}
}
...@@ -11,7 +11,7 @@ fragment IssueFragment on Issue { ...@@ -11,7 +11,7 @@ fragment IssueFragment on Issue {
title title
updatedAt updatedAt
upvotes upvotes
userDiscussionsCount userDiscussionsCount @include(if: $isSignedIn)
webUrl webUrl
assignees { assignees {
nodes { nodes {
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
#import "~/issues_list/queries/issue.fragment.graphql" #import "~/issues_list/queries/issue.fragment.graphql"
query getProjectIssues( query getProjectIssues(
$isSignedIn: Boolean = false
$projectPath: ID! $projectPath: ID!
$search: String $search: String
$sort: IssueSort $sort: IssueSort
...@@ -41,7 +42,6 @@ query getProjectIssues( ...@@ -41,7 +42,6 @@ query getProjectIssues(
first: $firstPageSize first: $firstPageSize
last: $lastPageSize last: $lastPageSize
) { ) {
count
pageInfo { pageInfo {
...PageInfo ...PageInfo
} }
......
query getProjectIssuesCount(
$projectPath: ID!
$search: String
$state: IssuableState
$assigneeId: String
$assigneeUsernames: [String!]
$authorUsername: String
$labelName: [String]
$milestoneTitle: [String]
$epicId: String
$iterationId: [ID]
$iterationWildcardId: IterationWildcardId
$weight: String
$not: NegatedIssueFilterInput
) {
project(fullPath: $projectPath) {
issues(
search: $search
state: $state
assigneeId: $assigneeId
assigneeUsernames: $assigneeUsernames
authorUsername: $authorUsername
labelName: $labelName
milestoneTitle: $milestoneTitle
epicId: $epicId
iterationId: $iterationId
iterationWildcardId: $iterationWildcardId
weight: $weight
not: $not
) {
count
}
}
}
...@@ -3633,6 +3633,9 @@ msgstr "" ...@@ -3633,6 +3633,9 @@ msgstr ""
msgid "An error occurred while getting files for - %{branchId}" msgid "An error occurred while getting files for - %{branchId}"
msgstr "" msgstr ""
msgid "An error occurred while getting issue counts"
msgstr ""
msgid "An error occurred while getting projects" msgid "An error occurred while getting projects"
msgstr "" msgstr ""
......
...@@ -5,6 +5,7 @@ import { cloneDeep } from 'lodash'; ...@@ -5,6 +5,7 @@ import { cloneDeep } from 'lodash';
import { nextTick } from 'vue'; import { nextTick } from 'vue';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import getIssuesQuery from 'ee_else_ce/issues_list/queries/get_issues.query.graphql'; import getIssuesQuery from 'ee_else_ce/issues_list/queries/get_issues.query.graphql';
import getIssuesCountQuery from 'ee_else_ce/issues_list/queries/get_issues_count.query.graphql';
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';
import waitForPromises from 'helpers/wait_for_promises'; import waitForPromises from 'helpers/wait_for_promises';
...@@ -13,6 +14,7 @@ import { ...@@ -13,6 +14,7 @@ import {
filteredTokens, filteredTokens,
locationSearch, locationSearch,
urlParams, urlParams,
getIssuesCountQueryResponse,
} from 'jest/issues_list/mock_data'; } from 'jest/issues_list/mock_data';
import createFlash from '~/flash'; import createFlash from '~/flash';
import { convertToGraphQLId } from '~/graphql_shared/utils'; import { convertToGraphQLId } from '~/graphql_shared/utils';
...@@ -63,7 +65,7 @@ describe('IssuesListApp component', () => { ...@@ -63,7 +65,7 @@ describe('IssuesListApp component', () => {
hasIssueWeightsFeature: true, hasIssueWeightsFeature: true,
hasIterationsFeature: true, hasIterationsFeature: true,
hasProjectIssues: true, hasProjectIssues: true,
isSignedIn: false, isSignedIn: true,
issuesPath: 'path/to/issues', issuesPath: 'path/to/issues',
jiraIntegrationPath: 'jira/integration/path', jiraIntegrationPath: 'jira/integration/path',
newIssuePath: 'new/issue/path', newIssuePath: 'new/issue/path',
...@@ -92,10 +94,14 @@ describe('IssuesListApp component', () => { ...@@ -92,10 +94,14 @@ describe('IssuesListApp component', () => {
const mountComponent = ({ const mountComponent = ({
provide = {}, provide = {},
response = defaultQueryResponse, issuesQueryResponse = jest.fn().mockResolvedValue(defaultQueryResponse),
issuesQueryCountResponse = jest.fn().mockResolvedValue(getIssuesCountQueryResponse),
mountFn = shallowMount, mountFn = shallowMount,
} = {}) => { } = {}) => {
const requestHandlers = [[getIssuesQuery, jest.fn().mockResolvedValue(response)]]; const requestHandlers = [
[getIssuesQuery, issuesQueryResponse],
[getIssuesCountQuery, issuesQueryCountResponse],
];
const apolloProvider = createMockApollo(requestHandlers); const apolloProvider = createMockApollo(requestHandlers);
return mountFn(IssuesListApp, { return mountFn(IssuesListApp, {
...@@ -136,8 +142,8 @@ describe('IssuesListApp component', () => { ...@@ -136,8 +142,8 @@ describe('IssuesListApp component', () => {
currentTab: IssuableStates.Opened, currentTab: IssuableStates.Opened,
tabCounts: { tabCounts: {
opened: 1, opened: 1,
closed: undefined, closed: 1,
all: undefined, all: 1,
}, },
issuablesLoading: false, issuablesLoading: false,
isManualOrdering: false, isManualOrdering: false,
...@@ -564,6 +570,29 @@ describe('IssuesListApp component', () => { ...@@ -564,6 +570,29 @@ describe('IssuesListApp component', () => {
}); });
}); });
describe('errors', () => {
describe.each`
error | mountOption | message
${'fetching issues'} | ${'issuesQueryResponse'} | ${IssuesListApp.i18n.errorFetchingIssues}
${'fetching issue counts'} | ${'issuesQueryCountResponse'} | ${IssuesListApp.i18n.errorFetchingCounts}
`('when there is an error $error', ({ mountOption, message }) => {
beforeEach(() => {
wrapper = mountComponent({
[mountOption]: jest.fn().mockRejectedValue(new Error('ERROR')),
});
jest.runOnlyPendingTimers();
});
it('shows an error message', () => {
expect(createFlash).toHaveBeenCalledWith({
captureError: true,
error: new Error('Network error: ERROR'),
message,
});
});
});
});
describe('events', () => { describe('events', () => {
describe('when "click-tab" event is emitted by IssuableList', () => { describe('when "click-tab" event is emitted by IssuableList', () => {
beforeEach(() => { beforeEach(() => {
...@@ -629,7 +658,7 @@ describe('IssuesListApp component', () => { ...@@ -629,7 +658,7 @@ describe('IssuesListApp component', () => {
}; };
beforeEach(() => { beforeEach(() => {
wrapper = mountComponent({ response }); wrapper = mountComponent({ issuesQueryResponse: jest.fn().mockResolvedValue(response) });
jest.runOnlyPendingTimers(); jest.runOnlyPendingTimers();
}); });
......
...@@ -7,7 +7,6 @@ export const getIssuesQueryResponse = { ...@@ -7,7 +7,6 @@ export const getIssuesQueryResponse = {
data: { data: {
project: { project: {
issues: { issues: {
count: 1,
pageInfo: { pageInfo: {
hasNextPage: true, hasNextPage: true,
hasPreviousPage: false, hasPreviousPage: false,
...@@ -70,6 +69,16 @@ export const getIssuesQueryResponse = { ...@@ -70,6 +69,16 @@ export const getIssuesQueryResponse = {
}, },
}; };
export const getIssuesCountQueryResponse = {
data: {
project: {
issues: {
count: 1,
},
},
},
};
export const locationSearch = [ export const locationSearch = [
'?search=find+issues', '?search=find+issues',
'author_username=homer', 'author_username=homer',
......
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