| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910 | 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,          isCommentOnly: false,        }];      }    },  },  'members.$.userId': {    type: String,  },  'members.$.isAdmin': {    type: Boolean,  },  'members.$.isActive': {    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 });  },  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,          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, 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}.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 APIif (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,            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,      });    }  });}
 |