2
0
Эх сурвалжийг харах

Teams/Organizations: Added more code to Admin Panel for saving and editing. In Progress.

Thanks to xet7 !

Related #802
Lauri Ojansivu 4 жил өмнө
parent
commit
1bc07b1b4a

+ 15 - 4
client/components/cards/cardDate.js

@@ -1,6 +1,9 @@
 // Helper function to replace HH with H for 24 hours format, because H allows also single-digit hours
 function adjustedTimeFormat() {
-  return moment.localeData().longDateFormat('LT').replace(/HH/i, 'H');
+  return moment
+    .localeData()
+    .longDateFormat('LT')
+    .replace(/HH/i, 'H');
 }
 
 // Edit received, start, due & end dates
@@ -64,7 +67,11 @@ BlazeComponent.extendComponent({
         },
         'keyup .js-time-field'() {
           // parse for localized time format in strict mode
-          const dateMoment = moment(this.find('#time').value, adjustedTimeFormat(), true);
+          const dateMoment = moment(
+            this.find('#time').value,
+            adjustedTimeFormat(),
+            true,
+          );
           if (dateMoment.isValid()) {
             this.error.set('');
           }
@@ -79,7 +86,11 @@ BlazeComponent.extendComponent({
           const newTime = moment(time, adjustedTimeFormat(), true);
           const newDate = moment(evt.target.date.value, 'L', true);
           const dateString = `${evt.target.date.value} ${time}`;
-          const newCompleteDate = moment(dateString, 'L ' + adjustedTimeFormat(), true);
+          const newCompleteDate = moment(
+            dateString,
+            'L ' + adjustedTimeFormat(),
+            true,
+          );
           if (!newTime.isValid()) {
             this.error.set('invalid-time');
             evt.target.time.focus();
@@ -92,7 +103,7 @@ BlazeComponent.extendComponent({
             this._storeDate(newCompleteDate.toDate());
             Popup.close();
           } else {
-            if (!this.error){
+            if (!this.error) {
               this.error.set('invalid');
             }
           }

+ 35 - 43
client/components/settings/peopleBody.jade

@@ -74,7 +74,6 @@ template(name="orgGeneral")
         th {{_ 'description'}}
         th {{_ 'shortName'}}
         th {{_ 'website'}}
-        th {{_ 'teams'}}
         th {{_ 'createdAt'}}
         th {{_ 'active'}}
         th
@@ -134,25 +133,21 @@ template(name="newUserRow")
 template(name="orgRow")
   tr
     if orgData.loginDisabled
-      td <s>{{ orgData.displayName }}</s>
+      td <s>{{ orgData.orgDisplayName }}</s>
     else
-      td {{ orgData.displayName }}
+      td {{ orgData.orgDisplayName }}
     if orgData.loginDisabled
       td <s>{{ orgData.orgDesc }}</s>
     else
-      td {{ orgData.desc }}
+      td {{ orgData.orgDesc }}
     if orgData.loginDisabled
-      td <s>{{ orgData.name }}</s>
+      td <s>{{ orgData.orgName }}</s>
     else
-      td {{ orgData.name }}
+      td {{ orgData.orgName }}
     if orgData.loginDisabled
-      td <s>{{ orgData.website }}</s>
+      td <s>{{ orgData.orgWebsite }}</s>
     else
-      td {{ orgData.website }}
-    if orgData.loginDisabled
-      td <s>{{ orgData.teams }}</s>
-    else
-      td {{ orgData.teams }}
+      td {{ orgData.orgWebsite }}
     if orgData.loginDisabled
       td <s>{{ moment orgData.createdAt 'LLL' }}</s>
     else
@@ -172,21 +167,21 @@ template(name="orgRow")
 template(name="teamRow")
   tr
     if teamData.loginDisabled
-      td <s>{{ teamData.displayName }}</s>
+      td <s>{{ teamData.teamDisplayName }}</s>
     else
-      td {{ teamData.displayName }}
+      td {{ teamData.teamDisplayName }}
     if teamData.loginDisabled
-      td <s>{{ teamData.desc }}</s>
+      td <s>{{ teamData.teamDesc }}</s>
     else
-      td {{ teamData.desc }}
+      td {{ teamData.teamDesc }}
     if teamData.loginDisabled
-      td <s>{{ teamData.dame }}</s>
+      td <s>{{ teamData.teamName }}</s>
     else
-      td {{ teamData.name }}
+      td {{ teamData.teamName }}
     if teamData.loginDisabled
-      td <s>{{ teamData.website }}</s>
+      td <s>{{ teamData.teamWebsite }}</s>
     else
-      td {{ teamData.website }}
+      td {{ teamData.teamWebsite }}
     if orgData.loginDisabled
       td <s>{{ moment teamData.createdAt 'LLL' }}</s>
     else
@@ -273,19 +268,19 @@ template(name="editOrgPopup")
   form
     label.hide.orgId(type="text" value=org._id)
     label
-      | {{_ 'orgDisplayName'}}
+      | {{_ 'displayName'}}
       input.js-orgDisplayName(type="text" value=org.displayName required)
       span.error.hide.orgname-taken
         | {{_ 'error-orgname-taken'}}
     label
-      | {{_ 'orgDesc'}}
-      input.js-orgDesc(type="text" value=org.desc required)
+      | {{_ 'description'}}
+      input.js-orgDesc(type="text" value=org.orgDesc required)
     label
-      | {{_ 'orgName'}}
-      input.js-orgName(type="text" value=org.name required)
+      | {{_ 'shortName'}}
+      input.js-orgShortName(type="text" value=org.orgShortName required)
     label
-      | {{_ 'orgWebsite'}}
-      input.js-orgWebsite(type="text" value=org.website required)
+      | {{_ 'website'}}
+      input.js-orgWebsite(type="text" value=org.orgWebsite required)
     label
       | {{_ 'active'}}
       select.select-active.js-org-isactive
@@ -300,18 +295,18 @@ template(name="editTeamPopup")
     label.hide.teamId(type="text" value=team._id)
     label
       | {{_ 'displayName'}}
-      input.js-teamDisplayName(type="text" value=team.displayName required)
+      input.js-teamDisplayName(type="text" value=team.teamDisplayName required)
       span.error.hide.teamname-taken
         | {{_ 'error-teamname-taken'}}
     label
       | {{_ 'description'}}
-      input.js-orgDesc(type="text" value=org.desc required)
+      input.js-teamDesc(type="text" value=team.teamDesc required)
     label
-      | {{_ 'name'}}
-      input.js-orgName(type="text" value=org.name required)
+      | {{_ 'shortName'}}
+      input.js-teamShortName(type="text" value=team.teamShortName required)
     label
       | {{_ 'website'}}
-      input.js-orgWebsite(type="text" value=org.website required)
+      input.js-teamWebsite(type="text" value=team.teamWebsite required)
     label
       | {{_ 'active'}}
       select.select-active.js-team-isactive
@@ -384,22 +379,19 @@ template(name="newOrgPopup")
     //label.hide.userId(type="text" value=user._id)
     label
       | {{_ 'displayName'}}
-      input.js-displayName(type="text" value="" required)
+      input.js-orgDisplayName(type="text" value="" required)
     label
       | {{_ 'description'}}
-      input.js-desc(type="text" value="" required)
+      input.js-orgDesc(type="text" value="" required)
     label
       | {{_ 'shortName'}}
-      input.js-name(type="text" value="")
-    label
-      | {{_ 'teams'}}
-      input.js-teams(type="text" value="")
+      input.js-orgName(type="text" value="" required)
     label
       | {{_ 'website'}}
-      input.js-website(type="text" value="")
+      input.js-orgWebsite(type="text" value="" required)
     label
       | {{_ 'active'}}
-      select.select-active.js-profile-isactive
+      select.select-active.js-org-isactive
         option(value="false" selected="selected") {{_ 'yes'}}
         option(value="true") {{_ 'no'}}
     hr
@@ -417,13 +409,13 @@ template(name="newTeamPopup")
       input.js-teamDesc(type="text" value="" required)
     label
       | {{_ 'shortName'}}
-      input.js-teamName(type="text" value="")
+      input.js-teamName(type="text" value="" required)
     label
       | {{_ 'website'}}
-      input.js-teamWebsite(type="text" value="")
+      input.js-teamWebsite(type="text" value="" required)
     label
       | {{_ 'active'}}
-      select.select-active.js-profile-isactive
+      select.select-active.js-team-isactive
         option(value="false" selected="selected") {{_ 'yes'}}
         option(value="true") {{_ 'no'}}
     hr

+ 121 - 30
client/components/settings/peopleBody.js

@@ -404,6 +404,88 @@ BlazeComponent.extendComponent({
   },
 }).register('newUserRow');
 
+Template.editOrgPopup.events({
+  submit(event, templateInstance) {
+    event.preventDefault();
+    const org = Orgs.findOne(this.orgId);
+
+    const orgDisplayName = templateInstance
+      .find('.js-orgDisplayName')
+      .value.trim();
+    const orgDesc = templateInstance.find('.js-orgDesc').value.trim();
+    const orgShortName = templateInstance.find('.js-orgShortName').value.trim();
+    const orgWebsite = templateInstance.find('.js-orgWebsite').value.trim();
+    const orgIsActive = templateInstance.find('.js-org-isactive').value.trim();
+
+    const isChangeOrgDisplayName = orgDisplayName !== org.orgDisplayName;
+    const isChangeOrgDesc = orgDesc !== org.orgDesc;
+    const isChangeOrgShortName = orgShortName !== org.orgShortName;
+    const isChangeOrgWebsite = orgWebsite !== org.orgWebsite;
+    const isChangeOrgIsActive = orgIsActive !== org.orgIsActive;
+
+    if (isChangeOrgDisplayName) {
+      Meteor.call('setOrgDisplayName', org, orgDisplayName);
+    }
+
+    if (isChangeOrgDesc) {
+      Meteor.call('setOrgDesc', org, orgDesc);
+    }
+
+    if (isChangeOrgShortName) {
+      Meteor.call('setOrgShortName', org, orgShortName);
+    }
+
+    if (isChangeOrgIsActive) {
+      Meteor.call('setOrgIsActive', org, orgIsActive);
+    }
+
+    Popup.close();
+  },
+});
+
+Template.editTeamPopup.events({
+  submit(event, templateInstance) {
+    event.preventDefault();
+    const team = Teams.findOne(this.teamId);
+
+    const teamDisplayName = templateInstance
+      .find('.js-teamDisplayName')
+      .value.trim();
+    const teamDesc = templateInstance.find('.js-teamDesc').value.trim();
+    const teamShortName = templateInstance
+      .find('.js-teamShortName')
+      .value.trim();
+    const teamWebsite = templateInstance.find('.js-teamWebsite').value.trim();
+    const teamIsActive = templateInstance
+      .find('.js-team-isactive')
+      .value.trim();
+
+    const isChangeTeamDisplayName = teamDisplayName !== team.teamDisplayName;
+    const isChangeTeamDesc = teamDesc !== team.teamDesc;
+    const isChangeTeamShortName = teamShortName !== team.teamShortName;
+    const isChangeTeamWebsite = teamWebsite !== team.teamWebsite;
+    const isChangeTeamIsActive = teamIsActive !== team.teamIsActive;
+
+    if (isChangeTeamDisplayName) {
+      Meteor.call('setTeamDisplayName', team, teamDisplayName);
+    }
+
+    if (isChangeTeamDesc) {
+      Meteor.call('setTeamDesc', team, teamDesc);
+    }
+
+    if (isChangeTeamShortName) {
+      Meteor.call('setTeamShortName', team, teamShortName);
+    }
+
+    if (isChangeTeamIsActive) {
+      Meteor.call('setTeamIsActive', team, teamIsActive);
+    }
+
+    Popup.close();
+  },
+});
+
 Template.editUserPopup.events({
   submit(event, templateInstance) {
     event.preventDefault();
@@ -520,39 +602,48 @@ Template.editUserPopup.events({
 Template.newOrgPopup.events({
   submit(event, templateInstance) {
     event.preventDefault();
-    const displayName = templateInstance.find('.js-displayName').value.trim();
-    const desc = templateInstance.find('.js-desc').value.trim();
-    const name = templateInstance.find('.js-name').value.trim();
-    const teams = templateInstance.find('.js-teams').value.trim();
-    const website = templateInstance.find('.js-website').value.trim();
-    const isActive = templateInstance.find('.js-profile-isactive').value.trim();
+    const orgDisplayName = templateInstance
+      .find('.js-orgDisplayName')
+      .value.trim();
+    const orgDesc = templateInstance.find('.js-orgDesc').value.trim();
+    const orgShortName = templateInstance.find('.js-orgShortName').value.trim();
+    const orgWebsite = templateInstance.find('.js-orgWebsite').value.trim();
+    const orgIsActive = templateInstance.find('.js-org-isactive').value.trim();
 
     Meteor.call(
       'setCreateOrg',
-      displayName,
-      desc,
-      name,
-      teams,
-      website,
-      isActive,
-      email.toLowerCase(),
-      function(error) {
-        const nameMessageElement = templateInstance.$('.name-taken');
-        if (error) {
-          const errorElement = error.error;
-          if (errorElement === 'name-already-taken') {
-            nameMessageElement.show();
-            emailMessageElement.hide();
-          } else if (errorElement === 'email-already-taken') {
-            usernameMessageElement.hide();
-            emailMessageElement.show();
-          }
-        } else {
-          usernameMessageElement.hide();
-          emailMessageElement.hide();
-          Popup.close();
-        }
-      },
+      orgDisplayName,
+      orgDesc,
+      orgShortName,
+      orgWebsite,
+      orgIsActive,
+    );
+    Popup.close();
+  },
+});
+
+Template.newTeamPopup.events({
+  submit(event, templateInstance) {
+    event.preventDefault();
+    const teamDisplayName = templateInstance
+      .find('.js-teamDisplayName')
+      .value.trim();
+    const teamDesc = templateInstance.find('.js-teamDesc').value.trim();
+    const teamShortName = templateInstance
+      .find('.js-teamShortName')
+      .value.trim();
+    const teamWebsite = templateInstance.find('.js-teamWebsite').value.trim();
+    const teamIsActive = templateInstance
+      .find('.js-team-isactive')
+      .value.trim();
+
+    Meteor.call(
+      'setCreateTeam',
+      teamDisplayName,
+      teamDesc,
+      teamShortName,
+      teamWebsite,
+      teamIsActive,
     );
     Popup.close();
   },

+ 15 - 4
client/lib/datepicker.js

@@ -1,6 +1,9 @@
 // Helper function to replace HH with H for 24 hours format, because H allows also single-digit hours
 function adjustedTimeFormat() {
-  return moment.localeData().longDateFormat('LT').replace(/HH/i, 'H');
+  return moment
+    .localeData()
+    .longDateFormat('LT')
+    .replace(/HH/i, 'H');
 }
 
 DatePicker = BlazeComponent.extendComponent({
@@ -82,7 +85,11 @@ DatePicker = BlazeComponent.extendComponent({
         },
         'keyup .js-time-field'() {
           // parse for localized time format in strict mode
-          const dateMoment = moment(this.find('#time').value, adjustedTimeFormat(), true);
+          const dateMoment = moment(
+            this.find('#time').value,
+            adjustedTimeFormat(),
+            true,
+          );
           if (dateMoment.isValid()) {
             this.error.set('');
           }
@@ -97,7 +104,11 @@ DatePicker = BlazeComponent.extendComponent({
           const newTime = moment(time, adjustedTimeFormat(), true);
           const newDate = moment(evt.target.date.value, 'L', true);
           const dateString = `${evt.target.date.value} ${time}`;
-          const newCompleteDate = moment(dateString, 'L ' + adjustedTimeFormat(), true);
+          const newCompleteDate = moment(
+            dateString,
+            'L ' + adjustedTimeFormat(),
+            true,
+          );
           if (!newTime.isValid()) {
             this.error.set('invalid-time');
             evt.target.time.focus();
@@ -110,7 +121,7 @@ DatePicker = BlazeComponent.extendComponent({
             this._storeDate(newCompleteDate.toDate());
             Popup.close();
           } else {
-            if (!this.error){
+            if (!this.error) {
               this.error.set('invalid');
             }
           }

+ 2 - 0
i18n/en.i18n.json

@@ -322,6 +322,8 @@
   "error-user-notAllowSelf": "You can not invite yourself",
   "error-user-notCreated": "This user is not created",
   "error-username-taken": "This username is already taken",
+  "error-orgname-taken": "This organization name is already taken",
+  "error-teamname-taken": "This team name is already taken",
   "error-email-taken": "Email has already been taken",
   "export-board": "Export board",
   "export-board-json": "Export board to JSON",

+ 3 - 3
models/cards.js

@@ -736,11 +736,11 @@ Cards.helpers({
     // at linked cards custom fields definition is not found
     ret.sort(
       (a, b) =>
-        a.definition      !== undefined &&
-        b.definition      !== undefined &&
+        a.definition !== undefined &&
+        b.definition !== undefined &&
         a.definition.name !== undefined &&
         b.definition.name !== undefined &&
-        a.definition.name.localeCompare(b.definition.name)
+        a.definition.name.localeCompare(b.definition.name),
     );
     return ret;
   },

+ 77 - 77
models/org.js

@@ -5,27 +5,14 @@ Org = new Mongo.Collection('org');
  */
 Org.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: {
+    orgDisplayName: {
       /**
        * the name to display for the organization
        */
       type: String,
       optional: true,
     },
-    desc: {
+    orgDesc: {
       /**
        * the description the organization
        */
@@ -33,7 +20,7 @@ Org.attachSchema(
       optional: true,
       max: 190,
     },
-    name: {
+    orgShortName: {
       /**
        * short name of the organization
        */
@@ -41,7 +28,7 @@ Org.attachSchema(
       optional: true,
       max: 255,
     },
-    website: {
+    orgWebsite: {
       /**
        * website of the organization
        */
@@ -49,66 +36,6 @@ Org.attachSchema(
       optional: true,
       max: 255,
     },
-    teams: {
-      /**
-       * List of teams of a organization
-       */
-      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,
-            },
-          ];
-        }
-      },
-    },
-    'teams.$.teamId': {
-      /**
-       * The uniq ID of the team
-       */
-      type: String,
-    },
-    'teams.$.isAdmin': {
-      /**
-       * Is the team an admin of the board?
-       */
-      type: Boolean,
-    },
-    'teams.$.isActive': {
-      /**
-       * Is the team active?
-       */
-      type: Boolean,
-    },
-    'teams.$.isNoComments': {
-      /**
-       * Is the team not allowed to make comments
-       */
-      type: Boolean,
-      optional: true,
-    },
-    'teams.$.isCommentOnly': {
-      /**
-       * Is the team only allowed to comment on the board
-       */
-      type: Boolean,
-      optional: true,
-    },
-    'teams.$.isWorker': {
-      /**
-       * Is the team only allowed to move card, assign himself to card and comment
-       */
-      type: Boolean,
-      optional: true,
-    },
     createdAt: {
       /**
        * creation date of the organization
@@ -140,6 +67,79 @@ Org.attachSchema(
   }),
 );
 
+if (Meteor.isServer) {
+  Meteor.methods({
+    setCreateOrg(
+      orgDisplayName,
+      orgDesc,
+      orgShortName,
+      orgWebsite,
+      orgIsActive,
+    ) {
+      if (Meteor.user() && Meteor.user().isAdmin) {
+        check(orgDisplayName, String);
+        check(orgDesc, String);
+        check(orgShortName, String);
+        check(orgWebsite, String);
+        check(orgIsActive, String);
+
+        const nOrgNames = Org.find({ orgShortName }).count();
+        if (nOrgNames > 0) {
+          throw new Meteor.Error('orgname-already-taken');
+        } else {
+          Org.insert({
+            orgDisplayName,
+            orgDesc,
+            orgShortName,
+            orgWebsite,
+            orgIsActive,
+          });
+        }
+      }
+    },
+
+    setOrgDisplayName(org, orgDisplayName) {
+      if (Meteor.user() && Meteor.user().isAdmin) {
+        check(org, String);
+        check(orgDisplayName, String);
+        Org.update(org, {
+          $set: { orgDisplayName: orgDisplayName },
+        });
+      }
+    },
+
+    setOrgDesc(org, orgDesc) {
+      if (Meteor.user() && Meteor.user().isAdmin) {
+        check(org, String);
+        check(orgDesc, String);
+        Org.update(org, {
+          $set: { orgDesc: orgDesc },
+        });
+      }
+    },
+
+    setOrgShortName(org, orgShortName) {
+      if (Meteor.user() && Meteor.user().isAdmin) {
+        check(org, String);
+        check(orgShortName, String);
+        Org.update(org, {
+          $set: { orgShortName: orgShortName },
+        });
+      }
+    },
+
+    setOrgIsActive(org, orgIsActive) {
+      if (Meteor.user() && Meteor.user().isAdmin) {
+        check(org, String);
+        check(orgIsActive, String);
+        Org.update(org, {
+          $set: { orgIsActive: orgIsActive },
+        });
+      }
+    },
+  });
+}
+
 if (Meteor.isServer) {
   // Index for Organization name.
   Meteor.startup(() => {

+ 77 - 17
models/team.js

@@ -5,27 +5,14 @@ Team = new Mongo.Collection('team');
  */
 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: {
+    teamDisplayName: {
       /**
        * the name to display for the team
        */
       type: String,
       optional: true,
     },
-    desc: {
+    teamDesc: {
       /**
        * the description the team
        */
@@ -33,7 +20,7 @@ Team.attachSchema(
       optional: true,
       max: 190,
     },
-    name: {
+    teamShortName: {
       /**
        * short name of the team
        */
@@ -41,7 +28,7 @@ Team.attachSchema(
       optional: true,
       max: 255,
     },
-    website: {
+    teamWebsite: {
       /**
        * website of the team
        */
@@ -80,6 +67,79 @@ Team.attachSchema(
   }),
 );
 
+if (Meteor.isServer) {
+  Meteor.methods({
+    setCreateTeam(
+      teamDisplayName,
+      teamDesc,
+      teamShortName,
+      teamWebsite,
+      teamIsActive,
+    ) {
+      if (Meteor.user() && Meteor.user().isAdmin) {
+        check(teamDisplayName, String);
+        check(teamDesc, String);
+        check(teamShortName, String);
+        check(teamWebsite, String);
+        check(teamIsActive, String);
+
+        const nTeamNames = Team.find({ teamShortName }).count();
+        if (nTeamNames > 0) {
+          throw new Meteor.Error('teamname-already-taken');
+        } else {
+          Team.insert({
+            teamDisplayName,
+            teamDesc,
+            teamShortName,
+            teamWebsite,
+            teamIsActive,
+          });
+        }
+      }
+    },
+
+    setTeamDisplayName(team, teamDisplayName) {
+      if (Meteor.user() && Meteor.user().isAdmin) {
+        check(team, String);
+        check(teamDisplayName, String);
+        Team.update(team, {
+          $set: { teamDisplayName: teamDisplayName },
+        });
+      }
+    },
+
+    setTeamDesc(team, teamDesc) {
+      if (Meteor.user() && Meteor.user().isAdmin) {
+        check(team, String);
+        check(teamDesc, String);
+        Team.update(team, {
+          $set: { teamDesc: teamDesc },
+        });
+      }
+    },
+
+    setTeamShortName(team, teamShortName) {
+      if (Meteor.user() && Meteor.user().isAdmin) {
+        check(team, String);
+        check(teamShortName, String);
+        Team.update(team, {
+          $set: { teamShortName: teamShortName },
+        });
+      }
+    },
+
+    setTeamIsActive(team, teamIsActive) {
+      if (Meteor.user() && Meteor.user().isAdmin) {
+        check(team, String);
+        check(teamIsActive, String);
+        Team.update(team, {
+          $set: { teamIsActive: teamIsActive },
+        });
+      }
+    },
+  });
+}
+
 if (Meteor.isServer) {
   // Index for Team name.
   Meteor.startup(() => {