Commit f45c07d7 authored by Kushal Pandya's avatar Kushal Pandya

Merge branch 'feature/swimlanes-filtering-issues' into 'master'

Swimlanes - Filtering for issues

See merge request gitlab-org/gitlab!41542
parents 0c5fd0ec 217004d7
...@@ -96,10 +96,11 @@ export default { ...@@ -96,10 +96,11 @@ export default {
showAssigneeListDetails() { showAssigneeListDetails() {
return this.list.type === 'assignee' && (this.list.isExpanded || !this.isSwimlanesHeader); return this.list.type === 'assignee' && (this.list.isExpanded || !this.isSwimlanesHeader);
}, },
issuesCount() {
return this.list.issuesSize;
},
issuesTooltipLabel() { issuesTooltipLabel() {
const { issuesSize } = this.list; return n__(`%d issue`, `%d issues`, this.issuesCount);
return n__(`%d issue`, `%d issues`, issuesSize);
}, },
chevronTooltip() { chevronTooltip() {
return this.list.isExpanded ? s__('Boards|Collapse') : s__('Boards|Expand'); return this.list.isExpanded ? s__('Boards|Collapse') : s__('Boards|Expand');
...@@ -299,7 +300,7 @@ export default { ...@@ -299,7 +300,7 @@ export default {
<gl-tooltip :target="() => $refs.issueCount" :title="issuesTooltipLabel" /> <gl-tooltip :target="() => $refs.issueCount" :title="issuesTooltipLabel" />
<span ref="issueCount" class="issue-count-badge-count"> <span ref="issueCount" class="issue-count-badge-count">
<gl-icon class="gl-mr-2" name="issues" /> <gl-icon class="gl-mr-2" name="issues" />
<issue-count :issues-size="list.issuesSize" :max-issue-count="list.maxIssueCount" /> <issue-count :issues-size="issuesCount" :max-issue-count="list.maxIssueCount" />
</span> </span>
<!-- The following is only true in EE. --> <!-- The following is only true in EE. -->
<template v-if="weightFeatureAvailable"> <template v-if="weightFeatureAvailable">
......
...@@ -161,7 +161,12 @@ export default () => { ...@@ -161,7 +161,12 @@ export default () => {
} }
}, },
methods: { methods: {
...mapActions(['setInitialBoardData', 'setFilters', 'fetchEpicsSwimlanes']), ...mapActions([
'setInitialBoardData',
'setFilters',
'fetchEpicsSwimlanes',
'fetchIssuesForAllLists',
]),
updateTokens() { updateTokens() {
this.filterManager.updateTokens(); this.filterManager.updateTokens();
}, },
...@@ -169,6 +174,7 @@ export default () => { ...@@ -169,6 +174,7 @@ export default () => {
this.setFilters(convertObjectPropsToCamelCase(urlParamsToObject(window.location.search))); this.setFilters(convertObjectPropsToCamelCase(urlParamsToObject(window.location.search)));
if (gon.features.boardsWithSwimlanes && this.isShowingEpicsSwimlanes) { if (gon.features.boardsWithSwimlanes && this.isShowingEpicsSwimlanes) {
this.fetchEpicsSwimlanes(false); this.fetchEpicsSwimlanes(false);
this.fetchIssuesForAllLists();
} }
}, },
updateDetailIssue(newIssue, multiSelect = false) { updateDetailIssue(newIssue, multiSelect = false) {
......
#import "./issue.fragment.graphql"
query GroupListIssues($fullPath: ID!, $boardId: ID!) {
group(fullPath: $fullPath) {
board(id: $boardId) {
lists {
nodes {
id
issues {
nodes {
...IssueNode
}
}
}
}
}
}
}
#import "./issue.fragment.graphql"
query ListIssues(
$fullPath: ID!
$boardId: ID!
$filters: BoardIssueInput
$isGroup: Boolean = false
$isProject: Boolean = false
) {
group(fullPath: $fullPath) @include(if: $isGroup) {
board(id: $boardId) {
lists {
nodes {
id
issues(filters: $filters) {
nodes {
...IssueNode
}
}
}
}
}
}
project(fullPath: $fullPath) @include(if: $isProject) {
board(id: $boardId) {
lists {
nodes {
id
issues(filters: $filters) {
nodes {
...IssueNode
}
}
}
}
}
}
}
#import "./issue.fragment.graphql"
query ProjectListIssues($fullPath: ID!, $boardId: ID!) {
project(fullPath: $fullPath) {
board(id: $boardId) {
lists {
nodes {
id
issues {
nodes {
...IssueNode
}
}
}
}
}
}
}
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import { sortBy } from 'lodash'; import { sortBy, pick } from 'lodash';
import createFlash from '~/flash'; import createFlash from '~/flash';
import { __ } from '~/locale'; import { __ } from '~/locale';
import { parseBoolean } from '~/lib/utils/common_utils'; import { parseBoolean } from '~/lib/utils/common_utils';
...@@ -10,8 +10,7 @@ import * as types from './mutation_types'; ...@@ -10,8 +10,7 @@ import * as types from './mutation_types';
import { formatListIssues, fullBoardId } from '../boards_util'; import { formatListIssues, fullBoardId } from '../boards_util';
import boardStore from '~/boards/stores/boards_store'; import boardStore from '~/boards/stores/boards_store';
import groupListsIssuesQuery from '../queries/group_lists_issues.query.graphql'; import listsIssuesQuery from '../queries/lists_issues.query.graphql';
import projectListsIssuesQuery from '../queries/project_lists_issues.query.graphql';
import projectBoardQuery from '../queries/project_board.query.graphql'; import projectBoardQuery from '../queries/project_board.query.graphql';
import groupBoardQuery from '../queries/group_board.query.graphql'; import groupBoardQuery from '../queries/group_board.query.graphql';
import createBoardListMutation from '../queries/board_list_create.mutation.graphql'; import createBoardListMutation from '../queries/board_list_create.mutation.graphql';
...@@ -38,7 +37,14 @@ export default { ...@@ -38,7 +37,14 @@ export default {
}, },
setFilters: ({ commit }, filters) => { setFilters: ({ commit }, filters) => {
const { scope, utf8, state, ...filterParams } = filters; const filterParams = pick(filters, [
'assigneeUsername',
'authorUsername',
'labelName',
'milestoneTitle',
'releaseTag',
'search',
]);
commit(types.SET_FILTERS, filterParams); commit(types.SET_FILTERS, filterParams);
}, },
...@@ -197,19 +203,20 @@ export default { ...@@ -197,19 +203,20 @@ export default {
fetchIssuesForAllLists: ({ state, commit }) => { fetchIssuesForAllLists: ({ state, commit }) => {
commit(types.REQUEST_ISSUES_FOR_ALL_LISTS); commit(types.REQUEST_ISSUES_FOR_ALL_LISTS);
const { endpoints, boardType } = state; const { endpoints, boardType, filterParams } = state;
const { fullPath, boardId } = endpoints; const { fullPath, boardId } = endpoints;
const query = boardType === BoardType.group ? groupListsIssuesQuery : projectListsIssuesQuery;
const variables = { const variables = {
fullPath, fullPath,
boardId: fullBoardId(boardId), boardId: fullBoardId(boardId),
filters: filterParams,
isGroup: boardType === BoardType.group,
isProject: boardType === BoardType.project,
}; };
return gqlClient return gqlClient
.query({ .query({
query, query: listsIssuesQuery,
variables, variables,
}) })
.then(({ data }) => { .then(({ data }) => {
......
...@@ -2,6 +2,11 @@ export function getMilestone({ milestone }) { ...@@ -2,6 +2,11 @@ export function getMilestone({ milestone }) {
return milestone || null; return milestone || null;
} }
export function fullEpicId(epicId) {
return `gid://gitlab/Epic/${epicId}`;
}
export default { export default {
getMilestone, getMilestone,
fullEpicId,
}; };
<script> <script>
import { mapState, mapActions } from 'vuex'; import { mapState, mapActions, mapGetters } from 'vuex';
import BoardListHeaderFoss from '~/boards/components/board_list_header.vue'; import BoardListHeaderFoss from '~/boards/components/board_list_header.vue';
import { __, sprintf, s__ } from '~/locale'; import { __, sprintf, s__, n__ } from '~/locale';
import boardsStore from '~/boards/stores/boards_store'; import boardsStore from '~/boards/stores/boards_store';
import { inactiveId, LIST } from '~/boards/constants'; import { inactiveId, LIST } from '~/boards/constants';
import eventHub from '~/sidebar/event_hub'; import eventHub from '~/sidebar/event_hub';
...@@ -14,13 +14,21 @@ export default { ...@@ -14,13 +14,21 @@ export default {
}; };
}, },
computed: { computed: {
...mapState(['activeId']), ...mapState(['activeId', 'issuesByListId']),
...mapGetters(['isSwimlanesOn']),
issuesCount() {
if (this.isSwimlanesOn) {
return this.issuesByListId[this.list.id] ? this.issuesByListId[this.list.id].length : 0;
}
return this.list.issuesSize;
},
issuesTooltip() { issuesTooltip() {
const { issuesSize, maxIssueCount } = this.list; const { maxIssueCount } = this.list;
if (maxIssueCount > 0) { if (maxIssueCount > 0) {
return sprintf(__('%{issuesSize} issues with a limit of %{maxIssueCount}'), { return sprintf(__('%{issuesCount} issues with a limit of %{maxIssueCount}'), {
issuesSize, issuesCount: this.issuesCount,
maxIssueCount, maxIssueCount,
}); });
} }
...@@ -28,6 +36,9 @@ export default { ...@@ -28,6 +36,9 @@ export default {
// TODO: Remove this pattern. // TODO: Remove this pattern.
return BoardListHeaderFoss.computed.issuesTooltip.call(this); return BoardListHeaderFoss.computed.issuesTooltip.call(this);
}, },
issuesTooltipLabel() {
return n__(`%d issue`, `%d issues`, this.issuesCount);
},
weightCountToolTip() { weightCountToolTip() {
const { totalWeight } = this.list; const { totalWeight } = this.list;
......
...@@ -134,7 +134,7 @@ export default { ...@@ -134,7 +134,7 @@ export default {
:disabled="disabled" :disabled="disabled"
:root-path="rootPath" :root-path="rootPath"
/> />
<div class="board-lane-unassigned-issues gl-sticky gl-display-inline-block gl-left-0"> <div class="board-lane-unassigned-issues-title gl-sticky gl-display-inline-block gl-left-0">
<div class="gl-left-0 gl-py-5 gl-px-3 gl-display-flex gl-align-items-center"> <div class="gl-left-0 gl-py-5 gl-px-3 gl-display-flex gl-align-items-center">
<span <span
class="gl-mr-3 gl-font-weight-bold gl-white-space-nowrap gl-text-overflow-ellipsis gl-overflow-hidden" class="gl-mr-3 gl-font-weight-bold gl-white-space-nowrap gl-text-overflow-ellipsis gl-overflow-hidden"
...@@ -154,7 +154,7 @@ export default { ...@@ -154,7 +154,7 @@ export default {
</span> </span>
</div> </div>
</div> </div>
<div class="gl-display-flex"> <div class="gl-display-flex" data-testid="board-lane-unassigned-issues">
<issues-lane-list <issues-lane-list
v-for="list in lists" v-for="list in lists"
:key="`${list.id}-issues`" :key="`${list.id}-issues`"
......
export const DRAGGABLE_TAG = 'div'; export const DRAGGABLE_TAG = 'div';
/* eslint-disable @gitlab/require-i18n-strings */
export const EpicFilterType = {
any: 'Any',
none: 'None',
};
export default { export default {
DRAGGABLE_TAG, DRAGGABLE_TAG,
EpicFilterType,
}; };
import { sortBy } from 'lodash'; import { sortBy, pick } from 'lodash';
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import boardsStore from '~/boards/stores/boards_store'; import boardsStore from '~/boards/stores/boards_store';
...@@ -6,11 +6,13 @@ import { __ } from '~/locale'; ...@@ -6,11 +6,13 @@ import { __ } from '~/locale';
import { parseBoolean } from '~/lib/utils/common_utils'; import { parseBoolean } from '~/lib/utils/common_utils';
import actionsCE from '~/boards/stores/actions'; import actionsCE from '~/boards/stores/actions';
import { BoardType, ListType } from '~/boards/constants'; import { BoardType, ListType } from '~/boards/constants';
import { EpicFilterType } from '../constants';
import boardsStoreEE from './boards_store_ee'; import boardsStoreEE from './boards_store_ee';
import * as types from './mutation_types'; import * as types from './mutation_types';
import { fullEpicId } from '../boards_util';
import createDefaultClient from '~/lib/graphql'; import createDefaultClient from '~/lib/graphql';
import groupEpicsSwimlanesQuery from '../queries/group_epics_swimlanes.query.graphql'; import epicsSwimlanesQuery from '../queries/epics_swimlanes.query.graphql';
const notImplemented = () => { const notImplemented = () => {
/* eslint-disable-next-line @gitlab/require-i18n-strings */ /* eslint-disable-next-line @gitlab/require-i18n-strings */
...@@ -22,6 +24,27 @@ const gqlClient = createDefaultClient(); ...@@ -22,6 +24,27 @@ const gqlClient = createDefaultClient();
export default { export default {
...actionsCE, ...actionsCE,
setFilters: ({ commit }, filters) => {
const filterParams = pick(filters, [
'assigneeUsername',
'authorUsername',
'epicId',
'labelName',
'milestoneTitle',
'releaseTag',
'search',
'weight',
]);
if (filterParams.epicId === EpicFilterType.any || filterParams.epicId === EpicFilterType.none) {
filterParams.epicWildcardId = filterParams.epicId.toUpperCase();
filterParams.epicId = undefined;
} else if (filterParams.epicId) {
filterParams.epicId = fullEpicId(filterParams.epicId);
}
commit(types.SET_FILTERS, filterParams);
},
fetchEpicsSwimlanes({ state, commit }, withLists = true) { fetchEpicsSwimlanes({ state, commit }, withLists = true) {
const { endpoints, boardType, filterParams } = state; const { endpoints, boardType, filterParams } = state;
const { fullPath, boardId } = endpoints; const { fullPath, boardId } = endpoints;
...@@ -37,7 +60,7 @@ export default { ...@@ -37,7 +60,7 @@ export default {
return gqlClient return gqlClient
.query({ .query({
query: groupEpicsSwimlanesQuery, query: epicsSwimlanesQuery,
variables, variables,
}) })
.then(({ data }) => { .then(({ data }) => {
......
...@@ -16,3 +16,4 @@ export const RECEIVE_BOARD_LISTS_SUCCESS = 'RECEIVE_BOARD_LISTS_SUCCESS'; ...@@ -16,3 +16,4 @@ export const RECEIVE_BOARD_LISTS_SUCCESS = 'RECEIVE_BOARD_LISTS_SUCCESS';
export const RECEIVE_SWIMLANES_FAILURE = 'RECEIVE_SWIMLANES_FAILURE'; export const RECEIVE_SWIMLANES_FAILURE = 'RECEIVE_SWIMLANES_FAILURE';
export const RECEIVE_EPICS_SUCCESS = 'RECEIVE_EPICS_SUCCESS'; export const RECEIVE_EPICS_SUCCESS = 'RECEIVE_EPICS_SUCCESS';
export const SET_SHOW_LABELS = 'SET_SHOW_LABELS'; export const SET_SHOW_LABELS = 'SET_SHOW_LABELS';
export const SET_FILTERS = 'SET_FILTERS';
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'epics swimlanes filtering', :js do
let_it_be(:user) { create(:user) }
let_it_be(:user2) { create(:user) }
let_it_be(:group) { create(:group, :public) }
let_it_be(:project) { create(:project, :public, group: group) }
let_it_be(:board) { create(:board, project: project) }
let_it_be(:milestone) { create(:milestone, project: project) }
let_it_be(:planning) { create(:label, project: project, name: 'Planning', description: 'Test') }
let_it_be(:development) { create(:label, project: project, name: 'Development') }
let_it_be(:testing) { create(:label, project: project, name: 'Testing') }
let_it_be(:backlog) { create(:label, project: project, name: 'Backlog') }
let_it_be(:closed) { create(:label, project: project, name: 'Closed') }
let_it_be(:list1) { create(:list, board: board, label: planning, position: 0) }
let_it_be(:list2) { create(:list, board: board, label: development, position: 1) }
let_it_be(:confidential_issue) { create(:labeled_issue, :confidential, project: project, author: user, labels: [planning], relative_position: 9) }
let_it_be(:issue1) { create(:labeled_issue, project: project, title: 'aaa', description: '111', assignees: [user], labels: [planning], relative_position: 8) }
let_it_be(:issue2) { create(:labeled_issue, project: project, title: 'bbb', description: '222', author: user2, labels: [planning], relative_position: 7) }
let_it_be(:issue3) { create(:labeled_issue, project: project, title: 'ccc', description: '333', labels: [planning, testing], relative_position: 6) }
let_it_be(:issue4) { create(:labeled_issue, project: project, title: 'ddd', description: '444', labels: [planning], relative_position: 5) }
let_it_be(:issue5) { create(:labeled_issue, project: project, title: 'eee', description: '555', labels: [planning], milestone: milestone, relative_position: 4) }
let_it_be(:issue6) { create(:labeled_issue, project: project, title: 'fff', description: '666', labels: [planning, development], relative_position: 3) }
let_it_be(:issue7) { create(:labeled_issue, project: project, title: 'ggg', description: '777', labels: [development], relative_position: 2) }
let_it_be(:issue8) { create(:closed_issue, project: project, title: 'hhh', description: '888') }
context 'filtering' do
before do
project.add_maintainer(user)
project.add_maintainer(user2)
stub_licensed_features(epics: true)
sign_in(user)
visit_board_page
page.within('.board-swimlanes-toggle-wrapper') do
page.find('.dropdown-toggle').click
page.find('.dropdown-item', text: 'Epic').click
end
stub_const("Gitlab::QueryLimiting::Transaction::THRESHOLD", 200)
end
it 'filters by author' do
set_filter("author", user2.username)
click_filter_link(user2.username)
submit_filter
wait_for_requests
wait_for_board_cards(2, 1)
wait_for_empty_boards((3..4))
end
it 'filters by assignee' do
set_filter("assignee", user.username)
click_filter_link(user.username)
submit_filter
wait_for_requests
wait_for_board_cards(2, 1)
wait_for_empty_boards((3..4))
end
it 'filters by milestone' do
set_filter("milestone", "\"#{milestone.title}")
click_filter_link(milestone.title)
submit_filter
wait_for_requests
wait_for_board_cards(2, 1)
wait_for_board_cards(3, 0)
wait_for_board_cards(4, 0)
end
it 'filters by label' do
set_filter("label", testing.title)
click_filter_link(testing.title)
submit_filter
wait_for_requests
wait_for_board_cards(2, 1)
wait_for_empty_boards((3..4))
end
end
def visit_board_page
visit project_boards_path(project)
wait_for_requests
end
def wait_for_board_cards(board_number, expected_cards)
page.within(find(".board-swimlanes-headers .board:nth-child(#{board_number})")) do
expect(page.find('.board-header')).to have_content(expected_cards.to_s)
end
page.within(find("[data-testid='board-lane-unassigned-issues'] .board:nth-child(#{board_number})")) do
expect(page).to have_selector('.board-card', count: expected_cards)
end
end
def wait_for_empty_boards(board_numbers)
board_numbers.each do |board|
wait_for_board_cards(board, 0)
end
end
def set_filter(type, text)
find('.filtered-search').native.send_keys("#{type}:=#{text}")
end
def submit_filter
find('.filtered-search').native.send_keys(:enter)
end
def click_filter_link(link_text)
page.within('.filtered-search-box') do
expect(page).to have_button(link_text)
click_button(link_text)
end
end
end
...@@ -22,29 +22,27 @@ RSpec.describe 'epics swimlanes', :js do ...@@ -22,29 +22,27 @@ RSpec.describe 'epics swimlanes', :js do
let_it_be(:epic_issue2) { create(:epic_issue, epic: epic2, issue: issue2) } let_it_be(:epic_issue2) { create(:epic_issue, epic: epic2, issue: issue2) }
context 'switch to swimlanes view' do context 'switch to swimlanes view' do
context 'feature flag on' do before do
before do stub_licensed_features(epics: true)
stub_licensed_features(epics: true) sign_in(user)
sign_in(user) visit_board_page
visit_board_page
page.within('.board-swimlanes-toggle-wrapper') do
page.within('.board-swimlanes-toggle-wrapper') do page.find('.dropdown-toggle').click
page.find('.dropdown-toggle').click page.find('.dropdown-item', text: 'Epic').click
page.find('.dropdown-item', text: 'Epic').click
end
end end
end
it 'displays epics swimlanes when selecting Epic in Group by dropdown' do it 'displays epics swimlanes when selecting Epic in Group by dropdown' do
expect(page).to have_css('.board-swimlanes') expect(page).to have_css('.board-swimlanes')
epic_lanes = page.all(:css, '.board-epic-lane') epic_lanes = page.all(:css, '.board-epic-lane')
expect(epic_lanes.length).to eq(2) expect(epic_lanes.length).to eq(2)
end end
it 'displays issue not assigned to epic in unassigned issues lane' do it 'displays issue not assigned to epic in unassigned issues lane' do
page.within('.board-lane-unassigned-issues') do page.within('.board-lane-unassigned-issues-title') do
expect(page.find('span[data-testid="issues-lane-issue-count"]')).to have_content('1') expect(page.find('span[data-testid="issues-lane-issue-count"]')).to have_content('1')
end
end end
end end
end end
......
...@@ -6,6 +6,7 @@ import AxiosMockAdapter from 'axios-mock-adapter'; ...@@ -6,6 +6,7 @@ import AxiosMockAdapter from 'axios-mock-adapter';
import BoardListHeader from 'ee/boards/components/board_list_header.vue'; import BoardListHeader from 'ee/boards/components/board_list_header.vue';
import { TEST_HOST } from 'helpers/test_constants'; import { TEST_HOST } from 'helpers/test_constants';
import { listObj } from 'jest/boards/mock_data'; import { listObj } from 'jest/boards/mock_data';
import getters from 'ee/boards/stores/getters';
import List from '~/boards/models/list'; import List from '~/boards/models/list';
import { ListType, inactiveId } from '~/boards/constants'; import { ListType, inactiveId } from '~/boards/constants';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
...@@ -28,7 +29,7 @@ describe('Board List Header Component', () => { ...@@ -28,7 +29,7 @@ describe('Board List Header Component', () => {
window.gon = {}; window.gon = {};
axiosMock = new AxiosMockAdapter(axios); axiosMock = new AxiosMockAdapter(axios);
axiosMock.onGet(`${TEST_HOST}/lists/1/issues`).reply(200, { issues: [] }); axiosMock.onGet(`${TEST_HOST}/lists/1/issues`).reply(200, { issues: [] });
store = new Vuex.Store({ state: { activeId: inactiveId } }); store = new Vuex.Store({ state: { activeId: inactiveId }, getters });
jest.spyOn(store, 'dispatch').mockImplementation(); jest.spyOn(store, 'dispatch').mockImplementation();
}); });
......
...@@ -13,6 +13,44 @@ const expectNotImplemented = action => { ...@@ -13,6 +13,44 @@ const expectNotImplemented = action => {
}); });
}; };
describe('setFilters', () => {
it('should commit mutation SET_FILTERS, updates epicId with global id', done => {
const state = {
filters: {},
};
const filters = { labelName: 'label', epicId: 1 };
const updatedFilters = { labelName: 'label', epicId: 'gid://gitlab/Epic/1' };
testAction(
actions.setFilters,
filters,
state,
[{ type: types.SET_FILTERS, payload: updatedFilters }],
[],
done,
);
});
it('should commit mutation SET_FILTERS, updates epicWildcardId', done => {
const state = {
filters: {},
};
const filters = { labelName: 'label', epicId: 'None' };
const updatedFilters = { labelName: 'label', epicWildcardId: 'NONE' };
testAction(
actions.setFilters,
filters,
state,
[{ type: types.SET_FILTERS, payload: updatedFilters }],
[],
done,
);
});
});
describe('setShowLabels', () => { describe('setShowLabels', () => {
it('should commit mutation SET_SHOW_LABELS', done => { it('should commit mutation SET_SHOW_LABELS', done => {
const state = { const state = {
......
...@@ -452,7 +452,7 @@ msgstr "" ...@@ -452,7 +452,7 @@ msgstr ""
msgid "%{issuableType} will be removed! Are you sure?" msgid "%{issuableType} will be removed! Are you sure?"
msgstr "" msgstr ""
msgid "%{issuesSize} issues with a limit of %{maxIssueCount}" msgid "%{issuesCount} issues with a limit of %{maxIssueCount}"
msgstr "" msgstr ""
msgid "%{issuesSize} with a limit of %{maxIssueCount}" msgid "%{issuesSize} with a limit of %{maxIssueCount}"
......
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