Unverified Commit fc1f1154 authored by Jesús Espino's avatar Jesús Espino Committed by GitHub
Browse files

MM-12067: Allow to remove the user profile picture (#1785)

* MM-12067: Allow to remove the user profile picture

* Upgrade to the last mattermost-redux version

* Simplification of defaultImageURLForUser util function

* Preventing default directly from the SettingPicture component
parent cb385d20
......@@ -713,3 +713,140 @@ exports[`components/SettingItemMin should match snapshot, team icon on source 1`
</li>
</ul>
`;
exports[`components/SettingItemMin should match snapshot, user icon on source 1`] = `
<ul
className="section-max form-horizontal"
>
<li
className="col-xs-12 section-title"
>
Profile Picture
</li>
<li
className="col-xs-offset-3 col-xs-8"
>
<ul
className="setting-list"
>
<li
className="setting-list-item"
>
<div
className="profile-img__container"
>
<div
className="img-preview__image"
>
<img
alt="profile image"
className="profile-img"
src="http://localhost:8065/api/v4/users/src_id"
/>
</div>
<OverlayTrigger
defaultOverlayShown={false}
delayShow={400}
overlay={
<Tooltip
bsClass="tooltip"
id="removeIcon"
placement="right"
>
<FormattedMessage
defaultMessage="Remove profile picture"
id="setting_picture.remove_profile_picture"
values={Object {}}
/>
</Tooltip>
}
placement="right"
trigger={
Array [
"hover",
"focus",
]
}
>
<a
className="profile-img__remove"
onClick={[Function]}
>
<span>
×
</span>
</a>
</OverlayTrigger>
</div>
</li>
<li
className="setting-list-item padding-top x2"
>
<FormattedMessage
defaultMessage="Upload a picture in BMP, JPG or PNG format. Maximum file size: {max}"
id="setting_picture.help.profile"
values={
Object {
"max": "200MB",
}
}
/>
</li>
<li
className="setting-list-item"
>
<hr />
<FormError
error={null}
errors={
Array [
"",
"",
]
}
type="modal"
/>
<div
className="btn btn-sm btn-primary btn-file sel-btn"
disabled={false}
>
<FormattedMessage
defaultMessage="Select"
id="setting_picture.select"
values={Object {}}
/>
<input
accept=".jpg,.png,.bmp"
disabled={false}
onChange={[Function]}
type="file"
/>
</div>
<a
className="btn btn-sm btn-inactive disabled"
onClick={[Function]}
>
<FormattedMessage
defaultMessage="Save"
id="setting_picture.save"
values={Object {}}
/>
</a>
<a
className="btn btn-sm theme"
href="#"
onClick={[Function]}
>
<FormattedMessage
defaultMessage="Cancel"
id="setting_picture.cancel"
values={Object {}}
/>
</a>
</li>
</ul>
</li>
</ul>
`;
......@@ -24,10 +24,12 @@ export default class SettingPicture extends Component {
clientError: PropTypes.string,
serverError: PropTypes.string,
src: PropTypes.string,
defaultImageSrc: PropTypes.string,
file: PropTypes.object,
loadingPicture: PropTypes.bool,
submitActive: PropTypes.bool,
onRemove: PropTypes.func,
onSetDefault: PropTypes.func,
onSubmit: PropTypes.func,
title: PropTypes.string,
onFileChange: PropTypes.func,
......@@ -42,6 +44,7 @@ export default class SettingPicture extends Component {
this.state = {
image: null,
removeSrc: false,
setDefaultSrc: false,
};
}
......@@ -60,15 +63,18 @@ export default class SettingPicture extends Component {
}
handleCancel = (e) => {
this.setState({removeSrc: false});
this.setState({removeSrc: false, setDefaultSrc: false});
this.props.updateSection(e);
}
handleSave = (e) => {
e.preventDefault();
if (this.state.removeSrc) {
this.props.onRemove(e);
this.props.onRemove();
} else if (this.state.setDefaultSrc) {
this.props.onSetDefault();
} else {
this.props.onSubmit(e);
this.props.onSubmit();
}
}
......@@ -77,6 +83,11 @@ export default class SettingPicture extends Component {
this.setState({removeSrc: true});
}
handleSetDefaultSrc = (e) => {
e.preventDefault();
this.setState({setDefaultSrc: true});
}
handleFileChange = (e) => {
this.setState({removeSrc: false});
this.props.onFileChange(e);
......@@ -147,18 +158,16 @@ export default class SettingPicture extends Component {
return {transform, transformOrigin};
}
render() {
renderImg = () => {
const imageContext = this.props.imageContext;
let img;
if (this.props.file) {
const imageStyles = {
backgroundImage: 'url(' + this.state.image + ')',
...this.state.orientationStyles,
};
img = (
return (
<div className={`${imageContext}-img-preview`}>
<div className='img-preview__image'>
<div
......@@ -169,49 +178,82 @@ export default class SettingPicture extends Component {
</div>
</div>
);
} else if (this.props.src && !this.state.removeSrc) {
img = (
}
if (this.state.setDefaultSrc) {
return (
<img
className={`${imageContext}-img`}
alt={`${imageContext} image`}
src={this.props.defaultImageSrc}
/>
);
}
if (this.props.src && !this.state.removeSrc) {
const imageElement = (
<img
className={`${imageContext}-img`}
alt={`${imageContext} image`}
src={this.props.src}
/>
);
if (!this.props.onRemove && !this.props.onSetDefault) {
return imageElement;
}
let title;
let handler;
if (this.props.onRemove) {
img = (
<div className={`${imageContext}-img__container`}>
<div className='img-preview__image'>
<img
className={`${imageContext}-img`}
alt={`${imageContext} image`}
src={this.props.src}
/>
</div>
<OverlayTrigger
trigger={['hover', 'focus']}
delayShow={Constants.OVERLAY_TIME_DELAY}
placement='right'
overlay={(
<Tooltip id='removeIcon'>
<FormattedMessage
id='setting_picture.remove'
defaultMessage='Remove this icon'
/>
</Tooltip>
)}
>
<a
className={`${imageContext}-img__remove`}
onClick={this.handleRemoveSrc}
>
<span>{'×'}</span>
</a>
</OverlayTrigger>
</div>
title = (
<FormattedMessage
id='setting_picture.remove'
defaultMessage='Remove this icon'
/>
);
handler = this.handleRemoveSrc;
} else if (this.props.onSetDefault) {
title = (
<FormattedMessage
id='setting_picture.remove_profile_picture'
defaultMessage='Remove profile picture'
/>
);
handler = this.handleSetDefaultSrc;
}
return (
<div className={`${imageContext}-img__container`}>
<div className='img-preview__image'>
{imageElement}
</div>
<OverlayTrigger
trigger={['hover', 'focus']}
delayShow={Constants.OVERLAY_TIME_DELAY}
placement='right'
overlay={(
<Tooltip id='removeIcon'>
{title}
</Tooltip>
)}
>
<a
className={`${imageContext}-img__remove`}
onClick={handler}
>
<span>{'×'}</span>
</a>
</OverlayTrigger>
</div>
);
}
return null;
}
render() {
const imageContext = this.props.imageContext;
const img = this.renderImg();
let confirmButton;
let selectButtonSpinner;
......@@ -232,7 +274,7 @@ export default class SettingPicture extends Component {
fileInputDisabled = true;
} else {
let confirmButtonClass = 'btn btn-sm';
if (this.props.submitActive || this.state.removeSrc) {
if (this.props.submitActive || this.state.removeSrc || this.state.setDefaultSrc) {
confirmButtonClass += ' btn-primary';
} else {
confirmButtonClass += ' btn-inactive disabled';
......
......@@ -37,6 +37,15 @@ describe('components/SettingItemMin', () => {
expect(wrapper).toMatchSnapshot();
});
test('should match snapshot, user icon on source', () => {
const props = {...baseProps, onSetDefault: jest.fn()};
const wrapper = shallow(
<SettingPicture {...props}/>
);
expect(wrapper).toMatchSnapshot();
});
test('should match snapshot, team icon on source', () => {
const props = {...baseProps, onRemove: jest.fn(), imageContext: 'team'};
const wrapper = shallow(
......@@ -105,7 +114,18 @@ describe('components/SettingItemMin', () => {
wrapper.instance().handleSave(evt);
expect(props.onRemove).toHaveBeenCalledTimes(1);
expect(props.onRemove).toHaveBeenCalledWith(evt);
});
test('should call props.onSetDefault on handleSave', () => {
const props = {...baseProps, onSetDefault: jest.fn()};
const wrapper = shallow(
<SettingPicture {...props}/>
);
wrapper.setState({setDefaultSrc: true});
const evt = {preventDefault: jest.fn()};
wrapper.instance().handleSave(evt);
expect(props.onSetDefault).toHaveBeenCalledTimes(1);
});
test('should match state and call props.onSubmit on handleSave', () => {
......@@ -118,7 +138,6 @@ describe('components/SettingItemMin', () => {
wrapper.instance().handleSave(evt);
expect(props.onSubmit).toHaveBeenCalledTimes(1);
expect(props.onSubmit).toHaveBeenCalledWith(evt);
wrapper.update();
expect(wrapper.state('removeSrc')).toEqual(false);
......
......@@ -246,9 +246,7 @@ export default class GeneralTab extends React.Component {
}
}
handleTeamIconSubmit = async (e) => {
e.preventDefault();
handleTeamIconSubmit = async () => {
if (!this.state.teamIconFile) {
return;
}
......@@ -279,9 +277,7 @@ export default class GeneralTab extends React.Component {
}
}
handleTeamIconRemove = async (e) => {
e.preventDefault();
handleTeamIconRemove = async () => {
this.setState({
loadingIcon: true,
clientError: '',
......
......@@ -3,7 +3,7 @@
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import {getMe, sendVerificationEmail} from 'mattermost-redux/actions/users';
import {getMe, sendVerificationEmail, setDefaultProfileImage} from 'mattermost-redux/actions/users';
import {getConfig} from 'mattermost-redux/selectors/entities/general';
import UserSettingsGeneralTab from './user_settings_general.jsx';
......@@ -41,6 +41,7 @@ function mapDispatchToProps(dispatch) {
actions: bindActionCreators({
getMe,
sendVerificationEmail,
setDefaultProfileImage,
}, dispatch),
};
}
......
......@@ -100,6 +100,7 @@ class UserSettingsGeneralTab extends React.Component {
actions: PropTypes.shape({
getMe: PropTypes.func.isRequired,
sendVerificationEmail: PropTypes.func.isRequred,
setDefaultProfileImage: PropTypes.func.isRequred,
}).isRequired,
sendEmailNotifications: PropTypes.bool,
requireEmailVerification: PropTypes.bool,
......@@ -283,9 +284,23 @@ class UserSettingsGeneralTab extends React.Component {
);
}
submitPicture = (e) => {
e.preventDefault();
setDefaultProfilePicture = async () => {
try {
await this.props.actions.setDefaultProfileImage(this.props.user.id);
this.updateSection('');
this.submitActive = false;
} catch (err) {
let serverError;
if (err.message) {
serverError = err.message;
} else {
serverError = err;
}
this.setState({serverError, emailError: '', clientError: '', sectionIsSaving: false});
}
}
submitPicture = () => {
if (!this.state.pictureFile) {
return;
}
......@@ -1181,7 +1196,9 @@ class UserSettingsGeneralTab extends React.Component {
<SettingPicture
title={formatMessage(holders.profilePicture)}
onSubmit={this.submitPicture}
onSetDefault={user.last_picture_update > 0 ? this.setDefaultProfilePicture : null}
src={Utils.imageURLForUser(user)}
defaultImageSrc={Utils.defaultImageURLForUser(user.id)}
serverError={serverError}
clientError={clientError}
updateSection={(e) => {
......
......@@ -2413,6 +2413,7 @@
"setting_picture.help.profile": "Upload a picture in BMP, JPG or PNG format. Maximum file size: {max}",
"setting_picture.help.team": "Upload a team icon in BMP, JPG or PNG format.\nSquare images with a solid background color are recommended.",
"setting_picture.remove": "Remove this icon",
"setting_picture.remove_profile_picture": "Remove profile picture",
"setting_picture.save": "Save",
"setting_picture.select": "Select",
"setting_upload.import": "Import",
......
......@@ -107,6 +107,33 @@
height: 128px;
width: 128px;
}
.profile-img__container {
height: 128px;
position: relative;
width: 128px;
}
.profile-img__remove {
@include border-radius(50%);
@include single-transition(all, .15s, ease);
background: $black;
color: $white;
height: 24px;
line-height: 23px;
position: absolute;
right: -8px;
text-align: center;
text-decoration: none;
top: -8px;
width: 24px;
span {
font-size: 22px;
left: .5px;
position: relative;
}
}
.team-img-preview,
.team-img__container {
......
......@@ -1316,6 +1316,10 @@ export function imageURLForUser(userIdOrObject) {
return Client4.getUsersRoute() + '/' + userIdOrObject.id + '/image?_=' + (userIdOrObject.last_picture_update || 0);
}
export function defaultImageURLForUser(userId) {
return Client4.getUsersRoute() + '/' + userId + '/image/default';
}
// in contrast to Client4.getTeamIconUrl, for ui logic this function returns null if last_team_icon_update is unset
export function imageURLForTeam(teamIdOrObject) {
if (typeof teamIdOrObject == 'string') {
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment