FE backport of group boards to reduce CE conflicts

This commit is contained in:
Simon Knox 2017-09-06 15:02:30 +10:00
parent b9aa55e1ea
commit c28d52a3a5
12 changed files with 116 additions and 53 deletions

View file

@ -6,7 +6,8 @@ const Api = {
namespacesPath: '/api/:version/namespaces.json', namespacesPath: '/api/:version/namespaces.json',
groupProjectsPath: '/api/:version/groups/:id/projects.json', groupProjectsPath: '/api/:version/groups/:id/projects.json',
projectsPath: '/api/:version/projects.json', projectsPath: '/api/:version/projects.json',
labelsPath: '/:namespace_path/:project_path/labels', projectLabelsPath: '/:namespace_path/:project_path/labels',
groupLabelsPath: '/groups/:namespace_path/labels',
licensePath: '/api/:version/templates/licenses/:key', licensePath: '/api/:version/templates/licenses/:key',
gitignorePath: '/api/:version/templates/gitignores/:key', gitignorePath: '/api/:version/templates/gitignores/:key',
gitlabCiYmlPath: '/api/:version/templates/gitlab_ci_ymls/:key', gitlabCiYmlPath: '/api/:version/templates/gitlab_ci_ymls/:key',
@ -74,9 +75,14 @@ const Api = {
}, },
newLabel(namespacePath, projectPath, data, callback) { newLabel(namespacePath, projectPath, data, callback) {
const url = Api.buildUrl(Api.labelsPath) let url;
.replace(':namespace_path', namespacePath) if (projectPath) {
.replace(':project_path', projectPath); url = Api.buildUrl(Api.projectLabelsPath)
.replace(':namespace_path', namespacePath)
.replace(':project_path', projectPath);
} else {
url = Api.buildUrl(Api.groupLabelsPath).replace(':namespace_path', namespacePath);
}
return $.ajax({ return $.ajax({
url, url,
type: 'POST', type: 'POST',

View file

@ -53,7 +53,8 @@ $(() => {
data: { data: {
state: Store.state, state: Store.state,
loading: true, loading: true,
endpoint: $boardApp.dataset.endpoint, boardsEndpoint: $boardApp.dataset.boardsEndpoint,
listsEndpoint: $boardApp.dataset.listsEndpoint,
boardId: $boardApp.dataset.boardId, boardId: $boardApp.dataset.boardId,
disabled: $boardApp.dataset.disabled === 'true', disabled: $boardApp.dataset.disabled === 'true',
issueLinkBase: $boardApp.dataset.issueLinkBase, issueLinkBase: $boardApp.dataset.issueLinkBase,
@ -68,7 +69,13 @@ $(() => {
}, },
}, },
created () { created () {
gl.boardService = new BoardService(this.endpoint, this.bulkUpdatePath, this.boardId); gl.boardService = new BoardService({
boardsEndpoint: this.boardsEndpoint,
listsEndpoint: this.listsEndpoint,
bulkUpdatePath: this.bulkUpdatePath,
boardId: this.boardId,
});
Store.rootPath = this.boardsEndpoint;
this.filterManager = new FilteredSearchBoards(Store.filter, true); this.filterManager = new FilteredSearchBoards(Store.filter, true);
this.filterManager.setup(); this.filterManager.setup();
@ -112,19 +119,21 @@ $(() => {
gl.IssueBoardsSearch = new Vue({ gl.IssueBoardsSearch = new Vue({
el: document.getElementById('js-add-list'), el: document.getElementById('js-add-list'),
data: { data: {
filters: Store.state.filters filters: Store.state.filters,
}, },
mounted () { mounted () {
gl.issueBoards.newListDropdownInit(); gl.issueBoards.newListDropdownInit();
} },
}); });
gl.IssueBoardsModalAddBtn = new Vue({ gl.IssueBoardsModalAddBtn = new Vue({
mixins: [gl.issueBoards.ModalMixins], mixins: [gl.issueBoards.ModalMixins],
el: document.getElementById('js-add-issues-btn'), el: document.getElementById('js-add-issues-btn'),
data: { data() {
modal: ModalStore.store, return {
store: Store.state, modal: ModalStore.store,
store: Store.state,
};
}, },
watch: { watch: {
disabled() { disabled() {
@ -133,6 +142,9 @@ $(() => {
}, },
computed: { computed: {
disabled() { disabled() {
if (!this.store) {
return true;
}
return !this.store.lists.filter(list => !list.preset).length; return !this.store.lists.filter(list => !list.preset).length;
}, },
tooltipTitle() { tooltipTitle() {
@ -145,7 +157,7 @@ $(() => {
}, },
methods: { methods: {
updateTooltip() { updateTooltip() {
const $tooltip = $(this.$el); const $tooltip = $(this.$refs.addIssuesButton);
this.$nextTick(() => { this.$nextTick(() => {
if (this.disabled) { if (this.disabled) {
@ -165,16 +177,19 @@ $(() => {
this.updateTooltip(); this.updateTooltip();
}, },
template: ` template: `
<button <div class="board-extra-actions">
class="btn btn-create pull-right prepend-left-10" <button
type="button" class="btn btn-create prepend-left-10"
data-placement="bottom" type="button"
:class="{ 'disabled': disabled }" data-placement="bottom"
:title="tooltipTitle" ref="addIssuesButton"
:aria-disabled="disabled" :class="{ 'disabled': disabled }"
@click="openModal"> :title="tooltipTitle"
Add issues :aria-disabled="disabled"
</button> @click="openModal">
Add issues
</button>
</div>
`, `,
}); });
}); });

View file

@ -77,7 +77,7 @@ export default {
this.showIssueForm = !this.showIssueForm; this.showIssueForm = !this.showIssueForm;
}, },
onScroll() { onScroll() {
if ((this.scrollTop() > this.scrollHeight() - this.scrollOffset) && !this.list.loadingMore) { if (!this.loadingMore && (this.scrollTop() > this.scrollHeight() - this.scrollOffset)) {
this.loadNextPage(); this.loadNextPage();
} }
}, },
@ -165,11 +165,9 @@ export default {
v-if="loading"> v-if="loading">
<loading-icon /> <loading-icon />
</div> </div>
<transition name="slide-down"> <board-new-issue
<board-new-issue :list="list"
:list="list" v-if="list.type !== 'closed' && showIssueForm"/>
v-if="list.type !== 'closed' && showIssueForm"/>
</transition>
<ul <ul
class="board-list" class="board-list"
v-show="!loading" v-show="!loading"

View file

@ -6,7 +6,10 @@ const Store = gl.issueBoards.BoardsStore;
export default { export default {
name: 'BoardNewIssue', name: 'BoardNewIssue',
props: { props: {
list: Object, list: {
type: Object,
required: true,
},
}, },
data() { data() {
return { return {

View file

@ -67,7 +67,10 @@ gl.issueBoards.IssueCardInner = Vue.extend({
return `${this.issueLinkBase}/${this.issue.id}`; return `${this.issueLinkBase}/${this.issue.id}`;
}, },
issueId() { issueId() {
return `#${this.issue.id}`; if (this.issue.iid) {
return `#${this.issue.iid}`;
}
return false;
}, },
showLabelFooter() { showLabelFooter() {
return this.issue.labels.find(l => this.showLabel(l)) !== undefined; return this.issue.labels.find(l => this.showLabel(l)) !== undefined;
@ -143,7 +146,7 @@ gl.issueBoards.IssueCardInner = Vue.extend({
:title="issue.title">{{ issue.title }}</a> :title="issue.title">{{ issue.title }}</a>
<span <span
class="card-number" class="card-number"
v-if="issue.id" v-if="issueId"
> >
{{ issueId }} {{ issueId }}
</span> </span>

View file

@ -29,7 +29,7 @@ gl.issueBoards.ModalFooter = Vue.extend({
const firstListIndex = 1; const firstListIndex = 1;
const list = this.modal.selectedList || this.state.lists[firstListIndex]; const list = this.modal.selectedList || this.state.lists[firstListIndex];
const selectedIssues = ModalStore.getSelectedIssues(); const selectedIssues = ModalStore.getSelectedIssues();
const issueIds = selectedIssues.map(issue => issue.globalId); const issueIds = selectedIssues.map(issue => issue.id);
// Post the data to the backend // Post the data to the backend
gl.boardService.bulkUpdate(issueIds, { gl.boardService.bulkUpdate(issueIds, {

View file

@ -27,7 +27,7 @@ gl.issueBoards.newListDropdownInit = () => {
$this.glDropdown({ $this.glDropdown({
data(term, callback) { data(term, callback) {
$.get($this.attr('data-labels')) $.get($this.attr('data-list-labels-path'))
.then((resp) => { .then((resp) => {
callback(resp); callback(resp);
}); });

View file

@ -18,17 +18,32 @@ gl.issueBoards.RemoveIssueBtn = Vue.extend({
type: Object, type: Object,
required: true, required: true,
}, },
issueUpdate: {
type: String,
required: true,
},
},
computed: {
updateUrl() {
return this.issueUpdate;
},
}, },
methods: { methods: {
removeIssue() { removeIssue() {
const issue = this.issue; const issue = this.issue;
const lists = issue.getLists(); const lists = issue.getLists();
const labelIds = lists.map(list => list.label.id); const listLabelIds = lists.map(list => list.label.id);
const labelIds = this.issue.labels
.map(label => label.id)
.filter(id => !listLabelIds.includes(id));
// Post the remove data // Post the remove data
gl.boardService.bulkUpdate([issue.globalId], { const data = {
remove_label_ids: labelIds, issue: {
}).catch(() => { label_ids: labelIds,
},
};
Vue.http.patch(this.updateUrl, data).catch(() => {
new Flash('Failed to remove issue from board, please try again.', 'alert'); new Flash('Failed to remove issue from board, please try again.', 'alert');
lists.forEach((list) => { lists.forEach((list) => {

View file

@ -7,8 +7,8 @@ import Vue from 'vue';
class ListIssue { class ListIssue {
constructor (obj, defaultAvatar) { constructor (obj, defaultAvatar) {
this.globalId = obj.id; this.id = obj.id;
this.id = obj.iid; this.iid = obj.iid;
this.title = obj.title; this.title = obj.title;
this.confidential = obj.confidential; this.confidential = obj.confidential;
this.dueDate = obj.due_date; this.dueDate = obj.due_date;

View file

@ -4,6 +4,7 @@ class ListLabel {
constructor (obj) { constructor (obj) {
this.id = obj.id; this.id = obj.id;
this.title = obj.title; this.title = obj.title;
this.type = obj.type;
this.color = obj.color; this.color = obj.color;
this.textColor = obj.text_color; this.textColor = obj.text_color;
this.description = obj.description; this.description = obj.description;

View file

@ -110,11 +110,12 @@ class List {
return gl.boardService.newIssue(this.id, issue) return gl.boardService.newIssue(this.id, issue)
.then(resp => resp.json()) .then(resp => resp.json())
.then((data) => { .then((data) => {
issue.id = data.iid; issue.id = data.id;
issue.iid = data.iid;
if (this.issuesSize > 1) { if (this.issuesSize > 1) {
const moveBeforeIid = this.issues[1].id; const moveBeforeId = this.issues[1].id;
gl.boardService.moveIssue(issue.id, null, null, null, moveBeforeIid); gl.boardService.moveIssue(issue.id, null, null, null, moveBeforeId);
} }
}); });
} }
@ -126,19 +127,19 @@ class List {
} }
addIssue (issue, listFrom, newIndex) { addIssue (issue, listFrom, newIndex) {
let moveBeforeIid = null; let moveBeforeId = null;
let moveAfterIid = null; let moveAfterId = null;
if (!this.findIssue(issue.id)) { if (!this.findIssue(issue.id)) {
if (newIndex !== undefined) { if (newIndex !== undefined) {
this.issues.splice(newIndex, 0, issue); this.issues.splice(newIndex, 0, issue);
if (this.issues[newIndex - 1]) { if (this.issues[newIndex - 1]) {
moveBeforeIid = this.issues[newIndex - 1].id; moveBeforeId = this.issues[newIndex - 1].id;
} }
if (this.issues[newIndex + 1]) { if (this.issues[newIndex + 1]) {
moveAfterIid = this.issues[newIndex + 1].id; moveAfterId = this.issues[newIndex + 1].id;
} }
} else { } else {
this.issues.push(issue); this.issues.push(issue);
@ -151,30 +152,30 @@ class List {
if (listFrom) { if (listFrom) {
this.issuesSize += 1; this.issuesSize += 1;
this.updateIssueLabel(issue, listFrom, moveBeforeIid, moveAfterIid); this.updateIssueLabel(issue, listFrom, moveBeforeId, moveAfterId);
} }
} }
} }
moveIssue (issue, oldIndex, newIndex, moveBeforeIid, moveAfterIid) { moveIssue (issue, oldIndex, newIndex, moveBeforeId, moveAfterId) {
this.issues.splice(oldIndex, 1); this.issues.splice(oldIndex, 1);
this.issues.splice(newIndex, 0, issue); this.issues.splice(newIndex, 0, issue);
gl.boardService.moveIssue(issue.id, null, null, moveBeforeIid, moveAfterIid) gl.boardService.moveIssue(issue.id, null, null, moveBeforeId, moveAfterId)
.catch(() => { .catch(() => {
// TODO: handle request error // TODO: handle request error
}); });
} }
updateIssueLabel(issue, listFrom, moveBeforeIid, moveAfterIid) { updateIssueLabel(issue, listFrom, moveBeforeId, moveAfterId) {
gl.boardService.moveIssue(issue.id, listFrom.id, this.id, moveBeforeIid, moveAfterIid) gl.boardService.moveIssue(issue.id, listFrom.id, this.id, moveBeforeId, moveAfterId)
.catch(() => { .catch(() => {
// TODO: handle request error // TODO: handle request error
}); });
} }
findIssue (id) { findIssue (id) {
return this.issues.filter(issue => issue.id === id)[0]; return this.issues.find(issue => issue.id === id);
} }
removeIssue (removeIssue) { removeIssue (removeIssue) {

View file

@ -145,7 +145,7 @@ describe('Api', () => {
}); });
}); });
describe('newLabel', () => { fdescribe('newLabel', () => {
it('creates a new label', (done) => { it('creates a new label', (done) => {
const namespace = 'some namespace'; const namespace = 'some namespace';
const project = 'some project'; const project = 'some project';
@ -167,6 +167,27 @@ describe('Api', () => {
done(); done();
}); });
}); });
it('creates a new group label', (done) => {
const namespace = 'some namespace';
const labelData = { some: 'data' };
const expectedUrl = `${dummyUrlRoot}/${namespace}/labels`;
const expectedData = {
label: labelData,
};
spyOn(jQuery, 'ajax').and.callFake((request) => {
expect(request.url).toEqual(expectedUrl);
expect(request.dataType).toEqual('json');
expect(request.type).toEqual('POST');
expect(request.data).toEqual(expectedData);
return sendDummyResponse();
});
Api.newLabel(namespace, null, labelData, (response) => {
expect(response).toBe(dummyResponse);
done();
});
});
}); });
describe('groupProjects', () => { describe('groupProjects', () => {