summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.meteor/versions5
-rw-r--r--History.md3
-rw-r--r--client/components/activities/activities.js3
-rw-r--r--client/components/import/import.jade47
-rw-r--r--client/components/import/import.js235
-rw-r--r--client/components/import/import.styl17
-rw-r--r--i18n/en.i18n.json6
-rw-r--r--models/cards.js5
-rw-r--r--models/import.js227
-rw-r--r--models/users.js2
10 files changed, 451 insertions, 99 deletions
diff --git a/.meteor/versions b/.meteor/versions
index 5c4c189b..a16f56bd 100644
--- a/.meteor/versions
+++ b/.meteor/versions
@@ -12,6 +12,7 @@ babel-runtime@0.1.4
base64@1.0.4
binary-heap@1.0.4
blaze@2.1.3
+blaze-html-templates@1.0.1
blaze-tools@1.0.4
boilerplate-generator@1.0.4
caching-compiler@1.0.0
@@ -63,7 +64,7 @@ idmontie:migrations@1.0.1
jquery@1.11.4
kadira:blaze-layout@2.2.0
kadira:dochead@1.3.2
-kadira:flow-router@2.8.0
+kadira:flow-router@2.9.0
kenton:accounts-sandstorm@0.1.8
launch-screen@1.0.4
livedata@1.0.15
@@ -124,7 +125,7 @@ seriousm:emoji-continued@1.4.0
service-configuration@1.0.5
session@1.1.1
sha@1.0.4
-softwarerero:accounts-t9n@1.1.4
+softwarerero:accounts-t9n@1.1.6
spacebars@1.0.7
spacebars-compiler@1.0.7
srp@1.0.4
diff --git a/History.md b/History.md
index 6b622699..3e4ae42c 100644
--- a/History.md
+++ b/History.md
@@ -2,7 +2,8 @@
This release features:
-* Card import from Trello
+* Trello boards and cards importation, including card history, assigned members,
+ labels, comments, and attachments;
* Autocompletion in the minicard editor. Start with <kbd>@</kbd> to start the
a board member autocompletion, or <kbd>#</kbd> for a label;
* Accelerate the initial page rendering by sending the data on the intial HTTP
diff --git a/client/components/activities/activities.js b/client/components/activities/activities.js
index 64e9865d..c1465b04 100644
--- a/client/components/activities/activities.js
+++ b/client/components/activities/activities.js
@@ -86,7 +86,8 @@ BlazeComponent.extendComponent({
attachmentLink() {
const attachment = this.currentData().attachment();
- return attachment && Blaze.toHTML(HTML.A({
+ // trying to display url before file is stored generates js errors
+ return attachment && attachment.url({ download: true }) && Blaze.toHTML(HTML.A({
href: FlowRouter.path(attachment.url({ download: true })),
target: '_blank',
}, attachment.name()));
diff --git a/client/components/import/import.jade b/client/components/import/import.jade
index f63661af..74b6ca13 100644
--- a/client/components/import/import.jade
+++ b/client/components/import/import.jade
@@ -4,4 +4,51 @@ template(name="importPopup")
form
p: label(for='import-textarea') {{_ getLabel}}
textarea#import-textarea.js-import-json(placeholder="{{_ 'import-json-placeholder'}}" autofocus)
+ | {{jsonText}}
+ if membersMapping
+ div
+ a.show-mapping
+ | {{_ 'import-show-user-mapping'}}
input.primary.wide(type="submit" value="{{_ 'import'}}")
+
+template(name="mapMembersPopup")
+ .map-members
+ p {{_ 'import-members-map'}}
+ .mapping-list
+ each members
+ .mapping
+ a.source
+ div.full-name
+ = fullName
+ div.username
+ | ({{username}})
+ .wekan
+ if wekan
+ +userAvatar(userId=wekan._id)
+ else
+ a.member.add-member.js-add-members
+ i.fa.fa-plus
+ form
+ input.primary.wide(type="submit" value="{{_ 'done'}}")
+
+ template(name="addMemberPopup")
+
+template(name="mapMembersAddPopup")
+ .select-member
+ p
+ | {{_ 'import-user-select'}}
+ .js-map-member
+ +esInput(index="users")
+ ul.pop-over-list
+ +esEach(index="users")
+ li.item.js-member-item
+ a.name.js-select-import(title="{{profile.name}} ({{username}})" data-id="{{_id}}")
+ +userAvatar(userId=_id esSearch=true)
+ span.full-name
+ = profile.name
+ | (<span class="username">{{username}}</span>)
+ +ifEsIsSearching(index='users')
+ +spinner
+ +ifEsHasNoResults(index="users")
+ .manage-member-section
+ p.quiet {{_ 'no-results'}}
diff --git a/client/components/import/import.js b/client/components/import/import.js
index c6957fa9..63285e57 100644
--- a/client/components/import/import.js
+++ b/client/components/import/import.js
@@ -11,48 +11,122 @@ const ImportPopup = BlazeComponent.extendComponent({
return 'importPopup';
},
- events() {
- return [{
- 'submit': (evt) => {
- evt.preventDefault();
- const dataJson = $(evt.currentTarget).find('.js-import-json').val();
- let dataObject;
- try {
- dataObject = JSON.parse(dataJson);
- } catch (e) {
- this.setError('error-json-malformed');
- return;
- }
- Meteor.call(this.getMethodName(), dataObject, this.getAdditionalData(),
- (error, response) => {
- if (error) {
- this.setError(error.error);
- } else {
- Filter.addException(response);
- this.onFinish(response);
- }
- }
- );
- },
- }];
+ jsonText() {
+ return Session.get('import.text');
+ },
+
+ membersMapping() {
+ return Session.get('import.membersToMap');
},
onCreated() {
this.error = new ReactiveVar('');
+ this.dataToImport = '';
+ },
+
+ onFinish() {
+ Popup.close();
+ },
+
+ onShowMapping(evt) {
+ this._storeText(evt);
+ Popup.open('mapMembers')(evt);
+ },
+
+ onSubmit(evt){
+ evt.preventDefault();
+ const dataJson = this._storeText(evt);
+ let dataObject;
+ try {
+ dataObject = JSON.parse(dataJson);
+ this.setError('');
+ } catch (e) {
+ this.setError('error-json-malformed');
+ return;
+ }
+ if(this._hasAllNeededData(dataObject)) {
+ this._import(dataObject);
+ } else {
+ this._prepareAdditionalData(dataObject);
+ Popup.open(this._screenAdditionalData())(evt);
+
+ }
+ },
+
+ events() {
+ return [{
+ submit: this.onSubmit,
+ 'click .show-mapping': this.onShowMapping,
+ }];
},
setError(error) {
this.error.set(error);
},
- onFinish() {
- Popup.close();
+ _import(dataObject) {
+ const additionalData = this.getAdditionalData();
+ const membersMapping = this.membersMapping();
+ if (membersMapping) {
+ const mappingById = {};
+ membersMapping.forEach((member) => {
+ if (member.wekan) {
+ mappingById[member.id] = member.wekan._id;
+ }
+ });
+ additionalData.membersMapping = mappingById;
+ }
+ Session.set('import.membersToMap', null);
+ Session.set('import.text', null);
+ Meteor.call(this.getMethodName(), dataObject, additionalData,
+ (error, response) => {
+ if (error) {
+ this.setError(error.error);
+ } else {
+ // ensure will display what we just imported
+ Filter.addException(response);
+ this.onFinish(response);
+ }
+ }
+ );
+ },
+
+ _hasAllNeededData(dataObject) {
+ // import has no members or they are already mapped
+ return dataObject.members.length === 0 || this.membersMapping();
+ },
+
+ _prepareAdditionalData(dataObject) {
+ // we will work on the list itself (an ordered array of objects)
+ // when a mapping is done, we add a 'wekan' field to the object representing the imported member
+ const membersToMap = dataObject.members;
+ // auto-map based on username
+ membersToMap.forEach((importedMember) => {
+ const wekanUser = Users.findOne({username: importedMember.username});
+ if(wekanUser) {
+ importedMember.wekan = wekanUser;
+ }
+ });
+ // store members data and mapping in Session
+ // (we go deep and 2-way, so storing in data context is not a viable option)
+ Session.set('import.membersToMap', membersToMap);
+ return membersToMap;
+ },
+
+ _screenAdditionalData() {
+ return 'mapMembers';
+ },
+
+ _storeText() {
+ const dataJson = this.$('.js-import-json').val();
+ Session.set('import.text', dataJson);
+ return dataJson;
},
});
ImportPopup.extendComponent({
getAdditionalData() {
- const listId = this.data()._id;
+ const listId = this.currentData()._id;
const selector = `#js-list-${this.currentData()._id} .js-minicard:first`;
const firstCardDom = $(selector).get(0);
const sortIndex = Utils.calculateIndex(null, firstCardDom).base;
@@ -88,3 +162,110 @@ ImportPopup.extendComponent({
},
}).register('boardImportBoardPopup');
+const ImportMapMembers = BlazeComponent.extendComponent({
+ members() {
+ return Session.get('import.membersToMap');
+ },
+ _refreshMembers(listOfMembers) {
+ Session.set('import.membersToMap', listOfMembers);
+ },
+ /**
+ * Will look into the list of members to import for the specified memberId,
+ * then set its property to the supplied value.
+ * If unset is true, it will remove the property from the rest of the list as well.
+ *
+ * use:
+ * - memberId = null to use selected member
+ * - value = null to unset a property
+ * - unset = true to ensure property is only set on 1 member at a time
+ */
+ _setPropertyForMember(property, value, memberId, unset = false) {
+ const listOfMembers = this.members();
+ let finder = null;
+ if(memberId) {
+ finder = (member) => member.id === memberId;
+ } else {
+ finder = (member) => member.selected;
+ }
+ listOfMembers.forEach((member) => {
+ if(finder(member)) {
+ if(value !== null) {
+ member[property] = value;
+ } else {
+ delete member[property];
+ }
+ if(!unset) {
+ // we shortcut if we don't care about unsetting the others
+ return false;
+ }
+ } else if(unset) {
+ delete member[property];
+ }
+ return true;
+ });
+ // Session.get gives us a copy, we have to set it back so it sticks
+ this._refreshMembers(listOfMembers);
+ },
+ setSelectedMember(memberId) {
+ return this._setPropertyForMember('selected', true, memberId, true);
+ },
+ /**
+ * returns the member with specified id,
+ * or the selected member if memberId is not specified
+ */
+ getMember(memberId = null) {
+ const allMembers = Session.get('import.membersToMap');
+ let finder = null;
+ if(memberId) {
+ finder = (user) => user.id === memberId;
+ } else {
+ finder = (user) => user.selected;
+ }
+ return allMembers.find(finder);
+ },
+ mapSelectedMember(wekan) {
+ return this._setPropertyForMember('wekan', wekan, null);
+ },
+ unmapMember(memberId){
+ return this._setPropertyForMember('wekan', null, memberId);
+ },
+});
+
+ImportMapMembers.extendComponent({
+ onMapMember(evt) {
+ const memberToMap = this.currentData();
+ if(memberToMap.wekan) {
+ // todo xxx ask for confirmation?
+ this.unmapMember(memberToMap.id);
+ } else {
+ this.setSelectedMember(memberToMap.id);
+ Popup.open('mapMembersAdd')(evt);
+ }
+ },
+ onSubmit(evt) {
+ evt.preventDefault();
+ Popup.back();
+ },
+ events() {
+ return [{
+ 'submit': this.onSubmit,
+ 'click .mapping': this.onMapMember,
+ }];
+ },
+}).register('mapMembersPopup');
+
+ImportMapMembers.extendComponent({
+ onSelectUser(){
+ this.mapSelectedMember(this.currentData());
+ Popup.back();
+ },
+ events() {
+ return [{
+ 'click .js-select-import': this.onSelectUser,
+ }];
+ },
+ onRendered() {
+ // todo XXX why do I not get the focus??
+ this.find('.js-map-member input').focus();
+ },
+}).register('mapMembersAddPopup');
diff --git a/client/components/import/import.styl b/client/components/import/import.styl
new file mode 100644
index 00000000..3c6cfdf3
--- /dev/null
+++ b/client/components/import/import.styl
@@ -0,0 +1,17 @@
+.map-members
+ .mapping:first-of-type
+ border-top: solid 1px #999
+ .mapping
+ padding: 10px 0
+ border-bottom: solid 1px #999
+ .source
+ display: inline-block
+ width: 80%
+ .wekan
+ display: inline-block
+ width: 35px
+ .member
+ float: none
+
+a.show-mapping
+ text-decoration underline
diff --git a/i18n/en.i18n.json b/i18n/en.i18n.json
index 66bd0155..4a6edfe9 100644
--- a/i18n/en.i18n.json
+++ b/i18n/en.i18n.json
@@ -115,6 +115,7 @@
"disambiguateMultiLabelPopup-title": "Disambiguate Label Action",
"disambiguateMultiMemberPopup-title": "Disambiguate Member Action",
"discard": "Discard",
+ "done": "Done",
"download": "Download",
"edit": "Edit",
"edit-avatar": "Change Avatar",
@@ -142,6 +143,9 @@
"import-card": "Import a Trello card",
"import-card-trello-instruction": "Go to a Trello card, select 'Share and more...' then 'Export JSON' and copy the resulting text",
"import-json-placeholder": "Paste your valid JSON data here",
+ "import-members-map": "Your imported board has some members. Please map the members you want to import to Wekan users",
+ "import-show-user-mapping": "Review members mapping",
+ "import-user-select": "Pick the Wekan user you want to use as this member",
"info": "Infos",
"initials": "Initials",
"joined": "joined",
@@ -165,6 +169,8 @@
"lists": "Lists",
"log-out": "Log Out",
"loginPopup-title": "Log In",
+ "mapMembersPopup-title": "Map members",
+ "mapMembersAddPopup-title": "Select Wekan member",
"memberMenuPopup-title": "Member Settings",
"members": "Members",
"menu": "Menu",
diff --git a/models/cards.js b/models/cards.js
index 2e16583d..1895fc69 100644
--- a/models/cards.js
+++ b/models/cards.js
@@ -108,7 +108,10 @@ Cards.helpers({
},
cover() {
- return Attachments.findOne(this.coverId);
+ const cover = Attachments.findOne(this.coverId);
+ // if we return a cover before it is fully stored, we will get errors when we try to display it
+ // todo XXX we could return a default "upload pending" image in the meantime?
+ return cover && cover.url() && cover;
},
absoluteUrl() {
diff --git a/models/import.js b/models/import.js
index a6e9f3d5..33f56d4b 100644
--- a/models/import.js
+++ b/models/import.js
@@ -4,7 +4,7 @@ const DateString = Match.Where(function (dateAsString) {
});
class TrelloCreator {
- constructor() {
+ constructor(data) {
// The object creation dates, indexed by Trello id (so we only parse actions
// once!)
this.createdAt = {
@@ -18,6 +18,11 @@ class TrelloCreator {
this.lists = {};
// The comments, indexed by Trello card id (to map when importing cards)
this.comments = {};
+ // the members, indexed by Trello member id => Wekan user ID
+ this.members = data.membersMapping ? data.membersMapping : {};
+
+ // maps a trelloCardId to an array of trelloAttachments
+ this.attachments = {};
}
checkActions(trelloActions) {
@@ -90,6 +95,24 @@ class TrelloCreator {
stars: 0,
title: trelloBoard.name,
};
+ // now add other members
+ if(trelloBoard.memberships) {
+ trelloBoard.memberships.forEach((trelloMembership) => {
+ const trelloId = trelloMembership.idMember;
+ // do we have a mapping?
+ if(this.members[trelloId]) {
+ const wekanId = this.members[trelloId];
+ // do we already have it in our list?
+ if(!boardToCreate.members.find((wekanMember) => wekanMember.userId === wekanId)) {
+ boardToCreate.members.push({
+ userId: wekanId,
+ isAdmin: false,
+ isActive: true,
+ });
+ }
+ }
+ });
+ }
trelloBoard.labels.forEach((label) => {
const labelToCreate = {
_id: Random.id(6),
@@ -121,56 +144,14 @@ class TrelloCreator {
return boardId;
}
- // Create labels if they do not exist and load this.labels.
- createLabels(trelloLabels, board) {
- trelloLabels.forEach((label) => {
- const color = label.color;
- const name = label.name;
- const existingLabel = board.getLabel(name, color);
- if (existingLabel) {
- this.labels[label.id] = existingLabel._id;
- } else {
- const idLabelCreated = board.pushLabel(name, color);
- this.labels[label.id] = idLabelCreated;
- }
- });
- }
-
- createLists(trelloLists, boardId) {
- trelloLists.forEach((list) => {
- const listToCreate = {
- archived: list.closed,
- boardId,
- // We are being defensing here by providing a default date (now) if the
- // creation date wasn't found on the action log. This happen on old
- // Trello boards (eg from 2013) that didn't log the 'createList' action
- // we require.
- createdAt: new Date(this.createdAt.lists[list.id] || Date.now()),
- title: list.name,
- userId: Meteor.userId(),
- };
- const listId = Lists.direct.insert(listToCreate);
- const now = new Date();
- Lists.direct.update(listId, {$set: {'updatedAt': now}});
- this.lists[list.id] = listId;
- // log activity
- Activities.direct.insert({
- activityType: 'importList',
- boardId,
- createdAt: now,
- listId,
- source: {
- id: list.id,
- system: 'Trello',
- },
- // We attribute the import to current user, not the one from the
- // original object
- userId: Meteor.userId(),
- });
- });
- }
-
- createCardsAndComments(trelloCards, boardId) {
+ /**
+ * Create the Wekan cards corresponding to the supplied Trello cards,
+ * as well as all linked data: activities, comments, and attachments
+ * @param trelloCards
+ * @param boardId
+ * @returns {Array}
+ */
+ createCards(trelloCards, boardId) {
const result = [];
trelloCards.forEach((card) => {
const cardToCreate = {
@@ -191,6 +172,25 @@ class TrelloCreator {
return this.labels[trelloId];
});
}
+ // add members {
+ if(card.idMembers) {
+ const wekanMembers = [];
+ // we can't just map, as some members may not have been mapped
+ card.idMembers.forEach((trelloId) => {
+ if(this.members[trelloId]) {
+ const wekanId = this.members[trelloId];
+ // we may map multiple Trello members to the same wekan user
+ // in which case we risk adding the same user multiple times
+ if(!wekanMembers.find((wId) => wId === wekanId)){
+ wekanMembers.push(wekanId);
+ }
+ }
+ return true;
+ });
+ if(wekanMembers.length>0) {
+ cardToCreate.members = wekanMembers;
+ }
+ }
// insert card
const cardId = Cards.direct.insert(cardToCreate);
// log activity
@@ -234,12 +234,90 @@ class TrelloCreator {
});
});
}
- // XXX add attachments
+ const attachments = this.attachments[card.id];
+ const trelloCoverId = card.idAttachmentCover;
+ if (attachments) {
+ attachments.forEach((att) => {
+ const file = new FS.File();
+ // Simulating file.attachData on the client generates multiple errors
+ // - HEAD returns null, which causes exception down the line
+ // - the template then tries to display the url to the attachment which causes other errors
+ // so we make it server only, and let UI catch up once it is done, forget about latency comp.
+ if(Meteor.isServer) {
+ file.attachData(att.url, function (error) {
+ file.boardId = boardId;
+ file.cardId = cardId;
+ if (error) {
+ throw(error);
+ } else {
+ const wekanAtt = Attachments.insert(file, () => {
+ // we do nothing
+ });
+ //
+ if(trelloCoverId === att.id) {
+ Cards.direct.update(cardId, { $set: {coverId: wekanAtt._id}});
+ }
+ }
+ });
+ }
+ // todo XXX set cover - if need be
+ });
+ }
result.push(cardId);
});
return result;
}
+ // Create labels if they do not exist and load this.labels.
+ createLabels(trelloLabels, board) {
+ trelloLabels.forEach((label) => {
+ const color = label.color;
+ const name = label.name;
+ const existingLabel = board.getLabel(name, color);
+ if (existingLabel) {
+ this.labels[label.id] = existingLabel._id;
+ } else {
+ const idLabelCreated = board.pushLabel(name, color);
+ this.labels[label.id] = idLabelCreated;
+ }
+ });
+ }
+
+ createLists(trelloLists, boardId) {
+ trelloLists.forEach((list) => {
+ const listToCreate = {
+ archived: list.closed,
+ boardId,
+ // We are being defensing here by providing a default date (now) if the
+ // creation date wasn't found on the action log. This happen on old
+ // Trello boards (eg from 2013) that didn't log the 'createList' action
+ // we require.
+ createdAt: new Date(this.createdAt.lists[list.id] || Date.now()),
+ title: list.name,
+ userId: Meteor.userId(),
+ };
+ const listId = Lists.direct.insert(listToCreate);
+ const now = new Date();
+ Lists.direct.update(listId, {$set: {'updatedAt': now}});
+ this.lists[list.id] = listId;
+ // log activity
+ Activities.direct.insert({
+ activityType: 'importList',
+ boardId,
+ createdAt: now,
+ listId,
+ source: {
+ id: list.id,
+ system: 'Trello',
+ },
+ // We attribute the import to current user, not the one from the
+ // original object
+ userId: Meteor.userId(),
+ });
+ });
+ }
+
+
getColor(trelloColorCode) {
// trello color name => wekan color
const mapColors = {
@@ -269,6 +347,29 @@ class TrelloCreator {
parseActions(trelloActions) {
trelloActions.forEach((action) => {
switch (action.type) {
+ case 'addAttachmentToCard':
+ // We have to be cautious, because the attachment could have been removed later.
+ // In that case Trello still reports its addition, but removes its 'url' field.
+ // So we test for that
+ const trelloAttachment = action.data.attachment;
+ if(trelloAttachment.url) {
+ // we cannot actually create the Wekan attachment, because we don't yet
+ // have the cards to attach it to, so we store it in the instance variable.
+ const trelloCardId = action.data.card.id;
+ if(!this.attachments[trelloCardId]) {
+ this.attachments[trelloCardId] = [];
+ }
+ this.attachments[trelloCardId].push(trelloAttachment);
+ }
+ break;
+ case 'commentCard':
+ const id = action.data.card.id;
+ if (this.comments[id]) {
+ this.comments[id].push(action);
+ } else {
+ this.comments[id] = [action];
+ }
+ break;
case 'createBoard':
this.createdAt.board = action.date;
break;
@@ -280,14 +381,6 @@ class TrelloCreator {
const listId = action.data.list.id;
this.createdAt.lists[listId] = action.date;
break;
- case 'commentCard':
- const id = action.data.card.id;
- if (this.comments[id]) {
- this.comments[id].push(action);
- } else {
- this.comments[id] = [action];
- }
- break;
default:
// do nothing
break;
@@ -298,12 +391,13 @@ class TrelloCreator {
Meteor.methods({
importTrelloBoard(trelloBoard, data) {
- const trelloCreator = new TrelloCreator();
+ const trelloCreator = new TrelloCreator(data);
// 1. check all parameters are ok from a syntax point of view
try {
- // we don't use additional data - this should be an empty object
- check(data, {});
+ check(data, {
+ membersMapping: Match.Optional(Object),
+ });
trelloCreator.checkActions(trelloBoard.actions);
trelloCreator.checkBoard(trelloBoard);
trelloCreator.checkLabels(trelloBoard.labels);
@@ -320,19 +414,20 @@ Meteor.methods({
trelloCreator.parseActions(trelloBoard.actions);
const boardId = trelloCreator.createBoardAndLabels(trelloBoard);
trelloCreator.createLists(trelloBoard.lists, boardId);
- trelloCreator.createCardsAndComments(trelloBoard.cards, boardId);
+ trelloCreator.createCards(trelloBoard.cards, boardId);
// XXX add members
return boardId;
},
importTrelloCard(trelloCard, data) {
- const trelloCreator = new TrelloCreator();
+ const trelloCreator = new TrelloCreator(data);
// 1. check parameters are ok from a syntax point of view
try {
check(data, {
listId: String,
sortIndex: Number,
+ membersMapping: Match.Optional(Object),
});
trelloCreator.checkCards([trelloCard]);
trelloCreator.checkLabels(trelloCard.labels);
@@ -358,7 +453,7 @@ Meteor.methods({
trelloCreator.parseActions(trelloCard.actions);
const board = list.board();
trelloCreator.createLabels(trelloCard.labels, board);
- const cardIds = trelloCreator.createCardsAndComments([trelloCard], board._id);
+ const cardIds = trelloCreator.createCards([trelloCard], board._id);
return cardIds[0];
},
});
diff --git a/models/users.js b/models/users.js
index 1e69564d..e85671bc 100644
--- a/models/users.js
+++ b/models/users.js
@@ -2,7 +2,7 @@ Users = Meteor.users; // eslint-disable-line meteor/collections
// Search a user in the complete server database by its name or username. This
// is used for instance to add a new user to a board.
-const searchInFields = ['username', 'profile.name'];
+const searchInFields = ['username', 'profile.fullname'];
Users.initEasySearch(searchInFields, {
use: 'mongo-db',
returnFields: [...searchInFields, 'profile.avatarUrl'],