mirror of https://github.com/wekan/wekan
parent
8b6a2eade3
commit
690a5b9703
@ -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 |
@ -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(); |
||||
}, |
||||
}); |
@ -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; |
@ -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, |
||||
}); |
||||
} |
||||
}); |
||||
|
||||
} |
Loading…
Reference in new issue