Commit e5d0d00e authored by Scott Stern's avatar Scott Stern Committed by Stan Hu

Wrk in prog limits on board lists foundation

Add functionality for boards list
feature flagged to see the wip limits
parent 2fc23fff
import $ from 'jquery';
import Sortable from 'sortablejs';
import Vue from 'vue';
import { GlButtonGroup, GlButton, GlTooltip } from '@gitlab/ui';
import { n__, s__ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
import Tooltip from '~/vue_shared/directives/tooltip';
import isWipLimitsOn from 'ee_else_ce/boards/mixins/is_wip_limits';
import AccessorUtilities from '../../lib/utils/accessor';
import BoardBlankState from './board_blank_state.vue';
import BoardDelete from './board_delete';
import BoardList from './board_list.vue';
import IssueCount from './issue_count.vue';
import boardsStore from '../stores/boards_store';
import { getBoardSortableDefaultOptions, sortableEnd } from '../mixins/sortable_default_options';
import { ListType } from '../constants';
export default Vue.extend({
components: {
......@@ -17,10 +21,15 @@ export default Vue.extend({
BoardDelete,
BoardList,
Icon,
GlButtonGroup,
IssueCount,
GlButton,
GlTooltip,
},
directives: {
Tooltip,
},
mixins: [isWipLimitsOn],
props: {
list: {
type: Object,
......@@ -53,6 +62,11 @@ export default Vue.extend({
isLoggedIn() {
return Boolean(gon.current_user_id);
},
showListHeaderButton() {
return (
!this.disabled && this.list.type !== ListType.closed && this.list.type !== ListType.blank
);
},
counterTooltip() {
const { issuesSize } = this.list;
return `${n__('%d issue', '%d issues', issuesSize)}`;
......@@ -61,11 +75,19 @@ export default Vue.extend({
return this.list.isExpanded ? s__('Boards|Collapse') : s__('Boards|Expand');
},
isNewIssueShown() {
return this.list.type === ListType.backlog || this.showListHeaderButton;
},
isSettingsShown() {
return (
this.list.type === 'backlog' ||
(!this.disabled && this.list.type !== 'closed' && this.list.type !== 'blank')
this.list.type !== ListType.backlog &&
this.showListHeaderButton &&
this.list.isExpanded &&
this.isWipLimitsOn
);
},
showBoardListAndBoardInfo() {
return this.list.type !== ListType.blank && this.list.type !== ListType.promotion;
},
uniqueKey() {
// eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings
return `boards.${this.boardId}.${this.list.type}.${this.list.id}`;
......
......@@ -71,6 +71,9 @@ export default {
total: this.list.issuesSize,
});
},
issuesSizeExceedsMax() {
return this.list.maxIssueCount > 0 && this.list.issuesSize > this.list.maxIssueCount;
},
},
watch: {
filters: {
......@@ -435,7 +438,7 @@ export default {
ref="list"
:data-board="list.id"
:data-board-type="list.type"
:class="{ 'is-smaller': showIssueForm }"
:class="{ 'is-smaller': showIssueForm, 'bg-danger-100': issuesSizeExceedsMax }"
class="board-list w-100 h-100 list-unstyled mb-0 p-1 js-board-list"
>
<board-card
......
<script>
export default {
name: 'IssueCount',
props: {
maxIssueCount: {
type: Number,
required: false,
default: 0,
},
issuesSize: {
type: Number,
required: false,
default: 0,
},
},
computed: {
isMaxLimitSet() {
return this.maxIssueCount !== 0;
},
issuesExceedMax() {
return this.isMaxLimitSet && this.issuesSize > this.maxIssueCount;
},
},
};
</script>
<template>
<div class="issue-count">
<span class="js-issue-size" :class="{ 'text-danger': issuesExceedMax }">
{{ issuesSize }}
</span>
<span v-if="isMaxLimitSet" class="js-max-issue-size">
{{ maxIssueCount }}
</span>
</div>
</template>
......@@ -4,6 +4,8 @@ export const ListType = {
backlog: 'backlog',
closed: 'closed',
label: 'label',
promotion: 'promotion',
blank: 'blank',
};
export default {
......
export default {
computed: {
isWipLimitsOn() {
return false;
},
},
};
......@@ -52,6 +52,9 @@ class List {
this.loadingMore = false;
this.issues = obj.issues || [];
this.issuesSize = obj.issuesSize ? obj.issuesSize : 0;
this.maxIssueCount = Object.hasOwnProperty.call(obj, 'max_issue_count')
? obj.max_issue_count
: 0;
this.defaultAvatar = defaultAvatar;
if (obj.label) {
......
......@@ -72,7 +72,7 @@ export default {
{{ __('Related merge requests') }}
</span>
<div v-if="totalCount" class="d-inline-flex lh-100 align-middle">
<div class="mr-count-badge">
<div class="mr-count-badge border-width-1px border-style-solid border-color-default">
<div class="mr-count-badge-count">
<svg class="s16 mr-1 text-secondary">
<icon name="merge-request" class="mr-1 text-secondary" />
......
......@@ -187,6 +187,10 @@
font-size: 1em;
border-bottom: 1px solid $border-color;
padding: $gl-padding-8 $gl-padding;
.js-max-issue-size::before {
content: '/';
}
}
.board-title-text {
......
......@@ -2,7 +2,6 @@
.mr-count-badge {
display: inline-flex;
border-radius: $border-radius-base;
border: 1px solid $border-color;
padding: 5px $gl-padding-8;
}
......
......@@ -9,6 +9,7 @@ module BoardsActions
before_action :boards, only: :index
before_action :board, only: :show
before_action :push_wip_limits, only: :index
end
def index
......@@ -24,6 +25,10 @@ module BoardsActions
private
# Noop on FOSS
def push_wip_limits
end
def boards
strong_memoize(:boards) do
Boards::ListService.new(parent, current_user).execute
......
......@@ -42,23 +42,27 @@
%button.board-delete.no-drag.p-0.border-0.has-tooltip.float-right{ type: "button", title: _("Delete list"), ":class": "{ 'd-none': !list.isExpanded }", "aria-label" => _("Delete list"), data: { placement: "bottom" }, "@click.stop" => "deleteBoard" }
= icon("trash")
.issue-count-badge.no-drag.text-secondary{ "v-if" => 'list.type !== "blank" && list.type !== "promotion"', ":title": "counterTooltip", "v-tooltip": true, data: { placement: "top" } }
.issue-count-badge.pr-0.no-drag.text-secondary{ "v-if" => "showBoardListAndBoardInfo", ":title": "counterTooltip", "v-tooltip": true, data: { placement: "top" } }
%span.d-inline-flex
%span.issue-count-badge-count
%icon.mr-1{ name: "issues" }
{{ list.issuesSize }}
%issue-count{ ":maxIssueCount" => "list.maxIssueCount",
":issuesSize" => "list.issuesSize" }
= render_if_exists "shared/boards/components/list_weight"
%button.issue-count-badge-add-button.no-drag.btn.btn-sm.btn-default.ml-1.has-tooltip{ type: "button",
%gl-button-group.board-list-button-group.pl-2{ "v-if" => "isNewIssueShown || isSettingsShown" }
%gl-button.issue-count-badge-add-button.no-drag{ type: "button",
"@click" => "showNewIssueForm",
"v-if" => "isNewIssueShown",
":class": "{ 'd-none': !list.isExpanded }",
":class": "{ 'd-none': !list.isExpanded, 'rounded-right': isNewIssueShown && !isSettingsShown }",
"aria-label" => _("New issue"),
"title" => _("New issue"),
data: { placement: "top", container: "body" } }
"ref" => "newIssueBtn" }
= icon("plus")
%gl-tooltip{ ":target" => "() => $refs.newIssueBtn" }
= _("New Issue")
= render_if_exists 'shared/boards/components/list_settings'
%board-list{ "v-if" => 'list.type !== "blank" && list.type !== "promotion"',
%board-list{ "v-if" => "showBoardListAndBoardInfo",
":list" => "list",
":issues" => "list.issues",
":loading" => "list.loading",
......
export default {
computed: {
isWipLimitsOn() {
return gon.features.wipLimits;
},
},
};
......@@ -180,7 +180,7 @@ export default {
</a>
<div class="d-inline-flex lh-100 align-middle">
<div
class="js-related-issues-header-issue-count related-issues-header-issue-count issue-count-badge mx-1"
class="js-related-issues-header-issue-count related-issues-header-issue-count issue-count-badge mx-1 border-width-1px border-style-solid border-color-default"
>
<span class="issue-count-badge-count">
<icon :name="issuableTypeIcon" class="mr-1 text-secondary" />
......
......@@ -7,5 +7,13 @@ module EE
prepended do
include ::MultipleBoardsActions
end
private
def push_wip_limits
if parent.feature_available?(:wip_limits)
push_frontend_feature_flag(:wip_limits)
end
end
end
end
......@@ -5,6 +5,10 @@ module EE
module ListsController
extend ::Gitlab::Utils::Override
included do
before_action :push_wip_limits
end
override :list_creation_attrs
def list_creation_attrs
additional_attrs = %i[assignee_id milestone_id]
......
- if current_board_parent.feature_available?(:wip_limits)
%gl-button.no-drag.rounded-right{ type: "button",
"v-if" => "isSettingsShown",
"aria-label" => _("List Settings"),
"ref" => "settingsBtn",
"title" => _("List Settings") }
= sprite_icon("settings")
%gl-tooltip{ ":target" => "() => $refs.settingsBtn" }
= _("List Settings")
......@@ -161,6 +161,63 @@ describe 'issue boards', :js do
end
end
context 'list header' do
let(:max_issue_count) { 2 }
let!(:label) { create(:label, project: project, name: 'Label 2') }
let!(:list) { create(:list, board: board, label: label, position: 0, max_issue_count: max_issue_count) }
let!(:issue) { create(:issue, project: project, labels: [label]) }
before do
project.add_developer(user)
login_as(user)
visit_board_page
end
context 'When FF is turned on' do
before do
stub_licensed_features(wip_limits: true)
end
context 'when max issue count is set' do
let(:total_development_issues) { "1" }
it 'displays issue and max issue size' do
page.within(find(".board:nth-child(2)")) do
expect(page.find('.js-issue-size')).to have_text(total_development_issues)
expect(page.find('.js-max-issue-size')).to have_text(max_issue_count)
end
end
end
end
end
context 'list settings' do
let!(:label) { create(:label, project: project, name: 'Label') }
let!(:list) { create(:list, board: board, label: label, position: 1) }
before do
project.add_developer(user)
login_as(user)
visit project_boards_path(project)
end
context 'When FF is turned on' do
before do
stub_licensed_features(wip_limits: true)
end
it 'shows the list settings button' do
expect(page).to have_selector(:button, "List Settings")
end
end
context 'When FF is turned off' do
it 'shows the list settings button' do
expect(page).to have_no_selector(:button, "List Settings")
end
end
end
def badge(list)
find(".board[data-id='#{list.id}'] .issue-count-badge")
end
......
......@@ -10291,6 +10291,9 @@ msgstr ""
msgid "List"
msgstr ""
msgid "List Settings"
msgstr ""
msgid "List Your Gitea Repositories"
msgstr ""
......
......@@ -5,6 +5,7 @@ FactoryBot.define do
board
label
list_type { :label }
max_issue_count { 0 }
sequence(:position)
end
......
......@@ -72,7 +72,6 @@ describe 'Issue Boards', :js do
let!(:closed) { create(:label, project: project, name: 'Closed') }
let!(:accepting) { create(:label, project: project, name: 'Accepting Merge Requests') }
let!(:a_plus) { create(:label, project: project, name: 'A+') }
let!(:list1) { create(:list, board: board, label: planning, position: 0) }
let!(:list2) { create(:list, board: board, label: development, position: 1) }
......@@ -289,6 +288,17 @@ describe 'Issue Boards', :js do
expect(page).to have_selector('.avatar', count: 1)
end
end
context 'list header' do
let(:total_planning_issues) { "8" }
it 'shows issue count on the list' do
page.within(find(".board:nth-child(2)")) do
expect(page.find('.js-issue-size')).to have_text(total_planning_issues)
expect(page).not_to have_selector('.js-max-issue-size')
end
end
end
end
context 'new list' do
......
import { shallowMount } from '@vue/test-utils';
import IssueCount from '~/boards/components/issue_count.vue';
describe('IssueCount', () => {
let vm;
let maxIssueCount;
let issuesSize;
const createComponent = props => {
vm = shallowMount(IssueCount, { propsData: props });
};
afterEach(() => {
maxIssueCount = 0;
issuesSize = 0;
if (vm) vm.destroy();
});
describe('when maxIssueCount is zero', () => {
beforeEach(() => {
issuesSize = 3;
createComponent({ maxIssueCount: 0, issuesSize });
});
it('contains issueSize in the template', () => {
expect(vm.find('.js-issue-size').text()).toEqual(String(issuesSize));
});
it('does not contains maxIssueCount in the template', () => {
expect(vm.contains('.js-max-issue-size')).toBe(false);
});
});
describe('when maxIssueCount is greater than zero', () => {
beforeEach(() => {
maxIssueCount = 2;
issuesSize = 1;
createComponent({ maxIssueCount, issuesSize });
});
afterEach(() => {
vm.destroy();
});
it('contains issueSize in the template', () => {
expect(vm.find('.js-issue-size').text()).toEqual(String(issuesSize));
});
it('contains maxIssueCount in the template', () => {
expect(vm.find('.js-max-issue-size').text()).toEqual(String(maxIssueCount));
});
it('does not have text-danger class when issueSize is less than maxIssueCount', () => {
expect(vm.classes('.text-danger')).toBe(false);
});
});
describe('when issueSize is greater than maxIssueCount', () => {
beforeEach(() => {
issuesSize = 3;
maxIssueCount = 2;
createComponent({ maxIssueCount, issuesSize });
});
afterEach(() => {
vm.destroy();
});
it('contains issueSize in the template', () => {
expect(vm.find('.js-issue-size').text()).toEqual(String(issuesSize));
});
it('contains maxIssueCount in the template', () => {
expect(vm.find('.js-max-issue-size').text()).toEqual(String(maxIssueCount));
});
it('has text-danger class', () => {
expect(vm.find('.text-danger').text()).toEqual(String(issuesSize));
});
});
});
......@@ -207,4 +207,56 @@ describe('Board list component', () => {
.catch(done.fail);
});
});
describe('max issue count warning', () => {
beforeEach(done => {
({ mock, component } = createComponent({
done,
listProps: { type: 'closed', collapsed: true, issuesSize: 50 },
}));
});
afterEach(() => {
mock.restore();
component.$destroy();
});
describe('when issue count exceeds max issue count', () => {
it('sets background to bg-danger-100', done => {
component.list.issuesSize = 4;
component.list.maxIssueCount = 3;
Vue.nextTick(() => {
expect(component.$el.querySelector('.bg-danger-100')).not.toBeNull();
done();
});
});
});
describe('when list issue count does NOT exceed list max issue count', () => {
it('does not sets background to bg-danger-100', done => {
component.list.issuesSize = 2;
component.list.maxIssueCount = 3;
Vue.nextTick(() => {
expect(component.$el.querySelector('.bg-danger-100')).toBeNull();
done();
});
});
});
describe('when list max issue count is 0', () => {
it('does not sets background to bg-danger-100', done => {
component.list.maxIssueCount = 0;
Vue.nextTick(() => {
expect(component.$el.querySelector('.bg-danger-100')).toBeNull();
done();
});
});
});
});
});
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