Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding attribute by team name #40

Merged
merged 4 commits into from
Jul 29, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 12 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
**Maintainer:** [@marianunez](https://github.com/marianunez)
**Co-Maintainer:** [@larkox](https://github.com/larkox)

This plugin adds custom attributes to users in your Mattermost instance. You can specify an Attribute, and then specify specific users or groups which will display that attribute on their public profile - so other users can identify them easily. This can be useful when there are Team Leads, Timezones, etc. and makes it easy to show who is on a particular team or Project.
This plugin adds custom attributes to users in your Mattermost instance. You can specify an Attribute, and then specify specific users, teams or groups which will display that attribute on their public profile - so other users can identify them easily. This can be useful when there are Team Leads, Timezones, etc. and makes it easy to show who is on a particular team or Project.

Currently the plugin only exposes the specified attributes in the user profile popover, but this plugin could be extended to allow displaying attributes elsewhere in the user interface, such as badges next to usernames.

Expand All @@ -31,12 +31,13 @@ Install via Plugin Marketplace (Recommended)

## Configuration

Before you start, Identify the attributes you want to display on a user's profile popover. These can contain emojis. Some examples could be "Timezone:PST", "Development Team", "Executive Team Member", "Mentor", etc. then Identify the groups or particular usernames that should display those atrributes. A spreadsheet can help to organize things.
Before you start, Identify the attributes you want to display on a user's profile popover. These can contain emojis. Some examples could be "Timezone:PST", "Development Team", "Executive Team Member", "Mentor", etc. then Identify the groups, teams or particular usernames that should display those atrributes. A spreadsheet can help to organize things.

1. Click "Add Custom Attribute" button, a text box will appear. Add the text that would appear in the user's profile popover. The text supports markdown and could include emojis and/or links, i.e. "[Integrations Team](https://developers.mattermost.com/internal/rd-teams/#integrations-team)"
![2020-04-14_12-12-46](https://user-images.githubusercontent.com/915956/79266979-3e3d2e80-7e4d-11ea-8a4d-80f78bd81d79.png)
2. Specify which users should have that attribute displayed on their profile. You can specify individual users or a Mattermost group ID (this ID needs to be copy/pasted from the group). The Mattermost group could be synched with an LDAP group to dynamically display attributes to user profiles, based on which LDAP group they currently belong to (this requires an E20 licence to enable AD/LDAP Groups).
![image](https://user-images.githubusercontent.com/915956/79267902-c07a2280-7e4e-11ea-8eed-96bc2fc9bde9.png)
2. Specify which users should have that attribute displayed on their profile. You can specify individual users, a Mattermost team name, or a Mattermost group ID (this ID needs to be copy/pasted from the group). The Mattermost group could be synched with an LDAP group to dynamically display attributes to user profiles, based on which LDAP group they currently belong to (this requires an E20 licence to enable AD/LDAP Groups).
![2020-06-21_22-51-13](https://user-images.githubusercontent.com/45119518/85234976-a726c100-b411-11ea-9477-7133c6a6d45b.png)


3. Click "Save"

Expand All @@ -46,7 +47,7 @@ Here are some example rules for two users:

![2020-04-14_12-18-50](https://user-images.githubusercontent.com/915956/79267023-4eeda480-7e4d-11ea-9279-e77c97d737be.png)

Their respective profile popvers display their information:
Their respective profile popovers display their information:

![2020-04-14_12-19-24](https://user-images.githubusercontent.com/915956/79267480-169a9600-7e4e-11ea-8c04-4775a395ff5b.png)

Expand All @@ -66,8 +67,11 @@ To add a custom attribute, edit your `config.json` file and add a "CustomAttribu

An attribute should have a `Name` field for what is displayed in the user interface as the attribute and an array of `UserIDs` for the users this attribute should apply to. The `Name` field can include Markdown, emojis and links.

You can fill an array of Mattermost team ID's to the `TeamIDs` parameter, and the `Name` will then be displayed
for all members of these teams.

You can also add an array of Mattermost group ID's to the `GroupIDs` parameter. The `Name` will then be displayed
for all memebers who are apart of that group.
for all members who are apart of that group.

Below is an example:

Expand All @@ -81,11 +85,13 @@ Below is an example:
{
"Name": ":mattermost: [Core Committer](https://developers.mattermost.com/contribute/getting-started/core-committers/)",
"UserIDs": ["someuserID1", "someuserID2"],
"TeamIDs": ["someteamID1", "someteamID2"],
"GroupIDs":["somegroupID1","somegroupID2"]
},
{
"Name": ":mattermost: Staff",
"UserIDs": ["someuserID3", "someuserID4"],
"TeamIDs": ["someteamID3", "someteamID4"],
"GroupIDs":["somegroupID3","somegroupID4"]
}
]
Expand Down
10 changes: 10 additions & 0 deletions server/configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ type configuration struct {
type CustomAttribute struct {
Name string
UserIDs []string
TeamIDs []string
GroupIDs []string
}

Expand All @@ -40,6 +41,15 @@ func (c *configuration) Clone() *configuration {
for i2, id := range ca.UserIDs {
caClone.UserIDs[i2] = id
}
caClone.TeamIDs = make([]string, len(ca.TeamIDs))
for i2, id := range ca.TeamIDs {
caClone.TeamIDs[i2] = id
}
caClone.GroupIDs = make([]string, len(ca.GroupIDs))
for i2, id := range ca.GroupIDs {
caClone.GroupIDs[i2] = id
}

clone.CustomAttributes[i1] = caClone
}

Expand Down
16 changes: 14 additions & 2 deletions server/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,13 @@ func (p *Plugin) handleGetAttributes(w http.ResponseWriter, r *http.Request) {
}

attributes := []string{}
usersTeams, _ := p.API.GetTeamsForUser(userID)
usersGroups, _ := p.API.GetGroupsForUser(userID)
for _, ca := range config.CustomAttributes {
if ca.UserIDs == nil && ca.GroupIDs == nil {
if ca.UserIDs == nil && ca.TeamIDs == nil && ca.GroupIDs == nil {
continue
}
if sliceContainsString(ca.UserIDs, userID) || sliceContainsUserGroup(ca.GroupIDs, usersGroups) {
if sliceContainsString(ca.UserIDs, userID) || sliceContainsUserTeam(ca.TeamIDs, usersTeams) || sliceContainsUserGroup(ca.GroupIDs, usersGroups) {
attributes = append(attributes, ca.Name)
}
}
Expand All @@ -85,6 +86,17 @@ func sliceContainsString(arr []string, str string) bool {
return false
}

func sliceContainsUserTeam(arr []string, userTeams []*model.Team) bool {
for _, a := range arr {
for _, userTeam := range userTeams {
if a == userTeam.Id {
return true
}
}
}
return false
}

func sliceContainsUserGroup(arr []string, userGroups []*model.Group) bool {
for _, a := range arr {
for _, userGroup := range userGroups {
Expand Down
16 changes: 11 additions & 5 deletions webapp/src/components/admin_settings/add_attribute.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export default class AddAttribute extends React.Component {
id: PropTypes.string,
name: PropTypes.string,
users: PropTypes.array,
teams: PropTypes.array,
groups: PropTypes.array,
onChange: PropTypes.func.isRequired,
}
Expand All @@ -21,6 +22,7 @@ export default class AddAttribute extends React.Component {
collapsed: true,
name: this.props.name,
users: this.props.users,
teams: this.props.teams,
groups: this.props.groups,
error: false,
};
Expand All @@ -31,29 +33,32 @@ export default class AddAttribute extends React.Component {
collapsed: true,
name: null,
users: null,
teams: null,
groups: null,
error: false,
});
}

onInput = ({name, users, groups}) => {
this.setState({name, users, groups, error: false});
onInput = ({name, users, teams, groups}) => {
this.setState({name, users, teams, groups, error: false});
}

handleSave = () => {
const usersEmpty = !this.state.users || !this.state.users.length;
const teamsEmpty = !this.state.teams || !this.state.teams.length;
const groupsEmpty = !this.state.groups || this.state.groups.trim() === '';

if (!this.state.name || this.state.name.trim() === '' || (usersEmpty && groupsEmpty)) {
if (!this.state.name || this.state.name.trim() === '' || (usersEmpty && teamsEmpty && groupsEmpty)) {
this.setState({error: true});
return;
}

this.props.onChange({id: this.props.id, name: this.state.name, users: this.state.users, groups: this.state.groups});
this.props.onChange({id: this.props.id, name: this.state.name, users: this.state.users, teams: this.state.teams, groups: this.state.groups});
this.setState({
collapsed: true,
name: null,
users: null,
teams: null,
groups: null,
});
}
Expand All @@ -76,7 +81,7 @@ export default class AddAttribute extends React.Component {
if (this.state.error) {
errorBanner = (
<div style={styles.alertDiv}>
<p style={styles.alertText}> {'You must provide a value for name and users or group.'}
<p style={styles.alertText}> {'You must provide a value for name and users, teams or group.'}
</p>
</div>
);
Expand All @@ -87,6 +92,7 @@ export default class AddAttribute extends React.Component {
<CustomAttribute
name={this.props.name}
users={this.props.users}
teams={this.props.teams}
groups={this.props.groups}
onChange={this.onInput}
hideDelete={true}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types';

import UsersInput from '../users_input';
import TeamsInput from '../teams_input';

const {formatText, messageHtmlToComponent} = window.PostUtils;

Expand All @@ -10,13 +11,15 @@ export default class CustomAttribute extends React.Component {
id: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
users: PropTypes.array,
teams: PropTypes.array,
groups: PropTypes.array,
hideDelete: PropTypes.bool,
markdownPreview: PropTypes.bool,
onDelete: PropTypes.func,
onChange: PropTypes.func.isRequired,
actions: PropTypes.shape({
getProfilesByIds: PropTypes.func.isRequired,
getTeam: PropTypes.func.isRequired,
getCustomEmojisInText: PropTypes.func.isRequired,
}).isRequired,
}
Expand All @@ -31,6 +34,7 @@ export default class CustomAttribute extends React.Component {
};

this.initUsers();
this.initTeams();
}

// initUsers fetches user profiles for the users ids passed in props
Expand Down Expand Up @@ -58,6 +62,19 @@ export default class CustomAttribute extends React.Component {
this.setState({users});
}

// initTeams fetches teams informations for the teams ids passed in props
async initTeams() {
if (!this.props.teams || !this.props.teams.length) {
return;
}

const teamPromises = this.props.teams.map(this.props.actions.getTeam);
const responses = await Promise.all(teamPromises);
const teams = responses.filter((res) => !res.error).map((res) => res.data);

this.setState({teams});
}

handleNameInput = (e) => {
if (!e.target.value || e.target.value.trim() === '') {
this.setState({error: 'Attribute name cannot be empty.'});
Expand All @@ -66,35 +83,52 @@ export default class CustomAttribute extends React.Component {
}

this.setState({name: e.target.value});
this.props.onChange({id: this.props.id, name: e.target.value, users: this.state.users, groups: this.state.groups});
this.props.onChange({id: this.props.id, name: e.target.value, users: this.state.users, teams: this.state.teams, groups: this.state.groups});
}

handleUsersInput = (userIds) => {
const usersEmpty = !userIds || !userIds.length;
const teamsEmpty = !this.state.teams || !this.state.teams.length;
const groupsEmpty = !this.state.groups || this.state.groups.trim() === '';

if (usersEmpty && groupsEmpty) {
this.setState({error: 'Attribute must include at least one user or group.'});
if (usersEmpty && teamsEmpty && groupsEmpty) {
this.setState({error: 'Attribute must include at least one user, team or group.'});
} else if (this.state.name) {
this.setState({error: null});
}

this.setState({users: userIds});
this.props.onChange({id: this.props.id, name: this.state.name, users: userIds, groups: this.state.groups});
this.props.onChange({id: this.props.id, name: this.state.name, users: userIds, teams: this.state.teams, groups: this.state.groups});
}

handleGroupsInput = (e) => {
const usersEmpty = !e.target.value || e.target.value.trim() === '';
handleTeamsInput = (teamsIds) => {
const usersEmpty = !this.state.users || !this.state.users.length;
const teamsEmpty = !teamsIds || !teamsIds.length;
const groupsEmpty = !this.state.groups || this.state.groups.trim() === '';

if (usersEmpty && groupsEmpty) {
this.setState({error: 'Attribute must include at least one user or group.'});
if (usersEmpty && teamsEmpty && groupsEmpty) {
this.setState({error: 'Attribute must include at least one user, team or group.'});
} else if (this.state.name) {
this.setState({error: null});
}

this.setState({teams: teamsIds});
this.props.onChange({id: this.props.id, name: this.state.name, users: this.state.users, teams: teamsIds, groups: this.state.groups});
}

handleGroupsInput = (e) => {
const usersEmpty = !this.state.users || !this.state.users.length;
const teamsEmpty = !this.state.teams || !this.state.teams.length;
const groupsEmpty = !e.target.value || e.target.value.trim() === '';

if (usersEmpty && teamsEmpty && groupsEmpty) {
this.setState({error: 'Attribute must include at least one user, team or group.'});
} else if (this.state.name) {
this.setState({error: null});
}

this.setState({groups: e.target.value});
this.props.onChange({id: this.props.id, name: this.state.name, users: this.state.users, groups: e.target.value});
this.props.onChange({id: this.props.id, name: this.state.name, users: this.state.users, teams: this.state.teams, groups: e.target.value});
}

handleDelete = () => {
Expand Down Expand Up @@ -154,14 +188,21 @@ export default class CustomAttribute extends React.Component {
onChange={this.handleNameInput}
/>
</div>
<div className='col-xs-12 col-sm-5'>
<div className='col-xs-12 col-sm-3'>
<UsersInput
placeholder='@username1 @username2'
users={this.state.users}
onChange={this.handleUsersInput}
/>
</div>
<div className='col-xs-12 col-sm-4'>
<div className='col-xs-12 col-sm-3'>
<TeamsInput
placeholder='teamName1 teamName2'
teams={this.state.teams}
onChange={this.handleTeamsInput}
/>
</div>
<div className='col-xs-12 col-sm-3'>
<input
id={`groups-${this.props.id}`}
className='form-control'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ import {bindActionCreators} from 'redux';
import {getCustomEmojisInText} from 'mattermost-redux/actions/emojis';

import {getProfilesByIds} from 'mattermost-redux/actions/users';
import {getTeam} from 'mattermost-redux/actions/teams';

import CustomAttribute from './custom_attribute.jsx';

function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
getProfilesByIds,
getTeam,
getCustomEmojisInText,
}, dispatch),
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export default class CustomAttributesSettings extends React.Component {
id={key}
name={value.Name}
users={value.UserIDs}
teams={value.TeamIDs}
groups={value.GroupIDs ? value.GroupIDs.join(' ') : ''}
markdownPreview={true}
onChange={this.handleChange}
Expand All @@ -73,7 +74,7 @@ export default class CustomAttributesSettings extends React.Component {
this.props.setSaveNeeded();
}

handleChange = ({id, name, users, groups}) => {
handleChange = ({id, name, users, teams, groups}) => {
let userIds = [];
if (users) {
userIds = users.map((v) => {
Expand All @@ -84,9 +85,21 @@ export default class CustomAttributesSettings extends React.Component {
return v;
});
}

let teamIds = [];
if (teams) {
teamIds = teams.map((team) => {
if (team.id) {
return team.id;
}
return team;
});
}

this.state.attributes.set(id, {
Name: name,
UserIDs: userIds,
TeamIDs: teamIds,
GroupIDs: groups ? groups.split(' ') : '',
});

Expand Down
Loading