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