mirror of
https://github.com/mattermost/mattermost.git
synced 2025-02-25 18:55:24 -06:00
Sysadmin manage user settings (#27583)
* Opened modal from system console * WIP * WIP * WIP * Handled saving user * Successfully updated user based settings * WIP * WIP * All settings are updating well * Fixed modal style * Added admin mode indicators in modal * Added confirmation dialog * Lint fixes * Added license check * Added permission check * Fixed i18n file order * type fix * Updated snapshots * Handled performance debugging setting * Some styling tweaks * Fixed text alighnment * Updated license required from professional to enterprise * Handled long user names * review fixes * Added manage setting option in user list page context menu * Added loader * Minor reordering * Removed confirm modal * Updated snapshots for removed modal * Added some tests * Lint fix * Used new selector in user detail page * Used new selector in user list page * Updated tests * Fixed an incorrect default test
This commit is contained in:
parent
0df1a62f61
commit
87d983cc7f
@ -21,7 +21,7 @@ func (api *API) InitReports() {
|
||||
}
|
||||
|
||||
func getUsersForReporting(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if !(c.IsSystemAdmin()) {
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadUserManagementUsers) {
|
||||
c.SetPermissionError(model.PermissionSysconsoleReadUserManagementUsers)
|
||||
return
|
||||
}
|
||||
@ -52,7 +52,7 @@ func getUsersForReporting(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func getUserCountForReporting(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if !(c.IsSystemAdmin()) {
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadUserManagementUsers) {
|
||||
c.SetPermissionError(model.PermissionSysconsoleReadUserManagementUsers)
|
||||
return
|
||||
}
|
||||
|
@ -864,7 +864,7 @@ func (a *App) SetDefaultProfileImage(c request.CTX, user *model.User) *model.App
|
||||
}
|
||||
|
||||
options := a.Config().GetSanitizeOptions()
|
||||
updatedUser.SanitizeProfile(options)
|
||||
updatedUser.SanitizeProfile(options, false)
|
||||
|
||||
message := model.NewWebSocketEvent(model.WebsocketEventUserUpdated, "", "", "", nil, "")
|
||||
message.Add("user", updatedUser)
|
||||
@ -1117,7 +1117,7 @@ func (a *App) GetSanitizeOptions(asAdmin bool) map[string]bool {
|
||||
func (a *App) SanitizeProfile(user *model.User, asAdmin bool) {
|
||||
options := a.ch.srv.userService.GetSanitizeOptions(asAdmin)
|
||||
|
||||
user.SanitizeProfile(options)
|
||||
user.SanitizeProfile(options, asAdmin)
|
||||
}
|
||||
|
||||
func (a *App) UpdateUserAsUser(c request.CTX, user *model.User, asAdmin bool) (*model.User, *model.AppError) {
|
||||
@ -2558,7 +2558,7 @@ func (a *App) invalidateUserCacheAndPublish(rctx request.CTX, userID string) {
|
||||
}
|
||||
|
||||
options := a.Config().GetSanitizeOptions()
|
||||
user.SanitizeProfile(options)
|
||||
user.SanitizeProfile(options, false)
|
||||
|
||||
message := model.NewWebSocketEvent(model.WebsocketEventUserUpdated, "", "", "", nil, "")
|
||||
message.Add("user", user)
|
||||
|
@ -42,7 +42,7 @@ func (us *UserService) sanitizeProfiles(users []*model.User, asAdmin bool) []*mo
|
||||
func (us *UserService) SanitizeProfile(user *model.User, asAdmin bool) {
|
||||
options := us.GetSanitizeOptions(asAdmin)
|
||||
|
||||
user.SanitizeProfile(options)
|
||||
user.SanitizeProfile(options, asAdmin)
|
||||
}
|
||||
|
||||
func (us *UserService) GetSanitizeOptions(asAdmin bool) map[string]bool {
|
||||
|
@ -1144,7 +1144,7 @@ func (s *SqlPostStore) prepareThreadedResponse(posts []*postWithExtra, extended,
|
||||
return nil, err
|
||||
}
|
||||
for _, user := range users {
|
||||
user.SanitizeProfile(sanitizeOptions)
|
||||
user.SanitizeProfile(sanitizeOptions, false)
|
||||
usersMap[user.Id] = user
|
||||
}
|
||||
} else {
|
||||
|
@ -144,7 +144,7 @@ func (u *UserReportOptions) IsValid() *AppError {
|
||||
}
|
||||
|
||||
func (u *UserReportQuery) ToReport() *UserReport {
|
||||
u.ClearNonProfileFields()
|
||||
u.ClearNonProfileFields(false)
|
||||
return &UserReport{
|
||||
User: u.User,
|
||||
UserPostStats: u.UserPostStats,
|
||||
|
@ -695,19 +695,22 @@ func (u *User) SanitizeInput(isAdmin bool) {
|
||||
u.Email = strings.TrimSpace(u.Email)
|
||||
}
|
||||
|
||||
func (u *User) ClearNonProfileFields() {
|
||||
func (u *User) ClearNonProfileFields(asAdmin bool) {
|
||||
u.Password = ""
|
||||
u.AuthData = NewString("")
|
||||
u.MfaSecret = ""
|
||||
u.EmailVerified = false
|
||||
u.AllowMarketing = false
|
||||
u.NotifyProps = StringMap{}
|
||||
u.LastPasswordUpdate = 0
|
||||
u.FailedAttempts = 0
|
||||
|
||||
if !asAdmin {
|
||||
u.NotifyProps = StringMap{}
|
||||
}
|
||||
}
|
||||
|
||||
func (u *User) SanitizeProfile(options map[string]bool) {
|
||||
u.ClearNonProfileFields()
|
||||
func (u *User) SanitizeProfile(options map[string]bool, asAdmin bool) {
|
||||
u.ClearNonProfileFields(asAdmin)
|
||||
|
||||
u.Sanitize(options)
|
||||
}
|
||||
|
@ -551,12 +551,12 @@ func TestSanitizeProfile(t *testing.T) {
|
||||
Props: StringMap{UserPropsKeyRemoteEmail: "remote@doe.com"},
|
||||
}
|
||||
|
||||
user.SanitizeProfile(nil)
|
||||
user.SanitizeProfile(nil, false)
|
||||
|
||||
require.Equal(t, "john@doe.com", user.Email)
|
||||
require.Equal(t, "remote@doe.com", user.Props[UserPropsKeyRemoteEmail])
|
||||
|
||||
user.SanitizeProfile(map[string]bool{"email": false})
|
||||
user.SanitizeProfile(map[string]bool{"email": false}, false)
|
||||
|
||||
require.Empty(t, user.Email)
|
||||
require.Empty(t, user.Props[UserPropsKeyRemoteEmail])
|
||||
|
@ -17,7 +17,7 @@
|
||||
height: 92px;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
padding: 30px 20px 12px 30px;
|
||||
padding: 0 20px 0 30px;
|
||||
background-color: #295eb9;
|
||||
}
|
||||
|
||||
@ -31,18 +31,23 @@
|
||||
}
|
||||
|
||||
.AdminUserCard__footer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding: 20px;
|
||||
border-top: solid 1px rgba(0, 0, 0, 0.2);
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.AdminUserCard__user-info {
|
||||
overflow: hidden;
|
||||
min-width: 0;
|
||||
align-self: flex-end;
|
||||
padding: 0;
|
||||
margin-left: 20px;
|
||||
color: #fff;
|
||||
font-size: 20px;
|
||||
font-weight: normal;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.AdminUserCard__user-nickname {
|
||||
|
@ -411,3 +411,630 @@ exports[`SystemUserDetail should match snapshot if MFA is enabled 1`] = `
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`SystemUserDetail should not show manage user settings button when user doesnt have permission 1`] = `
|
||||
<div
|
||||
className="SystemUserDetail wrapper--fixed"
|
||||
>
|
||||
<AdminHeader
|
||||
withBackButton={true}
|
||||
>
|
||||
<div>
|
||||
<Connect(Component)
|
||||
className="fa fa-angle-left back"
|
||||
to="/admin_console/user_management/users"
|
||||
/>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="User Configuration"
|
||||
id="admin.systemUserDetail.title"
|
||||
/>
|
||||
</div>
|
||||
</AdminHeader>
|
||||
<div
|
||||
className="admin-console__wrapper"
|
||||
>
|
||||
<div
|
||||
className="admin-console__content"
|
||||
>
|
||||
<AdminUserCard
|
||||
body={
|
||||
<React.Fragment>
|
||||
<span>
|
||||
|
||||
</span>
|
||||
<label>
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
defaultMessage="Email"
|
||||
id="admin.userManagement.userDetail.email"
|
||||
/>
|
||||
<Memo(EmailIcon) />
|
||||
<input
|
||||
className="form-control"
|
||||
disabled={false}
|
||||
onChange={[Function]}
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
defaultMessage="Username"
|
||||
id="admin.userManagement.userDetail.username"
|
||||
/>
|
||||
<AtIcon />
|
||||
<span />
|
||||
</label>
|
||||
<label>
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
defaultMessage="Authentication Method"
|
||||
id="admin.userManagement.userDetail.authenticationMethod"
|
||||
/>
|
||||
<Memo(ShieldOutlineIcon) />
|
||||
<span>
|
||||
|
||||
</span>
|
||||
</label>
|
||||
</React.Fragment>
|
||||
}
|
||||
footer={
|
||||
<React.Fragment>
|
||||
<button
|
||||
className="btn btn-secondary"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
defaultMessage="Reset Password"
|
||||
id="admin.user_item.resetPwd"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-secondary"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
defaultMessage="Activate"
|
||||
id="admin.user_item.makeActive"
|
||||
/>
|
||||
</button>
|
||||
</React.Fragment>
|
||||
}
|
||||
isLoading={true}
|
||||
/>
|
||||
<AdminPanel
|
||||
button={
|
||||
<div
|
||||
className="add-team-button"
|
||||
>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
disabled={true}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
defaultMessage="Add Team"
|
||||
id="admin.userManagement.userDetail.addTeam"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
subtitle={
|
||||
Object {
|
||||
"defaultMessage": "Teams to which this user belongs",
|
||||
"id": "admin.userManagement.userDetail.teamsSubtitle",
|
||||
}
|
||||
}
|
||||
title={
|
||||
Object {
|
||||
"defaultMessage": "Team Membership",
|
||||
"id": "admin.userManagement.userDetail.teamsTitle",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="teamlistLoading"
|
||||
>
|
||||
<Memo(LoadingSpinner) />
|
||||
</div>
|
||||
</AdminPanel>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="admin-console-save"
|
||||
>
|
||||
<SaveButton
|
||||
btnClass=""
|
||||
defaultMessage={
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
defaultMessage="Save"
|
||||
id="save_button.save"
|
||||
/>
|
||||
}
|
||||
disabled={true}
|
||||
extraClasses=""
|
||||
onClick={[Function]}
|
||||
saving={false}
|
||||
savingMessage={
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
defaultMessage="Saving"
|
||||
id="save_button.saving"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<div
|
||||
className="error-message"
|
||||
>
|
||||
<FormError
|
||||
error={null}
|
||||
errors={Array []}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<ConfirmModal
|
||||
confirmButtonClass="btn btn-danger"
|
||||
confirmButtonText={
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
defaultMessage="Deactivate"
|
||||
id="deactivate_member_modal.deactivate"
|
||||
/>
|
||||
}
|
||||
message={
|
||||
<div>
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
defaultMessage="This action deactivates {username}. They will be logged out and not have access to any teams or channels on this system. Are you sure you want to deactivate {username}?"
|
||||
id="deactivate_member_modal.desc"
|
||||
values={
|
||||
Object {
|
||||
"username": "",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<strong>
|
||||
<br />
|
||||
<br />
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
defaultMessage="You must also deactivate this user in the SSO provider or they will be reactivated on next login or sync."
|
||||
id="deactivate_member_modal.sso_warning"
|
||||
/>
|
||||
</strong>
|
||||
</div>
|
||||
}
|
||||
modalClass=""
|
||||
onCancel={[Function]}
|
||||
onConfirm={[Function]}
|
||||
show={false}
|
||||
title={
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
defaultMessage="Deactivate {username}"
|
||||
id="deactivate_member_modal.title"
|
||||
values={
|
||||
Object {
|
||||
"username": "",
|
||||
}
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`SystemUserDetail should show manage user settings button as activated 1`] = `
|
||||
<div
|
||||
className="SystemUserDetail wrapper--fixed"
|
||||
>
|
||||
<AdminHeader
|
||||
withBackButton={true}
|
||||
>
|
||||
<div>
|
||||
<Connect(Component)
|
||||
className="fa fa-angle-left back"
|
||||
to="/admin_console/user_management/users"
|
||||
/>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="User Configuration"
|
||||
id="admin.systemUserDetail.title"
|
||||
/>
|
||||
</div>
|
||||
</AdminHeader>
|
||||
<div
|
||||
className="admin-console__wrapper"
|
||||
>
|
||||
<div
|
||||
className="admin-console__content"
|
||||
>
|
||||
<AdminUserCard
|
||||
body={
|
||||
<React.Fragment>
|
||||
<span>
|
||||
|
||||
</span>
|
||||
<label>
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
defaultMessage="Email"
|
||||
id="admin.userManagement.userDetail.email"
|
||||
/>
|
||||
<Memo(EmailIcon) />
|
||||
<input
|
||||
className="form-control"
|
||||
disabled={false}
|
||||
onChange={[Function]}
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
defaultMessage="Username"
|
||||
id="admin.userManagement.userDetail.username"
|
||||
/>
|
||||
<AtIcon />
|
||||
<span />
|
||||
</label>
|
||||
<label>
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
defaultMessage="Authentication Method"
|
||||
id="admin.userManagement.userDetail.authenticationMethod"
|
||||
/>
|
||||
<Memo(ShieldOutlineIcon) />
|
||||
<span>
|
||||
|
||||
</span>
|
||||
</label>
|
||||
</React.Fragment>
|
||||
}
|
||||
footer={
|
||||
<React.Fragment>
|
||||
<button
|
||||
className="btn btn-secondary"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
defaultMessage="Reset Password"
|
||||
id="admin.user_item.resetPwd"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-secondary"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
defaultMessage="Activate"
|
||||
id="admin.user_item.makeActive"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
className="manageUserSettingsBtn btn btn-tertiary"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
defaultMessage="Manage User Settings"
|
||||
id="admin.user_item.manageSettings"
|
||||
/>
|
||||
</button>
|
||||
</React.Fragment>
|
||||
}
|
||||
isLoading={true}
|
||||
/>
|
||||
<AdminPanel
|
||||
button={
|
||||
<div
|
||||
className="add-team-button"
|
||||
>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
disabled={true}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
defaultMessage="Add Team"
|
||||
id="admin.userManagement.userDetail.addTeam"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
subtitle={
|
||||
Object {
|
||||
"defaultMessage": "Teams to which this user belongs",
|
||||
"id": "admin.userManagement.userDetail.teamsSubtitle",
|
||||
}
|
||||
}
|
||||
title={
|
||||
Object {
|
||||
"defaultMessage": "Team Membership",
|
||||
"id": "admin.userManagement.userDetail.teamsTitle",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="teamlistLoading"
|
||||
>
|
||||
<Memo(LoadingSpinner) />
|
||||
</div>
|
||||
</AdminPanel>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="admin-console-save"
|
||||
>
|
||||
<SaveButton
|
||||
btnClass=""
|
||||
defaultMessage={
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
defaultMessage="Save"
|
||||
id="save_button.save"
|
||||
/>
|
||||
}
|
||||
disabled={true}
|
||||
extraClasses=""
|
||||
onClick={[Function]}
|
||||
saving={false}
|
||||
savingMessage={
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
defaultMessage="Saving"
|
||||
id="save_button.saving"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<div
|
||||
className="error-message"
|
||||
>
|
||||
<FormError
|
||||
error={null}
|
||||
errors={Array []}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<ConfirmModal
|
||||
confirmButtonClass="btn btn-danger"
|
||||
confirmButtonText={
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
defaultMessage="Deactivate"
|
||||
id="deactivate_member_modal.deactivate"
|
||||
/>
|
||||
}
|
||||
message={
|
||||
<div>
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
defaultMessage="This action deactivates {username}. They will be logged out and not have access to any teams or channels on this system. Are you sure you want to deactivate {username}?"
|
||||
id="deactivate_member_modal.desc"
|
||||
values={
|
||||
Object {
|
||||
"username": "",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<strong>
|
||||
<br />
|
||||
<br />
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
defaultMessage="You must also deactivate this user in the SSO provider or they will be reactivated on next login or sync."
|
||||
id="deactivate_member_modal.sso_warning"
|
||||
/>
|
||||
</strong>
|
||||
</div>
|
||||
}
|
||||
modalClass=""
|
||||
onCancel={[Function]}
|
||||
onConfirm={[Function]}
|
||||
show={false}
|
||||
title={
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
defaultMessage="Deactivate {username}"
|
||||
id="deactivate_member_modal.title"
|
||||
values={
|
||||
Object {
|
||||
"username": "",
|
||||
}
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`SystemUserDetail should show manage user settings button as disabled when no license 1`] = `
|
||||
<div
|
||||
className="SystemUserDetail wrapper--fixed"
|
||||
>
|
||||
<AdminHeader
|
||||
withBackButton={true}
|
||||
>
|
||||
<div>
|
||||
<Connect(Component)
|
||||
className="fa fa-angle-left back"
|
||||
to="/admin_console/user_management/users"
|
||||
/>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="User Configuration"
|
||||
id="admin.systemUserDetail.title"
|
||||
/>
|
||||
</div>
|
||||
</AdminHeader>
|
||||
<div
|
||||
className="admin-console__wrapper"
|
||||
>
|
||||
<div
|
||||
className="admin-console__content"
|
||||
>
|
||||
<AdminUserCard
|
||||
body={
|
||||
<React.Fragment>
|
||||
<span>
|
||||
|
||||
</span>
|
||||
<label>
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
defaultMessage="Email"
|
||||
id="admin.userManagement.userDetail.email"
|
||||
/>
|
||||
<Memo(EmailIcon) />
|
||||
<input
|
||||
className="form-control"
|
||||
disabled={false}
|
||||
onChange={[Function]}
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
defaultMessage="Username"
|
||||
id="admin.userManagement.userDetail.username"
|
||||
/>
|
||||
<AtIcon />
|
||||
<span />
|
||||
</label>
|
||||
<label>
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
defaultMessage="Authentication Method"
|
||||
id="admin.userManagement.userDetail.authenticationMethod"
|
||||
/>
|
||||
<Memo(ShieldOutlineIcon) />
|
||||
<span>
|
||||
|
||||
</span>
|
||||
</label>
|
||||
</React.Fragment>
|
||||
}
|
||||
footer={
|
||||
<React.Fragment>
|
||||
<button
|
||||
className="btn btn-secondary"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
defaultMessage="Reset Password"
|
||||
id="admin.user_item.resetPwd"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-secondary"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
defaultMessage="Activate"
|
||||
id="admin.user_item.makeActive"
|
||||
/>
|
||||
</button>
|
||||
</React.Fragment>
|
||||
}
|
||||
isLoading={true}
|
||||
/>
|
||||
<AdminPanel
|
||||
button={
|
||||
<div
|
||||
className="add-team-button"
|
||||
>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
disabled={true}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
defaultMessage="Add Team"
|
||||
id="admin.userManagement.userDetail.addTeam"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
subtitle={
|
||||
Object {
|
||||
"defaultMessage": "Teams to which this user belongs",
|
||||
"id": "admin.userManagement.userDetail.teamsSubtitle",
|
||||
}
|
||||
}
|
||||
title={
|
||||
Object {
|
||||
"defaultMessage": "Team Membership",
|
||||
"id": "admin.userManagement.userDetail.teamsTitle",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="teamlistLoading"
|
||||
>
|
||||
<Memo(LoadingSpinner) />
|
||||
</div>
|
||||
</AdminPanel>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="admin-console-save"
|
||||
>
|
||||
<SaveButton
|
||||
btnClass=""
|
||||
defaultMessage={
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
defaultMessage="Save"
|
||||
id="save_button.save"
|
||||
/>
|
||||
}
|
||||
disabled={true}
|
||||
extraClasses=""
|
||||
onClick={[Function]}
|
||||
saving={false}
|
||||
savingMessage={
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
defaultMessage="Saving"
|
||||
id="save_button.saving"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<div
|
||||
className="error-message"
|
||||
>
|
||||
<FormError
|
||||
error={null}
|
||||
errors={Array []}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<ConfirmModal
|
||||
confirmButtonClass="btn btn-danger"
|
||||
confirmButtonText={
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
defaultMessage="Deactivate"
|
||||
id="deactivate_member_modal.deactivate"
|
||||
/>
|
||||
}
|
||||
message={
|
||||
<div>
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
defaultMessage="This action deactivates {username}. They will be logged out and not have access to any teams or channels on this system. Are you sure you want to deactivate {username}?"
|
||||
id="deactivate_member_modal.desc"
|
||||
values={
|
||||
Object {
|
||||
"username": "",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<strong>
|
||||
<br />
|
||||
<br />
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
defaultMessage="You must also deactivate this user in the SSO provider or they will be reactivated on next login or sync."
|
||||
id="deactivate_member_modal.sso_warning"
|
||||
/>
|
||||
</strong>
|
||||
</div>
|
||||
}
|
||||
modalClass=""
|
||||
onCancel={[Function]}
|
||||
onConfirm={[Function]}
|
||||
show={false}
|
||||
title={
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
defaultMessage="Deactivate {username}"
|
||||
id="deactivate_member_modal.title"
|
||||
values={
|
||||
Object {
|
||||
"username": "",
|
||||
}
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
@ -6,20 +6,27 @@ import {connect} from 'react-redux';
|
||||
|
||||
import type {GlobalState} from '@mattermost/types/store';
|
||||
|
||||
import {getUserPreferences} from 'mattermost-redux/actions/preferences';
|
||||
import {addUserToTeam} from 'mattermost-redux/actions/teams';
|
||||
import {updateUserActive, getUser, patchUser, updateUserMfa} from 'mattermost-redux/actions/users';
|
||||
import {getConfig} from 'mattermost-redux/selectors/entities/general';
|
||||
|
||||
import {setNavigationBlocked} from 'actions/admin_actions.jsx';
|
||||
import {openModal} from 'actions/views/modals';
|
||||
import {getShowLockedManageUserSettings, getShowManageUserSettings} from 'selectors/admin_console';
|
||||
|
||||
import SystemUserDetail from './system_user_detail';
|
||||
|
||||
function mapStateToProps(state: GlobalState) {
|
||||
const config = getConfig(state);
|
||||
|
||||
const showManageUserSettings = getShowManageUserSettings(state);
|
||||
const showLockedManageUserSettings = getShowLockedManageUserSettings(state);
|
||||
|
||||
return {
|
||||
mfaEnabled: config?.EnableMultifactorAuthentication === 'true' || false,
|
||||
showManageUserSettings,
|
||||
showLockedManageUserSettings,
|
||||
};
|
||||
}
|
||||
|
||||
@ -31,6 +38,7 @@ const mapDispatchToProps = {
|
||||
addUserToTeam,
|
||||
setNavigationBlocked,
|
||||
openModal,
|
||||
getUserPreferences,
|
||||
};
|
||||
const connector = connect(mapStateToProps, mapDispatchToProps);
|
||||
|
||||
|
@ -37,4 +37,11 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.AdminUserCard__footer {
|
||||
.manageUserSettingsBtn {
|
||||
margin-left: auto;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -16,6 +16,8 @@ import {shallowWithIntl, type MockIntl} from 'tests/helpers/intl-test-helper';
|
||||
|
||||
describe('SystemUserDetail', () => {
|
||||
const defaultProps: Props = {
|
||||
showManageUserSettings: false,
|
||||
showLockedManageUserSettings: false,
|
||||
mfaEnabled: false,
|
||||
patchUser: jest.fn(),
|
||||
updateUserMfa: jest.fn(),
|
||||
@ -24,6 +26,7 @@ describe('SystemUserDetail', () => {
|
||||
setNavigationBlocked: jest.fn(),
|
||||
addUserToTeam: jest.fn(),
|
||||
openModal: jest.fn(),
|
||||
getUserPreferences: jest.fn(),
|
||||
intl: {
|
||||
formatMessage: jest.fn(),
|
||||
} as MockIntl,
|
||||
@ -50,6 +53,33 @@ describe('SystemUserDetail', () => {
|
||||
const wrapper = shallowWithIntl(<SystemUserDetail {...props}/>);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should show manage user settings button as activated', () => {
|
||||
const props = {
|
||||
...defaultProps,
|
||||
showManageUserSettings: true,
|
||||
};
|
||||
const wrapper = shallowWithIntl(<SystemUserDetail {...props}/>);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should show manage user settings button as disabled when no license', () => {
|
||||
const props = {
|
||||
...defaultProps,
|
||||
showLockedManageUserSettings: false,
|
||||
};
|
||||
const wrapper = shallowWithIntl(<SystemUserDetail {...props}/>);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should not show manage user settings button when user doesnt have permission', () => {
|
||||
const props = {
|
||||
...defaultProps,
|
||||
showManageUserSettings: false,
|
||||
};
|
||||
const wrapper = shallowWithIntl(<SystemUserDetail {...props}/>);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUserAuthenticationTextField', () => {
|
||||
|
@ -1,6 +1,7 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import classNames from 'classnames';
|
||||
import React, {PureComponent} from 'react';
|
||||
import type {ChangeEvent, MouseEvent} from 'react';
|
||||
import type {IntlShape, WrappedComponentProps} from 'react-intl';
|
||||
@ -18,16 +19,19 @@ import AdminUserCard from 'components/admin_console/admin_user_card/admin_user_c
|
||||
import BlockableLink from 'components/admin_console/blockable_link';
|
||||
import ResetPasswordModal from 'components/admin_console/reset_password_modal';
|
||||
import TeamList from 'components/admin_console/system_user_detail/team_list';
|
||||
import {ConfirmManageUserSettingsModal} from 'components/admin_console/system_users/system_users_list_actions/confirmManageUserSettingsModal';
|
||||
import ConfirmModal from 'components/confirm_modal';
|
||||
import FormError from 'components/form_error';
|
||||
import SaveButton from 'components/save_button';
|
||||
import TeamSelectorModal from 'components/team_selector_modal';
|
||||
import UserSettingsModal from 'components/user_settings/modal';
|
||||
import AdminHeader from 'components/widgets/admin_console/admin_header';
|
||||
import AdminPanel from 'components/widgets/admin_console/admin_panel';
|
||||
import AtIcon from 'components/widgets/icons/at_icon';
|
||||
import EmailIcon from 'components/widgets/icons/email_icon';
|
||||
import SheidOutlineIcon from 'components/widgets/icons/shield_outline_icon';
|
||||
import LoadingSpinner from 'components/widgets/loading/loading_spinner';
|
||||
import WithTooltip from 'components/with_tooltip';
|
||||
|
||||
import {Constants, ModalIdentifiers} from 'utils/constants';
|
||||
import {toTitleCase} from 'utils/utils';
|
||||
@ -284,6 +288,37 @@ export class SystemUserDetail extends PureComponent<Props, State> {
|
||||
this.setState({showTeamSelectorModal: false});
|
||||
};
|
||||
|
||||
openConfirmEditUserSettingsModal = () => {
|
||||
if (!this.state.user) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.props.openModal({
|
||||
modalId: ModalIdentifiers.CONFIRM_MANAGE_USER_SETTINGS_MODAL,
|
||||
dialogType: ConfirmManageUserSettingsModal,
|
||||
dialogProps: {
|
||||
user: this.state.user,
|
||||
onConfirm: this.openUserSettingsModal,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
openUserSettingsModal = async () => {
|
||||
if (!this.state.user) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.props.openModal({
|
||||
modalId: ModalIdentifiers.USER_SETTINGS,
|
||||
dialogType: UserSettingsModal,
|
||||
dialogProps: {
|
||||
adminMode: true,
|
||||
isContentProductSettings: true,
|
||||
userID: this.state.user.id,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className='SystemUserDetail wrapper--fixed'>
|
||||
@ -385,14 +420,61 @@ export class SystemUserDetail extends PureComponent<Props, State> {
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{
|
||||
this.props.showManageUserSettings &&
|
||||
<button
|
||||
className='manageUserSettingsBtn btn btn-tertiary'
|
||||
onClick={this.openConfirmEditUserSettingsModal}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.user_item.manageSettings'
|
||||
defaultMessage='Manage User Settings'
|
||||
/>
|
||||
</button>
|
||||
}
|
||||
|
||||
{
|
||||
this.props.showLockedManageUserSettings &&
|
||||
<WithTooltip
|
||||
id='adminUserSettingUpdateDisabled'
|
||||
title={defineMessage({
|
||||
id: 'generic.enterprise_feature',
|
||||
defaultMessage: 'Enterprise feature',
|
||||
})}
|
||||
hint={defineMessage({
|
||||
id: 'admin.user_item.manageSettings.disabled_tooltip',
|
||||
defaultMessage: 'Please upgrade to Enterprise to manage user settings',
|
||||
})}
|
||||
placement='top'
|
||||
>
|
||||
<button
|
||||
className='manageUserSettingsBtn btn disabled'
|
||||
>
|
||||
<div className='RestrictedIndicator__content'>
|
||||
<i className={classNames('RestrictedIndicator__icon-tooltip', 'icon', 'icon-key-variant')}/>
|
||||
</div>
|
||||
<FormattedMessage
|
||||
id='admin.user_item.manageSettings'
|
||||
defaultMessage='Manage User Settings'
|
||||
/>
|
||||
</button>
|
||||
</WithTooltip>
|
||||
}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* User's team details */}
|
||||
<AdminPanel
|
||||
title={defineMessage({id: 'admin.userManagement.userDetail.teamsTitle', defaultMessage: 'Team Membership'})}
|
||||
subtitle={defineMessage({id: 'admin.userManagement.userDetail.teamsSubtitle', defaultMessage: 'Teams to which this user belongs'})}
|
||||
title={defineMessage({
|
||||
id: 'admin.userManagement.userDetail.teamsTitle',
|
||||
defaultMessage: 'Team Membership',
|
||||
})}
|
||||
subtitle={defineMessage({
|
||||
id: 'admin.userManagement.userDetail.teamsSubtitle',
|
||||
defaultMessage: 'Teams to which this user belongs',
|
||||
})}
|
||||
button={
|
||||
<div className='add-team-button'>
|
||||
<button
|
||||
@ -479,6 +561,7 @@ export class SystemUserDetail extends PureComponent<Props, State> {
|
||||
onConfirm={this.handleDeactivateMember}
|
||||
onCancel={this.toggleCloseModalDeactivateMember}
|
||||
/>
|
||||
|
||||
{this.state.showTeamSelectorModal && (
|
||||
<TeamSelectorModal
|
||||
onModalDismissed={this.toggleCloseTeamSelectorModal}
|
||||
|
@ -0,0 +1,56 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
import type {UserProfile} from '@mattermost/types/users';
|
||||
|
||||
import ConfirmModalRedux from 'components/confirm_modal_redux';
|
||||
|
||||
import {getDisplayName} from 'utils/utils';
|
||||
|
||||
type Props = {
|
||||
user: UserProfile;
|
||||
onConfirm: () => void;
|
||||
onExited: () => void;
|
||||
onHide: () => void;
|
||||
}
|
||||
|
||||
export function ConfirmManageUserSettingsModal(props: Props) {
|
||||
const title = (
|
||||
<FormattedMessage
|
||||
id='userSettings.adminMode.modal_header'
|
||||
defaultMessage="Manage {userDisplayName}'s Settings"
|
||||
values={{userDisplayName: getDisplayName(props.user)}}
|
||||
/>
|
||||
);
|
||||
|
||||
const message = (
|
||||
<FormattedMessage
|
||||
id='admin.user_item.manageSettings.confirm_dialog.body'
|
||||
defaultMessage="You are about to access {userDisplayName}'s account settings. Any modifications you make will take effect immediately in their account. {userDisplayName} retains the ability to view and modify these settings at any time.<br></br><br></br> Are you sure you want to proceed with managing {userDisplayName}'s settings?"
|
||||
values={{
|
||||
userDisplayName: getDisplayName(props.user),
|
||||
br: (x: React.ReactNode) => (<><br/>{x}</>),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
const confirmButtonText = (
|
||||
<FormattedMessage
|
||||
id='admin.user_item.manageSettings'
|
||||
defaultMessage='Manage User Settings'
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<ConfirmModalRedux
|
||||
title={title}
|
||||
message={message}
|
||||
confirmButtonText={confirmButtonText}
|
||||
onConfirm={props.onConfirm}
|
||||
onExited={props.onExited}
|
||||
/>
|
||||
);
|
||||
}
|
@ -2,7 +2,7 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import classNames from 'classnames';
|
||||
import React from 'react';
|
||||
import React, {useCallback} from 'react';
|
||||
import {FormattedMessage, useIntl} from 'react-intl';
|
||||
import {useDispatch, useSelector} from 'react-redux';
|
||||
|
||||
@ -18,14 +18,19 @@ import {isSystemAdmin, isGuest} from 'mattermost-redux/utils/user_utils';
|
||||
|
||||
import {adminResetMfa} from 'actions/admin_actions';
|
||||
import {openModal} from 'actions/views/modals';
|
||||
import {getShowManageUserSettings} from 'selectors/admin_console';
|
||||
|
||||
import ManageRolesModal from 'components/admin_console/manage_roles_modal';
|
||||
import ManageTeamsModal from 'components/admin_console/manage_teams_modal';
|
||||
import ManageTokensModal from 'components/admin_console/manage_tokens_modal';
|
||||
import ResetEmailModal from 'components/admin_console/reset_email_modal';
|
||||
import ResetPasswordModal from 'components/admin_console/reset_password_modal';
|
||||
import {
|
||||
ConfirmManageUserSettingsModal,
|
||||
} from 'components/admin_console/system_users/system_users_list_actions/confirmManageUserSettingsModal';
|
||||
import * as Menu from 'components/menu';
|
||||
import SystemPermissionGate from 'components/permissions_gates/system_permission_gate';
|
||||
import UserSettingsModal from 'components/user_settings/modal';
|
||||
|
||||
import Constants, {ModalIdentifiers} from 'utils/constants';
|
||||
|
||||
@ -49,6 +54,7 @@ export function SystemUsersListAction({user, currentUser, tableId, rowIndex, onE
|
||||
const dispatch = useDispatch();
|
||||
const config = useSelector(getConfig);
|
||||
const isLicensed = useSelector(getLicense)?.IsLicensed === 'true';
|
||||
const showManageUserSettings = useSelector(getShowManageUserSettings);
|
||||
|
||||
function getTranslatedUserRole(userRoles: UserProfile['roles']) {
|
||||
if (user.delete_at > 0) {
|
||||
@ -96,6 +102,18 @@ export function SystemUsersListAction({user, currentUser, tableId, rowIndex, onE
|
||||
const onPromoteToMember = () => updateUser({roles: user.roles.replace(General.SYSTEM_GUEST_ROLE, '')});
|
||||
const onDemoteToGuest = () => updateUser({roles: `${user.roles} ${General.SYSTEM_GUEST_ROLE}`});
|
||||
|
||||
const openUserSettingsModal = useCallback(() => {
|
||||
dispatch(openModal({
|
||||
modalId: ModalIdentifiers.USER_SETTINGS,
|
||||
dialogType: UserSettingsModal,
|
||||
dialogProps: {
|
||||
adminMode: true,
|
||||
isContentProductSettings: true,
|
||||
userID: user.id,
|
||||
},
|
||||
}));
|
||||
}, [dispatch, user.id]);
|
||||
|
||||
return (
|
||||
<Menu.Container
|
||||
menuButton={{
|
||||
@ -209,6 +227,29 @@ export function SystemUsersListAction({user, currentUser, tableId, rowIndex, onE
|
||||
}}
|
||||
/>
|
||||
|
||||
{
|
||||
showManageUserSettings &&
|
||||
<Menu.Item
|
||||
id={`${menuItemIdPrefix}-manageTeams`}
|
||||
labels={
|
||||
<FormattedMessage
|
||||
id='admin.user_item.manageSettings'
|
||||
defaultMessage='Manage User Settings'
|
||||
/>
|
||||
}
|
||||
onClick={() => {
|
||||
dispatch(openModal({
|
||||
modalId: ModalIdentifiers.CONFIRM_MANAGE_USER_SETTINGS_MODAL,
|
||||
dialogType: ConfirmManageUserSettingsModal,
|
||||
dialogProps: {
|
||||
user,
|
||||
onConfirm: openUserSettingsModal,
|
||||
},
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
}
|
||||
|
||||
{config.ServiceSettings?.EnableUserAccessTokens &&
|
||||
<Menu.Item
|
||||
id={`${menuItemIdPrefix}-manageTokens`}
|
||||
|
@ -6,7 +6,7 @@ import React, {useCallback, useState} from 'react';
|
||||
import ConfirmModal from 'components/confirm_modal';
|
||||
|
||||
type Props = Omit<React.ComponentProps<typeof ConfirmModal>, 'show'> & {
|
||||
onExited: () => void;
|
||||
onExited?: () => void;
|
||||
};
|
||||
|
||||
export default function ConfirmModalRedux(props: Props) {
|
||||
|
@ -8,34 +8,48 @@ import type {Dispatch} from 'redux';
|
||||
import {savePreferences} from 'mattermost-redux/actions/preferences';
|
||||
import {updateUserActive, revokeAllSessionsForUser} from 'mattermost-redux/actions/users';
|
||||
import {getConfig} from 'mattermost-redux/selectors/entities/general';
|
||||
import {get, getUnreadScrollPositionPreference, makeGetCategory, syncedDraftsAreAllowed} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {
|
||||
get,
|
||||
getFromPreferences, getUnreadScrollPositionFromPreference,
|
||||
getUnreadScrollPositionPreference,
|
||||
makeGetCategory, makeGetUserCategory,
|
||||
syncedDraftsAreAllowed,
|
||||
} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {getCurrentUser} from 'mattermost-redux/selectors/entities/users';
|
||||
|
||||
import {Preferences} from 'utils/constants';
|
||||
|
||||
import type {GlobalState} from 'types/store';
|
||||
|
||||
import type {OwnProps} from './user_settings_advanced';
|
||||
import AdvancedSettingsDisplay from './user_settings_advanced';
|
||||
|
||||
function makeMapStateToProps() {
|
||||
const getAdvancedSettingsCategory = makeGetCategory();
|
||||
function makeMapStateToProps(state: GlobalState, props: OwnProps) {
|
||||
const getAdvancedSettingsCategory = props.adminMode ? makeGetUserCategory(props.currentUser.id) : makeGetCategory();
|
||||
|
||||
return (state: GlobalState) => {
|
||||
return (state: GlobalState, props: OwnProps) => {
|
||||
const config = getConfig(state);
|
||||
|
||||
const enablePreviewFeatures = config.EnablePreviewFeatures === 'true';
|
||||
const enableUserDeactivation = config.EnableUserDeactivation === 'true';
|
||||
const enableJoinLeaveMessage = config.EnableJoinLeaveMessageByDefault === 'true';
|
||||
|
||||
let getPreference = (prefCategory: string, prefName: string, defaultValue: string) => get(state, prefCategory, prefName, defaultValue);
|
||||
if (props.adminMode && props.userPreferences) {
|
||||
// This ties the function to the current value of userPreferences for the current execution of this function
|
||||
const preferences = props.userPreferences;
|
||||
getPreference = (prefCategory, prefName, defaultValue) => getFromPreferences(preferences, prefCategory, prefName, defaultValue);
|
||||
}
|
||||
|
||||
return {
|
||||
advancedSettingsCategory: getAdvancedSettingsCategory(state, Preferences.CATEGORY_ADVANCED_SETTINGS),
|
||||
sendOnCtrlEnter: get(state, Preferences.CATEGORY_ADVANCED_SETTINGS, 'send_on_ctrl_enter', 'false'),
|
||||
codeBlockOnCtrlEnter: get(state, Preferences.CATEGORY_ADVANCED_SETTINGS, 'code_block_ctrl_enter', 'true'),
|
||||
formatting: get(state, Preferences.CATEGORY_ADVANCED_SETTINGS, 'formatting', 'true'),
|
||||
joinLeave: get(state, Preferences.CATEGORY_ADVANCED_SETTINGS, 'join_leave', enableJoinLeaveMessage.toString()),
|
||||
syncDrafts: get(state, Preferences.CATEGORY_ADVANCED_SETTINGS, 'sync_drafts', 'true'),
|
||||
currentUser: getCurrentUser(state),
|
||||
unreadScrollPosition: getUnreadScrollPositionPreference(state),
|
||||
sendOnCtrlEnter: getPreference(Preferences.CATEGORY_ADVANCED_SETTINGS, 'send_on_ctrl_enter', 'false'),
|
||||
codeBlockOnCtrlEnter: getPreference(Preferences.CATEGORY_ADVANCED_SETTINGS, 'code_block_ctrl_enter', 'true'),
|
||||
formatting: getPreference(Preferences.CATEGORY_ADVANCED_SETTINGS, 'formatting', 'true'),
|
||||
joinLeave: getPreference(Preferences.CATEGORY_ADVANCED_SETTINGS, 'join_leave', enableJoinLeaveMessage.toString()),
|
||||
syncDrafts: getPreference(Preferences.CATEGORY_ADVANCED_SETTINGS, 'sync_drafts', 'true'),
|
||||
currentUser: props.adminMode && props.currentUser ? props.currentUser : getCurrentUser(state),
|
||||
unreadScrollPosition: props.adminMode && props.userPreferences ? getUnreadScrollPositionFromPreference(props.userPreferences) : getUnreadScrollPositionPreference(state),
|
||||
enablePreviewFeatures,
|
||||
enableUserDeactivation,
|
||||
syncedDraftsAreAllowed: syncedDraftsAreAllowed(state),
|
||||
|
@ -8,26 +8,37 @@ import type {Dispatch} from 'redux';
|
||||
import {savePreferences} from 'mattermost-redux/actions/preferences';
|
||||
import {Preferences} from 'mattermost-redux/constants';
|
||||
import {getConfig} from 'mattermost-redux/selectors/entities/general';
|
||||
import {get as getPreference} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {get as getPreference, getFromPreferences} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
|
||||
|
||||
import type {GlobalState} from 'types/store';
|
||||
|
||||
import type {OwnProps} from './join_leave_section';
|
||||
import JoinLeaveSection from './join_leave_section';
|
||||
|
||||
export function mapStateToProps(state: GlobalState) {
|
||||
export function mapStateToProps(state: GlobalState, props: OwnProps) {
|
||||
const config = getConfig(state);
|
||||
const enableJoinLeaveMessage = config.EnableJoinLeaveMessageByDefault === 'true';
|
||||
|
||||
const joinLeave = getPreference(
|
||||
state,
|
||||
Preferences.CATEGORY_ADVANCED_SETTINGS,
|
||||
Preferences.ADVANCED_FILTER_JOIN_LEAVE,
|
||||
enableJoinLeaveMessage.toString(),
|
||||
);
|
||||
let joinLeave: string;
|
||||
if (props.adminMode && props.userPreferences) {
|
||||
joinLeave = getFromPreferences(
|
||||
props.userPreferences,
|
||||
Preferences.CATEGORY_ADVANCED_SETTINGS,
|
||||
Preferences.ADVANCED_FILTER_JOIN_LEAVE,
|
||||
enableJoinLeaveMessage.toString(),
|
||||
);
|
||||
} else {
|
||||
joinLeave = getPreference(
|
||||
state,
|
||||
Preferences.CATEGORY_ADVANCED_SETTINGS,
|
||||
Preferences.ADVANCED_FILTER_JOIN_LEAVE,
|
||||
enableJoinLeaveMessage.toString(),
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
currentUserId: getCurrentUserId(state),
|
||||
currentUserId: props.adminMode ? props.currentUserId : getCurrentUserId(state),
|
||||
joinLeave,
|
||||
};
|
||||
}
|
||||
|
@ -149,7 +149,7 @@ describe('mapStateToProps', () => {
|
||||
} as unknown as GlobalState;
|
||||
|
||||
test('configuration default to true', () => {
|
||||
const props = mapStateToProps(initialState);
|
||||
const props = mapStateToProps(initialState, {adminMode: false, currentUserId: ''});
|
||||
expect(props.joinLeave).toEqual('true');
|
||||
});
|
||||
|
||||
@ -163,7 +163,7 @@ describe('mapStateToProps', () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
const props = mapStateToProps(testState);
|
||||
const props = mapStateToProps(testState, {currentUserId: '', adminMode: false});
|
||||
expect(props.joinLeave).toEqual('false');
|
||||
});
|
||||
|
||||
@ -186,7 +186,7 @@ describe('mapStateToProps', () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
const props = mapStateToProps(testState);
|
||||
const props = mapStateToProps(testState, {adminMode: false, currentUserId: ''});
|
||||
expect(props.joinLeave).toEqual('true');
|
||||
});
|
||||
|
||||
@ -204,7 +204,42 @@ describe('mapStateToProps', () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
const props = mapStateToProps(testState);
|
||||
const props = mapStateToProps(testState, {adminMode: false, currentUserId: ''});
|
||||
expect(props.joinLeave).toEqual('false');
|
||||
});
|
||||
|
||||
test('should read from preferences in props in admin mode', () => {
|
||||
const testState = mergeObjects(initialState, {
|
||||
entities: {
|
||||
general: {
|
||||
config: {
|
||||
EnableJoinLeaveMessageByDefault: 'false',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const userPreferences = {
|
||||
[getPreferenceKey(Preferences.CATEGORY_ADVANCED_SETTINGS, Preferences.ADVANCED_FILTER_JOIN_LEAVE)]: {
|
||||
category: Preferences.CATEGORY_ADVANCED_SETTINGS,
|
||||
name: Preferences.ADVANCED_FILTER_JOIN_LEAVE,
|
||||
user_id: 'user_1',
|
||||
value: 'true',
|
||||
},
|
||||
};
|
||||
|
||||
const propsWithAdminMode = mapStateToProps(testState, {
|
||||
currentUserId: 'user_1',
|
||||
adminMode: true,
|
||||
userPreferences,
|
||||
});
|
||||
expect(propsWithAdminMode.joinLeave).toEqual('true');
|
||||
|
||||
const propsWithoutAdminMode = mapStateToProps(testState, {
|
||||
currentUserId: 'user_1',
|
||||
adminMode: false,
|
||||
userPreferences,
|
||||
});
|
||||
expect(propsWithoutAdminMode.joinLeave).toEqual('false');
|
||||
});
|
||||
});
|
||||
|
@ -5,7 +5,7 @@ import React from 'react';
|
||||
import type {ReactNode, RefObject} from 'react';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
import type {PreferenceType} from '@mattermost/types/preferences';
|
||||
import type {PreferencesType, PreferenceType} from '@mattermost/types/preferences';
|
||||
|
||||
import {Preferences} from 'mattermost-redux/constants';
|
||||
|
||||
@ -16,10 +16,15 @@ import type SettingItemMinComponent from 'components/setting_item_min';
|
||||
import {AdvancedSections} from 'utils/constants';
|
||||
import {a11yFocus} from 'utils/utils';
|
||||
|
||||
type Props = {
|
||||
export type OwnProps = {
|
||||
adminMode?: boolean;
|
||||
currentUserId: string;
|
||||
userPreferences?: PreferencesType;
|
||||
}
|
||||
|
||||
type Props = OwnProps & {
|
||||
active: boolean;
|
||||
areAllSectionsInactive: boolean;
|
||||
currentUserId: string;
|
||||
joinLeave?: string;
|
||||
onUpdateSection: (section?: string) => void;
|
||||
renderOnOffLabel: (label: string) => ReactNode;
|
||||
|
@ -7,20 +7,28 @@ import type {ConnectedProps} from 'react-redux';
|
||||
import {savePreferences} from 'mattermost-redux/actions/preferences';
|
||||
import {Preferences} from 'mattermost-redux/constants';
|
||||
import {isPerformanceDebuggingEnabled} from 'mattermost-redux/selectors/entities/general';
|
||||
import {getBool} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {getBool, getBoolFromPreferences, getUserPreferences} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
|
||||
|
||||
import type {GlobalState} from 'types/store';
|
||||
|
||||
import type {OwnProps} from './performance_debugging_section';
|
||||
import PerformanceDebuggingSection from './performance_debugging_section';
|
||||
|
||||
function mapStateToProps(state: GlobalState) {
|
||||
function mapStateToProps(state: GlobalState, props: OwnProps) {
|
||||
let getPreference = (prefCategory: string, prefName: string) => getBool(state, prefCategory, prefName);
|
||||
if (props.adminMode && props.currentUserId) {
|
||||
const userPreferences = getUserPreferences(state, props.currentUserId);
|
||||
getPreference = (prefCategory: string, prefName: string) => getBoolFromPreferences(userPreferences, prefCategory, prefName);
|
||||
}
|
||||
|
||||
return {
|
||||
currentUserId: getCurrentUserId(state),
|
||||
disableClientPlugins: getBool(state, Preferences.CATEGORY_PERFORMANCE_DEBUGGING, Preferences.NAME_DISABLE_CLIENT_PLUGINS),
|
||||
disableTelemetry: getBool(state, Preferences.CATEGORY_PERFORMANCE_DEBUGGING, Preferences.NAME_DISABLE_TELEMETRY),
|
||||
disableTypingMessages: getBool(state, Preferences.CATEGORY_PERFORMANCE_DEBUGGING, Preferences.NAME_DISABLE_TYPING_MESSAGES),
|
||||
currentUserId: props.adminMode ? props.currentUserId : getCurrentUserId(state),
|
||||
disableClientPlugins: getPreference(Preferences.CATEGORY_PERFORMANCE_DEBUGGING, Preferences.NAME_DISABLE_CLIENT_PLUGINS),
|
||||
disableTelemetry: getPreference(Preferences.CATEGORY_PERFORMANCE_DEBUGGING, Preferences.NAME_DISABLE_TELEMETRY),
|
||||
disableTypingMessages: getPreference(Preferences.CATEGORY_PERFORMANCE_DEBUGGING, Preferences.NAME_DISABLE_TYPING_MESSAGES),
|
||||
performanceDebuggingEnabled: isPerformanceDebuggingEnabled(state),
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -14,7 +14,12 @@ import {AdvancedSections} from 'utils/constants';
|
||||
|
||||
import type {PropsFromRedux} from './index';
|
||||
|
||||
type Props = PropsFromRedux & {
|
||||
export type OwnProps = {
|
||||
adminMode?: boolean;
|
||||
currentUserId?: string;
|
||||
}
|
||||
|
||||
type Props = PropsFromRedux & OwnProps & {
|
||||
active: boolean;
|
||||
areAllSectionsInactive: boolean;
|
||||
onUpdateSection: (section?: string) => void;
|
||||
@ -111,6 +116,10 @@ function PerformanceDebuggingSectionExpanded(props: Props) {
|
||||
const [disableTypingMessages, setDisableTypingMessages] = useState(props.disableTypingMessages);
|
||||
|
||||
const handleSubmit = useCallback(() => {
|
||||
if (!props.currentUserId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const preferences = [];
|
||||
|
||||
if (disableClientPlugins !== props.disableClientPlugins) {
|
||||
@ -138,7 +147,7 @@ function PerformanceDebuggingSectionExpanded(props: Props) {
|
||||
});
|
||||
}
|
||||
|
||||
if (preferences.length !== 0) {
|
||||
if (preferences.length !== 0 && props.currentUserId) {
|
||||
props.savePreferences(props.currentUserId, preferences);
|
||||
}
|
||||
|
||||
|
@ -7,7 +7,7 @@ import React from 'react';
|
||||
import type {ReactNode} from 'react';
|
||||
import {FormattedMessage, defineMessages} from 'react-intl';
|
||||
|
||||
import type {PreferenceType} from '@mattermost/types/preferences';
|
||||
import type {PreferencesType, PreferenceType} from '@mattermost/types/preferences';
|
||||
import type {UserProfile} from '@mattermost/types/users';
|
||||
|
||||
import type {ActionResult} from 'mattermost-redux/types/actions';
|
||||
@ -40,8 +40,13 @@ type Settings = {
|
||||
sync_drafts: Props['syncDrafts'];
|
||||
};
|
||||
|
||||
export type Props = {
|
||||
export type OwnProps = {
|
||||
adminMode?: boolean;
|
||||
currentUser: UserProfile;
|
||||
userPreferences?: PreferencesType;
|
||||
}
|
||||
|
||||
export type Props = OwnProps & {
|
||||
advancedSettingsCategory: PreferenceType[];
|
||||
sendOnCtrlEnter: string;
|
||||
codeBlockOnCtrlEnter: string;
|
||||
@ -161,6 +166,10 @@ export default class AdvancedSettingsDisplay extends React.PureComponent<Props,
|
||||
};
|
||||
|
||||
handleSubmit = async (settings: string[]): Promise<void> => {
|
||||
if (!this.props.currentUser) {
|
||||
return;
|
||||
}
|
||||
|
||||
const preferences: PreferenceType[] = [];
|
||||
const {actions, currentUser} = this.props;
|
||||
const userId = currentUser.id;
|
||||
@ -796,7 +805,7 @@ export default class AdvancedSettingsDisplay extends React.PureComponent<Props,
|
||||
let makeConfirmationModal: ReactNode = '';
|
||||
const currentUser = this.props.currentUser;
|
||||
|
||||
if (currentUser.auth_service === '' && this.props.enableUserDeactivation) {
|
||||
if (currentUser.auth_service === '' && this.props.enableUserDeactivation && !this.props.adminMode) {
|
||||
const active = this.props.activeSection === 'deactivateAccount';
|
||||
let max = null;
|
||||
if (active) {
|
||||
@ -928,6 +937,9 @@ export default class AdvancedSettingsDisplay extends React.PureComponent<Props,
|
||||
areAllSectionsInactive={this.props.activeSection === ''}
|
||||
onUpdateSection={this.handleUpdateSection}
|
||||
renderOnOffLabel={this.renderOnOffLabel}
|
||||
adminMode={this.props.adminMode}
|
||||
userPreferences={this.props.userPreferences}
|
||||
currentUserId={this.props.currentUser.id}
|
||||
/>
|
||||
{previewFeaturesSectionDivider}
|
||||
{previewFeaturesSection}
|
||||
@ -935,6 +947,8 @@ export default class AdvancedSettingsDisplay extends React.PureComponent<Props,
|
||||
active={this.props.activeSection === AdvancedSections.PERFORMANCE_DEBUGGING}
|
||||
onUpdateSection={this.handleUpdateSection}
|
||||
areAllSectionsInactive={this.props.activeSection === ''}
|
||||
adminMode={this.props.adminMode}
|
||||
currentUserId={this.props.currentUser.id}
|
||||
/>
|
||||
{unreadScrollPositionSectionDivider}
|
||||
{unreadScrollPositionSection}
|
||||
|
@ -319,6 +319,7 @@ exports[`components/user_settings/display/UserSettingsDisplay should match snaps
|
||||
describe="English (US)"
|
||||
max={
|
||||
<Memo(Connect(injectIntl(ManageLanguage)))
|
||||
adminMode={false}
|
||||
locale="en"
|
||||
updateSection={[Function]}
|
||||
user={
|
||||
@ -671,6 +672,7 @@ exports[`components/user_settings/display/UserSettingsDisplay should match snaps
|
||||
describe="English (US)"
|
||||
max={
|
||||
<Memo(Connect(injectIntl(ManageLanguage)))
|
||||
adminMode={false}
|
||||
locale="en"
|
||||
updateSection={[Function]}
|
||||
user={
|
||||
@ -1023,6 +1025,7 @@ exports[`components/user_settings/display/UserSettingsDisplay should match snaps
|
||||
describe="English (US)"
|
||||
max={
|
||||
<Memo(Connect(injectIntl(ManageLanguage)))
|
||||
adminMode={false}
|
||||
locale="en"
|
||||
updateSection={[Function]}
|
||||
user={
|
||||
@ -1375,6 +1378,7 @@ exports[`components/user_settings/display/UserSettingsDisplay should match snaps
|
||||
describe="English (US)"
|
||||
max={
|
||||
<Memo(Connect(injectIntl(ManageLanguage)))
|
||||
adminMode={false}
|
||||
locale="en"
|
||||
updateSection={[Function]}
|
||||
user={
|
||||
@ -1654,6 +1658,7 @@ exports[`components/user_settings/display/UserSettingsDisplay should match snaps
|
||||
describe="English (US)"
|
||||
max={
|
||||
<Memo(Connect(injectIntl(ManageLanguage)))
|
||||
adminMode={false}
|
||||
locale="en"
|
||||
updateSection={[Function]}
|
||||
user={
|
||||
@ -1915,6 +1920,7 @@ exports[`components/user_settings/display/UserSettingsDisplay should match snaps
|
||||
describe="English (US)"
|
||||
max={
|
||||
<Memo(Connect(injectIntl(ManageLanguage)))
|
||||
adminMode={false}
|
||||
locale="en"
|
||||
updateSection={[Function]}
|
||||
user={
|
||||
@ -2267,6 +2273,7 @@ exports[`components/user_settings/display/UserSettingsDisplay should match snaps
|
||||
describe="English (US)"
|
||||
max={
|
||||
<Memo(Connect(injectIntl(ManageLanguage)))
|
||||
adminMode={false}
|
||||
locale="en"
|
||||
updateSection={[Function]}
|
||||
user={
|
||||
@ -2637,6 +2644,7 @@ exports[`components/user_settings/display/UserSettingsDisplay should match snaps
|
||||
describe="English (US)"
|
||||
max={
|
||||
<Memo(Connect(injectIntl(ManageLanguage)))
|
||||
adminMode={false}
|
||||
locale="en"
|
||||
updateSection={[Function]}
|
||||
user={
|
||||
@ -2916,6 +2924,7 @@ exports[`components/user_settings/display/UserSettingsDisplay should match snaps
|
||||
describe="English (US)"
|
||||
max={
|
||||
<Memo(Connect(injectIntl(ManageLanguage)))
|
||||
adminMode={false}
|
||||
locale="en"
|
||||
updateSection={[Function]}
|
||||
user={
|
||||
@ -3195,6 +3204,7 @@ exports[`components/user_settings/display/UserSettingsDisplay should match snaps
|
||||
describe="English (US)"
|
||||
max={
|
||||
<Memo(Connect(injectIntl(ManageLanguage)))
|
||||
adminMode={false}
|
||||
locale="en"
|
||||
updateSection={[Function]}
|
||||
user={
|
||||
@ -3474,6 +3484,7 @@ exports[`components/user_settings/display/UserSettingsDisplay should match snaps
|
||||
describe="English (US)"
|
||||
max={
|
||||
<Memo(Connect(injectIntl(ManageLanguage)))
|
||||
adminMode={false}
|
||||
locale="en"
|
||||
updateSection={[Function]}
|
||||
user={
|
||||
@ -3766,6 +3777,7 @@ exports[`components/user_settings/display/UserSettingsDisplay should match snaps
|
||||
describe="English (US)"
|
||||
max={
|
||||
<Memo(Connect(injectIntl(ManageLanguage)))
|
||||
adminMode={false}
|
||||
locale="en"
|
||||
updateSection={[Function]}
|
||||
user={
|
||||
@ -4045,6 +4057,7 @@ exports[`components/user_settings/display/UserSettingsDisplay should match snaps
|
||||
describe="English (US)"
|
||||
max={
|
||||
<Memo(Connect(injectIntl(ManageLanguage)))
|
||||
adminMode={false}
|
||||
locale="en"
|
||||
updateSection={[Function]}
|
||||
user={
|
||||
@ -4300,6 +4313,7 @@ exports[`components/user_settings/display/UserSettingsDisplay should not show la
|
||||
describe="English (US)"
|
||||
max={
|
||||
<Memo(Connect(injectIntl(ManageLanguage)))
|
||||
adminMode={false}
|
||||
locale="en"
|
||||
updateSection={[Function]}
|
||||
user={
|
||||
|
@ -7,14 +7,23 @@ import type {Dispatch} from 'redux';
|
||||
import timezones from 'timezones.json';
|
||||
|
||||
import {CollapsedThreads} from '@mattermost/types/config';
|
||||
import type {UserProfile} from '@mattermost/types/users';
|
||||
|
||||
import {savePreferences} from 'mattermost-redux/actions/preferences';
|
||||
import {autoUpdateTimezone} from 'mattermost-redux/actions/timezone';
|
||||
import {updateMe} from 'mattermost-redux/actions/users';
|
||||
import {patchUser, updateMe} from 'mattermost-redux/actions/users';
|
||||
import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general';
|
||||
import {get, isCollapsedThreadsAllowed, getCollapsedThreadsPreference} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {getCurrentTimezoneFull, getCurrentTimezoneLabel} from 'mattermost-redux/selectors/entities/timezone';
|
||||
import {
|
||||
get,
|
||||
isCollapsedThreadsAllowed,
|
||||
getCollapsedThreadsPreference,
|
||||
getFromPreferences,
|
||||
} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {
|
||||
generateCurrentTimezoneLabel,
|
||||
getCurrentTimezoneFull,
|
||||
getCurrentTimezoneLabel,
|
||||
getTimezoneForUserProfile,
|
||||
} from 'mattermost-redux/selectors/entities/timezone';
|
||||
import {getCurrentUserId, getUser} from 'mattermost-redux/selectors/entities/users';
|
||||
import {getUserCurrentTimezone} from 'mattermost-redux/utils/timezone_utils';
|
||||
|
||||
@ -23,20 +32,17 @@ import {Preferences} from 'utils/constants';
|
||||
|
||||
import type {GlobalState} from 'types/store';
|
||||
|
||||
import type {OwnProps} from './user_settings_display';
|
||||
import UserSettingsDisplay from './user_settings_display';
|
||||
|
||||
type OwnProps = {
|
||||
user: UserProfile;
|
||||
}
|
||||
|
||||
export function makeMapStateToProps() {
|
||||
return (state: GlobalState, props: OwnProps) => {
|
||||
const config = getConfig(state);
|
||||
const currentUserId = getCurrentUserId(state);
|
||||
const userTimezone = getCurrentTimezoneFull(state);
|
||||
const userTimezone = props.adminMode ? getTimezoneForUserProfile(props.user) : getCurrentTimezoneFull(state);
|
||||
const automaticTimezoneNotSet = userTimezone && userTimezone.useAutomaticTimezone && !userTimezone.automaticTimezone;
|
||||
const shouldAutoUpdateTimezone = !userTimezone || automaticTimezoneNotSet;
|
||||
const timezoneLabel = getCurrentTimezoneLabel(state);
|
||||
const timezoneLabel = props.adminMode ? generateCurrentTimezoneLabel(getUserCurrentTimezone(userTimezone)) : getCurrentTimezoneLabel(state);
|
||||
const allowCustomThemes = config.AllowCustomThemes === 'true';
|
||||
const enableLinkPreviews = config.EnableLinkPreviews === 'true';
|
||||
const enableThemeSelection = config.EnableThemeSelection === 'true';
|
||||
@ -46,7 +52,8 @@ export function makeMapStateToProps() {
|
||||
const lastActiveTimeEnabled = config.EnableLastActiveTime === 'true';
|
||||
|
||||
let lastActiveDisplay = true;
|
||||
if (getUser(state, currentUserId).props?.show_last_active === 'false') {
|
||||
const user = props.adminMode ? props.user : getUser(state, currentUserId);
|
||||
if (user.props?.show_last_active === 'false') {
|
||||
lastActiveDisplay = false;
|
||||
}
|
||||
|
||||
@ -55,6 +62,12 @@ export function makeMapStateToProps() {
|
||||
userLocale = config.DefaultClientLocale as string;
|
||||
}
|
||||
|
||||
let getPreference = (prefCategory: string, prefName: string, defaultValue: string) => get(state, prefCategory, prefName, defaultValue);
|
||||
if (props.adminMode && props.userPreferences) {
|
||||
const preferences = props.userPreferences;
|
||||
getPreference = (prefCategory: string, prefName: string, defaultValue: string) => getFromPreferences(preferences, prefCategory, prefName, defaultValue);
|
||||
}
|
||||
|
||||
return {
|
||||
lockTeammateNameDisplay,
|
||||
allowCustomThemes,
|
||||
@ -68,18 +81,18 @@ export function makeMapStateToProps() {
|
||||
userTimezone,
|
||||
shouldAutoUpdateTimezone,
|
||||
currentUserTimezone: getUserCurrentTimezone(userTimezone) as string,
|
||||
availabilityStatusOnPosts: get(state, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.AVAILABILITY_STATUS_ON_POSTS, Preferences.AVAILABILITY_STATUS_ON_POSTS_DEFAULT),
|
||||
militaryTime: get(state, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.USE_MILITARY_TIME, Preferences.USE_MILITARY_TIME_DEFAULT),
|
||||
teammateNameDisplay: get(state, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.NAME_NAME_FORMAT, configTeammateNameDisplay),
|
||||
channelDisplayMode: get(state, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.CHANNEL_DISPLAY_MODE, Preferences.CHANNEL_DISPLAY_MODE_DEFAULT),
|
||||
messageDisplay: get(state, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.MESSAGE_DISPLAY, Preferences.MESSAGE_DISPLAY_DEFAULT),
|
||||
colorizeUsernames: get(state, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.COLORIZE_USERNAMES, Preferences.COLORIZE_USERNAMES_DEFAULT),
|
||||
collapseDisplay: get(state, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.COLLAPSE_DISPLAY, Preferences.COLLAPSE_DISPLAY_DEFAULT),
|
||||
availabilityStatusOnPosts: getPreference(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.AVAILABILITY_STATUS_ON_POSTS, Preferences.AVAILABILITY_STATUS_ON_POSTS_DEFAULT),
|
||||
militaryTime: getPreference(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.USE_MILITARY_TIME, Preferences.USE_MILITARY_TIME_DEFAULT),
|
||||
teammateNameDisplay: getPreference(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.NAME_NAME_FORMAT, configTeammateNameDisplay),
|
||||
channelDisplayMode: getPreference(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.CHANNEL_DISPLAY_MODE, Preferences.CHANNEL_DISPLAY_MODE_DEFAULT),
|
||||
messageDisplay: getPreference(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.MESSAGE_DISPLAY, Preferences.MESSAGE_DISPLAY_DEFAULT),
|
||||
colorizeUsernames: getPreference(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.COLORIZE_USERNAMES, Preferences.COLORIZE_USERNAMES_DEFAULT),
|
||||
collapseDisplay: getPreference(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.COLLAPSE_DISPLAY, Preferences.COLLAPSE_DISPLAY_DEFAULT),
|
||||
collapsedReplyThreadsAllowUserPreference: isCollapsedThreadsAllowed(state) && getConfig(state).CollapsedThreads !== CollapsedThreads.ALWAYS_ON,
|
||||
collapsedReplyThreads: getCollapsedThreadsPreference(state),
|
||||
clickToReply: get(state, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.CLICK_TO_REPLY, Preferences.CLICK_TO_REPLY_DEFAULT),
|
||||
linkPreviewDisplay: get(state, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.LINK_PREVIEW_DISPLAY, Preferences.LINK_PREVIEW_DISPLAY_DEFAULT),
|
||||
oneClickReactionsOnPosts: get(state, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.ONE_CLICK_REACTIONS_ENABLED, Preferences.ONE_CLICK_REACTIONS_ENABLED_DEFAULT),
|
||||
clickToReply: getPreference(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.CLICK_TO_REPLY, Preferences.CLICK_TO_REPLY_DEFAULT),
|
||||
linkPreviewDisplay: getPreference(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.LINK_PREVIEW_DISPLAY, Preferences.LINK_PREVIEW_DISPLAY_DEFAULT),
|
||||
oneClickReactionsOnPosts: getPreference(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.ONE_CLICK_REACTIONS_ENABLED, Preferences.ONE_CLICK_REACTIONS_ENABLED_DEFAULT),
|
||||
emojiPickerEnabled,
|
||||
lastActiveDisplay,
|
||||
lastActiveTimeEnabled,
|
||||
@ -93,6 +106,7 @@ function mapDispatchToProps(dispatch: Dispatch) {
|
||||
autoUpdateTimezone,
|
||||
savePreferences,
|
||||
updateMe,
|
||||
patchUser,
|
||||
}, dispatch),
|
||||
};
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ import {connect} from 'react-redux';
|
||||
import {bindActionCreators} from 'redux';
|
||||
import type {Dispatch} from 'redux';
|
||||
|
||||
import {updateMe} from 'mattermost-redux/actions/users';
|
||||
import {patchUser, updateMe} from 'mattermost-redux/actions/users';
|
||||
|
||||
import {getLanguages} from 'i18n/i18n';
|
||||
|
||||
@ -23,6 +23,7 @@ function mapDispatchToProps(dispatch: Dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators({
|
||||
updateMe,
|
||||
patchUser,
|
||||
}, dispatch),
|
||||
};
|
||||
}
|
||||
|
@ -23,6 +23,7 @@ describe('components/user_settings/display/manage_languages/manage_languages', (
|
||||
updateSection: jest.fn(),
|
||||
actions: {
|
||||
updateMe: jest.fn(() => Promise.resolve({})),
|
||||
patchUser: jest.fn(() => Promise.resolve({})),
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -20,6 +20,7 @@ import {isKeyPressed} from 'utils/keyboard';
|
||||
|
||||
type Actions = {
|
||||
updateMe: (user: UserProfile) => Promise<ActionResult>;
|
||||
patchUser: (user: UserProfile) => Promise<ActionResult>;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
@ -29,6 +30,7 @@ type Props = {
|
||||
locales: Record<string, Language>;
|
||||
updateSection: (section: string) => void;
|
||||
actions: Actions;
|
||||
adminMode?: boolean;
|
||||
};
|
||||
|
||||
type SelectedOption = {
|
||||
@ -122,9 +124,10 @@ export class ManageLanguage extends React.PureComponent<Props, State> {
|
||||
submitUser = (user: UserProfile) => {
|
||||
this.setState({isSaving: true});
|
||||
|
||||
this.props.actions.updateMe(user).then((res) => {
|
||||
const action = this.props.adminMode ? this.props.actions.patchUser : this.props.actions.updateMe;
|
||||
action(user).then((res) => {
|
||||
if ('data' in res) {
|
||||
// Do nothing since changing the locale essentially refreshes the page
|
||||
this.setState({isSaving: false});
|
||||
} else if ('error' in res) {
|
||||
let serverError;
|
||||
const {error} = res;
|
||||
|
@ -8,7 +8,7 @@ import timezones from 'timezones.json';
|
||||
|
||||
import type {GlobalState} from '@mattermost/types/store';
|
||||
|
||||
import {updateMe} from 'mattermost-redux/actions/users';
|
||||
import {patchUser, updateMe} from 'mattermost-redux/actions/users';
|
||||
import {getCurrentTimezoneLabel} from 'mattermost-redux/selectors/entities/timezone';
|
||||
|
||||
import ManageTimezones from './manage_timezones';
|
||||
@ -17,6 +17,7 @@ function mapDispatchToProps(dispatch: Dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators({
|
||||
updateMe,
|
||||
patchUser,
|
||||
}, dispatch),
|
||||
};
|
||||
}
|
||||
|
@ -24,6 +24,7 @@ describe('components/user_settings/display/manage_timezones/manage_timezones', (
|
||||
updateSection: jest.fn(),
|
||||
actions: {
|
||||
updateMe: jest.fn(() => Promise.resolve({})),
|
||||
patchUser: jest.fn(() => Promise.resolve({})),
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -18,6 +18,7 @@ import {getBrowserTimezone} from 'utils/timezone';
|
||||
|
||||
type Actions = {
|
||||
updateMe: (user: UserProfile) => Promise<ActionResult>;
|
||||
patchUser: (user: UserProfile) => Promise<ActionResult>;
|
||||
}
|
||||
|
||||
type Props = {
|
||||
@ -29,6 +30,7 @@ type Props = {
|
||||
timezones: Timezone[];
|
||||
timezoneLabel: string;
|
||||
actions: Actions;
|
||||
adminMode?: boolean;
|
||||
}
|
||||
type SelectedOption = {
|
||||
value: string;
|
||||
@ -97,7 +99,7 @@ export default class ManageTimezones extends React.PureComponent<Props, State> {
|
||||
};
|
||||
|
||||
submitUser = () => {
|
||||
const {user, actions} = this.props;
|
||||
const {user} = this.props;
|
||||
const {useAutomaticTimezone, automaticTimezone, manualTimezone} = this.state;
|
||||
|
||||
const timezone = {
|
||||
@ -111,7 +113,8 @@ export default class ManageTimezones extends React.PureComponent<Props, State> {
|
||||
timezone,
|
||||
};
|
||||
|
||||
actions.updateMe(updatedUser).
|
||||
const action = this.props.adminMode ? this.props.actions.patchUser : this.props.actions.updateMe;
|
||||
action(updatedUser).
|
||||
then((res) => {
|
||||
if ('data' in res) {
|
||||
this.props.updateSection('');
|
||||
|
@ -27,6 +27,7 @@ describe('components/user_settings/display/UserSettingsDisplay', () => {
|
||||
};
|
||||
|
||||
const requiredProps = {
|
||||
adminMode: false,
|
||||
user: user as UserProfile,
|
||||
updateSection: jest.fn(),
|
||||
activeSection: '',
|
||||
@ -72,6 +73,7 @@ describe('components/user_settings/display/UserSettingsDisplay', () => {
|
||||
autoUpdateTimezone: jest.fn(),
|
||||
savePreferences: jest.fn(),
|
||||
updateMe: jest.fn(),
|
||||
patchUser: jest.fn(),
|
||||
},
|
||||
|
||||
configTeammateNameDisplay: '',
|
||||
|
@ -9,7 +9,7 @@ import type {MessageDescriptor} from 'react-intl';
|
||||
import {FormattedMessage, defineMessage} from 'react-intl';
|
||||
import type {Timezone} from 'timezones.json';
|
||||
|
||||
import type {PreferenceType} from '@mattermost/types/preferences';
|
||||
import type {PreferencesType, PreferenceType} from '@mattermost/types/preferences';
|
||||
import type {UserProfile, UserTimezone} from '@mattermost/types/users';
|
||||
|
||||
import type {ActionResult} from 'mattermost-redux/types/actions';
|
||||
@ -20,8 +20,8 @@ import SettingItem from 'components/setting_item';
|
||||
import SettingItemMax from 'components/setting_item_max';
|
||||
import ThemeSetting from 'components/user_settings/display/user_settings_theme';
|
||||
|
||||
import type {Language} from 'i18n/i18n';
|
||||
import {getLanguageInfo} from 'i18n/i18n';
|
||||
import type {Language} from 'i18n/i18n';
|
||||
import Constants from 'utils/constants';
|
||||
import {getBrowserTimezone} from 'utils/timezone';
|
||||
import {a11yFocus} from 'utils/utils';
|
||||
@ -81,7 +81,13 @@ type SectionProps ={
|
||||
onSubmit?: () => void;
|
||||
}
|
||||
|
||||
type Props = {
|
||||
export type OwnProps = {
|
||||
user: UserProfile;
|
||||
adminMode?: boolean;
|
||||
userPreferences?: PreferencesType;
|
||||
}
|
||||
|
||||
type Props = OwnProps & {
|
||||
user: UserProfile;
|
||||
updateSection: (section: string) => void;
|
||||
activeSection?: string;
|
||||
@ -120,6 +126,7 @@ type Props = {
|
||||
savePreferences: (userId: string, preferences: PreferenceType[]) => void;
|
||||
autoUpdateTimezone: (deviceTimezone: string) => void;
|
||||
updateMe: (user: UserProfile) => Promise<ActionResult>;
|
||||
patchUser: (user: UserProfile) => Promise<ActionResult>;
|
||||
};
|
||||
}
|
||||
|
||||
@ -208,7 +215,8 @@ export default class UserSettingsDisplay extends React.PureComponent<Props, Stat
|
||||
},
|
||||
};
|
||||
|
||||
actions.updateMe(updatedUser).
|
||||
const action = this.props.adminMode ? actions.patchUser : actions.updateMe;
|
||||
action(updatedUser).
|
||||
then((res) => {
|
||||
if ('data' in res) {
|
||||
this.props.updateSection('');
|
||||
@ -873,6 +881,7 @@ export default class UserSettingsDisplay extends React.PureComponent<Props, Stat
|
||||
automaticTimezone={userTimezone.automaticTimezone}
|
||||
manualTimezone={userTimezone.manualTimezone}
|
||||
updateSection={this.updateSection}
|
||||
adminMode={this.props.adminMode}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -1076,6 +1085,7 @@ export default class UserSettingsDisplay extends React.PureComponent<Props, Stat
|
||||
user={this.props.user}
|
||||
locale={userLocale}
|
||||
updateSection={this.updateSection}
|
||||
adminMode={this.props.adminMode}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
@ -1088,7 +1098,7 @@ export default class UserSettingsDisplay extends React.PureComponent<Props, Stat
|
||||
}
|
||||
|
||||
let themeSection;
|
||||
if (this.props.enableThemeSelection) {
|
||||
if (this.props.enableThemeSelection && !this.props.adminMode) {
|
||||
themeSection = (
|
||||
<div>
|
||||
<ThemeSetting
|
||||
|
@ -4,6 +4,14 @@
|
||||
justify-content: space-between;
|
||||
|
||||
.userSettingDesktopHeaderInfo {
|
||||
margin-top: -20px;
|
||||
|
||||
span {
|
||||
.btn {
|
||||
height: unset;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import type {PreferencesType} from '@mattermost/types/preferences';
|
||||
import type {UserProfile} from '@mattermost/types/users';
|
||||
|
||||
import type {PluginConfiguration} from 'types/plugins/user_settings';
|
||||
@ -26,6 +27,8 @@ export type Props = {
|
||||
setEnforceFocus: () => void;
|
||||
setRequireConfirm: () => void;
|
||||
pluginSettings: {[tabName: string]: PluginConfiguration};
|
||||
userPreferences?: PreferencesType;
|
||||
adminMode?: boolean;
|
||||
};
|
||||
|
||||
export default function UserSettings(props: Props) {
|
||||
@ -64,6 +67,8 @@ export default function UserSettings(props: Props) {
|
||||
updateSection={props.updateSection}
|
||||
closeModal={props.closeModal}
|
||||
collapseModal={props.collapseModal}
|
||||
adminMode={props.adminMode}
|
||||
userPreferences={props.userPreferences}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@ -78,6 +83,8 @@ export default function UserSettings(props: Props) {
|
||||
collapseModal={props.collapseModal}
|
||||
setEnforceFocus={props.setEnforceFocus}
|
||||
setRequireConfirm={props.setRequireConfirm}
|
||||
adminMode={props.adminMode}
|
||||
userPreferences={props.userPreferences}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@ -89,6 +96,9 @@ export default function UserSettings(props: Props) {
|
||||
updateSection={props.updateSection}
|
||||
closeModal={props.closeModal}
|
||||
collapseModal={props.collapseModal}
|
||||
adminMode={props.adminMode}
|
||||
currentUserId={props.user.id}
|
||||
userPreferences={props.userPreferences}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@ -100,6 +110,9 @@ export default function UserSettings(props: Props) {
|
||||
updateSection={props.updateSection}
|
||||
closeModal={props.closeModal}
|
||||
collapseModal={props.collapseModal}
|
||||
adminMode={props.adminMode}
|
||||
currentUser={props.user}
|
||||
userPreferences={props.userPreferences}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@ -6,9 +6,11 @@ import {connect} from 'react-redux';
|
||||
import {bindActionCreators} from 'redux';
|
||||
import type {Dispatch} from 'redux';
|
||||
|
||||
import {sendVerificationEmail} from 'mattermost-redux/actions/users';
|
||||
import {getUserPreferences} from 'mattermost-redux/actions/preferences';
|
||||
import {getUser, sendVerificationEmail} from 'mattermost-redux/actions/users';
|
||||
import {getConfig} from 'mattermost-redux/selectors/entities/general';
|
||||
import {getCurrentUser} from 'mattermost-redux/selectors/entities/users';
|
||||
import {getUserPreferences as getUserPreferencesSelector} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {getCurrentUser, getUser as getUserSelector} from 'mattermost-redux/selectors/entities/users';
|
||||
|
||||
import {getPluginUserSettings} from 'selectors/plugins';
|
||||
|
||||
@ -18,14 +20,19 @@ import type {GlobalState} from 'types/store';
|
||||
|
||||
const UserSettingsModalAsync = makeAsyncComponent('UserSettingsModal', lazy(() => import('./user_settings_modal')));
|
||||
|
||||
function mapStateToProps(state: GlobalState) {
|
||||
import type {OwnProps} from './user_settings_modal';
|
||||
|
||||
function mapStateToProps(state: GlobalState, ownProps: OwnProps) {
|
||||
const config = getConfig(state);
|
||||
|
||||
const sendEmailNotifications = config.SendEmailNotifications === 'true';
|
||||
const requireEmailVerification = config.RequireEmailVerification === 'true';
|
||||
|
||||
const currentUser = ownProps.adminMode && ownProps.userID ? getUserSelector(state, ownProps.userID) : getCurrentUser(state);
|
||||
|
||||
return {
|
||||
currentUser: getCurrentUser(state),
|
||||
currentUser,
|
||||
userPreferences: ownProps.adminMode && ownProps.userID ? getUserPreferencesSelector(state, ownProps.userID) : undefined,
|
||||
sendEmailNotifications,
|
||||
requireEmailVerification,
|
||||
pluginSettings: getPluginUserSettings(state),
|
||||
@ -36,6 +43,8 @@ function mapDispatchToProps(dispatch: Dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators({
|
||||
sendVerificationEmail,
|
||||
getUserPreferences,
|
||||
getUser,
|
||||
}, dispatch),
|
||||
};
|
||||
}
|
||||
|
@ -4,9 +4,10 @@
|
||||
import React from 'react';
|
||||
import {Modal} from 'react-bootstrap';
|
||||
import ReactDOM from 'react-dom';
|
||||
import {injectIntl} from 'react-intl';
|
||||
import {FormattedMessage, injectIntl} from 'react-intl';
|
||||
import type {IntlShape} from 'react-intl';
|
||||
|
||||
import type {PreferencesType} from '@mattermost/types/preferences';
|
||||
import type {UserProfile} from '@mattermost/types/users';
|
||||
|
||||
import type {ActionResult} from 'mattermost-redux/types/actions';
|
||||
@ -14,20 +15,31 @@ import type {ActionResult} from 'mattermost-redux/types/actions';
|
||||
import ConfirmModal from 'components/confirm_modal';
|
||||
import SettingsSidebar from 'components/settings_sidebar';
|
||||
import UserSettings from 'components/user_settings';
|
||||
import LoadingSpinner from 'components/widgets/loading/loading_spinner';
|
||||
import SmartLoader from 'components/widgets/smartLoader';
|
||||
|
||||
import Constants from 'utils/constants';
|
||||
import {cmdOrCtrlPressed, isKeyPressed} from 'utils/keyboard';
|
||||
import {stopTryNotificationRing} from 'utils/notification_sounds';
|
||||
import {getDisplayName} from 'utils/utils';
|
||||
|
||||
import type {PluginConfiguration} from 'types/plugins/user_settings';
|
||||
|
||||
export type Props = {
|
||||
currentUser: UserProfile;
|
||||
export type OwnProps = {
|
||||
userID?: string;
|
||||
adminMode?: boolean;
|
||||
currentUser?: UserProfile;
|
||||
isContentProductSettings: boolean;
|
||||
userPreferences?: PreferencesType;
|
||||
}
|
||||
|
||||
export type Props = OwnProps & {
|
||||
onExited: () => void;
|
||||
intl: IntlShape;
|
||||
isContentProductSettings: boolean;
|
||||
actions: {
|
||||
sendVerificationEmail: (email: string) => Promise<ActionResult>;
|
||||
getUserPreferences: (userID: string) => Promise<unknown>;
|
||||
getUser: (userID: string) => Promise<unknown>;
|
||||
};
|
||||
pluginSettings: {[pluginId: string]: PluginConfiguration};
|
||||
}
|
||||
@ -39,6 +51,7 @@ type State = {
|
||||
enforceFocus?: boolean;
|
||||
show: boolean;
|
||||
resendStatus: string;
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
class UserSettingsModal extends React.PureComponent<Props, State> {
|
||||
@ -57,6 +70,7 @@ class UserSettingsModal extends React.PureComponent<Props, State> {
|
||||
enforceFocus: true,
|
||||
show: true,
|
||||
resendStatus: '',
|
||||
loading: false,
|
||||
};
|
||||
|
||||
this.requireConfirm = false;
|
||||
@ -84,6 +98,22 @@ class UserSettingsModal extends React.PureComponent<Props, State> {
|
||||
|
||||
componentDidMount() {
|
||||
document.addEventListener('keydown', this.handleKeyDown);
|
||||
|
||||
if (this.props.adminMode && this.props.userID) {
|
||||
this.setState({loading: true});
|
||||
|
||||
if (!this.props.userPreferences) {
|
||||
this.props.actions.getUserPreferences(this.props.userID);
|
||||
}
|
||||
|
||||
if (!this.props.currentUser) {
|
||||
this.props.actions.getUser(this.props.userID);
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.props.adminMode) {
|
||||
this.setState({loading: false});
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
@ -97,6 +127,10 @@ class UserSettingsModal extends React.PureComponent<Props, State> {
|
||||
}
|
||||
}
|
||||
|
||||
setLoadingFinished = () => {
|
||||
this.setState({loading: false});
|
||||
};
|
||||
|
||||
handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (cmdOrCtrlPressed(e) && e.shiftKey && isKeyPressed(e, Constants.KeyCodes.A)) {
|
||||
e.preventDefault();
|
||||
@ -275,17 +309,25 @@ class UserSettingsModal extends React.PureComponent<Props, State> {
|
||||
|
||||
render() {
|
||||
const {formatMessage} = this.props.intl;
|
||||
if (this.props.currentUser == null) {
|
||||
return (<div/>);
|
||||
}
|
||||
|
||||
const modalTitle = this.props.isContentProductSettings ? formatMessage({
|
||||
id: 'global_header.productSettings',
|
||||
defaultMessage: 'Settings',
|
||||
}) : formatMessage({
|
||||
id: 'user.settings.modal.title',
|
||||
defaultMessage: 'Profile',
|
||||
});
|
||||
let modalTitle: string;
|
||||
|
||||
if (this.props.adminMode && this.props.currentUser) {
|
||||
modalTitle = formatMessage({
|
||||
id: 'userSettings.adminMode.modal_header',
|
||||
defaultMessage: "{userDisplayName}'s Settings",
|
||||
}, {
|
||||
userDisplayName: getDisplayName(this.props.currentUser),
|
||||
});
|
||||
} else {
|
||||
modalTitle = this.props.isContentProductSettings ? formatMessage({
|
||||
id: 'global_header.productSettings',
|
||||
defaultMessage: 'Settings',
|
||||
}) : formatMessage({
|
||||
id: 'user.settings.modal.title',
|
||||
defaultMessage: 'Profile',
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
@ -308,37 +350,63 @@ class UserSettingsModal extends React.PureComponent<Props, State> {
|
||||
>
|
||||
{modalTitle}
|
||||
</Modal.Title>
|
||||
|
||||
{
|
||||
this.props.adminMode &&
|
||||
<div className='adminModeBadge'>
|
||||
<FormattedMessage
|
||||
id='userSettings.adminMode.admin_mode_badge'
|
||||
defaultMessage='Admin Mode'
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</Modal.Header>
|
||||
<Modal.Body ref={this.modalBodyRef}>
|
||||
<div className='settings-table'>
|
||||
<div className='settings-links'>
|
||||
<SettingsSidebar
|
||||
tabs={this.props.isContentProductSettings ? this.getUserSettingsTabs() : this.getProfileSettingsTab()}
|
||||
pluginTabs={this.props.isContentProductSettings ? this.getPluginsSettingsTab() : []}
|
||||
activeTab={this.state.active_tab}
|
||||
updateTab={this.updateTab}
|
||||
/>
|
||||
</div>
|
||||
<div className='settings-content minimize-settings'>
|
||||
<UserSettings
|
||||
activeTab={this.state.active_tab}
|
||||
activeSection={this.state.active_section}
|
||||
updateSection={this.updateSection}
|
||||
updateTab={this.updateTab}
|
||||
closeModal={this.closeModal}
|
||||
collapseModal={this.collapseModal}
|
||||
setEnforceFocus={(enforceFocus?: boolean) => this.setState({enforceFocus})}
|
||||
setRequireConfirm={
|
||||
(requireConfirm?: boolean, customConfirmAction?: () => () => void) => {
|
||||
this.requireConfirm = requireConfirm!;
|
||||
this.customConfirmAction = customConfirmAction!;
|
||||
{
|
||||
this.props.adminMode &&
|
||||
<SmartLoader
|
||||
loading={this.props.adminMode && (!this.props.userPreferences || !this.props.currentUser)}
|
||||
className='loadingIndicator'
|
||||
onLoaded={this.setLoadingFinished}
|
||||
>
|
||||
<LoadingSpinner/>
|
||||
</SmartLoader>
|
||||
}
|
||||
|
||||
{
|
||||
!this.state.loading && this.props.currentUser &&
|
||||
<div className='settings-table'>
|
||||
<div className='settings-links'>
|
||||
<SettingsSidebar
|
||||
tabs={this.props.isContentProductSettings ? this.getUserSettingsTabs() : this.getProfileSettingsTab()}
|
||||
pluginTabs={this.props.isContentProductSettings ? this.getPluginsSettingsTab() : []}
|
||||
activeTab={this.state.active_tab}
|
||||
updateTab={this.updateTab}
|
||||
/>
|
||||
</div>
|
||||
<div className='settings-content minimize-settings'>
|
||||
<UserSettings
|
||||
activeTab={this.state.active_tab}
|
||||
activeSection={this.state.active_section}
|
||||
updateSection={this.updateSection}
|
||||
updateTab={this.updateTab}
|
||||
closeModal={this.closeModal}
|
||||
collapseModal={this.collapseModal}
|
||||
setEnforceFocus={(enforceFocus?: boolean) => this.setState({enforceFocus})}
|
||||
setRequireConfirm={
|
||||
(requireConfirm?: boolean, customConfirmAction?: () => () => void) => {
|
||||
this.requireConfirm = requireConfirm!;
|
||||
this.customConfirmAction = customConfirmAction!;
|
||||
}
|
||||
}
|
||||
}
|
||||
pluginSettings={this.props.pluginSettings}
|
||||
user={this.props.currentUser}
|
||||
/>
|
||||
pluginSettings={this.props.pluginSettings}
|
||||
user={this.props.currentUser}
|
||||
adminMode={this.props.adminMode}
|
||||
userPreferences={this.props.userPreferences}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</Modal.Body>
|
||||
<ConfirmModal
|
||||
title={formatMessage({id: 'user.settings.modal.confirmTitle', defaultMessage: 'Discard Changes?'})}
|
||||
@ -346,7 +414,10 @@ class UserSettingsModal extends React.PureComponent<Props, State> {
|
||||
id: 'user.settings.modal.confirmMsg',
|
||||
defaultMessage: 'You have unsaved changes, are you sure you want to discard them?',
|
||||
})}
|
||||
confirmButtonText={formatMessage({id: 'user.settings.modal.confirmBtns', defaultMessage: 'Yes, Discard'})}
|
||||
confirmButtonText={formatMessage({
|
||||
id: 'user.settings.modal.confirmBtns',
|
||||
defaultMessage: 'Yes, Discard',
|
||||
})}
|
||||
show={this.state.showConfirmModal}
|
||||
onConfirm={this.handleConfirm}
|
||||
onCancel={this.handleCancelConfirmation}
|
||||
|
@ -3,10 +3,13 @@
|
||||
|
||||
import {connect, type ConnectedProps} from 'react-redux';
|
||||
|
||||
import {updateMe} from 'mattermost-redux/actions/users';
|
||||
import {patchUser, updateMe} from 'mattermost-redux/actions/users';
|
||||
import {getSubscriptionProduct} from 'mattermost-redux/selectors/entities/cloud';
|
||||
import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general';
|
||||
import {isCollapsedThreadsEnabled} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {
|
||||
isCollapsedThreadsEnabled,
|
||||
isCollapsedThreadsEnabledForUser,
|
||||
} from 'mattermost-redux/selectors/entities/preferences';
|
||||
|
||||
import {isCallsEnabled, isCallsRingingEnabledOnServer} from 'selectors/calls';
|
||||
|
||||
@ -14,9 +17,11 @@ import {isEnterpriseOrCloudOrSKUStarterFree} from 'utils/license_utils';
|
||||
|
||||
import type {GlobalState} from 'types/store';
|
||||
|
||||
import type {OwnProps} from './user_settings_notifications';
|
||||
import UserSettingsNotifications from './user_settings_notifications';
|
||||
|
||||
const mapStateToProps = (state: GlobalState) => {
|
||||
const mapStateToProps = (state: GlobalState, props: OwnProps) => {
|
||||
// server config, related to server configuration, not the user
|
||||
const config = getConfig(state);
|
||||
|
||||
const sendPushNotifications = config.SendPushNotifications === 'true';
|
||||
@ -30,16 +35,16 @@ const mapStateToProps = (state: GlobalState) => {
|
||||
return {
|
||||
sendPushNotifications,
|
||||
enableAutoResponder,
|
||||
isCollapsedThreadsEnabled: isCollapsedThreadsEnabled(state),
|
||||
isCollapsedThreadsEnabled: props.adminMode && props.userPreferences ? isCollapsedThreadsEnabledForUser(state, props.userPreferences) : isCollapsedThreadsEnabled(state),
|
||||
isCallsRingingEnabled: isCallsEnabled(state, '0.17.0') && isCallsRingingEnabledOnServer(state),
|
||||
isEnterpriseOrCloudOrSKUStarterFree: isEnterpriseOrCloudOrSKUStarterFree(license, subscriptionProduct, isEnterpriseReady),
|
||||
isEnterpriseReady,
|
||||
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = {
|
||||
updateMe,
|
||||
patchUser,
|
||||
};
|
||||
|
||||
const connector = connect(mapStateToProps, mapDispatchToProps);
|
||||
|
@ -18,6 +18,7 @@ describe('components/user_settings/display/UserSettingsDisplay', () => {
|
||||
closeModal: jest.fn(),
|
||||
collapseModal: jest.fn(),
|
||||
updateMe: jest.fn(() => Promise.resolve({})),
|
||||
patchUser: jest.fn(() => Promise.resolve({})),
|
||||
isCollapsedThreadsEnabled: true,
|
||||
sendPushNotifications: false,
|
||||
enableAutoResponder: false,
|
||||
|
@ -11,6 +11,7 @@ import type {Styles as ReactSelectStyles, ValueType} from 'react-select';
|
||||
import CreatableReactSelect from 'react-select/creatable';
|
||||
|
||||
import {LightbulbOutlineIcon} from '@mattermost/compass-icons/components';
|
||||
import type {PreferencesType} from '@mattermost/types/preferences';
|
||||
import type {UserNotifyProps, UserProfile} from '@mattermost/types/users';
|
||||
|
||||
import ExternalLink from 'components/external_link';
|
||||
@ -40,12 +41,14 @@ type MultiInputValue = {
|
||||
value: string;
|
||||
}
|
||||
|
||||
type OwnProps = {
|
||||
export type OwnProps = {
|
||||
user: UserProfile;
|
||||
updateSection: (section: string) => void;
|
||||
activeSection: string;
|
||||
closeModal: () => void;
|
||||
collapseModal: () => void;
|
||||
adminMode?: boolean;
|
||||
userPreferences?: PreferencesType;
|
||||
}
|
||||
|
||||
export type Props = PropsFromRedux & OwnProps & WrappedComponentProps;
|
||||
@ -284,7 +287,20 @@ class NotificationsTab extends React.PureComponent<Props, State> {
|
||||
this.setState({isSaving: true});
|
||||
stopTryNotificationRing();
|
||||
|
||||
const {data: updatedUser, error} = await this.props.updateMe({notify_props: data});
|
||||
let updatedUser: UserProfile | undefined;
|
||||
let error;
|
||||
|
||||
if (this.props.adminMode) {
|
||||
const payloadUser = {...this.props.user, notify_props: data};
|
||||
const response = await this.props.patchUser(payloadUser);
|
||||
updatedUser = response.data;
|
||||
error = response.error;
|
||||
} else {
|
||||
const response = await this.props.updateMe({notify_props: data});
|
||||
updatedUser = response.data;
|
||||
error = response.error;
|
||||
}
|
||||
|
||||
if (updatedUser) {
|
||||
this.handleUpdateSection('');
|
||||
this.setState(getDefaultStateFromProps(this.props));
|
||||
|
@ -4,17 +4,18 @@
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {savePreferences} from 'mattermost-redux/actions/preferences';
|
||||
import {getVisibleDmGmLimit} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {getUserVisibleDmGmLimit, getVisibleDmGmLimit} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
|
||||
|
||||
import type {GlobalState} from 'types/store';
|
||||
|
||||
import type {OwnProps} from './limit_visible_gms_dms';
|
||||
import LimitVisibleGMsDMs from './limit_visible_gms_dms';
|
||||
|
||||
function mapStateToProps(state: GlobalState) {
|
||||
function mapStateToProps(state: GlobalState, ownProps: OwnProps) {
|
||||
return {
|
||||
currentUserId: getCurrentUserId(state),
|
||||
dmGmLimit: getVisibleDmGmLimit(state),
|
||||
currentUserId: ownProps.adminMode ? ownProps.currentUserId : getCurrentUserId(state),
|
||||
dmGmLimit: ownProps.adminMode && ownProps.userPreferences ? getUserVisibleDmGmLimit(ownProps.userPreferences) : getVisibleDmGmLimit(state),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -7,7 +7,7 @@ import {FormattedMessage} from 'react-intl';
|
||||
import ReactSelect from 'react-select';
|
||||
import type {ValueType} from 'react-select';
|
||||
|
||||
import type {PreferenceType} from '@mattermost/types/preferences';
|
||||
import type {PreferencesType, PreferenceType} from '@mattermost/types/preferences';
|
||||
|
||||
import {Preferences} from 'mattermost-redux/constants';
|
||||
import type {ActionResult} from 'mattermost-redux/types/actions';
|
||||
@ -23,10 +23,15 @@ type Limit = {
|
||||
label: string;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
export type OwnProps = {
|
||||
adminMode?: boolean;
|
||||
currentUserId?: string;
|
||||
userPreferences?: PreferencesType;
|
||||
}
|
||||
|
||||
type Props = OwnProps & {
|
||||
active: boolean;
|
||||
areAllSectionsInactive: boolean;
|
||||
currentUserId: string;
|
||||
savePreferences: (userId: string, preferences: PreferenceType[]) => Promise<ActionResult>;
|
||||
dmGmLimit: number;
|
||||
updateSection: (section: string) => void;
|
||||
@ -99,6 +104,10 @@ export default class LimitVisibleGMsDMs extends React.PureComponent<Props, State
|
||||
};
|
||||
|
||||
handleSubmit = async () => {
|
||||
if (!this.props.currentUserId) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({isSaving: true});
|
||||
|
||||
await this.props.savePreferences(this.props.currentUserId, [{
|
||||
|
@ -4,17 +4,23 @@
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {savePreferences} from 'mattermost-redux/actions/preferences';
|
||||
import {shouldShowUnreadsCategory} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {getConfig} from 'mattermost-redux/selectors/entities/general';
|
||||
import {
|
||||
calculateUserShouldShowUnreadsCategory,
|
||||
shouldShowUnreadsCategory,
|
||||
} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
|
||||
|
||||
import type {GlobalState} from 'types/store';
|
||||
|
||||
import type {OwnProps} from './show_unreads_category';
|
||||
import ShowUnreadsCategory from './show_unreads_category';
|
||||
|
||||
function mapStateToProps(state: GlobalState) {
|
||||
function mapStateToProps(state: GlobalState, props: OwnProps) {
|
||||
const serverDefault = getConfig(state).ExperimentalGroupUnreadChannels;
|
||||
return {
|
||||
currentUserId: getCurrentUserId(state),
|
||||
showUnreadsCategory: shouldShowUnreadsCategory(state),
|
||||
currentUserId: props.adminMode ? props.currentUserId : getCurrentUserId(state),
|
||||
showUnreadsCategory: props.adminMode && props.userPreferences ? calculateUserShouldShowUnreadsCategory(props.userPreferences, serverDefault) : shouldShowUnreadsCategory(state),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -5,7 +5,7 @@ import React from 'react';
|
||||
import type {RefObject} from 'react';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
import type {PreferenceType} from '@mattermost/types/preferences';
|
||||
import type {PreferencesType, PreferenceType} from '@mattermost/types/preferences';
|
||||
|
||||
import {Preferences} from 'mattermost-redux/constants';
|
||||
import type {ActionResult} from 'mattermost-redux/types/actions';
|
||||
@ -16,10 +16,15 @@ import type SettingItemMinComponent from 'components/setting_item_min';
|
||||
|
||||
import {a11yFocus} from 'utils/utils';
|
||||
|
||||
type Props = {
|
||||
export type OwnProps = {
|
||||
adminMode?: boolean;
|
||||
currentUserId?: string;
|
||||
userPreferences?: PreferencesType;
|
||||
}
|
||||
|
||||
type Props = OwnProps & {
|
||||
active: boolean;
|
||||
areAllSectionsInactive: boolean;
|
||||
currentUserId: string;
|
||||
savePreferences: (userId: string, preferences: PreferenceType[]) => Promise<ActionResult>;
|
||||
showUnreadsCategory: boolean;
|
||||
updateSection: (section: string) => void;
|
||||
@ -75,6 +80,11 @@ export default class ShowUnreadsCategory extends React.PureComponent<Props, Stat
|
||||
};
|
||||
|
||||
handleSubmit = async () => {
|
||||
if (!this.props.currentUserId) {
|
||||
// Only for type safety, won't actually happen
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({isSaving: true});
|
||||
|
||||
await this.props.savePreferences(this.props.currentUserId, [{
|
||||
|
@ -4,6 +4,8 @@
|
||||
import React from 'react';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
import type {PreferencesType} from '@mattermost/types/preferences';
|
||||
|
||||
import LimitVisibleGMsDMs from './limit_visible_gms_dms';
|
||||
import ShowUnreadsCategory from './show_unreads_category';
|
||||
|
||||
@ -15,6 +17,9 @@ export interface Props {
|
||||
activeSection: string;
|
||||
closeModal: () => void;
|
||||
collapseModal: () => void;
|
||||
adminMode?: boolean;
|
||||
currentUserId?: string;
|
||||
userPreferences?: PreferencesType;
|
||||
}
|
||||
|
||||
export default function UserSettingsSidebar(props: Props): JSX.Element {
|
||||
@ -48,12 +53,18 @@ export default function UserSettingsSidebar(props: Props): JSX.Element {
|
||||
active={props.activeSection === 'showUnreadsCategory'}
|
||||
updateSection={props.updateSection}
|
||||
areAllSectionsInactive={props.activeSection === ''}
|
||||
adminMode={props.adminMode}
|
||||
currentUserId={props.currentUserId}
|
||||
userPreferences={props.userPreferences}
|
||||
/>
|
||||
<div className='divider-dark'/>
|
||||
<LimitVisibleGMsDMs
|
||||
active={props.activeSection === 'limitVisibleGMsDMs'}
|
||||
updateSection={props.updateSection}
|
||||
areAllSectionsInactive={props.activeSection === ''}
|
||||
adminMode={props.adminMode}
|
||||
currentUserId={props.currentUserId}
|
||||
userPreferences={props.userPreferences}
|
||||
/>
|
||||
<div className='divider-dark'/>
|
||||
</div>
|
||||
|
37
webapp/channels/src/components/widgets/smartLoader/index.tsx
Normal file
37
webapp/channels/src/components/widgets/smartLoader/index.tsx
Normal file
@ -0,0 +1,37 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {type ReactNode, useEffect, useState} from 'react';
|
||||
|
||||
const DEFAULT_MIN_LOADER_DURATION = 1500;
|
||||
|
||||
type Props = {
|
||||
loading: boolean;
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
onLoaded: () => void;
|
||||
}
|
||||
|
||||
const SmartLoader = ({loading, children, className, onLoaded}: Props) => {
|
||||
const [timeoutFinished, setTimeoutFinished] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
setTimeoutFinished(true);
|
||||
}, DEFAULT_MIN_LOADER_DURATION);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!loading && timeoutFinished) {
|
||||
onLoaded();
|
||||
}
|
||||
}, [loading, timeoutFinished, onLoaded]);
|
||||
|
||||
return loading || !timeoutFinished ? (
|
||||
<div className={`SmartLoader ${className}`}>
|
||||
{children}
|
||||
</div>
|
||||
) : null;
|
||||
};
|
||||
|
||||
export default SmartLoader;
|
@ -2702,6 +2702,9 @@
|
||||
"admin.user_item.makeActive": "Activate",
|
||||
"admin.user_item.makeMember": "Make Team Member",
|
||||
"admin.user_item.makeTeamAdmin": "Make Team Admin",
|
||||
"admin.user_item.manageSettings": "Manage User Settings",
|
||||
"admin.user_item.manageSettings.confirm_dialog.body": "You are about to access {userDisplayName}'s account settings. Any modifications you make will take effect immediately in their account. {userDisplayName} retains the ability to view and modify these settings at any time.<br></br><br></br> Are you sure you want to proceed with managing {userDisplayName}'s settings?",
|
||||
"admin.user_item.manageSettings.disabled_tooltip": "Please upgrade to Enterprise to manage user settings",
|
||||
"admin.user_item.manageTeams": "Manage Teams",
|
||||
"admin.user_item.member": "Member",
|
||||
"admin.user_item.menuAriaLabel": "User Actions Menu",
|
||||
@ -3768,6 +3771,7 @@
|
||||
"generic_modal.confirm": "Confirm",
|
||||
"generic.close": "Close",
|
||||
"generic.done": "Done",
|
||||
"generic.enterprise_feature": "Enterprise Feature",
|
||||
"generic.next": "Next",
|
||||
"generic.okay": "Okay",
|
||||
"generic.previous": "Previous",
|
||||
@ -5713,6 +5717,8 @@
|
||||
"userGuideHelp.trainingResources": "Training resources",
|
||||
"users_limits_announcement_bar.copyText": "User limits exceeded. Contact administrator with: {ErrorCode}",
|
||||
"users_limits_announcement_bar.ctaText": "Learn More",
|
||||
"userSettings.adminMode.admin_mode_badge": "Admin Mode",
|
||||
"userSettings.adminMode.modal_header": "Manage {userDisplayName}'s Settings",
|
||||
"userSettingsModal.pluginPreferences.header": "PLUGIN PREFERENCES",
|
||||
"version_bar.new": "A new version of Mattermost is available.",
|
||||
"version_bar.refresh": "Refresh the app now",
|
||||
|
@ -7,4 +7,6 @@ export default keyMirror({
|
||||
RECEIVED_PREFERENCES: null,
|
||||
RECEIVED_ALL_PREFERENCES: null,
|
||||
DELETED_PREFERENCES: null,
|
||||
RECEIVED_USER_PREFERENCES: null,
|
||||
RECEIVED_USER_ALL_PREFERENCES: null,
|
||||
});
|
||||
|
@ -48,6 +48,14 @@ export function getMyPreferences() {
|
||||
});
|
||||
}
|
||||
|
||||
// used for fetching some other user's preferences other than current user
|
||||
export function getUserPreferences(userID: string) {
|
||||
return bindClientFunc({
|
||||
clientFunc: () => Client4.getUserPreferences(userID),
|
||||
onSuccess: PreferenceTypes.RECEIVED_USER_ALL_PREFERENCES,
|
||||
});
|
||||
}
|
||||
|
||||
export function setActionsMenuInitialisationState(initializationState: Record<string, boolean>): ThunkActionFunc<void> {
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
@ -77,11 +85,15 @@ export function setCustomStatusInitialisationState(initializationState: Record<s
|
||||
}
|
||||
|
||||
export function savePreferences(userId: string, preferences: PreferenceType[]): ActionFuncAsync {
|
||||
return async (dispatch) => {
|
||||
return async (dispatch, getState) => {
|
||||
(async function savePreferencesWrapper() {
|
||||
const state = getState();
|
||||
const currentUserId = getCurrentUserId(state);
|
||||
const actionType = userId === currentUserId ? PreferenceTypes.RECEIVED_PREFERENCES : PreferenceTypes.RECEIVED_USER_PREFERENCES;
|
||||
|
||||
try {
|
||||
dispatch({
|
||||
type: PreferenceTypes.RECEIVED_PREFERENCES,
|
||||
type: actionType,
|
||||
data: preferences,
|
||||
});
|
||||
|
||||
|
@ -4,7 +4,7 @@
|
||||
import type {AnyAction} from 'redux';
|
||||
import {combineReducers} from 'redux';
|
||||
|
||||
import type {PreferenceType} from '@mattermost/types/preferences';
|
||||
import type {PreferencesType, PreferenceType} from '@mattermost/types/preferences';
|
||||
|
||||
import {PreferenceTypes, UserTypes} from 'mattermost-redux/action_types';
|
||||
|
||||
@ -24,6 +24,24 @@ function setAllPreferences(preferences: PreferenceType[]): any {
|
||||
return nextState;
|
||||
}
|
||||
|
||||
function setAllUserPreferences(preferences: PreferenceType[]): {[key: string]: PreferencesType} {
|
||||
const nextState: {[key: string]: PreferencesType} = {};
|
||||
if (preferences.length === 0) {
|
||||
return nextState;
|
||||
}
|
||||
|
||||
const userID = preferences[0].user_id;
|
||||
nextState[userID] = {};
|
||||
|
||||
if (preferences) {
|
||||
for (const preference of preferences) {
|
||||
nextState[userID][getKey(preference)] = preference;
|
||||
}
|
||||
}
|
||||
|
||||
return nextState;
|
||||
}
|
||||
|
||||
function myPreferences(state: Record<string, PreferenceType> = {}, action: AnyAction) {
|
||||
switch (action.type) {
|
||||
case PreferenceTypes.RECEIVED_ALL_PREFERENCES:
|
||||
@ -62,8 +80,37 @@ function myPreferences(state: Record<string, PreferenceType> = {}, action: AnyAc
|
||||
}
|
||||
}
|
||||
|
||||
function userPreferences(state: Record<string, PreferencesType> = {}, action: AnyAction) {
|
||||
switch (action.type) {
|
||||
case PreferenceTypes.RECEIVED_USER_ALL_PREFERENCES:
|
||||
return setAllUserPreferences(action.data);
|
||||
|
||||
case PreferenceTypes.RECEIVED_USER_PREFERENCES: {
|
||||
const nextState = {...state};
|
||||
|
||||
const data = action.data as PreferenceType[];
|
||||
if (action.data && data.length > 0) {
|
||||
const userID = data[0].user_id;
|
||||
nextState[userID] = nextState[userID] ? {...nextState[userID]} : {};
|
||||
|
||||
for (const preference of action.data) {
|
||||
nextState[preference.user_id][getKey(preference)] = preference;
|
||||
}
|
||||
}
|
||||
|
||||
return nextState;
|
||||
}
|
||||
|
||||
case UserTypes.LOGOUT_SUCCESS:
|
||||
return {};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
export default combineReducers({
|
||||
|
||||
// object where the key is the category-name and has the corresponding value
|
||||
myPreferences,
|
||||
userPreferences,
|
||||
});
|
||||
|
@ -2,7 +2,7 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {CollapsedThreads} from '@mattermost/types/config';
|
||||
import type {PreferenceType} from '@mattermost/types/preferences';
|
||||
import type {PreferencesType, PreferenceType} from '@mattermost/types/preferences';
|
||||
import type {GlobalState} from '@mattermost/types/store';
|
||||
|
||||
import {General, Preferences} from 'mattermost-redux/constants';
|
||||
@ -16,6 +16,10 @@ export function getMyPreferences(state: GlobalState): { [x: string]: PreferenceT
|
||||
return state.entities.preferences.myPreferences;
|
||||
}
|
||||
|
||||
export function getUserPreferences(state: GlobalState, userID: string): { [x: string]: PreferenceType } {
|
||||
return state.entities.preferences.userPreferences[userID];
|
||||
}
|
||||
|
||||
export function get(state: GlobalState, category: string, name: string, defaultValue: any = '') {
|
||||
const key = getPreferenceKey(category, name);
|
||||
const prefs = getMyPreferences(state);
|
||||
@ -27,11 +31,26 @@ export function get(state: GlobalState, category: string, name: string, defaultV
|
||||
return prefs[key].value;
|
||||
}
|
||||
|
||||
export function getFromPreferences(preferences: PreferencesType, category: string, name: string, defaultValue: any = '') {
|
||||
const key = getPreferenceKey(category, name);
|
||||
|
||||
if (!(key in preferences)) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
return preferences[key].value;
|
||||
}
|
||||
|
||||
export function getBool(state: GlobalState, category: string, name: string, defaultValue = false): boolean {
|
||||
const value = get(state, category, name, String(defaultValue));
|
||||
return value !== 'false';
|
||||
}
|
||||
|
||||
export function getBoolFromPreferences(userPreferences: PreferencesType, category: string, name: string, defaultValue = false): boolean {
|
||||
const value = getFromPreferences(userPreferences, category, name, String(defaultValue));
|
||||
return value !== 'false';
|
||||
}
|
||||
|
||||
export function getInt(state: GlobalState, category: string, name: string, defaultValue = 0): number {
|
||||
const value = get(state, category, name, defaultValue);
|
||||
return parseInt(value, 10);
|
||||
@ -57,6 +76,26 @@ export function makeGetCategory(): (state: GlobalState, category: string) => Pre
|
||||
);
|
||||
}
|
||||
|
||||
export function makeGetUserCategory(userID: string): (state: GlobalState, category: string) => PreferenceType[] {
|
||||
return createSelector(
|
||||
'makeGetCategory',
|
||||
(state) => getUserPreferences(state, userID),
|
||||
(state: GlobalState, category: string) => category,
|
||||
(preferences, category) => {
|
||||
const prefix = category + '--';
|
||||
const prefsInCategory: PreferenceType[] = [];
|
||||
|
||||
for (const key in preferences) {
|
||||
if (key.startsWith(prefix)) {
|
||||
prefsInCategory.push(preferences[key]);
|
||||
}
|
||||
}
|
||||
|
||||
return prefsInCategory;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const getDirectShowCategory = makeGetCategory();
|
||||
|
||||
export function getDirectShowPreferences(state: GlobalState) {
|
||||
@ -180,31 +219,44 @@ export function makeGetStyleFromTheme<Style>(): (state: GlobalState, getStyleFro
|
||||
);
|
||||
}
|
||||
|
||||
export function calculateUserShouldShowUnreadsCategory(userPreferences: PreferencesType, serverDefault?: string): boolean {
|
||||
const userPreference = getFromPreferences(userPreferences, Preferences.CATEGORY_SIDEBAR_SETTINGS, Preferences.SHOW_UNREAD_SECTION);
|
||||
const oldUserPreference = getFromPreferences(userPreferences, Preferences.CATEGORY_SIDEBAR_SETTINGS, '');
|
||||
|
||||
return calculateShouldShowUnreadsCategory(userPreference, oldUserPreference, serverDefault);
|
||||
}
|
||||
|
||||
export function calculateShouldShowUnreadsCategory(userPreference: string, oldUserPreference: string, serverDefault?: string): boolean {
|
||||
// Prefer the show_unread_section user preference over the previous version
|
||||
if (userPreference) {
|
||||
return userPreference === 'true';
|
||||
}
|
||||
|
||||
if (oldUserPreference) {
|
||||
return JSON.parse(oldUserPreference).unreads_at_top === 'true';
|
||||
}
|
||||
|
||||
// The user setting is not set, so use the system default
|
||||
return serverDefault === General.DEFAULT_ON;
|
||||
}
|
||||
|
||||
// shouldShowUnreadsCategory returns true if the user has unereads grouped separately with the new sidebar enabled.
|
||||
export const shouldShowUnreadsCategory: (state: GlobalState) => boolean = createSelector(
|
||||
'shouldShowUnreadsCategory',
|
||||
(state: GlobalState) => get(state, Preferences.CATEGORY_SIDEBAR_SETTINGS, Preferences.SHOW_UNREAD_SECTION),
|
||||
(state: GlobalState) => get(state, Preferences.CATEGORY_SIDEBAR_SETTINGS, ''),
|
||||
(state: GlobalState) => getConfig(state).ExperimentalGroupUnreadChannels,
|
||||
(userPreference, oldUserPreference, serverDefault) => {
|
||||
// Prefer the show_unread_section user preference over the previous version
|
||||
if (userPreference) {
|
||||
return userPreference === 'true';
|
||||
}
|
||||
|
||||
if (oldUserPreference) {
|
||||
return JSON.parse(oldUserPreference).unreads_at_top === 'true';
|
||||
}
|
||||
|
||||
// The user setting is not set, so use the system default
|
||||
return serverDefault === General.DEFAULT_ON;
|
||||
},
|
||||
calculateShouldShowUnreadsCategory,
|
||||
);
|
||||
|
||||
export function getUnreadScrollPositionPreference(state: GlobalState): string {
|
||||
return get(state, Preferences.CATEGORY_ADVANCED_SETTINGS, Preferences.UNREAD_SCROLL_POSITION, Preferences.UNREAD_SCROLL_POSITION_START_FROM_LEFT);
|
||||
}
|
||||
|
||||
export function getUnreadScrollPositionFromPreference(userPreferences: PreferencesType): string {
|
||||
return getFromPreferences(userPreferences, Preferences.CATEGORY_ADVANCED_SETTINGS, Preferences.UNREAD_SCROLL_POSITION, Preferences.UNREAD_SCROLL_POSITION_START_FROM_LEFT);
|
||||
}
|
||||
|
||||
export function getCollapsedThreadsPreference(state: GlobalState): string {
|
||||
const configValue = getConfig(state)?.CollapsedThreads;
|
||||
let preferenceDefault = Preferences.COLLAPSED_REPLY_THREADS_OFF;
|
||||
@ -221,6 +273,22 @@ export function getCollapsedThreadsPreference(state: GlobalState): string {
|
||||
);
|
||||
}
|
||||
|
||||
export function getCollapsedThreadsPreferenceFromPreferences(state: GlobalState, userPreferences: PreferencesType): string {
|
||||
const configValue = getConfig(state)?.CollapsedThreads;
|
||||
let preferenceDefault = Preferences.COLLAPSED_REPLY_THREADS_OFF;
|
||||
|
||||
if (configValue === CollapsedThreads.DEFAULT_ON || configValue === CollapsedThreads.ALWAYS_ON) {
|
||||
preferenceDefault = Preferences.COLLAPSED_REPLY_THREADS_ON;
|
||||
}
|
||||
|
||||
return getFromPreferences(
|
||||
userPreferences,
|
||||
Preferences.CATEGORY_DISPLAY_SETTINGS,
|
||||
Preferences.COLLAPSED_REPLY_THREADS,
|
||||
preferenceDefault,
|
||||
);
|
||||
}
|
||||
|
||||
export function isCollapsedThreadsAllowed(state: GlobalState): boolean {
|
||||
return Boolean(getConfig(state)) && getConfig(state).CollapsedThreads !== undefined && getConfig(state).CollapsedThreads !== CollapsedThreads.DISABLED;
|
||||
}
|
||||
@ -232,6 +300,13 @@ export function isCollapsedThreadsEnabled(state: GlobalState): boolean {
|
||||
return isAllowed && (userPreference === Preferences.COLLAPSED_REPLY_THREADS_ON || getConfig(state).CollapsedThreads === CollapsedThreads.ALWAYS_ON);
|
||||
}
|
||||
|
||||
export function isCollapsedThreadsEnabledForUser(state: GlobalState, userPreferences: PreferencesType): boolean {
|
||||
const isAllowed = isCollapsedThreadsAllowed(state);
|
||||
const userPreference = getCollapsedThreadsPreferenceFromPreferences(state, userPreferences);
|
||||
|
||||
return isAllowed && (userPreference === Preferences.COLLAPSED_REPLY_THREADS_ON || getConfig(state).CollapsedThreads === CollapsedThreads.ALWAYS_ON);
|
||||
}
|
||||
|
||||
export function isGroupChannelManuallyVisible(state: GlobalState, channelId: string): boolean {
|
||||
return getBool(state, Preferences.CATEGORY_GROUP_CHANNEL_SHOW, channelId, false);
|
||||
}
|
||||
@ -264,6 +339,12 @@ export function getVisibleDmGmLimit(state: GlobalState) {
|
||||
return getInt(state, Preferences.CATEGORY_SIDEBAR_SETTINGS, Preferences.LIMIT_VISIBLE_DMS_GMS, defaultLimit);
|
||||
}
|
||||
|
||||
export function getUserVisibleDmGmLimit(userPreferences: PreferencesType) {
|
||||
const defaultLimit = 40;
|
||||
const value = getFromPreferences(userPreferences, Preferences.CATEGORY_SIDEBAR_SETTINGS, Preferences.LIMIT_VISIBLE_DMS_GMS, defaultLimit);
|
||||
return parseInt(value, 10);
|
||||
}
|
||||
|
||||
export function onboardingTourTipsEnabled(state: GlobalState): boolean {
|
||||
return getFeatureFlagValue(state, 'OnboardingTourTips') === 'true';
|
||||
}
|
||||
|
@ -10,7 +10,7 @@ import {getTimezoneLabel, getUserCurrentTimezone} from 'mattermost-redux/utils/t
|
||||
|
||||
import {getCurrentUser} from './common';
|
||||
|
||||
function getTimezoneForUserProfile(profile: UserProfile) {
|
||||
export function getTimezoneForUserProfile(profile: UserProfile) {
|
||||
if (profile && profile.timezone) {
|
||||
return {
|
||||
...profile.timezone,
|
||||
@ -41,14 +41,16 @@ export const getCurrentTimezone = createSelector(
|
||||
},
|
||||
);
|
||||
|
||||
export function generateCurrentTimezoneLabel(timezone: string) {
|
||||
if (!timezone) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return getTimezoneLabel(timezones, timezone);
|
||||
}
|
||||
|
||||
export const getCurrentTimezoneLabel = createSelector(
|
||||
'getCurrentTimezoneLabel',
|
||||
getCurrentTimezone,
|
||||
(timezone) => {
|
||||
if (!timezone) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return getTimezoneLabel(timezones, timezone);
|
||||
},
|
||||
generateCurrentTimezoneLabel,
|
||||
);
|
||||
|
@ -97,6 +97,7 @@ const state: GlobalState = {
|
||||
},
|
||||
preferences: {
|
||||
myPreferences: {},
|
||||
userPreferences: {},
|
||||
},
|
||||
bots: {
|
||||
accounts: {},
|
||||
|
@ -318,10 +318,12 @@
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
width: 100%;
|
||||
background: transparent;
|
||||
color: v(center-channel-color);
|
||||
font-size: 22px;
|
||||
line-height: 28px;
|
||||
word-break: break-word;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -72,6 +72,36 @@
|
||||
flex-direction: column;
|
||||
padding: 0;
|
||||
margin: 0 auto;
|
||||
|
||||
.loadingIndicator {
|
||||
display: block;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
margin: 36px auto 48px;
|
||||
font-size: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
border-bottom: 1px solid rgba(var(--center-channel-color-rgb), 0.08);
|
||||
|
||||
.adminModeBadge {
|
||||
display: flex;
|
||||
width: 86px;
|
||||
min-width: 86px;
|
||||
height: 22px;
|
||||
align-items: center;
|
||||
padding: 2px var(--spacing-xxxs, 6px);
|
||||
border-radius: 4px;
|
||||
margin-top: 10px;
|
||||
margin-bottom: auto;
|
||||
margin-left: 24px;
|
||||
background: var(--error-text);
|
||||
color: white;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
gap: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
li {
|
||||
@ -189,6 +219,7 @@
|
||||
display: flex;
|
||||
width: 100%;
|
||||
max-width: 1000px;
|
||||
min-height: 475px;
|
||||
min-height: 0;
|
||||
flex: 1;
|
||||
margin: 0 auto;
|
||||
@ -203,11 +234,11 @@
|
||||
|
||||
.settings-links {
|
||||
overflow: auto;
|
||||
width: 180px;
|
||||
background: var(--sidebar-bg);
|
||||
width: 232px;
|
||||
padding: 16px;
|
||||
background: rgba(var(--center-channel-color-rgb), 0.04);
|
||||
|
||||
.nav {
|
||||
position: fixed;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
@ -439,11 +470,11 @@
|
||||
}
|
||||
|
||||
.divider-dark {
|
||||
border-bottom: 1px solid #aaa;
|
||||
border-bottom: 1px solid rgba(var(--center-channel-color-rgb), 0.12);
|
||||
}
|
||||
|
||||
.divider-light {
|
||||
border-bottom: 1px solid lightgrey;
|
||||
border-bottom: 1px solid rgba(var(--center-channel-color-rgb), 0.08);
|
||||
|
||||
& + .divider-light {
|
||||
display: none;
|
||||
@ -530,23 +561,29 @@
|
||||
}
|
||||
|
||||
.nav-pills > li button {
|
||||
color: rgba(var(--sidebar-text-rgb), 0.75);
|
||||
color: rgba(var(--center-channel-color-rgb), 0.75);
|
||||
}
|
||||
}
|
||||
|
||||
.nav-pills {
|
||||
> li {
|
||||
margin: 0;
|
||||
margin-bottom: 8px;
|
||||
|
||||
button {
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
padding: 8px 15px;
|
||||
border-radius: 0;
|
||||
padding: 6px 15px;
|
||||
border-radius: 4px;
|
||||
color: $gray;
|
||||
font-weight: 600;
|
||||
text-align: left;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(var(--center-channel-color-rgb), 0.04);
|
||||
color: rgba(var(--center-channel-color-rgb), 0.8);
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
@ -565,14 +602,6 @@
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
button,
|
||||
button:hover,
|
||||
button:focus {
|
||||
background: var(--sidebar-text-hover-bg);
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
div {
|
||||
background-color: #e1e1e1;
|
||||
@ -596,7 +625,8 @@
|
||||
}
|
||||
|
||||
button {
|
||||
@include alpha-property(background-color, $black, 0.1);
|
||||
background: rgba(var(--button-bg-rgb), 0.08);
|
||||
color: v(button-bg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,12 +3,16 @@
|
||||
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
|
||||
import Permissions from 'mattermost-redux/constants/permissions';
|
||||
import {ResourceToSysConsolePermissionsTable, RESOURCE_KEYS} from 'mattermost-redux/constants/permissions_sysconsole';
|
||||
import {createSelector} from 'mattermost-redux/selectors/create_selector';
|
||||
import {getMySystemPermissions} from 'mattermost-redux/selectors/entities/roles_helpers';
|
||||
import {getLicense} from 'mattermost-redux/selectors/entities/general';
|
||||
import {getMySystemPermissions, haveISystemPermission} from 'mattermost-redux/selectors/entities/roles_helpers';
|
||||
|
||||
import AdminDefinition from 'components/admin_console/admin_definition';
|
||||
|
||||
import {isEnterpriseOrE20License} from '../utils/license_utils';
|
||||
|
||||
export const getAdminDefinition = createSelector(
|
||||
'getAdminDefinition',
|
||||
() => AdminDefinition,
|
||||
@ -49,3 +53,29 @@ export const getConsoleAccess = createSelector(
|
||||
return consoleAccess;
|
||||
},
|
||||
);
|
||||
|
||||
export const getShowManageUserSettings = createSelector(
|
||||
'showManageUserSettings',
|
||||
getLicense,
|
||||
(state) => state,
|
||||
(license, state) => {
|
||||
const hasWriteUserManagementPermission = haveISystemPermission(state, {permission: Permissions.SYSCONSOLE_WRITE_USERMANAGEMENT_USERS});
|
||||
|
||||
const isEnterprise = isEnterpriseOrE20License(license);
|
||||
|
||||
return hasWriteUserManagementPermission && isEnterprise;
|
||||
},
|
||||
);
|
||||
|
||||
export const getShowLockedManageUserSettings = createSelector(
|
||||
'showLockedManageUserSettings',
|
||||
getLicense,
|
||||
(state) => state,
|
||||
(license, state) => {
|
||||
const hasWriteUserManagementPermission = haveISystemPermission(state, {permission: Permissions.SYSCONSOLE_WRITE_USERMANAGEMENT_USERS});
|
||||
|
||||
const isEnterprise = isEnterpriseOrE20License(license);
|
||||
|
||||
return hasWriteUserManagementPermission && !isEnterprise;
|
||||
},
|
||||
);
|
||||
|
@ -461,6 +461,7 @@ export const ModalIdentifiers = {
|
||||
EXPORT_ERROR_MODAL: 'export_error_modal',
|
||||
CHANNEL_BOOKMARK_DELETE: 'channel_bookmark_delete',
|
||||
CHANNEL_BOOKMARK_CREATE: 'channel_bookmark_create',
|
||||
CONFIRM_MANAGE_USER_SETTINGS_MODAL: 'confirm_switch_to_settings',
|
||||
};
|
||||
|
||||
export const UserStatuses = {
|
||||
|
@ -2491,6 +2491,13 @@ export default class Client4 {
|
||||
);
|
||||
};
|
||||
|
||||
getUserPreferences = (userId: string) => {
|
||||
return this.doFetch<PreferenceType[]>(
|
||||
`${this.getPreferencesRoute(userId)}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
deletePreferences = (userId: string, preferences: PreferenceType[]) => {
|
||||
return this.doFetch<StatusOK>(
|
||||
`${this.getPreferencesRoute(userId)}/delete`,
|
||||
|
@ -49,6 +49,11 @@ export type GlobalState = {
|
||||
myPreferences: {
|
||||
[x: string]: PreferenceType;
|
||||
};
|
||||
userPreferences: {
|
||||
[userID: string]: {
|
||||
[x: string]: PreferenceType;
|
||||
};
|
||||
};
|
||||
};
|
||||
admin: AdminState;
|
||||
jobs: JobsState;
|
||||
|
Loading…
Reference in New Issue
Block a user