avatarChooser.tsx 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231
  1. import * as React from 'react';
  2. import styled from '@emotion/styled';
  3. import {addErrorMessage, addSuccessMessage} from 'app/actionCreators/indicator';
  4. import {Client} from 'app/api';
  5. import Avatar from 'app/components/avatar';
  6. import AvatarCropper from 'app/components/avatarCropper';
  7. import Button from 'app/components/button';
  8. import ExternalLink from 'app/components/links/externalLink';
  9. import LoadingError from 'app/components/loadingError';
  10. import LoadingIndicator from 'app/components/loadingIndicator';
  11. import {Panel, PanelBody, PanelHeader} from 'app/components/panels';
  12. import Well from 'app/components/well';
  13. import {t} from 'app/locale';
  14. import {AvatarUser, Organization, Team} from 'app/types';
  15. import withApi from 'app/utils/withApi';
  16. import RadioGroup from 'app/views/settings/components/forms/controls/radioGroup';
  17. type Model = Pick<AvatarUser, 'avatar'>;
  18. type AvatarType = Required<Model>['avatar']['avatarType'];
  19. type AvatarChooserType = 'user' | 'team' | 'organization';
  20. type DefaultProps = {
  21. onSave: (model: Model) => void;
  22. allowGravatar?: boolean;
  23. allowLetter?: boolean;
  24. allowUpload?: boolean;
  25. type?: AvatarChooserType;
  26. };
  27. type Props = {
  28. api: Client;
  29. endpoint: string;
  30. model: Model;
  31. disabled?: boolean;
  32. savedDataUrl?: string;
  33. isUser?: boolean;
  34. } & DefaultProps;
  35. type State = {
  36. model: Model;
  37. hasError: boolean;
  38. savedDataUrl?: string | null;
  39. dataUrl?: string | null;
  40. };
  41. class AvatarChooser extends React.Component<Props, State> {
  42. static defaultProps: DefaultProps = {
  43. allowGravatar: true,
  44. allowLetter: true,
  45. allowUpload: true,
  46. type: 'user',
  47. onSave: () => {},
  48. };
  49. state: State = {
  50. model: this.props.model,
  51. savedDataUrl: null,
  52. dataUrl: null,
  53. hasError: false,
  54. };
  55. UNSAFE_componentWillReceiveProps(nextProps: Props) {
  56. // Update local state if defined in props
  57. if (typeof nextProps.model !== 'undefined') {
  58. this.setState({model: nextProps.model});
  59. }
  60. }
  61. updateState(model: Model) {
  62. this.setState({model});
  63. }
  64. handleError(msg: string) {
  65. addErrorMessage(msg);
  66. }
  67. handleSuccess(model: Model) {
  68. const {onSave} = this.props;
  69. this.setState({model});
  70. onSave(model);
  71. addSuccessMessage(t('Successfully saved avatar preferences'));
  72. }
  73. handleSaveSettings = (ev: React.MouseEvent) => {
  74. const {endpoint, api} = this.props;
  75. const {model, dataUrl} = this.state;
  76. ev.preventDefault();
  77. let data = {};
  78. const avatarType = model && model.avatar ? model.avatar.avatarType : undefined;
  79. const avatarPhoto = dataUrl ? dataUrl.split(',')[1] : undefined;
  80. data = {
  81. avatar_photo: avatarPhoto,
  82. avatar_type: avatarType,
  83. };
  84. api.request(endpoint, {
  85. method: 'PUT',
  86. data,
  87. success: resp => {
  88. this.setState({savedDataUrl: this.state.dataUrl});
  89. this.handleSuccess(resp);
  90. },
  91. error: this.handleError.bind(this, 'There was an error saving your preferences.'),
  92. });
  93. };
  94. handleChange = (id: AvatarType) =>
  95. this.updateState({
  96. ...this.state.model,
  97. avatar: {avatarUuid: this.state.model.avatar?.avatarUuid ?? '', avatarType: id},
  98. });
  99. render() {
  100. const {
  101. allowGravatar,
  102. allowUpload,
  103. allowLetter,
  104. savedDataUrl,
  105. type,
  106. isUser,
  107. disabled,
  108. } = this.props;
  109. const {hasError, model} = this.state;
  110. if (hasError) {
  111. return <LoadingError />;
  112. }
  113. if (!model) {
  114. return <LoadingIndicator />;
  115. }
  116. const avatarType = model.avatar?.avatarType ?? 'letter_avatar';
  117. const isLetter = avatarType === 'letter_avatar';
  118. const isTeam = type === 'team';
  119. const isOrganization = type === 'organization';
  120. const choices: [AvatarType, string][] = [];
  121. if (allowLetter) {
  122. choices.push(['letter_avatar', t('Use initials')]);
  123. }
  124. if (allowUpload) {
  125. choices.push(['upload', t('Upload an image')]);
  126. }
  127. if (allowGravatar) {
  128. choices.push(['gravatar', t('Use Gravatar')]);
  129. }
  130. return (
  131. <Panel>
  132. <PanelHeader>{t('Avatar')}</PanelHeader>
  133. <PanelBody>
  134. <AvatarForm>
  135. <AvatarGroup inline={isLetter}>
  136. <RadioGroup
  137. style={{flex: 1}}
  138. choices={choices}
  139. value={avatarType}
  140. label={t('Avatar Type')}
  141. onChange={this.handleChange}
  142. disabled={disabled}
  143. />
  144. {isLetter && (
  145. <Avatar
  146. gravatar={false}
  147. style={{width: 90, height: 90}}
  148. user={isUser ? (model as AvatarUser) : undefined}
  149. organization={isOrganization ? (model as Organization) : undefined}
  150. team={isTeam ? (model as Team) : undefined}
  151. />
  152. )}
  153. </AvatarGroup>
  154. <AvatarUploadSection>
  155. {allowGravatar && avatarType === 'gravatar' && (
  156. <Well>
  157. {t('Gravatars are managed through ')}
  158. <ExternalLink href="http://gravatar.com">Gravatar.com</ExternalLink>
  159. </Well>
  160. )}
  161. {model.avatar && avatarType === 'upload' && (
  162. <AvatarCropper
  163. {...this.props}
  164. type={type!}
  165. model={model}
  166. savedDataUrl={savedDataUrl}
  167. updateDataUrlState={dataState => this.setState(dataState)}
  168. />
  169. )}
  170. <AvatarSubmit className="form-actions">
  171. <Button
  172. type="button"
  173. priority="primary"
  174. onClick={this.handleSaveSettings}
  175. disabled={disabled}
  176. >
  177. {t('Save Avatar')}
  178. </Button>
  179. </AvatarSubmit>
  180. </AvatarUploadSection>
  181. </AvatarForm>
  182. </PanelBody>
  183. </Panel>
  184. );
  185. }
  186. }
  187. const AvatarGroup = styled('div')<{inline: boolean}>`
  188. display: flex;
  189. flex-direction: ${p => (p.inline ? 'row' : 'column')};
  190. `;
  191. const AvatarForm = styled('div')`
  192. line-height: 1.5em;
  193. padding: 1em 1.25em;
  194. `;
  195. const AvatarSubmit = styled('fieldset')`
  196. display: flex;
  197. justify-content: flex-end;
  198. margin-top: 1em;
  199. `;
  200. const AvatarUploadSection = styled('div')`
  201. margin-top: 1em;
  202. `;
  203. export default withApi(AvatarChooser);