فهرست منبع

Teams/Organizations to Admin Panel. In Progress.

Thanks to xet7 !

Related #802
Lauri Ojansivu 4 سال پیش
والد
کامیت
9e2093d6ae

+ 285 - 11
client/components/settings/peopleBody.jade

@@ -5,28 +5,95 @@ template(name="people")
     else
       .content-title.ext-box
         .ext-box-left
-          span
-            i.fa.fa-users
-            | {{_ 'people'}}
-          input#searchInput(placeholder="{{_ 'search'}}")
-          button#searchButton
-            i.fa.fa-search
-            | {{_ 'search'}}
-        .ext-box-right
-          span {{_ 'people-number'}} #{peopleNumber}
+          if loading.get
+            +spinner
+          else if orgSetting.get
+            span
+              i.fa.fa-sitemap
+              | {{_ 'organizations'}}
+            input#searchOrgInput(placeholder="{{_ 'search'}}")
+            button#searchOrgButton
+              i.fa.fa-search
+              | {{_ 'search'}}
+            .ext-box-right
+              span {{_ 'org-number'}} #{orgNumber}
+          else if teamSetting.get
+            span
+              i.fa.fa-users
+              | {{_ 'teams'}}
+            input#searchTeamInput(placeholder="{{_ 'search'}}")
+            button#searchTeamButton
+              i.fa.fa-search
+              | {{_ 'search'}}
+            .ext-box-right
+              span {{_ 'team-number'}} #{teamNumber}
+          else if peopleSetting.get
+            span
+              i.fa.fa-user
+              | {{_ 'people'}}
+            input#searchInput(placeholder="{{_ 'search'}}")
+            button#searchButton
+              i.fa.fa-search
+              | {{_ 'search'}}
+            .ext-box-right
+              span {{_ 'people-number'}} #{peopleNumber}
       .content-body
         .side-menu
           ul
             li.active
-              a.js-setting-menu(data-id="people-setting")
+              a.js-org-menu(data-id="org-setting")
+                i.fa.fa-sitemap
+                | {{_ 'organizations'}}
+            li
+              a.js-team-menu(data-id="team-setting")
                 i.fa.fa-users
+                | {{_ 'teams'}}
+            li
+              a.js-people-menu(data-id="people-setting")
+                i.fa.fa-user
                 | {{_ 'people'}}
         .main-body
           if loading.get
             +spinner
-          else if people.get
+          else if orgSetting.get
+            +orgGeneral
+          else if teamSetting.get
+            +teamGeneral
+          else if peopleSetting.get
             +peopleGeneral
 
+
+template(name="orgGeneral")
+  table
+    tbody
+      tr
+        th {{_ 'displayName'}}
+        th {{_ 'description'}}
+        th {{_ 'shortName'}}
+        th {{_ 'website'}}
+        th {{_ 'teams'}}
+        th {{_ 'createdAt'}}
+        th {{_ 'active'}}
+        th
+          +newOrgRow
+      each user in orgList
+        +orgRow(orgId=org._id)
+
+template(name="teamGeneral")
+  table
+    tbody
+      tr
+        th {{_ 'displayName'}}
+        th {{_ 'description'}}
+        th {{_ 'shortName'}}
+        th {{_ 'website'}}
+        th {{_ 'createdAt'}}
+        th {{_ 'active'}}
+        th
+          +newTeamRow
+      each team in teamList
+        +teamRow(teamId=team._id)
+
 template(name="peopleGeneral")
   table
     tbody
@@ -44,11 +111,93 @@ template(name="peopleGeneral")
       each user in peopleList
         +peopleRow(userId=user._id)
 
+template(name="newOrgRow")
+  a.new-org
+    i.fa.fa-edit
+    | {{_ 'new'}}
+
+template(name="newTeamRow")
+  a.new-team
+    i.fa.fa-edit
+    | {{_ 'new'}}
+
 template(name="newUserRow")
   a.new-user
     i.fa.fa-edit
     | {{_ 'new'}}
 
