PLT-4332 Position field for Users (#4632)
* Add User.Position field to store & model. * GOFMT * Add Position to user settings. * Unit tests. * Add position to profile popup. * i18n * Fix log message for invalid position. * Add Position field attribute to LDAP config. * Add Position field attribute to SAML config. * Reword empty position message. * Change Position Max Length to 35. * Better invalid position error message. * Add new fields to config.json. * Ensure position is never longer than max when displayed. * Hard limit of 64 chars with soft limit still 35 * Put field with other attributes.
Этот коммит содержится в:
коммит произвёл
enahum
родитель
9735854503
Коммит
8406e854aa
@@ -186,6 +186,7 @@
|
||||
"UsernameAttribute": "",
|
||||
"NicknameAttribute": "",
|
||||
"IdAttribute": "",
|
||||
"PositionAttribute": "",
|
||||
"SyncIntervalMinutes": 60,
|
||||
"SkipCertificateVerification": false,
|
||||
"QueryTimeout": 60,
|
||||
@@ -218,6 +219,7 @@
|
||||
"UsernameAttribute": "",
|
||||
"NicknameAttribute": "",
|
||||
"LocaleAttribute": "",
|
||||
"PositionAttribute": "",
|
||||
"LoginButtonText": "With SAML"
|
||||
},
|
||||
"NativeAppSettings": {
|
||||
|
||||
@@ -3875,6 +3875,10 @@
|
||||
"id": "model.user.is_valid.nickname.app_error",
|
||||
"translation": "Invalid nickname"
|
||||
},
|
||||
{
|
||||
"id": "model.user.is_valid.position.app_error",
|
||||
"translation": "Invalid position: must not be longer than 35 characters."
|
||||
},
|
||||
{
|
||||
"id": "model.user.is_valid.pwd.app_error",
|
||||
"translation": "Your password must contain at least {{.Min}} characters."
|
||||
|
||||
@@ -251,6 +251,7 @@ type LdapSettings struct {
|
||||
UsernameAttribute *string
|
||||
NicknameAttribute *string
|
||||
IdAttribute *string
|
||||
PositionAttribute *string
|
||||
|
||||
// Syncronization
|
||||
SyncIntervalMinutes *int
|
||||
@@ -297,6 +298,7 @@ type SamlSettings struct {
|
||||
UsernameAttribute *string
|
||||
NicknameAttribute *string
|
||||
LocaleAttribute *string
|
||||
PositionAttribute *string
|
||||
|
||||
LoginButtonText *string
|
||||
}
|
||||
@@ -690,6 +692,11 @@ func (o *Config) SetDefaults() {
|
||||
*o.LdapSettings.IdAttribute = ""
|
||||
}
|
||||
|
||||
if o.LdapSettings.PositionAttribute == nil {
|
||||
o.LdapSettings.PositionAttribute = new(string)
|
||||
*o.LdapSettings.PositionAttribute = ""
|
||||
}
|
||||
|
||||
if o.LdapSettings.SyncIntervalMinutes == nil {
|
||||
o.LdapSettings.SyncIntervalMinutes = new(int)
|
||||
*o.LdapSettings.SyncIntervalMinutes = 60
|
||||
@@ -911,6 +918,11 @@ func (o *Config) SetDefaults() {
|
||||
*o.SamlSettings.NicknameAttribute = ""
|
||||
}
|
||||
|
||||
if o.SamlSettings.PositionAttribute == nil {
|
||||
o.SamlSettings.PositionAttribute = new(string)
|
||||
*o.SamlSettings.PositionAttribute = ""
|
||||
}
|
||||
|
||||
if o.SamlSettings.LocaleAttribute == nil {
|
||||
o.SamlSettings.LocaleAttribute = new(string)
|
||||
*o.SamlSettings.LocaleAttribute = ""
|
||||
|
||||
@@ -37,6 +37,7 @@ type User struct {
|
||||
Nickname string `json:"nickname"`
|
||||
FirstName string `json:"first_name"`
|
||||
LastName string `json:"last_name"`
|
||||
Position string `json:"position"`
|
||||
Roles string `json:"roles"`
|
||||
AllowMarketing bool `json:"allow_marketing,omitempty"`
|
||||
Props StringMap `json:"props,omitempty"`
|
||||
@@ -78,6 +79,10 @@ func (u *User) IsValid() *AppError {
|
||||
return NewLocAppError("User.IsValid", "model.user.is_valid.nickname.app_error", nil, "user_id="+u.Id)
|
||||
}
|
||||
|
||||
if utf8.RuneCountInString(u.Position) > 35 {
|
||||
return NewLocAppError("User.IsValid", "model.user.is_valid.position.app_error", nil, "user_id="+u.Id)
|
||||
}
|
||||
|
||||
if utf8.RuneCountInString(u.FirstName) > 64 {
|
||||
return NewLocAppError("User.IsValid", "model.user.is_valid.first_name.app_error", nil, "user_id="+u.Id)
|
||||
}
|
||||
|
||||
@@ -128,6 +128,17 @@ func TestUserIsValid(t *testing.T) {
|
||||
if err := user.IsValid(); err == nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
user.LastName = ""
|
||||
user.Position = ""
|
||||
if err := user.IsValid(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
user.Position = strings.Repeat("01234567890", 20)
|
||||
if err := user.IsValid(); err == nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserGetFullName(t *testing.T) {
|
||||
|
||||
@@ -222,6 +222,9 @@ func UpgradeDatabaseToVersion36(sqlStore *SqlStore) {
|
||||
// Create Team Description column
|
||||
sqlStore.CreateColumnIfNotExists("Teams", "Description", "varchar(255)", "varchar(255)", "")
|
||||
|
||||
// Add a Position column to users.
|
||||
sqlStore.CreateColumnIfNotExists("Users", "Position", "varchar(64)", "varchar(64)", "")
|
||||
|
||||
//saveSchemaVersion(sqlStore, VERSION_3_6_0)
|
||||
//}
|
||||
}
|
||||
|
||||
@@ -66,6 +66,7 @@ func NewSqlUserStore(sqlStore *SqlStore) UserStore {
|
||||
table.ColMap("NotifyProps").SetMaxSize(2000)
|
||||
table.ColMap("Locale").SetMaxSize(5)
|
||||
table.ColMap("MfaSecret").SetMaxSize(128)
|
||||
table.ColMap("Position").SetMaxSize(64)
|
||||
}
|
||||
|
||||
return us
|
||||
|
||||
@@ -38,6 +38,7 @@ export default class LdapSettings extends AdminSettings {
|
||||
config.LdapSettings.NicknameAttribute = this.state.nicknameAttribute;
|
||||
config.LdapSettings.EmailAttribute = this.state.emailAttribute;
|
||||
config.LdapSettings.UsernameAttribute = this.state.usernameAttribute;
|
||||
config.LdapSettings.PositionAttribute = this.state.positionAttribute;
|
||||
config.LdapSettings.IdAttribute = this.state.idAttribute;
|
||||
config.LdapSettings.SyncIntervalMinutes = this.parseIntNonZero(this.state.syncIntervalMinutes);
|
||||
config.LdapSettings.SkipCertificateVerification = this.state.skipCertificateVerification;
|
||||
@@ -63,6 +64,7 @@ export default class LdapSettings extends AdminSettings {
|
||||
nicknameAttribute: config.LdapSettings.NicknameAttribute,
|
||||
emailAttribute: config.LdapSettings.EmailAttribute,
|
||||
usernameAttribute: config.LdapSettings.UsernameAttribute,
|
||||
positionAttribute: config.LdapSettings.PositionAttribute,
|
||||
idAttribute: config.LdapSettings.IdAttribute,
|
||||
syncIntervalMinutes: config.LdapSettings.SyncIntervalMinutes,
|
||||
skipCertificateVerification: config.LdapSettings.SkipCertificateVerification,
|
||||
@@ -299,6 +301,25 @@ export default class LdapSettings extends AdminSettings {
|
||||
onChange={this.handleChange}
|
||||
disabled={!this.state.enable}
|
||||
/>
|
||||
<TextSetting
|
||||
id='positionAttribute'
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='admin.ldap.positionAttrTitle'
|
||||
defaultMessage='Position Attribute:'
|
||||
/>
|
||||
}
|
||||
placeholder={Utils.localizeMessage('admin.ldap.positionAttrEx', 'E.g.: "title"')}
|
||||
helpText={
|
||||
<FormattedMessage
|
||||
id='admin.ldap.positionAttrDesc'
|
||||
defaultMessage='(Optional) The attribute in the AD/LDAP server that will be used to populate the position field in Mattermost.'
|
||||
/>
|
||||
}
|
||||
value={this.state.positionAttribute}
|
||||
onChange={this.handleChange}
|
||||
disabled={!this.state.enable}
|
||||
/>
|
||||
<TextSetting
|
||||
id='emailAttribute'
|
||||
label={
|
||||
|
||||
@@ -41,6 +41,7 @@ export default class SamlSettings extends AdminSettings {
|
||||
config.SamlSettings.EmailAttribute = this.state.emailAttribute;
|
||||
config.SamlSettings.UsernameAttribute = this.state.usernameAttribute;
|
||||
config.SamlSettings.NicknameAttribute = this.state.nicknameAttribute;
|
||||
config.SamlSettings.PositionAttribute = this.state.positionAttribute;
|
||||
config.SamlSettings.LocaleAttribute = this.state.localeAttribute;
|
||||
config.SamlSettings.LoginButtonText = this.state.loginButtonText;
|
||||
|
||||
@@ -65,6 +66,7 @@ export default class SamlSettings extends AdminSettings {
|
||||
emailAttribute: settings.EmailAttribute,
|
||||
usernameAttribute: settings.UsernameAttribute,
|
||||
nicknameAttribute: settings.NicknameAttribute,
|
||||
positionAttribute: settings.PositionAttribute,
|
||||
localeAttribute: settings.LocaleAttribute,
|
||||
loginButtonText: settings.LoginButtonText
|
||||
};
|
||||
@@ -508,6 +510,25 @@ export default class SamlSettings extends AdminSettings {
|
||||
onChange={this.handleChange}
|
||||
disabled={!this.state.enable}
|
||||
/>
|
||||
<TextSetting
|
||||
id='positionAttribute'
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='admin.saml.positionAttrTitle'
|
||||
defaultMessage='Position Attribute:'
|
||||
/>
|
||||
}
|
||||
placeholder={Utils.localizeMessage('admin.saml.positionAttrEx', 'E.g.: "Role"')}
|
||||
helpText={
|
||||
<FormattedMessage
|
||||
id='admin.saml.positionAttrDesc'
|
||||
defaultMessage='(Optional) The attribute in the SAML Assertion that will be used to populate the position of users in Mattermost.'
|
||||
/>
|
||||
}
|
||||
value={this.state.positionAttribute}
|
||||
onChange={this.handleChange}
|
||||
disabled={!this.state.enable}
|
||||
/>
|
||||
<TextSetting
|
||||
id='localeAttribute'
|
||||
label={
|
||||
|
||||
@@ -182,6 +182,23 @@ export default class UserProfile extends React.Component {
|
||||
|
||||
dataContent.push(webrtc);
|
||||
|
||||
const position = this.props.user.position.substring(0, Constants.MAX_POSITION_LENGTH);
|
||||
if (position) {
|
||||
dataContent.push(
|
||||
<div
|
||||
data-toggle='tooltip'
|
||||
title={position}
|
||||
key='user-popover-position'
|
||||
>
|
||||
<p
|
||||
className='text-nowrap'
|
||||
>
|
||||
{position}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (global.window.mm_config.ShowEmailAddress === 'true' || UserStore.isSystemAdminForCurrentUser() || this.props.user === UserStore.getCurrentUser()) {
|
||||
dataContent.push(
|
||||
<div
|
||||
|
||||
@@ -69,6 +69,10 @@ const holders = defineMessages({
|
||||
close: {
|
||||
id: 'user.settings.general.close',
|
||||
defaultMessage: 'Close'
|
||||
},
|
||||
position: {
|
||||
id: 'user.settings.general.position',
|
||||
defaultMessage: 'Position'
|
||||
}
|
||||
});
|
||||
|
||||
@@ -85,6 +89,7 @@ class UserSettingsGeneralTab extends React.Component {
|
||||
this.submitEmail = this.submitEmail.bind(this);
|
||||
this.submitUser = this.submitUser.bind(this);
|
||||
this.submitPicture = this.submitPicture.bind(this);
|
||||
this.submitPosition = this.submitPosition.bind(this);
|
||||
|
||||
this.updateUsername = this.updateUsername.bind(this);
|
||||
this.updateFirstName = this.updateFirstName.bind(this);
|
||||
@@ -94,6 +99,7 @@ class UserSettingsGeneralTab extends React.Component {
|
||||
this.updateConfirmEmail = this.updateConfirmEmail.bind(this);
|
||||
this.updatePicture = this.updatePicture.bind(this);
|
||||
this.updateSection = this.updateSection.bind(this);
|
||||
this.updatePosition = this.updatePosition.bind(this);
|
||||
|
||||
this.state = this.setupInitialState(props);
|
||||
}
|
||||
@@ -249,6 +255,22 @@ class UserSettingsGeneralTab extends React.Component {
|
||||
);
|
||||
}
|
||||
|
||||
submitPosition(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const user = Object.assign({}, this.props.user);
|
||||
const position = this.state.position.trim();
|
||||
|
||||
if (user.position === position) {
|
||||
this.updateSection('');
|
||||
return;
|
||||
}
|
||||
|
||||
user.position = position;
|
||||
|
||||
this.submitUser(user, Constants.UserUpdateEvents.Position, false);
|
||||
}
|
||||
|
||||
updateUsername(e) {
|
||||
this.setState({username: e.target.value});
|
||||
}
|
||||
@@ -265,6 +287,10 @@ class UserSettingsGeneralTab extends React.Component {
|
||||
this.setState({nickname: e.target.value});
|
||||
}
|
||||
|
||||
updatePosition(e) {
|
||||
this.setState({position: e.target.value});
|
||||
}
|
||||
|
||||
updateEmail(e) {
|
||||
this.setState({email: e.target.value});
|
||||
}
|
||||
@@ -302,6 +328,7 @@ class UserSettingsGeneralTab extends React.Component {
|
||||
firstName: user.first_name,
|
||||
lastName: user.last_name,
|
||||
nickname: user.nickname,
|
||||
position: user.position,
|
||||
email: user.email,
|
||||
confirmEmail: '',
|
||||
picture: null,
|
||||
@@ -936,6 +963,99 @@ class UserSettingsGeneralTab extends React.Component {
|
||||
);
|
||||
}
|
||||
|
||||
let positionSection;
|
||||
if (this.props.activeSection === 'position') {
|
||||
let extraInfo;
|
||||
let submit = null;
|
||||
if ((this.props.user.auth_service === 'ldap' || this.props.user.auth_service === Constants.SAML_SERVICE) && global.window.mm_config.PositionAttributeSet === 'true') {
|
||||
extraInfo = (
|
||||
<span>
|
||||
<FormattedMessage
|
||||
id='user.settings.general.field_handled_externally'
|
||||
defaultMessage='This field is handled through your login provider. If you want to change it, you need to do so though your login provider.'
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
} else {
|
||||
let positionLabel = (
|
||||
<FormattedMessage
|
||||
id='user.settings.general.position'
|
||||
defaultMessage='Position'
|
||||
/>
|
||||
);
|
||||
if (Utils.isMobile()) {
|
||||
positionLabel = '';
|
||||
}
|
||||
|
||||
inputs.push(
|
||||
<div
|
||||
key='positionSetting'
|
||||
className='form-group'
|
||||
>
|
||||
<label className='col-sm-5 control-label'>{positionLabel}</label>
|
||||
<div className='col-sm-7'>
|
||||
<input
|
||||
className='form-control'
|
||||
type='text'
|
||||
onChange={this.updatePosition}
|
||||
value={this.state.position}
|
||||
maxLength={Constants.MAX_POSITION_LENGTH}
|
||||
autoCapitalize='off'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
extraInfo = (
|
||||
<span>
|
||||
<FormattedMessage
|
||||
id='user.settings.general.positionExtra'
|
||||
defaultMessage='Tell your teammates what you do.'
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
|
||||
submit = this.submitPosition;
|
||||
}
|
||||
|
||||
positionSection = (
|
||||
<SettingItemMax
|
||||
title={formatMessage(holders.position)}
|
||||
inputs={inputs}
|
||||
submit={submit}
|
||||
server_error={serverError}
|
||||
client_error={clientError}
|
||||
updateSection={(e) => {
|
||||
this.updateSection('');
|
||||
e.preventDefault();
|
||||
}}
|
||||
extraInfo={extraInfo}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
let describe = '';
|
||||
if (user.position) {
|
||||
describe = user.position;
|
||||
} else {
|
||||
describe = (
|
||||
<FormattedMessage
|
||||
id='user.settings.general.emptyPosition'
|
||||
defaultMessage="Click 'Edit' to add your job title / position"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
positionSection = (
|
||||
<SettingItemMin
|
||||
title={formatMessage(holders.position)}
|
||||
describe={describe}
|
||||
updateSection={() => {
|
||||
this.updateSection('position');
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const emailSection = this.createEmailSection();
|
||||
|
||||
let pictureSection;
|
||||
@@ -1030,6 +1150,8 @@ class UserSettingsGeneralTab extends React.Component {
|
||||
<div className='divider-light'/>
|
||||
{nicknameSection}
|
||||
<div className='divider-light'/>
|
||||
{positionSection}
|
||||
<div className='divider-light'/>
|
||||
{emailSection}
|
||||
<div className='divider-light'/>
|
||||
{pictureSection}
|
||||
|
||||
@@ -441,6 +441,9 @@
|
||||
"admin.ldap.portDesc": "The port Mattermost will use to connect to the AD/LDAP server. Default is 389.",
|
||||
"admin.ldap.portEx": "E.g.: \"389\"",
|
||||
"admin.ldap.portTitle": "AD/LDAP Port:",
|
||||
"admin.ldap.positionAttrEx": "E.g.: \"title\"",
|
||||
"admin.ldap.positionAttrDesc": "(Optional) The attribute in the AD/LDAP server that will be used to populate the position field in Mattermost.",
|
||||
"admin.ldap.positionAttrTitle": "Position Attribute:",
|
||||
"admin.ldap.queryDesc": "The timeout value for queries to the AD/LDAP server. Increase if you are getting timeout errors caused by a slow AD/LDAP server.",
|
||||
"admin.ldap.queryEx": "E.g.: \"60\"",
|
||||
"admin.ldap.queryTitle": "Query Timeout (seconds):",
|
||||
@@ -614,6 +617,9 @@
|
||||
"admin.saml.nicknameAttrDesc": "(Optional) The attribute in the SAML Assertion that will be used to populate the nickname of users in Mattermost.",
|
||||
"admin.saml.nicknameAttrEx": "E.g.: \"Nickname\"",
|
||||
"admin.saml.nicknameAttrTitle": "Nickname Attribute:",
|
||||
"admin.saml.positionAttrDesc": "(Optional) The attribute in the SAML Assertion that will be used to populate the position of users in Mattermost.",
|
||||
"admin.saml.positionAttrEx": "E.g.: \"Role\"",
|
||||
"admin.saml.positionAttrTitle": "Position Attribute:",
|
||||
"admin.saml.privateKeyFileFileDesc": "The private key used to decrypt SAML Assertions from the Identity Provider.",
|
||||
"admin.saml.privateKeyFileFileRemoveDesc": "Remove the private key used to decrypt SAML Assertions from the Identity Provider.",
|
||||
"admin.saml.privateKeyFileTitle": "Service Provider Private Key:",
|
||||
@@ -1953,6 +1959,7 @@
|
||||
"user.settings.general.emailUnchanged": "Your new email address is the same as your old email address.",
|
||||
"user.settings.general.emptyName": "Click 'Edit' to add your full name",
|
||||
"user.settings.general.emptyNickname": "Click 'Edit' to add a nickname",
|
||||
"user.settings.general.emptyPosition": "Click 'Edit' to add your job title / position",
|
||||
"user.settings.general.field_handled_externally": "This field is handled through your login provider. If you want to change it, you need to do so through your login provider.",
|
||||
"user.settings.general.firstName": "First Name",
|
||||
"user.settings.general.fullName": "Full Name",
|
||||
@@ -1969,6 +1976,8 @@
|
||||
"user.settings.general.nicknameExtra": "Use Nickname for a name you might be called that is different from your first name and username. This is most often used when two or more people have similar sounding names and usernames.",
|
||||
"user.settings.general.notificationsExtra": "By default, you will receive mention notifications when someone types your first name. Go to {notify} settings to change this default.",
|
||||
"user.settings.general.notificationsLink": "Notifications",
|
||||
"user.settings.general.position": "Position",
|
||||
"user.settings.general.positionExtra": "Tell your teammates what you do.",
|
||||
"user.settings.general.primaryEmail": "Primary Email",
|
||||
"user.settings.general.profilePicture": "Profile Picture",
|
||||
"user.settings.general.title": "General Settings",
|
||||
|
||||
@@ -264,7 +264,8 @@ export const Constants = {
|
||||
FULLNAME: 'fullname',
|
||||
NICKNAME: 'nickname',
|
||||
EMAIL: 'email',
|
||||
LANGUAGE: 'language'
|
||||
LANGUAGE: 'language',
|
||||
POSITION: 'position'
|
||||
},
|
||||
|
||||
ScrollTypes: {
|
||||
@@ -835,6 +836,7 @@ export const Constants = {
|
||||
MAX_NICKNAME_LENGTH: 22,
|
||||
MIN_PASSWORD_LENGTH: 5,
|
||||
MAX_PASSWORD_LENGTH: 64,
|
||||
MAX_POSITION_LENGTH: 35,
|
||||
MIN_TRIGGER_LENGTH: 1,
|
||||
MAX_TRIGGER_LENGTH: 128,
|
||||
MAX_TEXTSETTING_LENGTH: 1024,
|
||||
|
||||
Ссылка в новой задаче
Block a user