123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555 |
- 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',
- ],
- },
- // 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,
- },
- }));
- 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';
- },
- lists() {
- return Lists.find({ boardId: this._id, archived: false }, { sort: { sort: 1 }});
- },
- 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}`;
- },
- // 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;
- },
- });
- 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 }}};
- },
- 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,
- },
- };
- },
- });
- 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,
- });
- });
- }
- });
- }
|