Skip to content

Commit

Permalink
Merge pull request #1589 from entando/ENG-5427_GravatarManagement
Browse files Browse the repository at this point in the history
ENG-5427 Gravatar Management in My Profile
  • Loading branch information
ryanjpburgos authored Jan 18, 2024
2 parents a426b66 + f8475cf commit 4f5253e
Show file tree
Hide file tree
Showing 15 changed files with 156 additions and 24 deletions.
1 change: 1 addition & 0 deletions src/locales/en.js
Original file line number Diff line number Diff line change
Expand Up @@ -472,6 +472,7 @@ export default {
'fileBrowser.createFolder': 'Create folder',
'fileBrowser.uploadFiles': 'Upload files',
'fileBrowser.uploadFileComplete': 'Upload file complete',
'fileBrowser.removeFileComplete': 'Remove file complete',
'fileBrowser.uploadFileError': 'Error uploading file - {errmsg}',
'fileBrowser.downloadFile': 'Download',
'fileBrowser.newFolder': 'New folder name',
Expand Down
1 change: 1 addition & 0 deletions src/locales/it.js
Original file line number Diff line number Diff line change
Expand Up @@ -472,6 +472,7 @@ export default {
'fileBrowser.createFolder': 'Crea cartella',
'fileBrowser.uploadFiles': 'Upload File',
'fileBrowser.uploadFileComplete': 'Upload del file completato',
'fileBrowser.removeFileComplete': 'Rimozione file completata',
'fileBrowser.uploadFileError': 'Errore nell\'upload del file - {errmsg}',
'fileBrowser.downloadFile': 'Download',
'fileBrowser.newFolder': 'Nome nuova cartella',
Expand Down
1 change: 1 addition & 0 deletions src/locales/pt.js
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,7 @@ export default {
'fileBrowser.createFolder': 'Criar pasta',
'fileBrowser.uploadFiles': 'Upload Files',
'fileBrowser.uploadFileComplete': 'Upload de arquivo concluído',
'fileBrowser.removeFileComplete': 'Remover arquivo concluído',
'fileBrowser.uploadFileError': 'Erro no upload de arquivo - {errmsg}',
'fileBrowser.downloadFile': 'Download',
'fileBrowser.newFolder': 'Nova da Nova Pasta',
Expand Down
48 changes: 42 additions & 6 deletions src/state/avatar/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { postAvatar, getAvatar, deleteAvatar } from 'api/avatar';
import { getBase64 } from 'state/file-browser/actions';
import { toggleLoading } from 'state/loading/actions';
import { addToast, addErrors, TOAST_SUCCESS, TOAST_ERROR } from '@entando/messages';
import { SET_AVATAR_FILE_NAME } from './types';
import { SET_AVATAR_FILE_NAME, SET_USE_GRAVATAR } from './types';

export const setAvatarFilename = filename => ({
type: SET_AVATAR_FILE_NAME,
Expand All @@ -11,6 +11,13 @@ export const setAvatarFilename = filename => ({
},
});

export const setUseGravatar = useGravatar => ({
type: SET_USE_GRAVATAR,
payload: {
useGravatar,
},
});


export const createFileObject = async (avatar) => {
const base64 = await getBase64(avatar);
Expand All @@ -24,8 +31,10 @@ export const uploadAvatar =
dispatch(toggleLoading(loader));
const requestObject = await createFileObject(avatar);
const response = await postAvatar(requestObject);
const avatarCreated = await response.json();
dispatch(setAvatarFilename(`${avatarCreated.payload.filename}?${Date.now()}`));
const { payload } = await response.json();
const { filename } = payload;
dispatch(setUseGravatar(false));
dispatch(setAvatarFilename(`${filename}?${Date.now()}`));
dispatch(toggleLoading(loader));
dispatch(addToast({ id: 'fileBrowser.uploadFileComplete' }, TOAST_SUCCESS));
} catch (error) {
Expand All @@ -36,12 +45,38 @@ export const uploadAvatar =
}
};

export const setGravatar = (loader = 'useGravatar') =>
async (dispatch) => {
try {
dispatch(toggleLoading(loader));
const response = await postAvatar({ useGravatar: true });
const { payload } = await response.json();
const { useGravatar } = payload;
dispatch(setUseGravatar(useGravatar));
dispatch(addToast({ id: 'fileBrowser.uploadFileComplete' }, TOAST_SUCCESS));
dispatch(toggleLoading(loader));
} catch (error) {
dispatch(toggleLoading(loader));
const message = { id: 'fileBrowser.uploadFileError', values: { errmsg: error } };
dispatch(addErrors(error));
dispatch(addToast(message, TOAST_ERROR));
}
};

export const fetchAvatar = (loader = 'fetchAvatar') => async (dispatch) => {
try {
dispatch(toggleLoading(loader));
const response = await getAvatar();
const avatar = await response.json();
if (avatar.payload.filename) dispatch(setAvatarFilename(`${avatar.payload.filename}?${Date.now()}`));
const { ok, status } = response;
if (ok) {
const { payload } = await response.json();
const { filename, useGravatar } = payload;
dispatch(setUseGravatar(useGravatar));
if (filename) dispatch(setAvatarFilename(`${filename}?${Date.now()}`));
} else if (!ok && status === 404) {
dispatch(setUseGravatar(false));
dispatch(setAvatarFilename(''));
}
dispatch(toggleLoading(loader));
} catch (error) {
dispatch(toggleLoading(loader));
Expand All @@ -58,8 +93,9 @@ export const removeAvatar =
dispatch(toggleLoading(loader));
await deleteAvatar();
dispatch(setAvatarFilename(''));
dispatch(setUseGravatar(false));
dispatch(toggleLoading(loader));
dispatch(addToast({ id: 'fileBrowser.uploadFileComplete' }, TOAST_SUCCESS));
dispatch(addToast({ id: 'fileBrowser.removeFileComplete' }, TOAST_SUCCESS));
} catch (error) {
dispatch(toggleLoading(loader));
const message = { id: 'fileBrowser.uploadFileError', values: { errmsg: error } };
Expand Down
14 changes: 13 additions & 1 deletion src/state/avatar/reducer.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { combineReducers } from 'redux';
import { SET_AVATAR_FILE_NAME } from './types';
import { SET_AVATAR_FILE_NAME, SET_USE_GRAVATAR } from './types';

const initialState = {
filename: '',
useGravatar: false,
};

const filename = (state = initialState.filename, action = {}) => {
Expand All @@ -15,7 +16,18 @@ const filename = (state = initialState.filename, action = {}) => {
}
};

const useGravatar = (state = initialState.useGravatar, action = {}) => {
switch (action.type) {
case SET_USE_GRAVATAR: {
return action.payload.useGravatar;
}
default:
return state;
}
};

export default combineReducers({
filename,
useGravatar,
});

5 changes: 5 additions & 0 deletions src/state/avatar/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,8 @@ export const getAvatarFilename = createSelector(
avatar => avatar.filename,
);

export const getUseGravatar = createSelector(
getAvatar,
avatar => avatar.useGravatar,
);

2 changes: 1 addition & 1 deletion src/state/avatar/types.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
// eslint-disable-next-line import/prefer-default-export
export const SET_AVATAR_FILE_NAME = 'avatar/filename';
export const SET_USE_GRAVATAR = 'avatar/useGravatar';

13 changes: 13 additions & 0 deletions src/state/profile-types/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
TYPE_LIST,
TYPE_COMPOSITE,
} from 'state/profile-types/const';
import { getUserProfile } from 'state/user-profile/selectors';

const NO_ATTRIBUTE_FOR_TYPE_MONOLIST = [TYPE_LIST, TYPE_MONOLIST];

Expand Down Expand Up @@ -180,3 +181,15 @@ export const getProfileTypeReferencesStatus = createSelector([getProfileTypeRefe
}
return { type: 'success', status: 'ready', profileTypesCode: [] };
});

export const getUserProfileEmail = createSelector(
[getSelectedProfileTypeAttributes, getUserProfile],
(selectedUserProfileAttributes, userProfile) => {
const emailAttribute = selectedUserProfileAttributes
&& selectedUserProfileAttributes.find(attribute => attribute.roles && attribute.roles.find(role => role.code === 'userprofile:email'));
const { attributes } = userProfile;
const userProfileEmailAttribute =
attributes.find(attr => emailAttribute && attr.code === emailAttribute.code);
return userProfileEmailAttribute && userProfileEmailAttribute.value;
},
);
2 changes: 1 addition & 1 deletion src/state/user-profile/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@
export const getUserProfile = state => state.userProfile;
export const getUserEmail = (state) => {
const { attributes } = state.userProfile;
const emailAttribute = attributes.find(attr => attr.code === 'email');
const emailAttribute = attributes && attributes.find(attr => attr.code === 'email');
return emailAttribute && emailAttribute.value;
};
9 changes: 6 additions & 3 deletions src/ui/users/common/ProfileImageUploader.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const publicUrl = process.env.PUBLIC_URL;
const toMd5 = string => md5(string.trim().toLowerCase());

const ProfileImageUploader = ({
image, onChange, gravatarEmail, editable,
image, onChange, gravatarEmail, editable, useGravatar, onSetGravatar,
}) => {
const [edit, setEdit] = useState(false);
const inputFileRef = useRef(null);
Expand Down Expand Up @@ -44,7 +44,7 @@ const ProfileImageUploader = ({
let userPicture = `${publicUrl}/images/user-icon.svg`;
if (edit) {
userPicture = `${publicUrl}/images/user-edit.svg`;
} else if (image === GRAVATAR && gravatarEmail) {
} else if (useGravatar && gravatarEmail) {
userPicture = `${GRAVATAR_URL}/${toMd5(gravatarEmail)}`;
} else if (image) {
userPicture = `${imageProvider}/${image}`;
Expand All @@ -67,7 +67,7 @@ const ProfileImageUploader = ({
</Dropdown.Toggle>
<Dropdown.Menu>
<MenuItem eventKey="1" onClick={handleUploadClick}>Upload Image</MenuItem>
{ gravatarEmail && <MenuItem eventKey="2" onClick={() => onChange(GRAVATAR)}>Use Gravatar</MenuItem>}
{ gravatarEmail && <MenuItem eventKey="2" onClick={() => onSetGravatar(GRAVATAR)}>Use Gravatar</MenuItem>}
<MenuItem eventKey="3" onClick={() => onChange('')}>Remove Image</MenuItem>
</Dropdown.Menu>
</Dropdown>
Expand All @@ -79,13 +79,16 @@ ProfileImageUploader.propTypes = {
image: PropTypes.string,
gravatarEmail: PropTypes.string,
onChange: PropTypes.func.isRequired,
onSetGravatar: PropTypes.func.isRequired,
editable: PropTypes.bool,
useGravatar: PropTypes.bool,
};

ProfileImageUploader.defaultProps = {
image: '',
gravatarEmail: '',
editable: false,
useGravatar: false,
};

export default ProfileImageUploader;
7 changes: 6 additions & 1 deletion src/ui/users/my-profile/MyProfileEditForm.js
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ export class MyProfileEditFormBody extends Component {
render() {
const {
profileTypesAttributes, defaultLanguage, languages, intl, userEmail, onChangeProfilePicture,
avatar,
avatar, useGravatar, onSetGravatar,
} = this.props;

const { editMode } = this.state;
Expand Down Expand Up @@ -233,6 +233,8 @@ export class MyProfileEditFormBody extends Component {
image={avatar}
onChange={onChangeProfilePicture}
gravatarEmail={userEmail}
onSetGravatar={onSetGravatar}
useGravatar={useGravatar}
editable
/>

Expand Down Expand Up @@ -281,6 +283,7 @@ MyProfileEditFormBody.propTypes = {
onSubmit: PropTypes.func.isRequired,
onCancel: PropTypes.func.isRequired,
username: PropTypes.string.isRequired,
onSetGravatar: PropTypes.string.isRequired,
profileTypesAttributes: PropTypes.arrayOf(PropTypes.shape({
type: PropTypes.string,
code: PropTypes.string,
Expand Down Expand Up @@ -322,13 +325,15 @@ MyProfileEditFormBody.propTypes = {
}),
onChangeProfilePicture: PropTypes.func.isRequired,
avatar: PropTypes.string,
useGravatar: PropTypes.bool,
};

MyProfileEditFormBody.defaultProps = {
profileTypesAttributes: [],
userEmail: undefined,
userProfileForm: {},
avatar: '',
useGravatar: false,
};

const MyProfileEditForm = reduxForm({
Expand Down
12 changes: 7 additions & 5 deletions src/ui/users/my-profile/MyProfileEditFormContainer.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ import { getUsername } from '@entando/apimanager';

import { fetchMyUserProfile, updateMyUserProfile } from 'state/user-profile/actions';
import { fetchLanguages } from 'state/languages/actions';
import { fetchAvatar, removeAvatar } from 'state/avatar/actions';
import { fetchAvatar, removeAvatar, setGravatar } from 'state/avatar/actions';
import { getDefaultLanguage, getActiveLanguages } from 'state/languages/selectors';
import { getSelectedProfileTypeAttributes } from 'state/profile-types/selectors';
import { getSelectedProfileTypeAttributes, getUserProfileEmail } from 'state/profile-types/selectors';
import MyProfileEditForm from 'ui/users/my-profile/MyProfileEditForm';
import { getPayloadForForm } from 'helpers/entities';
import { getUserProfile, getUserEmail } from 'state/user-profile/selectors';
import { getUserProfile } from 'state/user-profile/selectors';
import { getUserProfileForm } from 'state/forms/selectors';
import { getAvatarFilename } from 'state/avatar/selectors';
import { getAvatarFilename, getUseGravatar } from 'state/avatar/selectors';

export const mapStateToProps = state => ({
username: getUsername(state),
Expand All @@ -24,9 +24,10 @@ export const mapStateToProps = state => ({
getDefaultLanguage(state),
getActiveLanguages(state),
),
userEmail: getUserEmail(state),
userEmail: getUserProfileEmail(state),
userProfileForm: getUserProfileForm(state),
avatar: getAvatarFilename(state),
useGravatar: getUseGravatar(state),
});

export const mapDispatchToProps = dispatch => ({
Expand All @@ -46,6 +47,7 @@ export const mapDispatchToProps = dispatch => ({
dispatch(removeAvatar());
}
},
onSetGravatar: () => dispatch(setGravatar()),
});

export default connect(
Expand Down
7 changes: 4 additions & 3 deletions test/state/avatar/actions.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ describe('uploadAvatar', () => {
expect(dispatch).toHaveBeenCalledWith({ payload: { filename: `test.png?${new Date('2019-04-07T10:20:30Z').getTime()}` }, type: 'avatar/filename' });
expect(dispatch).toHaveBeenCalledWith({ payload: { id: 'uploadAvatar' }, type: 'loading/toggle-loading' });
expect(dispatch).toHaveBeenCalledWith({ payload: { message: { id: 'fileBrowser.uploadFileComplete' }, type: 'success' }, type: 'toasts/add-toast' });
expect(dispatch).toHaveBeenCalledTimes(4);
expect(dispatch).toHaveBeenCalledTimes(5);
});

it('should handle avatar upload error', async () => {
Expand Down Expand Up @@ -54,12 +54,13 @@ describe('fetchAvatar', () => {

it('should retrieve the avatar successfully', async () => {
const dispatch = jest.fn();
jest.spyOn(apiHelper, 'getAvatar').mockResolvedValue({ json: () => Promise.resolve({ payload: { filename: 'test.png' } }) });
jest.spyOn(apiHelper, 'getAvatar').mockResolvedValue({ ok: true, json: () => Promise.resolve({ payload: { filename: 'test.png', useGravatar: false } }) });
await fetchAvatar()(dispatch);
expect(dispatch).toHaveBeenCalledWith({ payload: { id: 'fetchAvatar' }, type: 'loading/toggle-loading' });
expect(dispatch).toHaveBeenCalledWith({ payload: { useGravatar: false }, type: 'avatar/useGravatar' });
expect(dispatch).toHaveBeenCalledWith({ payload: { filename: `test.png?${new Date('2019-04-07T10:20:30Z').getTime()}` }, type: 'avatar/filename' });
expect(dispatch).toHaveBeenCalledWith({ payload: { id: 'fetchAvatar' }, type: 'loading/toggle-loading' });
expect(dispatch).toHaveBeenCalledTimes(3);
expect(dispatch).toHaveBeenCalledTimes(4);
});

it('should handle avatar fetch error', async () => {
Expand Down
15 changes: 12 additions & 3 deletions test/state/avatar/selectors.test.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { createStore } from 'redux';
import { getAvatar, getAvatarFilename } from 'state/avatar/selectors';
import { getAvatar, getAvatarFilename, getUseGravatar } from 'state/avatar/selectors';
import rootReducer from 'state/rootReducer';

describe('getAvatar', () => {
it('should return the avatar state', () => {
const initialState = { avatar: { filename: 'test.jpg' } };
const initialState = { avatar: { filename: 'test.jpg', useGravatar: false } };
const store = createStore(rootReducer, initialState);
const result = getAvatar(store.getState());
expect(result).toEqual(initialState.avatar);
Expand All @@ -13,10 +13,19 @@ describe('getAvatar', () => {

describe('getAvatarFilename selector', () => {
it('should return the avatar filename', () => {
const initialState = { avatar: { filename: 'test.jpg' } };
const initialState = { avatar: { filename: 'test.jpg', useGravatar: false } };
const store = createStore(rootReducer, initialState);
const result = getAvatarFilename(store.getState());
expect(result).toBe('test.jpg');
});
});

describe('getUseGravatar selector', () => {
it('should return the useGravatar value', () => {
const initialState = { avatar: { filename: 'test.jpg', useGravatar: false } };
const store = createStore(rootReducer, initialState);
const result = getUseGravatar(store.getState());
expect(result).toBeFalsy();
});
});

Loading

0 comments on commit 4f5253e

Please sign in to comment.