+template(name="orgRow")
+  tr
+    if orgData.loginDisabled
+      td <s>{{ orgData.displayName }}</s>
+    else
+      td {{ orgData.displayName }}
+    if orgData.loginDisabled
+      td <s>{{ orgData.orgDesc }}</s>
+    else
+      td {{ orgData.desc }}
+    if orgData.loginDisabled
+      td <s>{{ orgData.name }}</s>
+    else
+      td {{ orgData.name }}
+    if orgData.loginDisabled
+      td <s>{{ orgData.website }}</s>
+    else
+      td {{ orgData.website }}
+    if orgData.loginDisabled
+      td <s>{{ orgData.teams }}</s>
+    else
+      td {{ orgData.teams }}
+    if orgData.loginDisabled
+      td <s>{{ moment orgData.createdAt 'LLL' }}</s>
+    else
+      td {{ moment orgData.createdAt 'LLL' }}
+    td
+      if orgData.loginDisabled
+        | {{_ 'no'}}
+      else
+        | {{_ 'yes'}}
+    td
+      a.edit-org
+        i.fa.fa-edit
+        | {{_ 'edit'}}
+      a.more-settings-org
+        i.fa.fa-ellipsis-h
+
+template(name="teamRow")
+  tr
+    if teamData.loginDisabled
+      td <s>{{ teamData.displayName }}</s>
+    else
+      td {{ teamData.displayName }}
+    if teamData.loginDisabled
+      td <s>{{ teamData.desc }}</s>
+    else
+      td {{ teamData.desc }}
+    if teamData.loginDisabled
+      td <s>{{ teamData.dame }}</s>
+    else
+      td {{ teamData.name }}
+    if teamData.loginDisabled
+      td <s>{{ teamData.website }}</s>
+    else
+      td {{ teamData.website }}
+    if orgData.loginDisabled
+      td <s>{{ moment teamData.createdAt 'LLL' }}</s>
+    else
+      td {{ moment teamData.createdAt 'LLL' }}
+    td
+      if teamData.loginDisabled
+        | {{_ 'no'}}
+      else
+        | {{_ 'yes'}}
+    td
+      a.edit-team
+        i.fa.fa-edit
+        | {{_ 'edit'}}
+      a.more-settings-team
+        i.fa.fa-ellipsis-h
+
 template(name="peopleRow")
   tr
     if userData.loginDisabled
@@ -107,6 +256,58 @@ template(name="peopleRow")
       a.more-settings-user
         i.fa.fa-ellipsis-h
 
+template(name="editOrgPopup")
+  form
+    label.hide.orgId(type="text" value=org._id)
+    label
+      | {{_ 'orgDisplayName'}}
+      input.js-orgDisplayName(type="text" value=org.orgDisplayName required)
+      span.error.hide.orgname-taken
+        | {{_ 'error-orgname-taken'}}
+    label
+      | {{_ 'orgDesc'}}
+      input.js-orgDesc(type="text" value=org.orgDesc required)
+    label
+      | {{_ 'orgName'}}
+      input.js-orgName(type="text" value=org.orgName required)
+    label
+      | {{_ 'orgWebsite'}}
+      input.js-orgWebsite(type="text" value=org.orgWebsite required)
+    label
+      | {{_ 'active'}}
+      select.select-active.js-org-isactive
+        option(value="false") {{_ 'yes'}}
+        option(value="true" selected="{{org.loginDisabled}}") {{_ 'no'}}
+    hr
+    div.buttonsContainer
+      input.primary.wide(type="submit" value="{{_ 'save'}}")
+
+template(name="editTeamPopup")
+  form
+    label.hide.teamId(type="text" value=team._id)
+    label
+      | {{_ 'displayName'}}
+      input.js-teamDisplayName(type="text" value=team.displayName required)
+      span.error.hide.teamname-taken
+        | {{_ 'error-teamname-taken'}}
+    label
+      | {{_ 'desc'}}
+      input.js-orgDesc(type="text" value=org.desc required)
+    label
+      | {{_ 'name'}}
+      input.js-orgName(type="text" value=org.name required)
+    label
+      | {{_ 'website'}}
+      input.js-orgWebsite(type="text" value=org.website required)
+    label
+      | {{_ 'active'}}
+      select.select-active.js-team-isactive
+        option(value="false") {{_ 'yes'}}
+        option(value="true" selected="{{team.loginDisabled}}") {{_ 'no'}}
+    hr
+    div.buttonsContainer
+      input.primary.wide(type="submit" value="{{_ 'save'}}")
+
 template(name="editUserPopup")
   form
     label.hide.userId(type="text" value=user._id)
