123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921 |
- Boards = new Mongo.Collection('boards');
- Boards.attachSchema(new SimpleSchema({
- title: {
- type: String,
- },
- slug: {
- type: String,
- autoValue() { // eslint-disable-line consistent-return
- // XXX We need to improve slug management. Only the id should be necessary
- // to identify a board in the code.
- // XXX If the board title is updated, the slug should also be updated.
- // In some cases (Chinese and Japanese for instance) the `getSlug` function
- // return an empty string. This is causes bugs in our application so we set
- // a default slug in this case.
- if (this.isInsert && !this.isSet) {
- let slug = 'board';
- const title = this.field('title');
- if (title.isSet) {
- slug = getSlug(title.value) || slug;
- }
- return slug;
- }
- },
- },
- archived: {
- type: Boolean,
- autoValue() { // eslint-disable-line consistent-return
- if (this.isInsert && !this.isSet) {
- return false;
- }
- },
- },
- createdAt: {
- type: Date,
- autoValue() { // eslint-disable-line consistent-return
- if (this.isInsert) {
- return new Date();
- } else {
- this.unset();
- }
- },
- },
- // XXX Inconsistent field naming
- modifiedAt: {
- type: Date,
- optional: true,
- autoValue() { // eslint-disable-line consistent-return
- if (this.isUpdate) {
- return new Date();
- } else {
- this.unset();
- }
- },
- },
- // De-normalized number of users that have starred this board
- stars: {
- type: Number,
- autoValue() { // eslint-disable-line consistent-return
- if (this.isInsert) {
- return 0;
- }
- },
- },
- // De-normalized label system
- 'labels': {
- type: [Object],
- autoValue() { // eslint-disable-line consistent-return
- if (this.isInsert && !this.isSet) {
- const colors = Boards.simpleSchema()._schema['labels.$.color'].allowedValues;
- const defaultLabelsColors = _.clone(colors).splice(0, 6);
- return defaultLabelsColors.map((color) => ({
- color,
- _id: Random.id(6),
- name: '',
- }));
- }
- },
- },
- 'labels.$._id': {
- // We don't specify that this field must be unique in the board because that
- // will cause performance penalties and is not necessary since this field is
- // always set on the server.
- // XXX Actually if we create a new label, the `_id` is set on the client
- // without being overwritten by the server, could it be a problem?
- type: String,
- },
- 'labels.$.name': {
- type: String,
- optional: true,
- },
- 'labels.$.color': {
- type: String,
- allowedValues: [
- 'green', 'yellow', 'orange', 'red', 'purple',
- 'blue', 'sky', 'lime', 'pink', 'black',
- 'silver', 'peachpuff', 'crimson', 'plum', 'darkgreen',
- 'slateblue', 'magenta', 'gold', 'navy', 'gray',
- 'saddlebrown', 'paleturquoise', 'mistyrose', 'indigo',
- ],
- },
- // XXX We might want to maintain more informations under the member sub-
- // documents like de-normalized meta-data (the date the member joined the
- // board, the number of contributions, etc.).
- 'members': {
- type: [Object],
- autoValue() { // eslint-disable-line consistent-return
- if (this.isInsert && !this.isSet) {
- return [{
- userId: this.userId,
- isAdmin: true,
- isActive: true,
- isNoComments: false,
- isCommentOnly: false,
- }];
- }
- },
- },
- 'members.$.userId': {
- type: String,
- },
- 'members.$.isAdmin': {
- type: Boolean,
- },
- 'members.$.isActive': {
- type: Boolean,
- },
- 'members.$.isNoComments': {
- type: Boolean,
- },
- 'members.$.isCommentOnly': {
- type: Boolean,
- },
- permission: {
- type: String,
- allowedValues: ['public', 'private'],
- },
- color: {
- type: String,
- allowedValues: [
- 'belize',
- 'nephritis',
- 'pomegranate',
- 'pumpkin',
- 'wisteria',
- 'midnight',
- ],
- autoValue() { // eslint-disable-line consistent-return
- if (this.isInsert && !this.isSet) {
- return Boards.simpleSchema()._schema.color.allowedValues[0];
- }
- },
- },
- description: {
- type: String,
- optional: true,
- },
- subtasksDefaultBoardId: {
- type: String,
- optional: true,
- defaultValue: null,
- },
- subtasksDefaultListId: {
- type: String,
- optional: true,
- defaultValue: null,
- },
- allowsSubtasks: {
- type: Boolean,
- defaultValue: true,
- },
- presentParentTask: {
- type: String,
- allowedValues: [
- 'prefix-with-full-path',
- 'prefix-with-parent',
- 'subtext-with-full-path',
- 'subtext-with-parent',
- 'no-parent',
- ],
- optional: true,
- defaultValue: 'no-parent',
- },
- startAt: {
- type: Date,
- optional: true,
- },
- dueAt: {
- type: Date,
- optional: true,
- },
- endAt: {
- type: Date,
- optional: true,
- },
- spentTime: {
- type: Number,
- decimal: true,
- optional: true,
- },
- isOvertime: {
- type: Boolean,
- defaultValue: false,
- optional: true,
- },
- }));
- Boards.helpers({
- /**
- * Is supplied user authorized to view this board?
- */
- isVisibleBy(user) {
- if (this.isPublic()) {
- // public boards are visible to everyone
- return true;
- } else {
- // otherwise you have to be logged-in and active member
- return user && this.isActiveMember(user._id);
- }
- },
- /**
- * Is the user one of the active members of the board?
- *
- * @param userId
- * @returns {boolean} the member that matches, or undefined/false
- */
- isActiveMember(userId) {
- if (userId) {
- return this.members.find((member) => (member.userId === userId && member.isActive));
- } else {
- return false;
- }
- },
- isPublic() {
- return this.permission === 'public';
- },
- cards() {
- return Cards.find({ boardId: this._id, archived: false }, { sort: { title: 1 } });
- },
- lists() {
- return Lists.find({ boardId: this._id, archived: false }, { sort: { sort: 1 } });
- },
- swimlanes() {
- return Swimlanes.find({ boardId: this._id, archived: false }, { sort: { sort: 1 } });
- },
- hasOvertimeCards(){
- const card = Cards.findOne({isOvertime: true, boardId: this._id, archived: false} );
- return card !== undefined;
- },
- hasSpentTimeCards(){
- const card = Cards.findOne({spentTime: { $gt: 0 }, boardId: this._id, archived: false} );
- return card !== undefined;
- },
- activities() {
- return Activities.find({ boardId: this._id }, { sort: { createdAt: -1 } });
- },
- activeMembers() {
- return _.where(this.members, { isActive: true });
- },
- activeAdmins() {
- return _.where(this.members, { isActive: true, isAdmin: true });
- },
- memberUsers() {
- return Users.find({ _id: { $in: _.pluck(this.members, 'userId') } });
- },
- getLabel(name, color) {
- return _.findWhere(this.labels, { name, color });
- },
- labelIndex(labelId) {
- return _.pluck(this.labels, '_id').indexOf(labelId);
- },
- memberIndex(memberId) {
- return _.pluck(this.members, 'userId').indexOf(memberId);
- },
- hasMember(memberId) {
- return !!_.findWhere(this.members, { userId: memberId, isActive: true });
- },
- hasAdmin(memberId) {
- return !!_.findWhere(this.members, { userId: memberId, isActive: true, isAdmin: true });
- },
- hasNoComments(memberId) {
- return !!_.findWhere(this.members, { userId: memberId, isActive: true, isAdmin: false, isNoComments: true });
- },
- hasCommentOnly(memberId) {
- return !!_.findWhere(this.members, { userId: memberId, isActive: true, isAdmin: false, isCommentOnly: true });
- },
- absoluteUrl() {
- return FlowRouter.url('board', { id: this._id, slug: this.slug });
- },
- colorClass() {
- return `board-color-${this.color}`;
- },
- customFields() {
- return CustomFields.find({ boardId: this._id }, { sort: { name: 1 } });
- },
- // XXX currently mutations return no value so we have an issue when using addLabel in import
- // XXX waiting on https://github.com/mquandalle/meteor-collection-mutations/issues/1 to remove...
- pushLabel(name, color) {
- const _id = Random.id(6);
- Boards.direct.update(this._id, { $push: { labels: { _id, name, color } } });
- return _id;
- },
- searchCards(term, excludeLinked) {
- check(term, Match.OneOf(String, null, undefined));
- const query = { boardId: this._id };
- if (excludeLinked) {
- query.linkedId = null;
- }
- const projection = { limit: 10, sort: { createdAt: -1 } };
- if (term) {
- const regex = new RegExp(term, 'i');
- query.$or = [
- { title: regex },
- { description: regex },
- ];
- }
- return Cards.find(query, projection);
- },
- // A board alwasy has another board where it deposits subtasks of thasks
- // that belong to itself.
- getDefaultSubtasksBoardId() {
- if ((this.subtasksDefaultBoardId === null) || (this.subtasksDefaultBoardId === undefined)) {
- this.subtasksDefaultBoardId = Boards.insert({
- title: `^${this.title}^`,
- permission: this.permission,
- members: this.members,
- color: this.color,
- description: TAPi18n.__('default-subtasks-board', {board: this.title}),
- });
- Swimlanes.insert({
- title: TAPi18n.__('default'),
- boardId: this.subtasksDefaultBoardId,
- });
- Boards.update(this._id, {$set: {
- subtasksDefaultBoardId: this.subtasksDefaultBoardId,
- }});
- }
- return this.subtasksDefaultBoardId;
- },
- getDefaultSubtasksBoard() {
- return Boards.findOne(this.getDefaultSubtasksBoardId());
- },
- getDefaultSubtasksListId() {
- if ((this.subtasksDefaultListId === null) || (this.subtasksDefaultListId === undefined)) {
- this.subtasksDefaultListId = Lists.insert({
- title: TAPi18n.__('queue'),
- boardId: this._id,
- });
- Boards.update(this._id, {$set: {
- subtasksDefaultListId: this.subtasksDefaultListId,
- }});
- }
- return this.subtasksDefaultListId;
- },
- getDefaultSubtasksList() {
- return Lists.findOne(this.getDefaultSubtasksListId());
- },
- getDefaultSwimline() {
- let result = Swimlanes.findOne({boardId: this._id});
- if (result === undefined) {
- Swimlanes.insert({
- title: TAPi18n.__('default'),
- boardId: this._id,
- });
- result = Swimlanes.findOne({boardId: this._id});
- }
- return result;
- },
- cardsInInterval(start, end) {
- return Cards.find({
- boardId: this._id,
- $or: [
- {
- startAt: {
- $lte: start,
- }, endAt: {
- $gte: start,
- },
- }, {
- startAt: {
- $lte: end,
- }, endAt: {
- $gte: end,
- },
- }, {
- startAt: {
- $gte: start,
- }, endAt: {
- $lte: end,
- },
- },
- ],
- });
- },
- });
- Boards.mutations({
- archive() {
- return { $set: { archived: true } };
- },
- restore() {
- return { $set: { archived: false } };
- },
- rename(title) {
- return { $set: { title } };
- },
- setDescription(description) {
- return { $set: { description } };
- },
- setColor(color) {
- return { $set: { color } };
- },
- setVisibility(visibility) {
- return { $set: { permission: visibility } };
- },
- addLabel(name, color) {
- // If label with the same name and color already exists we don't want to
- // create another one because they would be indistinguishable in the UI
- // (they would still have different `_id` but that is not exposed to the
- // user).
- if (!this.getLabel(name, color)) {
- const _id = Random.id(6);
- return { $push: { labels: { _id, name, color } } };
- }
- return {};
- },
- editLabel(labelId, name, color) {
- if (!this.getLabel(name, color)) {
- const labelIndex = this.labelIndex(labelId);
- return {
- $set: {
- [`labels.${labelIndex}.name`]: name,
- [`labels.${labelIndex}.color`]: color,
- },
- };
- }
- return {};
- },
- removeLabel(labelId) {
- return { $pull: { labels: { _id: labelId } } };
- },
- changeOwnership(fromId, toId) {
- const memberIndex = this.memberIndex(fromId);
- return {
- $set: {
- [`members.${memberIndex}.userId`]: toId,
- },
- };
- },
- addMember(memberId) {
- const memberIndex = this.memberIndex(memberId);
- if (memberIndex >= 0) {
- return {
- $set: {
- [`members.${memberIndex}.isActive`]: true,
- },
- };
- }
- return {
- $push: {
- members: {
- userId: memberId,
- isAdmin: false,
- isActive: true,
- isNoComments: false,
- isCommentOnly: false,
- },
- },
- };
- },
- removeMember(memberId) {
- const memberIndex = this.memberIndex(memberId);
- // we do not allow the only one admin to be removed
- const allowRemove = (!this.members[memberIndex].isAdmin) || (this.activeAdmins().length > 1);
- if (!allowRemove) {
- return {
- $set: {
- [`members.${memberIndex}.isActive`]: true,
- },
- };
- }
- return {
- $set: {
- [`members.${memberIndex}.isActive`]: false,
- [`members.${memberIndex}.isAdmin`]: false,
- },
- };
- },
- setMemberPermission(memberId, isAdmin, isNoComments, isCommentOnly) {
- const memberIndex = this.memberIndex(memberId);
- // do not allow change permission of self
- if (memberId === Meteor.userId()) {
- isAdmin = this.members[memberIndex].isAdmin;
- }
- return {
- $set: {
- [`members.${memberIndex}.isAdmin`]: isAdmin,
- [`members.${memberIndex}.isNoComments`]: isNoComments,
- [`members.${memberIndex}.isCommentOnly`]: isCommentOnly,
- },
- };
- },
- setAllowsSubtasks(allowsSubtasks) {
- return { $set: { allowsSubtasks } };
- },
- setSubtasksDefaultBoardId(subtasksDefaultBoardId) {
- return { $set: { subtasksDefaultBoardId } };
- },
- setSubtasksDefaultListId(subtasksDefaultListId) {
- return { $set: { subtasksDefaultListId } };
- },
- setPresentParentTask(presentParentTask) {
- return { $set: { presentParentTask } };
- },
- });
- if (Meteor.isServer) {
- Boards.allow({
- insert: Meteor.userId,
- update: allowIsBoardAdmin,
- remove: allowIsBoardAdmin,
- fetch: ['members'],
- });
- // The number of users that have starred this board is managed by trusted code
- // and the user is not allowed to update it
- Boards.deny({
- update(userId, board, fieldNames) {
- return _.contains(fieldNames, 'stars');
- },
- fetch: [],
- });
- // We can't remove a member if it is the last administrator
- Boards.deny({
- update(userId, doc, fieldNames, modifier) {
- if (!_.contains(fieldNames, 'members'))
- return false;
- // We only care in case of a $pull operation, ie remove a member
- if (!_.isObject(modifier.$pull && modifier.$pull.members))
- return false;
- // If there is more than one admin, it's ok to remove anyone
- const nbAdmins = _.where(doc.members, { isActive: true, isAdmin: true }).length;
- if (nbAdmins > 1)
- return false;
- // If all the previous conditions were verified, we can't remove
- // a user if it's an admin
- const removedMemberId = modifier.$pull.members.userId;
- return Boolean(_.findWhere(doc.members, {
- userId: removedMemberId,
- isAdmin: true,
- }));
- },
- fetch: ['members'],
- });
- Meteor.methods({
- quitBoard(boardId) {
- check(boardId, String);
- const board = Boards.findOne(boardId);
- if (board) {
- const userId = Meteor.userId();
- const index = board.memberIndex(userId);
- if (index >= 0) {
- board.removeMember(userId);
- return true;
- } else throw new Meteor.Error('error-board-notAMember');
- } else throw new Meteor.Error('error-board-doesNotExist');
- },
- });
- }
- if (Meteor.isServer) {
- // Let MongoDB ensure that a member is not included twice in the same board
- Meteor.startup(() => {
- Boards._collection._ensureIndex({
- _id: 1,
- 'members.userId': 1,
- }, { unique: true });
- Boards._collection._ensureIndex({ 'members.userId': 1 });
- });
- // Genesis: the first activity of the newly created board
- Boards.after.insert((userId, doc) => {
- Activities.insert({
- userId,
- type: 'board',
- activityTypeId: doc._id,
- activityType: 'createBoard',
- boardId: doc._id,
- });
- });
- // If the user remove one label from a board, we cant to remove reference of
- // this label in any card of this board.
- Boards.after.update((userId, doc, fieldNames, modifier) => {
- if (!_.contains(fieldNames, 'labels') ||
- !modifier.$pull ||
- !modifier.$pull.labels ||
- !modifier.$pull.labels._id) {
- return;
- }
- const removedLabelId = modifier.$pull.labels._id;
- Cards.update(
- { boardId: doc._id },
- {
- $pull: {
- labelIds: removedLabelId,
- },
- },
- { multi: true }
- );
- });
- const foreachRemovedMember = (doc, modifier, callback) => {
- Object.keys(modifier).forEach((set) => {
- if (modifier[set] !== false) {
- return;
- }
- const parts = set.split('.');
- if (parts.length === 3 && parts[0] === 'members' && parts[2] === 'isActive') {
- callback(doc.members[parts[1]].userId);
- }
- });
- };
- // Remove a member from all objects of the board before leaving the board
- Boards.before.update((userId, doc, fieldNames, modifier) => {
- if (!_.contains(fieldNames, 'members')) {
- return;
- }
- if (modifier.$set) {
- const boardId = doc._id;
- foreachRemovedMember(doc, modifier.$set, (memberId) => {
- Cards.update(
- { boardId },
- {
- $pull: {
- members: memberId,
- watchers: memberId,
- },
- },
- { multi: true }
- );
- Lists.update(
- { boardId },
- {
- $pull: {
- watchers: memberId,
- },
- },
- { multi: true }
- );
- const board = Boards._transform(doc);
- board.setWatcher(memberId, false);
- // Remove board from users starred list
- if (!board.isPublic()) {
- Users.update(
- memberId,
- {
- $pull: {
- 'profile.starredBoards': boardId,
- },
- }
- );
- }
- });
- }
- });
- // Add a new activity if we add or remove a member to the board
- Boards.after.update((userId, doc, fieldNames, modifier) => {
- if (!_.contains(fieldNames, 'members')) {
- return;
- }
- // Say hello to the new member
- if (modifier.$push && modifier.$push.members) {
- const memberId = modifier.$push.members.userId;
- Activities.insert({
- userId,
- memberId,
- type: 'member',
- activityType: 'addBoardMember',
- boardId: doc._id,
- });
- }
- // Say goodbye to the former member
- if (modifier.$set) {
- foreachRemovedMember(doc, modifier.$set, (memberId) => {
- Activities.insert({
- userId,
- memberId,
- type: 'member',
- activityType: 'removeBoardMember',
- boardId: doc._id,
- });
- });
- }
- });
- }
- //BOARDS REST API
- if (Meteor.isServer) {
- JsonRoutes.add('GET', '/api/users/:userId/boards', function (req, res) {
- try {
- Authentication.checkLoggedIn(req.userId);
- const paramUserId = req.params.userId;
- // A normal user should be able to see their own boards,
- // admins can access boards of any user
- Authentication.checkAdminOrCondition(req.userId, req.userId === paramUserId);
- const data = Boards.find({
- archived: false,
- 'members.userId': paramUserId,
- }, {
- sort: ['title'],
- }).map(function(board) {
- return {
- _id: board._id,
- title: board.title,
- };
- });
- JsonRoutes.sendResult(res, {code: 200, data});
- }
- catch (error) {
- JsonRoutes.sendResult(res, {
- code: 200,
- data: error,
- });
- }
- });
- JsonRoutes.add('GET', '/api/boards', function (req, res) {
- try {
- Authentication.checkUserId(req.userId);
- JsonRoutes.sendResult(res, {
- code: 200,
- data: Boards.find({ permission: 'public' }).map(function (doc) {
- return {
- _id: doc._id,
- title: doc.title,
- };
- }),
- });
- }
- catch (error) {
- JsonRoutes.sendResult(res, {
- code: 200,
- data: error,
- });
- }
- });
- JsonRoutes.add('GET', '/api/boards/:id', function (req, res) {
- try {
- const id = req.params.id;
- Authentication.checkBoardAccess(req.userId, id);
- JsonRoutes.sendResult(res, {
- code: 200,
- data: Boards.findOne({ _id: id }),
- });
- }
- catch (error) {
- JsonRoutes.sendResult(res, {
- code: 200,
- data: error,
- });
- }
- });
- JsonRoutes.add('POST', '/api/boards', function (req, res) {
- try {
- Authentication.checkUserId(req.userId);
- const id = Boards.insert({
- title: req.body.title,
- members: [
- {
- userId: req.body.owner,
- isAdmin: true,
- isActive: true,
- isNoComments: false,
- isCommentOnly: false,
- },
- ],
- permission: 'public',
- color: 'belize',
- });
- JsonRoutes.sendResult(res, {
- code: 200,
- data: {
- _id: id,
- },
- });
- }
- catch (error) {
- JsonRoutes.sendResult(res, {
- code: 200,
- data: error,
- });
- }
- });
- JsonRoutes.add('DELETE', '/api/boards/:id', function (req, res) {
- try {
- Authentication.checkUserId(req.userId);
- const id = req.params.id;
- Boards.remove({ _id: id });
- JsonRoutes.sendResult(res, {
- code: 200,
- data:{
- _id: id,
- },
- });
- }
- catch (error) {
- JsonRoutes.sendResult(res, {
- code: 200,
- data: error,
- });
- }
- });
- JsonRoutes.add('PUT', '/api/boards/:id/labels', function (req, res) {
- Authentication.checkUserId(req.userId);
- const id = req.params.id;
- try {
- if (req.body.hasOwnProperty('label')) {
- const board = Boards.findOne({ _id: id });
- const color = req.body.label.color;
- const name = req.body.label.name;
- const labelId = Random.id(6);
- if (!board.getLabel(name, color)) {
- Boards.direct.update({ _id: id }, { $push: { labels: { _id: labelId, name, color } } });
- JsonRoutes.sendResult(res, {
- code: 200,
- data: labelId,
- });
- } else {
- JsonRoutes.sendResult(res, {
- code: 200,
- });
- }
- }
- }
- catch (error) {
- JsonRoutes.sendResult(res, {
- data: error,
- });
- }
- });
- }
|