From e89f4d260c216a75069801efff379a0f73044a36 Mon Sep 17 00:00:00 2001 From: Lauri Ojansivu Date: Wed, 14 Jan 2026 00:38:56 +0200 Subject: [PATCH] Fixed Change Avatar. Improved Admin Panel: People columns order, selected tab background color. Thanks to xet7 ! --- client/components/settings/peopleBody.jade | 63 +++++++++---------- client/components/settings/peopleBody.js | 35 +++++------ client/components/settings/settingHeader.css | 1 + client/components/settings/settingHeader.jade | 12 ++-- client/components/settings/settingHeader.js | 20 ++++++ client/components/users/userAvatar.css | 3 + client/components/users/userAvatar.jade | 20 +++--- client/components/users/userAvatar.js | 52 +++++++-------- imports/i18n/data/en.i18n.json | 2 + models/users.js | 60 ++++++++++++++++++ 10 files changed, 169 insertions(+), 99 deletions(-) create mode 100644 client/components/settings/settingHeader.js 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) {