@@ -154,6 +355,54 @@ template(name="editUserPopup")
     div.buttonsContainer
       input.primary.wide(type="submit" value="{{_ 'save'}}")
 
+template(name="newOrgPopup")
+  form
+    //label.hide.userId(type="text" value=user._id)
+    label
+      | {{_ 'orgDisplayName'}}
+      input.js-orgDisplayName(type="text" value="" required)
+    label
+      | {{_ 'orgDesc'}}
+      input.js-orgDesc(type="text" value="" required)
+    label
+      | {{_ 'orgName'}}
+      input.js-orgName(type="text" value="")
+    label
+      | {{_ 'orgWebsite'}}
+      input.js-orgWebsite(type="text" value="")
+    label
+      | {{_ 'active'}}
+      select.select-active.js-profile-isactive
+        option(value="false" selected="selected") {{_ 'yes'}}
+        option(value="true") {{_ 'no'}}
+    hr
+    div.buttonsContainer
+      input.primary.wide(type="submit" value="{{_ 'save'}}")
+
+template(name="newTeamPopup")
+  form
+    //label.hide.teamId(type="text" value=team._id)
+    label
+      | {{_ 'displayName'}}
+      input.js-teamDisplayName(type="text" value="" required)
+    label
+      | {{_ 'desc'}}
+      input.js-teamDesc(type="text" value="" required)
+    label
+      | {{_ 'shortName'}}
+      input.js-teamName(type="text" value="")
+    label
+      | {{_ 'website'}}
+      input.js-teamWebsite(type="text" value="")
+    label
+      | {{_ 'active'}}
+      select.select-active.js-profile-isactive
+        option(value="false" selected="selected") {{_ 'yes'}}
+        option(value="true") {{_ 'no'}}
+    hr
+    div.buttonsContainer
+      input.primary.wide(type="submit" value="{{_ 'save'}}")
+
 template(name="newUserPopup")
   form
     //label.hide.userId(type="text" value=user._id)
@@ -201,6 +450,31 @@ template(name="newUserPopup")
     div.buttonsContainer
       input.primary.wide(type="submit" value="{{_ 'save'}}")
 
+template(name="settingsOrgPopup")
+  ul.pop-over-list
+    li
+      a.impersonate-org
+        i.fa.fa-user
+        | {{_ 'impersonate-org'}}
+  // Delete is not enabled yet, because it does leave empty user avatars
+  // to boards: boards members, card members and assignees have
+  // empty users. See:
+  // - wekan/client/components/settings/peopleBody.jade deleteButton
+  // - wekan/client/components/settings/peopleBody.js deleteButton
+  // - wekan/client/components/sidebar/sidebar.js Popup.afterConfirm('removeMember'
+  //   that does now remove member from board, card members and assignees correctly,
+  //   but that should be used to remove user from all boards similarly
+  // - wekan/models/users.js Delete is not enabled
+  //li
+  //  br
+  //  br
+  //  hr
+  //li
+  //  form
+  //    label.hide.userId(type="text" value=user._id)
+  //    div.buttonsContainer
+  //      input#deleteButton.card-details-red.right.wide(type="button" value="{{_ 'delete'}}")
+
 template(name="settingsUserPopup")
   ul.pop-over-list
     li

+ 191 - 6
client/components/settings/peopleBody.js

