mirror of
https://github.com/wekan/wekan.git
synced 2025-12-16 07:20:12 +01:00
First swimlane draft, no functionality
This commit is contained in:
parent
8b6a2eade3
commit
690a5b9703
16 changed files with 474 additions and 173 deletions
|
|
@ -20,22 +20,10 @@ template(name="boardBody")
|
||||||
class="{{#if draggingActive.get}}is-dragging-active{{/if}}")
|
class="{{#if draggingActive.get}}is-dragging-active{{/if}}")
|
||||||
if showOverlay.get
|
if showOverlay.get
|
||||||
.board-overlay
|
.board-overlay
|
||||||
.lists.js-lists
|
each currentBoard.swimlanes
|
||||||
if isMiniScreen
|
+swimlane(this)
|
||||||
if currentList
|
// if currentUser.isBoardMember
|
||||||
+list(currentList)
|
// +addSwimlaneForm
|
||||||
else
|
|
||||||
each currentBoard.lists
|
|
||||||
+miniList(this)
|
|
||||||
if currentUser.isBoardMember
|
|
||||||
+addListForm
|
|
||||||
else
|
|
||||||
each currentBoard.lists
|
|
||||||
+list(this)
|
|
||||||
if currentCardIsInThisList
|
|
||||||
+cardDetails(currentCard)
|
|
||||||
if currentUser.isBoardMember
|
|
||||||
+addListForm
|
|
||||||
|
|
||||||
template(name="addListForm")
|
template(name="addListForm")
|
||||||
.list.js-list.list-composer.js-list-composer
|
.list.js-list.list-composer.js-list-composer
|
||||||
|
|
|
||||||
|
|
@ -29,10 +29,6 @@ BlazeComponent.extendComponent({
|
||||||
this.mouseHasEnterCardDetails = false;
|
this.mouseHasEnterCardDetails = false;
|
||||||
},
|
},
|
||||||
|
|
||||||
openNewListForm() {
|
|
||||||
this.childComponents('addListForm')[0].open();
|
|
||||||
},
|
|
||||||
|
|
||||||
// XXX Flow components allow us to avoid creating these two setter methods by
|
// XXX Flow components allow us to avoid creating these two setter methods by
|
||||||
// exposing a public API to modify the component state. We need to investigate
|
// exposing a public API to modify the component state. We need to investigate
|
||||||
// best practices here.
|
// best practices here.
|
||||||
|
|
@ -47,12 +43,6 @@ BlazeComponent.extendComponent({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
currentCardIsInThisList() {
|
|
||||||
const currentCard = Cards.findOne(Session.get('currentCard'));
|
|
||||||
const listId = this.currentData()._id;
|
|
||||||
return currentCard && currentCard.listId === listId;
|
|
||||||
},
|
|
||||||
|
|
||||||
onlyShowCurrentCard() {
|
onlyShowCurrentCard() {
|
||||||
return Utils.isMiniScreen() && Session.get('currentCard');
|
return Utils.isMiniScreen() && Session.get('currentCard');
|
||||||
},
|
},
|
||||||
|
|
@ -66,147 +56,11 @@ BlazeComponent.extendComponent({
|
||||||
this.showOverlay.set(false);
|
this.showOverlay.set(false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// Click-and-drag action
|
|
||||||
'mousedown .board-canvas'(evt) {
|
|
||||||
// Translating the board canvas using the click-and-drag action can
|
|
||||||
// conflict with the build-in browser mechanism to select text. We
|
|
||||||
// define a list of elements in which we disable the dragging because
|
|
||||||
// the user will legitimately expect to be able to select some text with
|
|
||||||
// his mouse.
|
|
||||||
const noDragInside = ['a', 'input', 'textarea', 'p', '.js-list-header'];
|
|
||||||
if ($(evt.target).closest(noDragInside.join(',')).length === 0 && $('.lists').prop('clientHeight') > evt.offsetY) {
|
|
||||||
this._isDragging = true;
|
|
||||||
this._lastDragPositionX = evt.clientX;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
'mouseup'() {
|
'mouseup'() {
|
||||||
if (this._isDragging) {
|
if (this._isDragging) {
|
||||||
this._isDragging = false;
|
this._isDragging = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'mousemove'(evt) {
|
|
||||||
if (this._isDragging) {
|
|
||||||
// Update the canvas position
|
|
||||||
this.listsDom.scrollLeft -= evt.clientX - this._lastDragPositionX;
|
|
||||||
this._lastDragPositionX = evt.clientX;
|
|
||||||
// Disable browser text selection while dragging
|
|
||||||
evt.stopPropagation();
|
|
||||||
evt.preventDefault();
|
|
||||||
// Don't close opened card or inlined form at the end of the
|
|
||||||
// click-and-drag.
|
|
||||||
EscapeActions.executeUpTo('popup-close');
|
|
||||||
EscapeActions.preventNextClick();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}];
|
}];
|
||||||
},
|
},
|
||||||
}).register('board');
|
}).register('board');
|
||||||
|
|
||||||
Template.boardBody.onRendered(function() {
|
|
||||||
const self = BlazeComponent.getComponentForElement(this.firstNode);
|
|
||||||
|
|
||||||
self.listsDom = this.find('.js-lists');
|
|
||||||
|
|
||||||
if (!Session.get('currentCard')) {
|
|
||||||
self.scrollLeft();
|
|
||||||
}
|
|
||||||
|
|
||||||
// We want to animate the card details window closing. We rely on CSS
|
|
||||||
// transition for the actual animation.
|
|
||||||
self.listsDom._uihooks = {
|
|
||||||
removeElement(node) {
|
|
||||||
const removeNode = _.once(() => {
|
|
||||||
node.parentNode.removeChild(node);
|
|
||||||
});
|
|
||||||
if ($(node).hasClass('js-card-details')) {
|
|
||||||
$(node).css({
|
|
||||||
flexBasis: 0,
|
|
||||||
padding: 0,
|
|
||||||
});
|
|
||||||
$(self.listsDom).one(CSSEvents.transitionend, removeNode);
|
|
||||||
} else {
|
|
||||||
removeNode();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
$(self.listsDom).sortable({
|
|
||||||
tolerance: 'pointer',
|
|
||||||
helper: 'clone',
|
|
||||||
handle: '.js-list-header',
|
|
||||||
items: '.js-list:not(.js-list-composer)',
|
|
||||||
placeholder: 'list placeholder',
|
|
||||||
distance: 7,
|
|
||||||
start(evt, ui) {
|
|
||||||
ui.placeholder.height(ui.helper.height());
|
|
||||||
Popup.close();
|
|
||||||
},
|
|
||||||
stop() {
|
|
||||||
$(self.listsDom).find('.js-list:not(.js-list-composer)').each(
|
|
||||||
(i, list) => {
|
|
||||||
const data = Blaze.getData(list);
|
|
||||||
Lists.update(data._id, {
|
|
||||||
$set: {
|
|
||||||
sort: i,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
function userIsMember() {
|
|
||||||
return Meteor.user() && Meteor.user().isBoardMember();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Disable drag-dropping while in multi-selection mode, or if the current user
|
|
||||||
// is not a board member
|
|
||||||
self.autorun(() => {
|
|
||||||
const $listDom = $(self.listsDom);
|
|
||||||
if ($listDom.data('sortable')) {
|
|
||||||
$(self.listsDom).sortable('option', 'disabled',
|
|
||||||
MultiSelection.isActive() || !userIsMember());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// If there is no data in the board (ie, no lists) we autofocus the list
|
|
||||||
// creation form by clicking on the corresponding element.
|
|
||||||
const currentBoard = Boards.findOne(Session.get('currentBoard'));
|
|
||||||
if (userIsMember() && currentBoard.lists().count() === 0) {
|
|
||||||
self.openNewListForm();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
BlazeComponent.extendComponent({
|
|
||||||
// Proxy
|
|
||||||
open() {
|
|
||||||
this.childComponents('inlinedForm')[0].open();
|
|
||||||
},
|
|
||||||
|
|
||||||
events() {
|
|
||||||
return [{
|
|
||||||
submit(evt) {
|
|
||||||
evt.preventDefault();
|
|
||||||
const titleInput = this.find('.list-name-input');
|
|
||||||
const title = titleInput.value.trim();
|
|
||||||
if (title) {
|
|
||||||
Lists.insert({
|
|
||||||
title,
|
|
||||||
boardId: Session.get('currentBoard'),
|
|
||||||
sort: $('.list').length,
|
|
||||||
});
|
|
||||||
|
|
||||||
titleInput.value = '';
|
|
||||||
titleInput.focus();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}];
|
|
||||||
},
|
|
||||||
}).register('addListForm');
|
|
||||||
|
|
||||||
Template.boardBody.helpers({
|
|
||||||
canSeeAddList() {
|
|
||||||
return Meteor.user() && Meteor.user().isBoardMember() && !Meteor.user().isCommentOnly();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,8 @@ position()
|
||||||
&.is-sibling-sidebar-open
|
&.is-sibling-sidebar-open
|
||||||
margin-right: 248px
|
margin-right: 248px
|
||||||
|
|
||||||
.lists
|
.swimlane
|
||||||
|
border-bottom: 1px solid #CCC
|
||||||
align-items: flex-start
|
align-items: flex-start
|
||||||
display: flex
|
display: flex
|
||||||
flex-direction: row
|
flex-direction: row
|
||||||
|
|
@ -49,8 +50,9 @@ position()
|
||||||
|
|
||||||
.board-canvas
|
.board-canvas
|
||||||
|
|
||||||
.lists
|
.swimlane
|
||||||
align-items: flex-start
|
align-items: flex-start
|
||||||
|
border-bottom: 1px solid #CCC
|
||||||
display: flex
|
display: flex
|
||||||
flex-direction: column
|
flex-direction: column
|
||||||
margin: 0
|
margin: 0
|
||||||
|
|
|
||||||
|
|
@ -164,6 +164,11 @@ const CreateBoard = BlazeComponent.extendComponent({
|
||||||
permission: visibility,
|
permission: visibility,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
Swimlanes.insert({
|
||||||
|
title: 'Default',
|
||||||
|
boardId: this.boardId.get(),
|
||||||
|
});
|
||||||
|
|
||||||
Utils.goBoardId(this.boardId.get());
|
Utils.goBoardId(this.boardId.get());
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,8 +20,8 @@ BlazeComponent.extendComponent({
|
||||||
|
|
||||||
onCreated() {
|
onCreated() {
|
||||||
this.isLoaded = new ReactiveVar(false);
|
this.isLoaded = new ReactiveVar(false);
|
||||||
this.parentComponent().showOverlay.set(true);
|
this.parentComponent().parentComponent().showOverlay.set(true);
|
||||||
this.parentComponent().mouseHasEnterCardDetails = false;
|
this.parentComponent().parentComponent().mouseHasEnterCardDetails = false;
|
||||||
this.calculateNextPeak();
|
this.calculateNextPeak();
|
||||||
|
|
||||||
Meteor.subscribe('unsaved-edits');
|
Meteor.subscribe('unsaved-edits');
|
||||||
|
|
@ -42,7 +42,7 @@ BlazeComponent.extendComponent({
|
||||||
|
|
||||||
scrollParentContainer() {
|
scrollParentContainer() {
|
||||||
const cardPanelWidth = 510;
|
const cardPanelWidth = 510;
|
||||||
const bodyBoardComponent = this.parentComponent();
|
const bodyBoardComponent = this.parentComponent().parentComponent();
|
||||||
|
|
||||||
const $cardContainer = bodyBoardComponent.$('.js-lists');
|
const $cardContainer = bodyBoardComponent.$('.js-lists');
|
||||||
const $cardView = this.$(this.firstNode());
|
const $cardView = this.$(this.firstNode());
|
||||||
|
|
@ -69,7 +69,7 @@ BlazeComponent.extendComponent({
|
||||||
},
|
},
|
||||||
|
|
||||||
onDestroyed() {
|
onDestroyed() {
|
||||||
this.parentComponent().showOverlay.set(false);
|
this.parentComponent().parentComponent().showOverlay.set(false);
|
||||||
},
|
},
|
||||||
|
|
||||||
events() {
|
events() {
|
||||||
|
|
@ -104,8 +104,8 @@ BlazeComponent.extendComponent({
|
||||||
'click .js-add-members': Popup.open('cardMembers'),
|
'click .js-add-members': Popup.open('cardMembers'),
|
||||||
'click .js-add-labels': Popup.open('cardLabels'),
|
'click .js-add-labels': Popup.open('cardLabels'),
|
||||||
'mouseenter .js-card-details' () {
|
'mouseenter .js-card-details' () {
|
||||||
this.parentComponent().showOverlay.set(true);
|
this.parentComponent().parentComponent().showOverlay.set(true);
|
||||||
this.parentComponent().mouseHasEnterCardDetails = true;
|
this.parentComponent().parentComponent().mouseHasEnterCardDetails = true;
|
||||||
},
|
},
|
||||||
'click #toggleButton'() {
|
'click #toggleButton'() {
|
||||||
Meteor.call('toggleSystemMessages');
|
Meteor.call('toggleSystemMessages');
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ BlazeComponent.extendComponent({
|
||||||
// callback, we basically solve all issues related to reactive updates. A
|
// callback, we basically solve all issues related to reactive updates. A
|
||||||
// comment below provides further details.
|
// comment below provides further details.
|
||||||
onRendered() {
|
onRendered() {
|
||||||
const boardComponent = this.parentComponent();
|
const boardComponent = this.parentComponent().parentComponent();
|
||||||
const itemsSelector = '.js-minicard:not(.placeholder, .js-card-composer)';
|
const itemsSelector = '.js-minicard:not(.placeholder, .js-card-composer)';
|
||||||
const $cards = this.$('.js-minicards');
|
const $cards = this.$('.js-minicards');
|
||||||
$cards.sortable({
|
$cards.sortable({
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ template(name="listBody")
|
||||||
if cards.count
|
if cards.count
|
||||||
+inlinedForm(autoclose=false position="top")
|
+inlinedForm(autoclose=false position="top")
|
||||||
+addCardForm(listId=_id position="top")
|
+addCardForm(listId=_id position="top")
|
||||||
each cards
|
each cards ../../_id
|
||||||
a.minicard-wrapper.js-minicard(href=absoluteUrl
|
a.minicard-wrapper.js-minicard(href=absoluteUrl
|
||||||
class="{{#if cardIsSelected}}is-selected{{/if}}"
|
class="{{#if cardIsSelected}}is-selected{{/if}}"
|
||||||
class="{{#if MultiSelection.isSelected _id}}is-checked{{/if}}")
|
class="{{#if MultiSelection.isSelected _id}}is-checked{{/if}}")
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ BlazeComponent.extendComponent({
|
||||||
const members = formComponent.members.get();
|
const members = formComponent.members.get();
|
||||||
const labelIds = formComponent.labels.get();
|
const labelIds = formComponent.labels.get();
|
||||||
|
|
||||||
|
const swimlaneId = this.parentComponent().parentComponent().data()._id;
|
||||||
if (title) {
|
if (title) {
|
||||||
const _id = Cards.insert({
|
const _id = Cards.insert({
|
||||||
title,
|
title,
|
||||||
|
|
@ -44,6 +45,7 @@ BlazeComponent.extendComponent({
|
||||||
listId: this.data()._id,
|
listId: this.data()._id,
|
||||||
boardId: this.data().board()._id,
|
boardId: this.data().board()._id,
|
||||||
sort: sortIndex,
|
sort: sortIndex,
|
||||||
|
swimlaneId: swimlaneId,
|
||||||
});
|
});
|
||||||
// In case the filter is active we need to add the newly inserted card in
|
// In case the filter is active we need to add the newly inserted card in
|
||||||
// the list of exceptions -- cards that are not filtered. Otherwise the
|
// the list of exceptions -- cards that are not filtered. Otherwise the
|
||||||
|
|
|
||||||
20
client/components/swimlanes/swimlanes.jade
Normal file
20
client/components/swimlanes/swimlanes.jade
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
template(name="swimlane")
|
||||||
|
.swimlane.js-lists
|
||||||
|
.swimlane-header-wrap
|
||||||
|
.swimlane-header
|
||||||
|
= title
|
||||||
|
if isMiniScreen
|
||||||
|
if currentList
|
||||||
|
+list(currentList)
|
||||||
|
else
|
||||||
|
each currentBoard.lists
|
||||||
|
+miniList(this)
|
||||||
|
if currentUser.isBoardMember
|
||||||
|
+addListForm
|
||||||
|
else
|
||||||
|
each currentBoard.lists
|
||||||
|
+list(this)
|
||||||
|
if currentCardIsInThisList
|
||||||
|
+cardDetails(currentCard)
|
||||||
|
if currentUser.isBoardMember
|
||||||
|
+addListForm
|
||||||
181
client/components/swimlanes/swimlanes.js
Normal file
181
client/components/swimlanes/swimlanes.js
Normal file
|
|
@ -0,0 +1,181 @@
|
||||||
|
BlazeComponent.extendComponent({
|
||||||
|
onCreated() {
|
||||||
|
this.draggingActive = new ReactiveVar(false);
|
||||||
|
|
||||||
|
this._isDragging = false;
|
||||||
|
this._lastDragPositionX = 0;
|
||||||
|
},
|
||||||
|
|
||||||
|
openNewListForm() {
|
||||||
|
this.childComponents('addListForm')[0].open();
|
||||||
|
},
|
||||||
|
|
||||||
|
id() {
|
||||||
|
return this._id;
|
||||||
|
},
|
||||||
|
|
||||||
|
// XXX Flow components allow us to avoid creating these two setter methods by
|
||||||
|
// exposing a public API to modify the component state. We need to investigate
|
||||||
|
// best practices here.
|
||||||
|
setIsDragging(bool) {
|
||||||
|
this.draggingActive.set(bool);
|
||||||
|
},
|
||||||
|
|
||||||
|
scrollLeft(position = 0) {
|
||||||
|
const lists = this.$('.js-lists');
|
||||||
|
lists && lists.animate({
|
||||||
|
scrollLeft: position,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
currentCardIsInThisList() {
|
||||||
|
const currentCard = Cards.findOne(Session.get('currentCard'));
|
||||||
|
const listId = this.currentData()._id;
|
||||||
|
return currentCard && currentCard.listId === listId; //TODO: AND IN THIS SWIMLANE
|
||||||
|
},
|
||||||
|
|
||||||
|
events() {
|
||||||
|
return [{
|
||||||
|
// Click-and-drag action
|
||||||
|
'mousedown .board-canvas'(evt) {
|
||||||
|
// Translating the board canvas using the click-and-drag action can
|
||||||
|
// conflict with the build-in browser mechanism to select text. We
|
||||||
|
// define a list of elements in which we disable the dragging because
|
||||||
|
// the user will legitimately expect to be able to select some text with
|
||||||
|
// his mouse.
|
||||||
|
const noDragInside = ['a', 'input', 'textarea', 'p', '.js-list-header'];
|
||||||
|
if ($(evt.target).closest(noDragInside.join(',')).length === 0 && this.$('.swimlane').prop('clientHeight') > evt.offsetY) {
|
||||||
|
this._isDragging = true;
|
||||||
|
this._lastDragPositionX = evt.clientX;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'mouseup'() {
|
||||||
|
if (this._isDragging) {
|
||||||
|
this._isDragging = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'mousemove'(evt) {
|
||||||
|
if (this._isDragging) {
|
||||||
|
// Update the canvas position
|
||||||
|
this.listsDom.scrollLeft -= evt.clientX - this._lastDragPositionX;
|
||||||
|
this._lastDragPositionX = evt.clientX;
|
||||||
|
// Disable browser text selection while dragging
|
||||||
|
evt.stopPropagation();
|
||||||
|
evt.preventDefault();
|
||||||
|
// Don't close opened card or inlined form at the end of the
|
||||||
|
// click-and-drag.
|
||||||
|
EscapeActions.executeUpTo('popup-close');
|
||||||
|
EscapeActions.preventNextClick();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}];
|
||||||
|
},
|
||||||
|
}).register('swimlane');
|
||||||
|
|
||||||
|
Template.swimlane.onRendered(function() {
|
||||||
|
const self = BlazeComponent.getComponentForElement(this.firstNode);
|
||||||
|
|
||||||
|
self.listsDom = this.find('.js-lists');
|
||||||
|
|
||||||
|
if (!Session.get('currentCard')) {
|
||||||
|
self.scrollLeft();
|
||||||
|
}
|
||||||
|
|
||||||
|
// We want to animate the card details window closing. We rely on CSS
|
||||||
|
// transition for the actual animation.
|
||||||
|
self.listsDom._uihooks = {
|
||||||
|
removeElement(node) {
|
||||||
|
const removeNode = _.once(() => {
|
||||||
|
node.parentNode.removeChild(node);
|
||||||
|
});
|
||||||
|
if ($(node).hasClass('js-card-details')) {
|
||||||
|
$(node).css({
|
||||||
|
flexBasis: 0,
|
||||||
|
padding: 0,
|
||||||
|
});
|
||||||
|
$(self.listsDom).one(CSSEvents.transitionend, removeNode);
|
||||||
|
} else {
|
||||||
|
removeNode();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
$(self.listsDom).sortable({
|
||||||
|
tolerance: 'pointer',
|
||||||
|
helper: 'clone',
|
||||||
|
handle: '.js-list-header',
|
||||||
|
items: '.js-list:not(.js-list-composer)',
|
||||||
|
placeholder: 'list placeholder',
|
||||||
|
distance: 7,
|
||||||
|
start(evt, ui) {
|
||||||
|
ui.placeholder.height(ui.helper.height());
|
||||||
|
Popup.close();
|
||||||
|
},
|
||||||
|
stop() {
|
||||||
|
$(self.listsDom).find('.js-list:not(.js-list-composer)').each(
|
||||||
|
(i, list) => {
|
||||||
|
const data = Blaze.getData(list);
|
||||||
|
Lists.update(data._id, {
|
||||||
|
$set: {
|
||||||
|
sort: i,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
function userIsMember() {
|
||||||
|
return Meteor.user() && Meteor.user().isBoardMember();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disable drag-dropping while in multi-selection mode, or if the current user
|
||||||
|
// is not a board member
|
||||||
|
self.autorun(() => {
|
||||||
|
const $listDom = $(self.listsDom);
|
||||||
|
if ($listDom.data('sortable')) {
|
||||||
|
$(self.listsDom).sortable('option', 'disabled',
|
||||||
|
MultiSelection.isActive() || !userIsMember());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// If there is no data in the board (ie, no lists) we autofocus the list
|
||||||
|
// creation form by clicking on the corresponding element.
|
||||||
|
const currentBoard = Boards.findOne(Session.get('currentBoard'));
|
||||||
|
if (userIsMember() && currentBoard.lists().count() === 0) {
|
||||||
|
self.openNewListForm();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
BlazeComponent.extendComponent({
|
||||||
|
// Proxy
|
||||||
|
open() {
|
||||||
|
this.childComponents('inlinedForm')[0].open();
|
||||||
|
},
|
||||||
|
|
||||||
|
events() {
|
||||||
|
return [{
|
||||||
|
submit(evt) {
|
||||||
|
evt.preventDefault();
|
||||||
|
const titleInput = this.find('.list-name-input');
|
||||||
|
const title = titleInput.value.trim();
|
||||||
|
if (title) {
|
||||||
|
Lists.insert({
|
||||||
|
title,
|
||||||
|
boardId: Session.get('currentBoard'),
|
||||||
|
sort: $('.list').length,
|
||||||
|
});
|
||||||
|
|
||||||
|
titleInput.value = '';
|
||||||
|
titleInput.focus();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}];
|
||||||
|
},
|
||||||
|
}).register('addListForm');
|
||||||
|
|
||||||
|
Template.swimlane.helpers({
|
||||||
|
canSeeAddList() {
|
||||||
|
return Meteor.user() && Meteor.user().isBoardMember() && !Meteor.user().isCommentOnly();
|
||||||
|
},
|
||||||
|
});
|
||||||
21
client/components/swimlanes/swimlanes.styl
Normal file
21
client/components/swimlanes/swimlanes.styl
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
@import 'nib'
|
||||||
|
|
||||||
|
.swimlane-header-wrap
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 0 0 50px;
|
||||||
|
|
||||||
|
.swimlane-header
|
||||||
|
writing-mode: sideways-lr;
|
||||||
|
height: 100%;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 50px;
|
||||||
|
margin: 0;
|
||||||
|
font-weight: bold;
|
||||||
|
min-height: 9px;
|
||||||
|
min-width: 30px;
|
||||||
|
overflow: hidden;
|
||||||
|
-o-text-overflow: ellipsis;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
word-wrap: break-word;
|
||||||
|
text-align: center;
|
||||||
|
|
@ -187,6 +187,10 @@ Boards.helpers({
|
||||||
return Lists.find({ boardId: this._id, archived: false }, { sort: { sort: 1 } });
|
return Lists.find({ boardId: this._id, archived: false }, { sort: { sort: 1 } });
|
||||||
},
|
},
|
||||||
|
|
||||||
|
swimlanes() {
|
||||||
|
return Swimlanes.find({ boardId: this._id, archived: false }, { sort: { sort: 1 } });
|
||||||
|
},
|
||||||
|
|
||||||
hasOvertimeCards(){
|
hasOvertimeCards(){
|
||||||
const card = Cards.findOne({isOvertime: true, boardId: this._id, archived: false} );
|
const card = Cards.findOne({isOvertime: true, boardId: this._id, archived: false} );
|
||||||
return card !== undefined;
|
return card !== undefined;
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,9 @@ Cards.attachSchema(new SimpleSchema({
|
||||||
listId: {
|
listId: {
|
||||||
type: String,
|
type: String,
|
||||||
},
|
},
|
||||||
|
swimlaneId: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
// The system could work without this `boardId` information (we could deduce
|
// The system could work without this `boardId` information (we could deduce
|
||||||
// the board identifier from the card), but it would make the system more
|
// the board identifier from the card), but it would make the system more
|
||||||
// difficult to manage and less efficient.
|
// difficult to manage and less efficient.
|
||||||
|
|
|
||||||
|
|
@ -75,10 +75,11 @@ Lists.allow({
|
||||||
});
|
});
|
||||||
|
|
||||||
Lists.helpers({
|
Lists.helpers({
|
||||||
cards() {
|
cards(swimlaneId) {
|
||||||
return Cards.find(Filter.mongoSelector({
|
return Cards.find(Filter.mongoSelector({
|
||||||
listId: this._id,
|
listId: this._id,
|
||||||
archived: false,
|
archived: false,
|
||||||
|
swimlaneId: swimlaneId,
|
||||||
}), { sort: ['sort'] });
|
}), { sort: ['sort'] });
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
219
models/swimlanes.js
Normal file
219
models/swimlanes.js
Normal file
|
|
@ -0,0 +1,219 @@
|
||||||
|
Swimlanes = new Mongo.Collection('swimlanes');
|
||||||
|
|
||||||
|
Swimlanes.attachSchema(new SimpleSchema({
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
archived: {
|
||||||
|
type: Boolean,
|
||||||
|
autoValue() { // eslint-disable-line consistent-return
|
||||||
|
if (this.isInsert && !this.isSet) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
boardId: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
createdAt: {
|
||||||
|
type: Date,
|
||||||
|
autoValue() { // eslint-disable-line consistent-return
|
||||||
|
if (this.isInsert) {
|
||||||
|
return new Date();
|
||||||
|
} else {
|
||||||
|
this.unset();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
sort: {
|
||||||
|
type: Number,
|
||||||
|
decimal: true,
|
||||||
|
// XXX We should probably provide a default
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
updatedAt: {
|
||||||
|
type: Date,
|
||||||
|
optional: true,
|
||||||
|
autoValue() { // eslint-disable-line consistent-return
|
||||||
|
if (this.isUpdate) {
|
||||||
|
return new Date();
|
||||||
|
} else {
|
||||||
|
this.unset();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
Swimlanes.allow({
|
||||||
|
insert(userId, doc) {
|
||||||
|
return allowIsBoardMemberNonComment(userId, Boards.findOne(doc.boardId));
|
||||||
|
},
|
||||||
|
update(userId, doc) {
|
||||||
|
return allowIsBoardMemberNonComment(userId, Boards.findOne(doc.boardId));
|
||||||
|
},
|
||||||
|
remove(userId, doc) {
|
||||||
|
return allowIsBoardMemberNonComment(userId, Boards.findOne(doc.boardId));
|
||||||
|
},
|
||||||
|
fetch: ['boardId'],
|
||||||
|
});
|
||||||
|
|
||||||
|
Swimlanes.helpers({
|
||||||
|
cards() {
|
||||||
|
return Cards.find(Filter.mongoSelector({
|
||||||
|
swimlaneId: this._id,
|
||||||
|
archived: false,
|
||||||
|
}), { sort: ['sort'] });
|
||||||
|
},
|
||||||
|
|
||||||
|
allCards() {
|
||||||
|
return Cards.find({ swimlaneId: this._id });
|
||||||
|
},
|
||||||
|
|
||||||
|
board() {
|
||||||
|
return Boards.findOne(this.boardId);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
Swimlanes.mutations({
|
||||||
|
rename(title) {
|
||||||
|
return { $set: { title } };
|
||||||
|
},
|
||||||
|
|
||||||
|
archive() {
|
||||||
|
return { $set: { archived: true } };
|
||||||
|
},
|
||||||
|
|
||||||
|
restore() {
|
||||||
|
return { $set: { archived: false } };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
Swimlanes.hookOptions.after.update = { fetchPrevious: false };
|
||||||
|
|
||||||
|
if (Meteor.isServer) {
|
||||||
|
Meteor.startup(() => {
|
||||||
|
Swimlanes._collection._ensureIndex({ boardId: 1 });
|
||||||
|
});
|
||||||
|
|
||||||
|
Swimlanes.after.insert((userId, doc) => {
|
||||||
|
Activities.insert({
|
||||||
|
userId,
|
||||||
|
type: 'swimlane',
|
||||||
|
activityType: 'createSwimlane',
|
||||||
|
boardId: doc.boardId,
|
||||||
|
swimlaneId: doc._id,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
Swimlanes.before.remove((userId, doc) => {
|
||||||
|
Activities.insert({
|
||||||
|
userId,
|
||||||
|
type: 'swimlane',
|
||||||
|
activityType: 'removeSwimlane',
|
||||||
|
boardId: doc.boardId,
|
||||||
|
swimlaneId: doc._id,
|
||||||
|
title: doc.title,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
Swimlanes.after.update((userId, doc) => {
|
||||||
|
if (doc.archived) {
|
||||||
|
Activities.insert({
|
||||||
|
userId,
|
||||||
|
type: 'swimlane',
|
||||||
|
activityType: 'archivedSwimlane',
|
||||||
|
swimlaneId: doc._id,
|
||||||
|
boardId: doc.boardId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
//SWIMLANE REST API
|
||||||
|
if (Meteor.isServer) {
|
||||||
|
JsonRoutes.add('GET', '/api/boards/:boardId/swimlanes', function (req, res, next) {
|
||||||
|
try {
|
||||||
|
const paramBoardId = req.params.boardId;
|
||||||
|
Authentication.checkBoardAccess( req.userId, paramBoardId);
|
||||||
|
|
||||||
|
JsonRoutes.sendResult(res, {
|
||||||
|
code: 200,
|
||||||
|
data: Swimlanes.find({ boardId: paramBoardId, archived: false }).map(function (doc) {
|
||||||
|
return {
|
||||||
|
_id: doc._id,
|
||||||
|
title: doc.title,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
JsonRoutes.sendResult(res, {
|
||||||
|
code: 200,
|
||||||
|
data: error,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
JsonRoutes.add('GET', '/api/boards/:boardId/swimlanes/:swimlaneId', function (req, res, next) {
|
||||||
|
try {
|
||||||
|
const paramBoardId = req.params.boardId;
|
||||||
|
const paramSwimlaneId = req.params.swimlaneId;
|
||||||
|
Authentication.checkBoardAccess( req.userId, paramBoardId);
|
||||||
|
JsonRoutes.sendResult(res, {
|
||||||
|
code: 200,
|
||||||
|
data: Swimlanes.findOne({ _id: paramSwimlaneId, boardId: paramBoardId, archived: false }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
JsonRoutes.sendResult(res, {
|
||||||
|
code: 200,
|
||||||
|
data: error,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
JsonRoutes.add('POST', '/api/boards/:boardId/swimlanes', function (req, res, next) {
|
||||||
|
try {
|
||||||
|
Authentication.checkUserId( req.userId);
|
||||||
|
const paramBoardId = req.params.boardId;
|
||||||
|
const id = Swimlanes.insert({
|
||||||
|
title: req.body.title,
|
||||||
|
boardId: paramBoardId,
|
||||||
|
});
|
||||||
|
JsonRoutes.sendResult(res, {
|
||||||
|
code: 200,
|
||||||
|
data: {
|
||||||
|
_id: id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
JsonRoutes.sendResult(res, {
|
||||||
|
code: 200,
|
||||||
|
data: error,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
JsonRoutes.add('DELETE', '/api/boards/:boardId/swimlanes/:swimlaneId', function (req, res, next) {
|
||||||
|
try {
|
||||||
|
Authentication.checkUserId( req.userId);
|
||||||
|
const paramBoardId = req.params.boardId;
|
||||||
|
const paramSwimlaneId = req.params.swimlaneId;
|
||||||
|
Swimlanes.remove({ _id: paramSwimlaneId, boardId: paramBoardId });
|
||||||
|
JsonRoutes.sendResult(res, {
|
||||||
|
code: 200,
|
||||||
|
data: {
|
||||||
|
_id: paramSwimlaneId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
JsonRoutes.sendResult(res, {
|
||||||
|
code: 200,
|
||||||
|
data: error,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -73,6 +73,7 @@ Meteor.publishRelations('board', function(boardId) {
|
||||||
],
|
],
|
||||||
}, { limit: 1 }), function(boardId, board) {
|
}, { limit: 1 }), function(boardId, board) {
|
||||||
this.cursor(Lists.find({ boardId }));
|
this.cursor(Lists.find({ boardId }));
|
||||||
|
this.cursor(Swimlanes.find({ boardId }));
|
||||||
this.cursor(Integrations.find({ boardId }));
|
this.cursor(Integrations.find({ boardId }));
|
||||||
|
|
||||||
// Cards and cards comments
|
// Cards and cards comments
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue