Fixed Change Avatar. Improved Admin Panel: People columns order, selected tab background color.

Thanks to xet7 !
This commit is contained in:
Lauri Ojansivu 2026-01-14 00:38:56 +02:00
parent eca42f32bb
commit e89f4d260c
10 changed files with 169 additions and 99 deletions

View file

@ -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 <s>{{ userData.username }}</s>
else if isUserLocked
@ -262,9 +251,9 @@ template(name="peopleRow")
else
td.username {{ userData.username }}
if userData.loginDisabled
td <s>{{ userData.profile.fullname }}</s>
td <s>{{ userData.emails.[0].address }}</s>
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 <s>{{ userData.emails.[0].address }}</s>
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 <s>{{ moment userData.createdAt 'LLL' }}</s>
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

View file

@ -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);
}
});

View file

@ -1,4 +1,5 @@
#header #header-main-bar .setting-header-btn {
border-radius: 3px;
color: #f2f2f2;
margin-left: 20px;
padding-right: 10px;

View file

@ -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'}}

View file

@ -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' : '';
},
});

View file

@ -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;

View file

@ -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

View file

@ -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);

View file

@ -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",

View file

@ -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) {