diff --git a/client/components/settings/peopleBody.jade b/client/components/settings/peopleBody.jade
index dda0197d9..59b4718cc 100644
--- a/client/components/settings/peopleBody.jade
+++ b/client/components/settings/peopleBody.jade
@@ -135,16 +135,15 @@ template(name="peopleGeneral")
thead
tr
th
- +selectAllUser
- th {{_ 'accounts-lockout-status'}}
- th {{_ 'admin-people-active-status'}}
+ +newUserRow
th {{_ 'username'}}
- th {{_ 'fullname'}}
- th {{_ 'admin'}}
th {{_ 'email'}}
+ th {{_ 'admin'}}
+ th {{_ 'admin-people-active-status'}}
+ th {{_ 'accounts-lockout-status'}}
th {{_ 'createdAt'}}
th
- +newUserRow
+ +selectAllUser
tbody
tr
each user in peopleList
@@ -239,22 +238,12 @@ template(name="teamRow")
template(name="peopleRow")
tr
- if userData.loginDisabled
- td
- input.selectUserChkBox(type="checkbox", disabled="disabled", id="{{userData._id}}")
- else
- td
- input.selectUserChkBox(type="checkbox", id="{{userData._id}}")
- td.account-status
- if isUserLocked
- span.text-red.js-toggle-lock-status.emoji-icon(data-user-id=userData._id, data-is-locked="true", title="{{_ 'accounts-lockout-click-to-unlock'}}") đ
- else
- span.text-green.js-toggle-lock-status.emoji-icon(data-user-id=userData._id, data-is-locked="false", title="{{_ 'accounts-lockout-user-unlocked'}}") đ
- td.account-active-status
- if userData.loginDisabled
- span.text-red.js-toggle-active-status(data-user-id=userData._id, data-is-active="false", title="{{_ 'admin-people-user-inactive'}}") đĢ
- else
- span.text-green.js-toggle-active-status(data-user-id=userData._id, data-is-active="true", title="{{_ 'admin-people-user-active'}}") â
+ td
+ a.edit-user
+ | âī¸
+ | {{_ 'edit'}}
+ a.more-settings-user
+ | â¯
if userData.loginDisabled
td.username {{ userData.username }}
else if isUserLocked
@@ -262,9 +251,9 @@ template(name="peopleRow")
else
td.username {{ userData.username }}
if userData.loginDisabled
- td {{ userData.profile.fullname }}
+ td {{ userData.emails.[0].address }}
else
- td {{ userData.profile.fullname }}
+ td {{ userData.emails.[0].address }}
if userData.loginDisabled
td
if userData.isAdmin
@@ -277,20 +266,26 @@ template(name="peopleRow")
| {{_ 'yes'}}
else
| {{_ 'no'}}
- if userData.loginDisabled
- td {{ userData.emails.[0].address }}
- else
- td {{ userData.emails.[0].address }}
+ td.account-active-status
+ if userData.loginDisabled
+ span.text-red.js-toggle-active-status(data-user-id=userData._id, data-is-active="false", title="{{_ 'admin-people-user-inactive'}}") đĢ
+ else
+ span.text-green.js-toggle-active-status(data-user-id=userData._id, data-is-active="true", title="{{_ 'admin-people-user-active'}}") â
+ td.account-status
+ if isUserLocked
+ span.text-red.js-toggle-lock-status.emoji-icon(data-user-id=userData._id, data-is-locked="true", title="{{_ 'accounts-lockout-click-to-unlock'}}") đ
+ else
+ span.text-green.js-toggle-lock-status.emoji-icon(data-user-id=userData._id, data-is-locked="false", title="{{_ 'accounts-lockout-user-unlocked'}}") đ
if userData.loginDisabled
td {{ moment userData.createdAt 'LLL' }}
else
td {{ moment userData.createdAt 'LLL' }}
- td
- a.edit-user
- | âī¸
- | {{_ 'edit'}}
- a.more-settings-user
- | â¯
+ if userData.loginDisabled
+ td
+ input.selectUserChkBox(type="checkbox", disabled="disabled", id="{{userData._id}}")
+ else
+ td
+ input.selectUserChkBox(type="checkbox", id="{{userData._id}}")
template(name="editOrgPopup")
form
diff --git a/client/components/settings/peopleBody.js b/client/components/settings/peopleBody.js
index 2e3601f23..e06ef49d6 100644
--- a/client/components/settings/peopleBody.js
+++ b/client/components/settings/peopleBody.js
@@ -840,16 +840,7 @@ Template.editUserPopup.events({
? user.emails[0].address.toLowerCase()
: false);
- Users.update(this.userId, {
- $set: {
- 'profile.fullname': fullname,
- isAdmin: isAdmin === 'true',
- loginDisabled: isActive === 'true',
- authenticationMethod: authentication,
- importUsernames: Users.parseImportUsernames(importUsernames),
- },
- });
-
+ // Build user teams list
let userTeamsList = userTeams.split(",");
let userTeamsIdsList = userTeamsIds.split(",");
let userTms = [];
@@ -862,12 +853,7 @@ Template.editUserPopup.events({
}
}
- Users.update(this.userId, {
- $set:{
- teams: userTms
- }
- });
-
+ // Build user orgs list
let userOrgsList = userOrgs.split(",");
let userOrgsIdsList = userOrgsIds.split(",");
let userOrganizations = [];
@@ -880,9 +866,20 @@ Template.editUserPopup.events({
}
}
- Users.update(this.userId, {
- $set:{
- orgs: userOrganizations
+ // Update user via Meteor method (for admin to edit other users)
+ const updateData = {
+ fullname: fullname,
+ isAdmin: isAdmin === 'true',
+ loginDisabled: isActive === 'true',
+ authenticationMethod: authentication,
+ importUsernames: Users.parseImportUsernames(importUsernames),
+ teams: userTms,
+ orgs: userOrganizations,
+ };
+
+ Meteor.call('editUser', this.userId, updateData, (error) => {
+ if (error) {
+ console.error('Error updating user:', error);
}
});
diff --git a/client/components/settings/settingHeader.css b/client/components/settings/settingHeader.css
index dbd8582e3..5b880b9f2 100644
--- a/client/components/settings/settingHeader.css
+++ b/client/components/settings/settingHeader.css
@@ -1,4 +1,5 @@
#header #header-main-bar .setting-header-btn {
+ border-radius: 3px;
color: #f2f2f2;
margin-left: 20px;
padding-right: 10px;
diff --git a/client/components/settings/settingHeader.jade b/client/components/settings/settingHeader.jade
index 7ce77ba4e..9fbc89394 100644
--- a/client/components/settings/settingHeader.jade
+++ b/client/components/settings/settingHeader.jade
@@ -4,27 +4,27 @@ template(name="settingHeaderBar")
.setting-header-btns.left
if currentUser
- a.setting-header-btn.settings(href="{{pathFor 'setting'}}")
+ a.setting-header-btn.settings(class=isSettingsActive href="{{pathFor 'setting'}}")
span.emoji-icon âī¸
span {{_ 'settings'}}
- a.setting-header-btn.people(href="{{pathFor 'people'}}")
+ a.setting-header-btn.people(class=isPeopleActive href="{{pathFor 'people'}}")
span.emoji-icon đĨ
span {{_ 'people'}}
- a.setting-header-btn.informations(href="{{pathFor 'admin-reports'}}")
+ a.setting-header-btn.informations(class=isAdminReportsActive href="{{pathFor 'admin-reports'}}")
span.emoji-icon đ
span {{_ 'reports'}}
- a.setting-header-btn.informations(href="{{pathFor 'attachments'}}")
+ a.setting-header-btn.informations(class=isAttachmentsActive href="{{pathFor 'attachments'}}")
span.emoji-icon đ
span {{_ 'attachments'}}
- a.setting-header-btn.informations(href="{{pathFor 'translation'}}")
+ a.setting-header-btn.informations(class=isTranslationActive href="{{pathFor 'translation'}}")
span.emoji-icon đ¤
span {{_ 'translation'}}
- a.setting-header-btn.informations(href="{{pathFor 'information'}}")
+ a.setting-header-btn.informations(class=isInformationActive href="{{pathFor 'information'}}")
span.emoji-icon âšī¸
span {{_ 'info'}}
diff --git a/client/components/settings/settingHeader.js b/client/components/settings/settingHeader.js
new file mode 100644
index 000000000..673108d03
--- /dev/null
+++ b/client/components/settings/settingHeader.js
@@ -0,0 +1,20 @@
+Template.settingHeaderBar.helpers({
+ isSettingsActive() {
+ return FlowRouter.getRouteName() === 'setting' ? 'active' : '';
+ },
+ isPeopleActive() {
+ return FlowRouter.getRouteName() === 'people' ? 'active' : '';
+ },
+ isAdminReportsActive() {
+ return FlowRouter.getRouteName() === 'admin-reports' ? 'active' : '';
+ },
+ isAttachmentsActive() {
+ return FlowRouter.getRouteName() === 'attachments' ? 'active' : '';
+ },
+ isTranslationActive() {
+ return FlowRouter.getRouteName() === 'translation' ? 'active' : '';
+ },
+ isInformationActive() {
+ return FlowRouter.getRouteName() === 'information' ? 'active' : '';
+ },
+});
diff --git a/client/components/users/userAvatar.css b/client/components/users/userAvatar.css
index 7e2b2a923..27d8993b7 100644
--- a/client/components/users/userAvatar.css
+++ b/client/components/users/userAvatar.css
@@ -23,6 +23,9 @@
background-color: #dbdbdb;
color: #444;
position: absolute;
+ display: flex;
+ align-items: center;
+ justify-content: center;
}
.member .avatar.avatar-image {
object-fit: cover;
diff --git a/client/components/users/userAvatar.jade b/client/components/users/userAvatar.jade
index e00fc188f..652c0eedf 100644
--- a/client/components/users/userAvatar.jade
+++ b/client/components/users/userAvatar.jade
@@ -17,7 +17,7 @@ template(name="userAvatar")
template(name="userAvatarInitials")
svg.avatar.avatar-initials(viewBox="0 0 {{viewPortWidth}} 15")
- text(x="50%" y="13" text-anchor="middle")= initials
+ text(x="50%" y="11" text-anchor="middle" dominant-baseline="middle" font-size="16")= initials
template(name="orgAvatar")
a.member.orgOrTeamMember(class="js-member" title="{{orgData.orgDisplayName}}")
@@ -53,7 +53,7 @@ template(name="boardTeamRow")
template(name="boardOrgName")
svg.avatar.avatar-initials(viewBox="0 0 {{orgViewPortWidth}} 15")
- text(x="50%" y="13" text-anchor="middle")= orgName
+ text(x="50%" y="11" text-anchor="middle" dominant-baseline="middle" font-size="16")= orgName
template(name="teamAvatar")
a.member.orgOrTeamMember(class="js-member" title="{{teamData.teamDisplayName}}")
@@ -61,7 +61,7 @@ template(name="teamAvatar")
template(name="boardTeamName")
svg.avatar.avatar-initials(viewBox="0 0 {{teamViewPortWidth}} 15")
- text(x="50%" y="13" text-anchor="middle")= teamName
+ text(x="50%" y="11" text-anchor="middle" dominant-baseline="middle" font-size="16")= teamName
template(name="userPopup")
.board-member-menu
@@ -88,13 +88,11 @@ template(name="changeAvatarPopup")
li: a.js-select-avatar
.member
img.avatar.avatar-image(src="{{link}}")
- | {{_ 'uploaded-avatar'}}
if isSelected
| â
p.sub-name
- unless isSelected
- a.js-delete-avatar {{_ 'delete'}}
- | -
+ a.js-delete-avatar {{_ 'delete'}}
+ | -
= name
li: a.js-select-initials
.member
@@ -102,7 +100,7 @@ template(name="changeAvatarPopup")
| {{_ 'initials' }}
if noAvatarUrl
| â
- p.sub-name {{_ 'default-avatar'}}
+ p.sub-name {{_ 'default-avatar'}}
input.hide.js-upload-avatar-input(accept="image/*;capture=camera" type="file")
if Meteor.settings.public.avatarsUploadMaxSize
| {{_ 'max-avatar-filesize'}} {{Meteor.settings.public.avatarsUploadMaxSize}}
@@ -113,7 +111,11 @@ template(name="changeAvatarPopup")
| {{_ 'invalid-file'}}
button.full.js-upload-avatar
| đ¤
- | {{_ 'upload-avatar'}}
+ | {{_ 'upload-avatar' }}
+
+template(name="deleteAvatarPopup")
+ p {{_ 'delete-avatar-confirm'}}
+ button.js-confirm.negate.full(type="submit") {{_ 'delete'}}
template(name="cardMemberPopup")
.board-member-menu
diff --git a/client/components/users/userAvatar.js b/client/components/users/userAvatar.js
index f2db90ee3..c81ee7714 100644
--- a/client/components/users/userAvatar.js
+++ b/client/components/users/userAvatar.js
@@ -187,7 +187,7 @@ BlazeComponent.extendComponent({
},
uploadedAvatars() {
- const ret = ReactiveCache.getAvatars({ userId: Meteor.userId() }, {}, true).each();
+ const ret = ReactiveCache.getAvatars({ userId: Meteor.userId() }, {}, true);
return ret;
},
@@ -205,7 +205,11 @@ BlazeComponent.extendComponent({
},
setAvatar(avatarUrl) {
- ReactiveCache.getCurrentUser().setAvatarUrl(avatarUrl);
+ Meteor.call('setAvatarUrl', avatarUrl, (err) => {
+ if (err) {
+ this.setError(err.reason || 'Error setting avatar');
+ }
+ });
},
setError(error) {
@@ -234,44 +238,30 @@ BlazeComponent.extendComponent({
uploader.start();
}
},
- 'click .js-select-avatar'() {
- const avatarUrl = this.currentData().link();
- this.setAvatar(avatarUrl);
+ 'click .js-select-avatar'(event) {
+ event.preventDefault();
+ event.stopPropagation();
+ const data = Blaze.getData(event.currentTarget);
+ if (data && typeof data.link === 'function') {
+ const avatarUrl = data.link();
+ this.setAvatar(avatarUrl);
+ }
},
- 'click .js-select-initials'() {
+ 'click .js-select-initials'(event) {
+ event.preventDefault();
+ event.stopPropagation();
this.setAvatar('');
},
- 'click .js-delete-avatar'(event) {
- Avatars.remove(this.currentData()._id);
+ 'click .js-delete-avatar': Popup.afterConfirm('deleteAvatar', function(event) {
+ Avatars.remove(this._id);
+ Popup.back();
event.stopPropagation();
- },
+ }),
},
];
},
}).register('changeAvatarPopup');
-Template.cardMembersPopup.helpers({
- isCardMember() {
- const card = Template.parentData();
- const cardMembers = card.getMembers();
-
- return _.contains(cardMembers, this.userId);
- },
-
- user() {
- return ReactiveCache.getUser(this.userId);
- },
-});
-
-Template.cardMembersPopup.events({
- 'click .js-select-member'(event) {
- const card = Utils.getCurrentCard();
- const memberId = this.userId;
- card.toggleMember(memberId);
- event.preventDefault();
- },
-});
-
Template.cardMemberPopup.helpers({
user() {
return ReactiveCache.getUser(this.userId);
diff --git a/imports/i18n/data/en.i18n.json b/imports/i18n/data/en.i18n.json
index d48099de1..74bb4c655 100644
--- a/imports/i18n/data/en.i18n.json
+++ b/imports/i18n/data/en.i18n.json
@@ -281,6 +281,8 @@
"change-permissions": "Change permissions",
"change-settings": "Change Settings",
"changeAvatarPopup-title": "Change Avatar",
+ "delete-avatar-confirm": "Are you sure you want to delete this avatar?",
+ "deleteAvatarPopup-title": "Delete Avatar?",
"changeLanguagePopup-title": "Change Language",
"changePasswordPopup-title": "Change Password",
"changePermissionsPopup-title": "Change Permissions",
diff --git a/models/users.js b/models/users.js
index f83ecd3c2..e022c36de 100644
--- a/models/users.js
+++ b/models/users.js
@@ -1970,10 +1970,70 @@ Meteor.methods({
Users.remove(targetUserId);
return { success: true, message: 'User deleted successfully' };
},
+ editUser(targetUserId, updateData) {
+ check(targetUserId, String);
+ check(updateData, Object);
+
+ const currentUserId = Meteor.userId();
+ if (!currentUserId) {
+ throw new Meteor.Error('not-authorized', 'User must be logged in');
+ }
+
+ const currentUser = ReactiveCache.getUser(currentUserId);
+ if (!currentUser) {
+ throw new Meteor.Error('not-authorized', 'Current user not found');
+ }
+
+ // Check if current user is admin
+ if (!currentUser.isAdmin) {
+ throw new Meteor.Error('not-authorized', 'Only administrators can edit other users');
+ }
+
+ const targetUser = ReactiveCache.getUser(targetUserId);
+ if (!targetUser) {
+ throw new Meteor.Error('user-not-found', 'Target user not found');
+ }
+
+ // Only allow updating specific fields
+ const updateObject = {};
+ if (updateData.fullname !== undefined) {
+ updateObject['profile.fullname'] = updateData.fullname;
+ }
+ if (updateData.initials !== undefined) {
+ updateObject['profile.initials'] = updateData.initials;
+ }
+ if (updateData.isAdmin !== undefined) {
+ updateObject.isAdmin = updateData.isAdmin;
+ }
+ if (updateData.loginDisabled !== undefined) {
+ updateObject.loginDisabled = updateData.loginDisabled;
+ }
+ if (updateData.authenticationMethod !== undefined) {
+ updateObject.authenticationMethod = updateData.authenticationMethod;
+ }
+ if (updateData.importUsernames !== undefined) {
+ updateObject.importUsernames = updateData.importUsernames;
+ }
+ if (updateData.teams !== undefined) {
+ updateObject.teams = updateData.teams;
+ }
+ if (updateData.orgs !== undefined) {
+ updateObject.orgs = updateData.orgs;
+ }
+
+ Users.update(targetUserId, { $set: updateObject });
+ },
setListSortBy(value) {
check(value, String);
ReactiveCache.getCurrentUser().setListSortBy(value);
},
+ setAvatarUrl(avatarUrl) {
+ check(avatarUrl, String);
+ if (!this.userId) {
+ throw new Meteor.Error('not-logged-in', 'User must be logged in');
+ }
+ Users.update(this.userId, { $set: { 'profile.avatarUrl': avatarUrl } });
+ },
toggleBoardStar(boardId) {
check(boardId, String);
if (!this.userId) {