Commit f4a20ddb authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab master

parents cc7dfcd7 36b1c49b
...@@ -98,6 +98,7 @@ If you enjoy taking notes using post-it notes make sure you have available some ...@@ -98,6 +98,7 @@ If you enjoy taking notes using post-it notes make sure you have available some
- [ ] Finalise participant list - `decider` and `facilitator` - [ ] Finalise participant list - `decider` and `facilitator`
- [ ] Create [participation form](https://docs.google.com/forms/d/e/1FAIpQLSc0_BNltvRW8yXXaJd8sIKzgDmrSGqILMfkoCJrAj6sFcsMcg/viewform?usp=sf_link) and send to participants (**deadline**: [date]) - `facilitator` - [ ] Create [participation form](https://docs.google.com/forms/d/e/1FAIpQLSc0_BNltvRW8yXXaJd8sIKzgDmrSGqILMfkoCJrAj6sFcsMcg/viewform?usp=sf_link) and send to participants (**deadline**: [date]) - `facilitator`
- [ ] Create a dedicated Slack channel and add participants - `facilitator`
- [ ] Promote this issue to an epic - `facilitator` - [ ] Promote this issue to an epic - `facilitator`
- [ ] Create issues under the epic for the pre-workshop tasks: Expert interviews ([example](https://gitlab.com/groups/gitlab-org/configure/-/epics/3#note_332412524)), Lightning walkthroughs and How might we.. notetaking assignment ([example](https://gitlab.com/gitlab-org/configure/general/-/issues/52)), Voting How might we... notes assignment ([example](https://gitlab.com/gitlab-org/configure/general/-/issues/54)) - `facilitator` - [ ] Create issues under the epic for the pre-workshop tasks: Expert interviews ([example](https://gitlab.com/groups/gitlab-org/configure/-/epics/3#note_332412524)), Lightning walkthroughs and How might we.. notetaking assignment ([example](https://gitlab.com/gitlab-org/configure/general/-/issues/52)), Voting How might we... notes assignment ([example](https://gitlab.com/gitlab-org/configure/general/-/issues/54)) - `facilitator`
- [ ] Create sync meetings in calendar and invite all participants (**deadline**: [date]) - `decider` or `facilitator` - [ ] Create sync meetings in calendar and invite all participants (**deadline**: [date]) - `decider` or `facilitator`
......
...@@ -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',
......
...@@ -48,7 +48,7 @@ RSpec.describe Integrations::Asana do ...@@ -48,7 +48,7 @@ RSpec.describe Integrations::Asana do
) )
end end
it 'calls Asana service to create a story' do it 'calls Asana integration to create a story' do
data = create_data_for_commits("Message from commit. related to ##{gid}") data = create_data_for_commits("Message from commit. related to ##{gid}")
expected_message = "#{data[:user_name]} pushed to branch #{data[:ref]} of #{project.full_name} ( #{data[:commits][0][:url]} ): #{data[:commits][0][:message]}" expected_message = "#{data[:user_name]} pushed to branch #{data[:ref]} of #{project.full_name} ( #{data[:commits][0][:url]} ): #{data[:commits][0][:message]}"
...@@ -59,7 +59,7 @@ RSpec.describe Integrations::Asana do ...@@ -59,7 +59,7 @@ RSpec.describe Integrations::Asana do
@asana.execute(data) @asana.execute(data)
end end
it 'calls Asana service to create a story and close a task' do it 'calls Asana integration to create a story and close a task' do
data = create_data_for_commits('fix #456789') data = create_data_for_commits('fix #456789')
d1 = double('Asana::Resources::Task') d1 = double('Asana::Resources::Task')
expect(d1).to receive(:add_comment) expect(d1).to receive(:add_comment)
......
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