Commit 59d29e78 authored by Simon Knox's avatar Simon Knox

Merge branch 'issue-board-edit-modal' into edit-board

parents 58565ac5 605c9059
......@@ -13,6 +13,7 @@
</p>
<form
v-else
class="js-board-config-modal"
>
<div
v-if="!readonly"
......@@ -52,9 +53,6 @@
<!-- TODO: if current_board_parent.issue_board_milestone_available?(current_user) -->
<form-block
title="Milestone"
defaultText="Any milestone"
:canEdit="canAdminBoard"
>
<div
v-if="board.milestone"
......@@ -65,31 +63,21 @@
<board-milestone-select
:board="board"
:milestone-path="milestonePath"
v-model="board.milestone_id">
</board-milestone-select>
</form-block>
<form-block
title="Labels"
defaultText="Any label"
:canEdit="canAdminBoard"
>
</form-block>
<form-block
title="Assignee"
defaultText="Any assignee"
:fieldName="'board_filter[assignee]'"
:canEdit="canAdminBoard"
>
v-model="board.milestone_id"
title="Milestone"
defaultText="Any milestone"
:canEdit="canAdminBoard"
/>
</form-block>
<form-block
title="Author"
defaultText="Any author"
:fieldName="'board_filter[author]'"
:canEdit="canAdminBoard"
>
<form-block>
<board-labels-select
:board="board"
title="Labels"
defaultText="Any label"
:canEdit="canAdminBoard"
:labelsPath="labelsPath"
/>
</form-block>
<form-block
......@@ -98,6 +86,13 @@
:fieldName="'board_filter[weight]'"
:canEdit="canAdminBoard"
>
<board-weight-select
:board="board"
v-model="board.weight"
title="Weight"
defaultText="Any weight"
:canEdit="canAdminBoard"
/>
</form-block>
</div>
</form>
......@@ -115,6 +110,8 @@ import Vue from 'vue';
import PopupDialog from '~/vue_shared/components/popup_dialog.vue';
import FormBlock from './form_block.vue';
import BoardMilestoneSelect from './milestone_select.vue';
import BoardWeightSelect from './weight_select.vue';
import BoardLabelsSelect from './labels_select.vue';
window.gl = window.gl || {};
window.gl.issueBoards = window.gl.issueBoards || {};
......@@ -127,6 +124,11 @@ export default Vue.extend({
type: String,
required: true,
},
labelsPath: {
type: String,
required: false,
default: '/root/my-rails/labels.json',
},
canAdminBoard: {
type: Boolean,
required: true,
......@@ -134,10 +136,7 @@ export default Vue.extend({
},
data() {
return {
board: {
id: false,
name: '',
},
board: Store.boardConfig,
expanded: false,
issue: {},
currentBoard: Store.state.currentBoard,
......@@ -148,13 +147,26 @@ export default Vue.extend({
},
components: {
BoardMilestoneSelect,
BoardLabelsSelect,
BoardWeightSelect,
PopupDialog,
FormBlock,
},
mounted() {
if (this.currentBoard && Object.keys(this.currentBoard).length && this.currentPage !== 'new') {
this.board = Vue.util.extend({}, this.currentBoard);
Store.updateBoardConfig(this.currentBoard);
} else {
Store.updateBoardConfig({
name: '',
id: false,
label_ids: [],
});
}
if (!this.board.labels) {
this.board.labels = [];
}
if (this.$refs.name) {
this.$refs.name.focus();
}
......@@ -199,7 +211,7 @@ export default Vue.extend({
return false;
},
expandButtonText() {
return this.expanded ? 'Collapse' : 'Expand'
return this.expanded ? 'Collapse' : 'Expand';
},
collapseScope() {
return this.currentPage === 'new';
......@@ -212,19 +224,6 @@ export default Vue.extend({
refreshPage() {
location.href = location.pathname;
},
loadMilestones(e) {
this.milestoneDropdownOpen = !this.milestoneDropdownOpen;
BoardService.loadMilestones.call(this);
if (this.milestoneDropdownOpen) {
this.$nextTick(() => {
const milestoneDropdown = this.$refs.milestoneDropdown;
const rect = e.target.getBoundingClientRect();
milestoneDropdown.style.width = `${rect.width}px`;
});
}
},
submit() {
gl.boardService.createBoard(this.board)
.then(resp => resp.json())
......@@ -252,13 +251,6 @@ export default Vue.extend({
cancel() {
Store.state.currentPage = '';
},
selectMilestone(milestone) {
this.milestoneDropdownOpen = false;
this.board.milestone_id = milestone.id;
this.board.milestone = {
title: milestone.title,
};
},
},
});
</script>
......@@ -42,6 +42,7 @@ export default {
this.error = false;
// TODO: add board labels
const labels = this.list.label ? [this.list.label] : [];
const issue = new ListIssue({
title: this.title,
......
<template>
<div class="list-item">
<div class="media">
<label class="label-light media-body">{{ title }}</label>
<a
v-if="canEdit"
class="edit-link"
href="#"
@click.prevent="toggleEditing"
>
Edit
</a>
</div>
<slot></slot>
<div>
<slot name="currentValue">
{{ defaultText }}
</slot>
</div>
</div>
</template>
......@@ -25,23 +9,6 @@ import eventHub from '../eventhub';
export default {
props: {
defaultText: {
type: String,
required: true,
},
title: {
type: String,
required: true,
},
fieldName: {
type: String,
required: false,
},
canEdit: {
type: Boolean,
required: false,
default: false,
}
},
data() {
return {
......@@ -54,4 +21,4 @@ export default {
},
},
};
</script>
\ No newline at end of file
</script>
<template>
<div class="block labels">
<div class="title append-bottom-10">
Labels <i aria-hidden="true" class="fa fa-spinner fa-spin block-loading" data-hidden="true" style="display: none;"></i> <a class="edit-link pull-right" href="#">Edit</a>
</div>
<div class="value issuable-show-labels">
<span v-if="board.labels.length === 0" class="no-value">
Any label
</span>
<a
href="#"
v-for="label in board.labels"
:key="label.id"
>
<span
class="label color-label has-tooltip"
:style="`background-color: ${label.color}; color: ${label.textColor};`"
title=""
>
{{ label.title }}
</span>
</a>
</div>
<div class="selectbox" style="display: none">
<div class="dropdown">
<button
class="dropdown-menu-toggle wide js-label-select js-multiselect js-board-config-modal"
data-field-name="issue[label_names][]"
v-bind:data-labels="labelsPath"
data-toggle="dropdown"
type="button"
>
<span class="dropdown-toggle-text">
Label
</span> <i aria-hidden="true" class="fa fa-chevron-down" data-hidden="true"></i>
</button>
<div class="dropdown-menu dropdown-select dropdown-menu-paging dropdown-menu-labels dropdown-menu-selectable">
<div class="dropdown-input">
<input
autocomplete="off"
class="dropdown-input-field" id=""
placeholder="Search"
type="search"
value=""
>
<i aria-hidden="true" class="fa fa-search dropdown-input-search" data-hidden="true"></i>
<i aria-hidden="true" class="fa fa-times dropdown-input-clear js-dropdown-input-clear" data-hidden="true" role="button"></i>
</div>
<div class="dropdown-content"></div>
<div class="dropdown-loading">
<i aria-hidden="true" class="fa fa-spinner fa-spin" data-hidden="true"></i>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
/* global LabelsSelect */
import loadingIcon from '~/vue_shared/components/loading_icon.vue';
import eventHub from '../eventhub';
export default {
props: {
board: {
type: Object,
required: true,
},
labelsPath: {
type: String,
required: true,
},
value: {
type: Array,
required: false,
},
defaultText: {
type: String,
required: true,
},
title: {
type: String,
required: true,
},
canEdit: {
type: Boolean,
required: false,
default: false,
},
},
components: {
loadingIcon,
},
data() {
return {
isOpen: false,
loading: true,
};
},
mounted() {
new LabelsSelect();
},
methods: {
open() {
this.isOpen = true;
},
close() {
this.isOpen = false;
},
toggle() {
this.isOpen = !this.isOpen;
},
},
};
</script>
<template>
<div class="dropdown" :class="{ open: isOpen }">
<div class="media">
<label class="label-light media-body">{{ title }}</label>
<a
v-if="canEdit"
class="edit-link"
href="#"
@click.prevent="toggle"
>
Edit
</a>
</div>
<div
class="dropdown-menu dropdown-menu-wide"
>
<div class="dropdown-input">
<input
ref="search"
class="dropdown-input-field"
type="search"
placeholder="Search milestones">
<i aria-hidden="true" data-hidden="true" class="fa fa-search dropdown-input-search"></i>
</div>
<ul
ref="list"
>
......@@ -47,6 +50,9 @@
</li>
</ul>
</div>
<div>
{{ milestoneTitle }}
</div>
</div>
</template>
......@@ -71,10 +77,28 @@ export default {
type: Number,
required: false,
},
defaultText: {
type: String,
required: true,
},
title: {
type: String,
required: true,
},
canEdit: {
type: Boolean,
required: false,
default: false,
},
},
components: {
loadingIcon,
},
computed: {
milestoneTitle() {
return this.board.milestone ? this.board.milestone.title : this.defaultText;
},
},
data() {
return {
isOpen: false,
......@@ -84,13 +108,18 @@ export default {
};
},
mounted() {
BoardService.loadMilestones.call(this).then(() => this.loading = false);
this.$http.get(this.milestonePath)
.then(resp => resp.json())
.then((data) => {
this.milestones = data;
this.loading = false;
})
.catch(() => {
this.loading = false;
});
eventHub.$on('open', this.open);
eventHub.$on('close', this.close);
eventHub.$on('toggle', this.toggle);
this.$nextTick(() => {
this.$refs.search.focus();
});
},
beforeDestroy() {
eventHub.$off('open', this.open);
......@@ -99,7 +128,7 @@ export default {
},
methods: {
selectMilestone(milestone) {
this.board.milestone = milestone;
this.$set(this.board, 'milestone', milestone);
this.$emit('input', milestone.id);
this.close();
},
......
<template>
<div class="dropdown" :class="{ open: isOpen }">
<div class="media">
<label class="label-light media-body">{{ title }}</label>
<a
v-if="canEdit"
class="edit-link"
href="#"
@click.prevent="toggle"
>
Edit
</a>
</div>
<div
class="dropdown-menu dropdown-menu-wide"
>
<ul
ref="list"
>
<li>
<a
href="#"
@click.prevent.stop="selectWeight(0)"
>
<i
class="fa fa-check"
v-if="0 === value"></i>
No weight
</a>
</li>
<li
v-for="weight in weights"
:key="weight.id"
>
<a
href="#"
@click.prevent.stop="selectWeight(weight)">
<i
class="fa fa-check"
v-if="weight === value"></i>
{{ weight }}
</a>
</li>
</ul>
</div>
<div>
{{ weight }}
</div>
</div>
</template>
<script>
/* global BoardService */
import loadingIcon from '~/vue_shared/components/loading_icon.vue';
import eventHub from '../eventhub';
export default {
props: {
board: {
type: Object,
required: true,
},
value: {
type: Number,
required: false,
},
defaultText: {
type: String,
required: true,
},
title: {
type: String,
required: true,
},
canEdit: {
type: Boolean,
required: false,
default: false,
},
},
components: {
loadingIcon,
},
computed: {
weight() {
if (parseInt(this.board.weight, 10) === 0) {
return 'No weight';
}
return this.board.weight || 'Any weight';
},
},
data() {
return {
isOpen: false,
// TODO: use Issue.weight_options from backend
weights: [1, 2, 3, 4, 5, 6, 7, 8, 9],
};
},
methods: {
selectWeight(weight) {
this.$set(this.board, 'weight', weight);
// this.$emit('input', weight);
this.close();
},
open() {
this.isOpen = true;
},
close() {
this.isOpen = false;
},
toggle() {
this.isOpen = !this.isOpen;
},
},
};
</script>
......@@ -99,17 +99,6 @@ class BoardService {
return this.issues.bulkUpdate(data);
}
static loadMilestones(path) {
this.loading = true;
return this.$http.get(this.milestonePath)
.then(resp => resp.json())
.then((data) => {
this.milestones = data;
this.loading = false;
});
}
}
window.BoardService = BoardService;
......@@ -19,7 +19,12 @@ gl.issueBoards.BoardsStore = {
reload: false,
},
detail: {
issue: {}
issue: {},
},
boardConfig: {
id: false,
name: '',
labels: [],
},
moving: {
issue: {},
......@@ -28,7 +33,9 @@ gl.issueBoards.BoardsStore = {
create () {
this.state.lists = [];
this.filter.path = getUrlParamsArray().join('&');
this.detail = { issue: {} };
this.detail = {
issue: {},
};
},
createNewListDropdownData() {
this.state.currentBoard = {};
......@@ -39,6 +46,19 @@ gl.issueBoards.BoardsStore = {
this.state.reload = false;
this.state.currentPage = page;
},
updateBoardConfig({
name,
id,
milestone,
milestone_id,
labels = [],
}) {
this.boardConfig.name = name;
this.boardConfig.milestone = milestone;
this.boardConfig.milestone_id = milestone_id;
this.boardConfig.id = id;
this.boardConfig.labels = labels;
},
addList (listObj, defaultAvatar) {
const list = new List(listObj, defaultAvatar);
this.state.lists.push(list);
......
......@@ -390,6 +390,23 @@ import DropdownUtils from './filtered_search/dropdown_utils';
.then(fadeOutLoader)
.catch(fadeOutLoader);
}
else if ($dropdown.hasClass('js-board-config-modal')) {
if ($el.hasClass('is-active')) {
gl.issueBoards.BoardsStore.boardConfig.labels.push(new ListLabel({
id: label.id,
title: label.title,
color: label.color[0],
textColor: '#fff'
}));
}
else {
let labels = gl.issueBoards.BoardsStore.boardConfig.labels;
labels = labels.filter(function (selectedLabel) {
return selectedLabel.id !== label.id;
});
gl.issueBoards.BoardsStore.boardConfig.labels = labels;
}
}
else {
if ($dropdown.hasClass('js-multiselect')) {
......
......@@ -442,6 +442,8 @@ function UsersSelect(currentUser, els) {
}
if ($el.closest('.add-issues-modal').length) {
gl.issueBoards.ModalStore.store.filter[$dropdown.data('field-name')] = user.id;
} else if ($el.closest('js-board-config-modal').length) {
gl.issueBoards.BoardsStore.boardConfig.authorId = user.id;
} else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
return Issuable.filterResults($dropdown.closest('form'));
} else if ($dropdown.hasClass('js-filter-submit')) {
......
......@@ -54,9 +54,7 @@ body.modal-open {
}
.modal.popup-dialog {
display: flex;
justify-content: center;
align-items: center;
display: block;
@media (min-width: $screen-md-min) {
.modal-dialog {
......
require 'rails_helper'
describe 'issue board config', :js do
let(:user) { create(:user) }
let(:project) { create(:project, :public) }
let!(:planning) { create(:label, project: project, name: 'Planning') }
let!(:board) { create(:board, project: project) }
before do
stub_licensed_features(multiple_issue_boards: true)
end
context 'user with edit permissions' do
before do
project.team << [user, :master]
login_as(user)
visit project_boards_path(project)
wait_for_requests
end
it 'edits board' do
click_button 'Edit board'
page.within('.popup-dialog') do
fill_in 'board-new-name', with: 'Testing'
click_button 'Save'
end
expect('.dropdown-menu-toggle', text: 'Testing').to exist
end
end
context 'user without edit permissions' do
before do
visit project_boards_path(project)
wait_for_requests
end
it 'shows board scope' do
click_button 'View scope'
page.within('.popup-dialog') do
expect(page).not_to have_link('Edit')
expect(page).not_to have_button('Edit')
expect(page).not_to have_button('Save')
end
end
end
end
\ No newline at end of file
require 'rails_helper'
describe 'Board with milestone', :js do
let(:user) { create(:user) }
let(:project) { create(:project, :public) }
let!(:milestone) { create(:milestone, project: project) }
let!(:issue) { create(:closed_issue, project: project) }
let!(:issue_milestone) { create(:closed_issue, project: project, milestone: milestone) }
before do
allow_any_instance_of(ApplicationHelper).to receive(:collapsed_sidebar?).and_return(true)
project.team << [user, :master]
sign_in(user)
end
context 'with the feature enabled' do
before do
stub_licensed_features(scoped_issue_board: true)
end
context 'new board' do
before do
visit project_boards_path(project)
end
it 'creates board with milestone' do
create_board_with_milestone
expect(find('.tokens-container')).to have_content(milestone.title)
wait_for_requests
find('.card', match: :first)
expect(all('.board').last).to have_selector('.card', count: 1)
end
end
context 'update board' do
let!(:milestone_two) { create(:milestone, project: project) }
let!(:board) { create(:board, project: project, milestone: milestone) }
before do
visit project_boards_path(project)
end
it 'defaults milestone filter' do
page.within '#js-multiple-boards-switcher' do
find('.dropdown-menu-toggle').click
wait_for_requests
click_link board.name
end
expect(find('.tokens-container')).to have_content(milestone.title)
find('.card', match: :first)
expect(all('.board').last).to have_selector('.card', count: 1)
end
it 'sets board to any milestone' do
update_board_milestone('Any Milestone')
expect(page).not_to have_css('.js-visual-token')
expect(find('.tokens-container')).not_to have_content(milestone.title)
find('.card', match: :first)
expect(page).to have_selector('.board', count: 3)
expect(all('.board').last).to have_selector('.card', count: 2)
end
it 'sets board to upcoming milestone' do
update_board_milestone('Upcoming')
expect(find('.tokens-container')).not_to have_content(milestone.title)
find('.board', match: :first)
expect(all('.board')[1]).to have_selector('.card', count: 0)
end
it 'does not allow milestone in filter to be editted' do
find('.filtered-search').native.send_keys(:backspace)
page.within('.tokens-container') do
expect(page).to have_selector('.value')
end
end
it 'does not render milestone in hint dropdown' do
find('.filtered-search').click
page.within('#js-dropdown-hint') do
expect(page).not_to have_button('Milestone')
end
end
end
context 'removing issue from board' do
let(:label) { create(:label, project: project) }
let!(:issue) { create(:labeled_issue, project: project, labels: [label], milestone: milestone) }
let!(:board) { create(:board, project: project, milestone: milestone) }
let!(:list) { create(:list, board: board, label: label, position: 0) }
before do
visit project_boards_path(project)
end
it 'removes issues milestone when removing from the board' do
wait_for_requests
first('.card .card-number').click
click_button('Remove from board')
wait_for_requests
expect(issue.reload.milestone).to be_nil
end
end
context 'new issues' do
let(:label) { create(:label, project: project) }
let!(:list1) { create(:list, board: board, label: label, position: 0) }
let!(:board) { create(:board, project: project, milestone: milestone) }
let!(:issue) { create(:issue, project: project) }
before do
visit project_boards_path(project)
end
it 'creates new issue with boards milestone' do
wait_for_requests
page.within(first('.board')) do
find('.btn-default').click
find('.form-control').set('testing new issue with milestone')
click_button('Submit issue')
wait_for_requests
click_link('testing new issue with milestone')
end
expect(page).to have_content(milestone.title)
end
it 'updates issue with milestone from add issues modal' do
wait_for_requests
click_button 'Add issues'
page.within('.add-issues-modal') do
card = find('.card', :first)
expect(page).to have_selector('.card', count: 1)
card.click
click_button 'Add 1 issue'
end
click_link(issue.title)
expect(page).to have_content(milestone.title)
end
end
end
context 'with the feature disabled' do
before do
stub_licensed_features(scoped_issue_board: false)
visit project_boards_path(project)
end
it "doesn't show the input when creating a board" do
page.within '#js-multiple-boards-switcher' do
find('.dropdown-menu-toggle').click
click_link 'Create new board'
# To make sure the form is shown
expect(page).to have_selector('#board-new-name')
expect(page).not_to have_button('Milestone')
end
end
it "doesn't show the option to edit the milestone" do
page.within '#js-multiple-boards-switcher' do
find('.dropdown-menu-toggle').click
# To make sure the dropdown is open
expect(page).to have_link('Edit board name')
expect(page).not_to have_link('Edit board milestone')
end
end
end
def create_board_with_milestone
page.within '#js-multiple-boards-switcher' do
find('.dropdown-menu-toggle').click
click_link 'Create new board'
find('#board-new-name').set 'test'
find('button', text: 'Any Milestone').trigger('click')
find('a', text: milestone.title).trigger('click')
click_button 'Create'
end
end
def update_board_milestone(milestone_title)
page.within '#js-multiple-boards-switcher' do
find('.dropdown-menu-toggle').click
click_link 'Edit board milestone'
click_link milestone_title
click_button 'Save'
end
end
end
......@@ -67,24 +67,6 @@ describe 'Multiple Issue Boards', :js do
expect(page).to have_button('This is a new board')
end
it 'edits board name' do
click_button board.name
page.within('.dropdown-menu') do
click_link 'Edit board name'
fill_in 'board-new-name', with: 'Testing'
click_button 'Save'
end
wait_for_requests
page.within('.dropdown-menu') do
expect(page).to have_content('Testing')
end
end
it 'deletes board' do
click_button board.name
......@@ -156,7 +138,6 @@ describe 'Multiple Issue Boards', :js do
page.within('.dropdown-menu') do
expect(page).not_to have_content('Create new board')
expect(page).not_to have_content('Edit board name')
expect(page).not_to have_content('Delete board')
end
end
......@@ -178,7 +159,7 @@ describe 'Multiple Issue Boards', :js do
click_button board.name
page.within('.dropdown-menu') do
expect(page).to have_content('Edit board name')
expect(page).to have_content('Edit board')
expect(page).not_to have_content('Create new board')
expect(page).not_to have_content('Delete board')
end
......
......@@ -4,7 +4,7 @@
/* global mockBoardService */
import Vue from 'vue';
import milestoneSelect from '~/boards/components/milestone_select';
import MilestoneSelect from '~/boards/components/milestone_select.vue';
import '~/boards/services/board_service';
import '~/boards/stores/boards_store';
import './mock_data';
......@@ -14,8 +14,6 @@ describe('Milestone select component', () => {
let vm;
beforeEach(() => {
const MilestoneComp = Vue.extend(milestoneSelect);
Vue.http.interceptors.push(boardsMockInterceptor);
gl.boardService = mockBoardService();
gl.issueBoards.BoardsStore.create();
......@@ -24,7 +22,7 @@ describe('Milestone select component', () => {
vm.board.milestone_id = milestone.id;
});
vm = new MilestoneComp({
vm = new MilestoneSelect({
propsData: {
board: boardObj,
milestonePath: '/test/issue-boards/milestones.json',
......
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