@@ -1,3 +1,5 @@
+const orgsPerPage = 25;
+const teamsPerPage = 25;
 const usersPerPage = 25;
 
 BlazeComponent.extendComponent({
@@ -7,17 +9,45 @@ BlazeComponent.extendComponent({
   onCreated() {
     this.error = new ReactiveVar('');
     this.loading = new ReactiveVar(false);
-    this.people = new ReactiveVar(true);
+    this.orgSetting = new ReactiveVar(true);
+    this.teamSetting = new ReactiveVar(true);
+    this.peopleSetting = new ReactiveVar(true);
+    this.findOrgsOptions = new ReactiveVar({});
+    this.findTeamsOptions = new ReactiveVar({});
     this.findUsersOptions = new ReactiveVar({});
-    this.number = new ReactiveVar(0);
+    this.numberOrgs = new ReactiveVar(0);
+    this.numberTeams = new ReactiveVar(0);
+    this.numberPeople = new ReactiveVar(0);
 
     this.page = new ReactiveVar(1);
     this.loadNextPageLocked = false;
     this.callFirstWith(null, 'resetNextPeak');
     this.autorun(() => {
-      const limit = this.page.get() * usersPerPage;
+      const limitOrgs = this.page.get() * orgsPerPage;
+      const limitTeams = this.page.get() * teamsPerPage;
+      const limitUsers = this.page.get() * usersPerPage;
 
-      this.subscribe('people', this.findUsersOptions.get(), limit, () => {
+      this.subscribe('org', this.findOrgsOptions.get(), limitOrgs, () => {
+        this.loadNextPageLocked = false;
+        const nextPeakBefore = this.callFirstWith(null, 'getNextPeak');
+        this.calculateNextPeak();
+        const nextPeakAfter = this.callFirstWith(null, 'getNextPeak');
+        if (nextPeakBefore === nextPeakAfter) {
+          this.callFirstWith(null, 'resetNextPeak');
+        }
+      });
+
+      this.subscribe('team', this.findTeamsOptions.get(), limitTeams, () => {
+        this.loadNextPageLocked = false;
+        const nextPeakBefore = this.callFirstWith(null, 'getNextPeak');
+        this.calculateNextPeak();
+        const nextPeakAfter = this.callFirstWith(null, 'getNextPeak');
+        if (nextPeakBefore === nextPeakAfter) {
+          this.callFirstWith(null, 'resetNextPeak');
+        }
+      });
+
+      this.subscribe('people', this.findUsersOptions.get(), limitUsers, () => {
         this.loadNextPageLocked = false;
         const nextPeakBefore = this.callFirstWith(null, 'getNextPeak');
         this.calculateNextPeak();
@@ -31,6 +61,22 @@ BlazeComponent.extendComponent({
   events() {
     return [
       {
+        'click #searchOrgButton'() {
+          this.filterOrg();
+        },
+        'keydown #searchOrgInput'(event) {
+          if (event.keyCode === 13 && !event.shiftKey) {
+            this.filterOrg();
+          }
+        },
+        'click #searchTeamButton'() {
+          this.filterTeam();
+        },
+        'keydown #searchTeamInput'(event) {
+          if (event.keyCode === 13 && !event.shiftKey) {
+            this.filterTeam();
+          }
+        },
         'click #searchButton'() {
           this.filterPeople();
         },
@@ -39,9 +85,18 @@ BlazeComponent.extendComponent({
             this.filterPeople();
           }
         },
+        'click #newOrgButton'() {
+          Popup.open('newOrg');
+        },
+        'click #newTeamButton'() {
+          Popup.open('newTeam');
+        },
         'click #newUserButton'() {
           Popup.open('newUser');
         },
+        'click a.js-org-menu': this.switchMenu,
+        'click a.js-team-menu': this.switchMenu,
+        'click a.js-people-menu': this.switchMenu,
       },
     ];
   },
@@ -84,18 +139,63 @@ BlazeComponent.extendComponent({
   setLoading(w) {
     this.loading.set(w);
   },
+  orgList() {
+    const orgs = Org.find(this.findOrgsOptions.get(), {
+      fields: { _id: true },
+    });
+    this.numberOrgs.set(org.count(false));
+    return orgs;
+  },
+  teamList() {
+    const teams = Team.find(this.findTeamsOptions.get(), {
+      fields: { _id: true },
+    });
+    this.numberTeams.set(team.count(false));
+    return teams;
+  },
   peopleList() {
     const users = Users.find(this.findUsersOptions.get(), {
       fields: { _id: true },
     });
-    this.number.set(users.count(false));
+    this.numberPeople.set(users.count(false));
     return users;
   },
+  orgNumber() {
+    return this.numberOrgs.get();
+  },
+  teamNumber() {
+    return this.numberTeams.get();
+  },
   peopleNumber() {
-    return this.number.get();
+    return this.numberPeople.get();
+  },
+  switchMenu(event) {
+    const target = $(event.target);
+    if (!target.hasClass('active')) {
+      $('.side-menu li.active').removeClass('active');
+      target.parent().addClass('active');
+      const targetID = target.data('id');
+      this.orgSetting.set('org-setting' === targetID);
+      this.teamSetting.set('team-setting' === targetID);
+      this.peopleSetting.set('people-setting' === targetID);
+    }
   },
 }).register('people');
 
+Template.orgRow.helpers({
+  orgData() {
+    const orgCollection = this.esSearch ? ESSearchResults : Org;
+    return orgCollection.findOne(this.orgId);
+  },
+});
+
+Template.teamRow.helpers({
+  teamData() {
+    const teamCollection = this.esSearch ? ESSearchResults : Team;
+    return teamCollection.findOne(this.teamId);
+  },
+});
+
 Template.peopleRow.helpers({
   userData() {
     const userCollection = this.esSearch ? ESSearchResults : Users;
@@ -122,6 +222,51 @@ Template.editUserPopup.onCreated(function() {
   });
 });
 
+Template.editOrgPopup.helpers({
+  org() {
+    return Org.findOne(this.orgId);
+  },
+  /*
+  isSelected(match) {
+    const orgId = Template.instance().data.orgId;
+    const selected = Org.findOne(orgId).authenticationMethod;
+    return selected === match;
+  },
+  isLdap() {
+    const userId = Template.instance().data.userId;
+    const selected = Users.findOne(userId).authenticationMethod;
+    return selected === 'ldap';
+  },
+  */
+  errorMessage() {
+    return Template.instance().errorMessage.get();
+  },
+});
+
+Template.editTeamPopup.helpers({
+  team() {
+    return Team.findOne(this.teamId);
+  },
+  /*
+  authentications() {
+    return Template.instance().authenticationMethods.get();
+  },
+  isSelected(match) {
+    const userId = Template.instance().data.userId;
+    const selected = Users.findOne(userId).authenticationMethod;
+    return selected === match;
+  },
+  isLdap() {
+    const userId = Template.instance().data.userId;
+    const selected = Users.findOne(userId).authenticationMethod;
+    return selected === 'ldap';
+  },
+  */
+  errorMessage() {
+    return Template.instance().errorMessage.get();
+  },
+});
+
 Template.editUserPopup.helpers({
   user() {
     return Users.findOne(this.userId);
@@ -144,6 +289,46 @@ Template.editUserPopup.helpers({
   },
 });
 
+Template.newOrgPopup.onCreated(function() {
+  //this.authenticationMethods = new ReactiveVar([]);
+  this.errorMessage = new ReactiveVar('');
+  /*
+  Meteor.call('getAuthenticationsEnabled', (_, result) => {
+    if (result) {
+      // TODO : add a management of different languages
+      // (ex {value: ldap, text: TAPi18n.__('ldap', {}, T9n.getLanguage() || 'en')})
+      this.authenticationMethods.set([
+        { value: 'password' },
+        // Gets only the authentication methods availables
+        ...Object.entries(result)
+          .filter(e => e[1])
+          .map(e => ({ value: e[0] })),
+      ]);
+    }
+  });
+*/
+});
+
+Template.newTeamPopup.onCreated(function() {
+  //this.authenticationMethods = new ReactiveVar([]);
+  this.errorMessage = new ReactiveVar('');
+  /*
+  Meteor.call('getAuthenticationsEnabled', (_, result) => {
+    if (result) {
+      // TODO : add a management of different languages
+      // (ex {value: ldap, text: TAPi18n.__('ldap', {}, T9n.getLanguage() || 'en')})
+      this.authenticationMethods.set([
+        { value: 'password' },
+        // Gets only the authentication methods availables
+        ...Object.entries(result)
+          .filter(e => e[1])
+          .map(e => ({ value: e[0] })),
+      ]);
+    }
+  });
+*/
+});
+
 Template.newUserPopup.onCreated(function() {
   this.authenticationMethods = new ReactiveVar([]);
   this.errorMessage = new ReactiveVar('');

+ 2 - 2
client/components/settings/peopleBody.styl

@@ -21,7 +21,7 @@ table
 
   .ext-box-left
     display: flex;
-    width: 40%
+    width: 100%
 
   span
     vertical-align: center;
@@ -47,5 +47,5 @@ table
   div
     margin: auto
 
-.more-settings-user
+.more-settings-user,.more-settings-team,.more-settings-org
   margin-left: 10px;

+ 9 - 1
i18n/en.i18n.json

@@ -774,6 +774,8 @@
   "display-authentication-method": "Display Authentication Method",
   "default-authentication-method": "Default Authentication Method",
   "duplicate-board": "Duplicate Board",
+  "org-number": "The number of organizations is: ",
+  "team-number": "The number of teams is: ",
   "people-number": "The number of people is: ",
   "swimlaneDeletePopup-title": "Delete Swimlane ?",
   "swimlane-delete-pop": "All actions will be removed from the activity feed and you won't be able to recover the swimlane. There is no undo.",
@@ -836,5 +838,11 @@
   "hide-checked-items": "Hide checked items",
   "task": "Task",
   "create-task": "Create Task",
-  "ok": "OK"
+  "ok": "OK",
+  "organizations": "Organizations",
+  "teams": "Teams",
+  "displayName": "Display Name",
+  "shortName": "Short Name",
+  "website": "Website",
+  "person": "Person"
 }

+ 51 - 31
models/org.js

@@ -1,7 +1,7 @@
 Org = new Mongo.Collection('org');
 
 /**
- * A Organization in wekan
+ * A Organization in Wekan. A Enterprise in Trello.
  */
 Org.attachSchema(
   new SimpleSchema({
@@ -18,76 +18,96 @@ Org.attachSchema(
         }
       },
     },
-    version: {
+    displayName: {
       /**
-       * the version of the organization
+       * the name to display for the organization
        */
-      type: Number,
+      type: String,
       optional: true,
     },
-    name: {
+    desc: {
       /**
-       * name of the organization
+       * the description the organization
        */
       type: String,
       optional: true,
       max: 190,
     },
-    address1: {
+    name: {
       /**
-       * address1 of the organization
+       * short name of the organization
        */
       type: String,
       optional: true,
       max: 255,
     },
-    address2: {
+    website: {
       /**
-       * address2 of the organization
+       * website of the organization
        */
       type: String,
       optional: true,
       max: 255,
     },
-    city: {
+    teams: {
       /**
-       * city of the organization
+       * List of teams of a organization
        */
-      type: String,
-      optional: true,
-      max: 255,
+      type: [Object],
+      // eslint-disable-next-line consistent-return
+      autoValue() {
+        if (this.isInsert && !this.isSet) {
+          return [
+            {
+              teamId: this.teamId,
+              isAdmin: true,
+              isActive: true,
+              isNoComments: false,
+              isCommentOnly: false,
+              isWorker: false,
+            },
+          ];
+        }
+      },
     },
-    state: {
+    'teams.$.teamId': {
       /**
-       * state of the organization
+       * The uniq ID of the team
        */
       type: String,
-      optional: true,
-      max: 255,
     },
-    zipCode: {
+    'teams.$.isAdmin': {
       /**
-       * zipCode of the organization
+       * Is the team an admin of the board?
        */
-      type: String,
+      type: Boolean,
+    },
+    'teams.$.isActive': {
+      /**
+       * Is the team active?
+       */
+      type: Boolean,
+    },
+    'teams.$.isNoComments': {
+      /**
+       * Is the team not allowed to make comments
+       */
+      type: Boolean,
       optional: true,
-      max: 50,
     },
-    country: {
+    'teams.$.isCommentOnly': {
       /**
-       * country of the organization
+       * Is the team only allowed to comment on the board
        */
-      type: String,
+      type: Boolean,
       optional: true,
-      max: 255,
     },
-    billingEmail: {
+    'teams.$.isWorker': {
       /**
-       * billingEmail of the organization
+       * Is the team only allowed to move card, assign himself to card and comment
        */
-      type: String,
+      type: Boolean,
       optional: true,
-      max: 255,
     },
     createdAt: {
       /**

+ 90 - 0
models/team.js

@@ -0,0 +1,90 @@
+Team = new Mongo.Collection('team');
+
+/**
+ * A Team in Wekan. Organization in Trello.
+ */
+Team.attachSchema(
+  new SimpleSchema({
+    _id: {
+      /**
+       * the organization id
+       */
+      type: Number,
+      optional: true,
+      // eslint-disable-next-line consistent-return
+      autoValue() {
+        if (this.isInsert && !this.isSet) {
+          return incrementCounter('counters', 'orgId', 1);
+        }
+      },
+    },
+    displayName: {
+      /**
+       * the name to display for the team
+       */
+      type: String,
+      optional: true,
+    },
+    desc: {
+      /**
+       * the description the team
+       */
+      type: String,
+      optional: true,
+      max: 190,
+    },
+    name: {
+      /**
+       * short name of the team
+       */
+      type: String,
+      optional: true,
+      max: 255,
+    },
+    website: {
+      /**
+       * website of the team
+       */
+      type: String,
+      optional: true,
+      max: 255,
+    },
+    createdAt: {
+      /**
+       * creation date of the team
+       */
+      type: Date,
+      // eslint-disable-next-line consistent-return
+      autoValue() {
+        if (this.isInsert) {
+          return new Date();
+        } else if (this.isUpsert) {
+          return { $setOnInsert: new Date() };
+        } else {
+          this.unset();
+        }
+      },
+    },
+    modifiedAt: {
+      type: Date,
+      denyUpdate: false,
+      // eslint-disable-next-line consistent-return
+      autoValue() {
+        if (this.isInsert || this.isUpsert || this.isUpdate) {
+          return new Date();
+        } else {
+          this.unset();
+        }
+      },
+    },
+  }),
+);
+
+if (Meteor.isServer) {
+  // Index for Team name.
+  Meteor.startup(() => {
+    Team._collection._ensureIndex({ name: -1 });
+  });
+}
+
+export default Team;

+ 27 - 0
server/publications/org.js

@@ -0,0 +1,27 @@
+Meteor.publish('org', function(query, limit) {
+  check(query, Match.OneOf(Object, null));
+  check(limit, Number);
+
+  if (!Match.test(this.userId, String)) {
+    return [];
+  }
+
+  const user = Users.findOne(this.userId);
+  if (user && user.isAdmin) {
+    return Org.find(query, {
+      limit,
+      sort: { createdAt: -1 },
+      fields: {
+        displayName: 1,
+        desc: 1,
+        name: 1,
+        website: 1,
+        teams: 1,
+        createdAt: 1,
+        loginDisabled: 1,
+      },
+    });
+  }
+
+  return [];
+});

+ 27 - 0
server/publications/team.js

@@ -0,0 +1,27 @@
+Meteor.publish('team', function(query, limit) {
+  check(query, Match.OneOf(Object, null));
+  check(limit, Number);
+
+  if (!Match.test(this.userId, String)) {
+    return [];
+  }
+
+  const user = Users.findOne(this.userId);
+  if (user && user.isAdmin) {
+    return Team.find(query, {
+      limit,
+      sort: { createdAt: -1 },
+      fields: {
+        displayName: 1,
+        desc: 1,
+        name: 1,
+        website: 1,
+        teams: 1,
+        createdAt: 1,
+        loginDisabled: 1,
+      },
+    });
+  }
+
+  return [];
+});