diff --git a/client/components/settings/peopleBody.jade b/client/components/settings/peopleBody.jade index f9f5b72a9..5b171b744 100644 --- a/client/components/settings/peopleBody.jade +++ b/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 {{ orgData.displayName }} + else + td {{ orgData.displayName }} + if orgData.loginDisabled + td {{ orgData.orgDesc }} + else + td {{ orgData.desc }} + if orgData.loginDisabled + td {{ orgData.name }} + else + td {{ orgData.name }} + if orgData.loginDisabled + td {{ orgData.website }} + else + td {{ orgData.website }} + if orgData.loginDisabled + td {{ orgData.teams }} + else + td {{ orgData.teams }} + if orgData.loginDisabled + td {{ moment orgData.createdAt 'LLL' }} + 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 {{ teamData.displayName }} + else + td {{ teamData.displayName }} + if teamData.loginDisabled + td {{ teamData.desc }} + else + td {{ teamData.desc }} + if teamData.loginDisabled + td {{ teamData.dame }} + else + td {{ teamData.name }} + if teamData.loginDisabled + td {{ teamData.website }} + else + td {{ teamData.website }} + if orgData.loginDisabled + td {{ moment teamData.createdAt 'LLL' }} + 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 diff --git a/client/components/settings/peopleBody.js b/client/components/settings/peopleBody.js index 0cd288d41..571a8540b 100644 --- a/client/components/settings/peopleBody.js +++ b/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(''); diff --git a/client/components/settings/peopleBody.styl b/client/components/settings/peopleBody.styl index 8f3c10c27..028db164c 100644 --- a/client/components/settings/peopleBody.styl +++ b/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; diff --git a/i18n/en.i18n.json b/i18n/en.i18n.json index 7027613c8..9fbb4ed34 100644 --- a/i18n/en.i18n.json +++ b/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" } diff --git a/models/org.js b/models/org.js index a24d829db..adbf7a729 100644 --- a/models/org.js +++ b/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, - optional: true, - max: 50, + type: Boolean, }, - country: { + 'teams.$.isActive': { /** - * country of the organization + * Is the team active? */ - type: String, - optional: true, - max: 255, + type: Boolean, }, - billingEmail: { + 'teams.$.isNoComments': { /** - * billingEmail of the organization + * Is the team not allowed to make comments */ - type: String, + 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, - max: 255, }, createdAt: { /** diff --git a/models/team.js b/models/team.js new file mode 100644 index 000000000..dfcbedfcb --- /dev/null +++ b/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; diff --git a/server/publications/org.js b/server/publications/org.js new file mode 100644 index 000000000..e7dcfdd63 --- /dev/null +++ b/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 []; +}); diff --git a/server/publications/team.js b/server/publications/team.js new file mode 100644 index 000000000..aa18a5da6 --- /dev/null +++ b/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 []; +});