diff --git a/static/app/components/avatarChooser.tsx b/static/app/components/avatarChooser.tsx index df51837b76ba12..c32fd4b1625a4e 100644 --- a/static/app/components/avatarChooser.tsx +++ b/static/app/components/avatarChooser.tsx @@ -1,8 +1,7 @@ -import {Component} from 'react'; +import {useState} from 'react'; import styled from '@emotion/styled'; import {addErrorMessage, addSuccessMessage} from 'sentry/actionCreators/indicator'; -import type {Client} from 'sentry/api'; import {AvatarUploader} from 'sentry/components/avatarUploader'; import {OrganizationAvatar} from 'sentry/components/core/avatar/organizationAvatar'; import {SentryAppAvatar} from 'sentry/components/core/avatar/sentryAppAvatar'; @@ -22,7 +21,7 @@ import {space} from 'sentry/styles/space'; import type {SentryApp, SentryAppAvatarPhotoType} from 'sentry/types/integrations'; import type {Organization, Team} from 'sentry/types/organization'; import type {AvatarUser} from 'sentry/types/user'; -import withApi from 'sentry/utils/withApi'; +import useApi from 'sentry/utils/useApi'; export type Model = Pick; type AvatarType = Required['avatar']['avatarType']; @@ -33,71 +32,55 @@ type AvatarChooserType = | 'sentryAppColor' | 'sentryAppSimple' | 'docIntegration'; + type DefaultChoice = { allowDefault?: boolean; choiceText?: string; preview?: React.ReactNode; }; -type DefaultProps = { - onSave: (model: Model) => void; +interface AvatarChooserProps { + endpoint: string; + model: Model; allowGravatar?: boolean; allowLetter?: boolean; allowUpload?: boolean; defaultChoice?: DefaultChoice; - type?: AvatarChooserType; - uploadDomain?: string; -}; - -type Props = { - api: Client; - endpoint: string; - model: Model; disabled?: boolean; help?: React.ReactNode; isUser?: boolean; + onSave?: (model: Model) => void; savedDataUrl?: string; title?: string; -} & DefaultProps; - -type State = { - hasError: boolean; - model: Model; - dataUrl?: string | null; - savedDataUrl?: string | null; -}; - -class AvatarChooser extends Component { - static defaultProps: DefaultProps = { - allowGravatar: true, - allowLetter: true, - allowUpload: true, - type: 'user', - onSave: () => {}, - defaultChoice: { - allowDefault: false, - }, - uploadDomain: '', - }; - - state: State = { - model: this.props.model, - savedDataUrl: null, - dataUrl: null, - hasError: false, - }; - - componentDidUpdate(prevProps: Props) { - const {model} = this.props; - - // Update local state if defined in props - if (model !== undefined && model !== prevProps.model) { - this.setState({model}); - } - } + type?: AvatarChooserType; + uploadDomain?: string; +} - getModelFromResponse(resp: any): Model { - const {type} = this.props; +function AvatarChooser(props: AvatarChooserProps) { + const { + endpoint, + model: propsModel, + savedDataUrl, + isUser, + disabled, + title, + help, + allowGravatar = true, + allowLetter = true, + allowUpload = true, + type = 'user', + onSave, + defaultChoice = {allowDefault: false}, + uploadDomain = '', + } = props; + + const api = useApi(); + const [model, setModel] = useState(propsModel); + const [newAvatar, setNewAvatar] = useState(null); + + const hasError = false; + + const getModelFromResponse = (resp: any): Model => { const isSentryApp = type?.startsWith('sentryApp'); // SentryApp endpoint returns all avatars, we need to return only the edited one if (!isSentryApp) { @@ -107,26 +90,11 @@ class AvatarChooser extends Component { return { avatar: resp?.avatars?.find(({color}: any) => color === isColor) ?? undefined, }; - } - - handleError(msg: string) { - addErrorMessage(msg); - } - - handleSuccess(model: Model) { - const {onSave} = this.props; - this.setState({model}); - onSave(model); - addSuccessMessage(t('Successfully saved avatar preferences')); - } - - handleSaveSettings = (ev: React.MouseEvent) => { - const {endpoint, api, type} = this.props; - const {model, dataUrl} = this.state; + }; - ev.preventDefault(); + const handleSaveSettings = () => { const avatarType = model?.avatar?.avatarType; - const avatarPhoto = dataUrl?.split(',')[1]; + const avatarPhoto = newAvatar?.split(',')[1]; const data: { avatar_photo?: string; @@ -149,140 +117,127 @@ class AvatarChooser extends Component { method: 'PUT', data, success: resp => { - this.setState({savedDataUrl: this.state.dataUrl}); - this.handleSuccess(this.getModelFromResponse(resp)); + const newModel = getModelFromResponse(resp); + setModel(newModel); + onSave?.(newModel); + addSuccessMessage(t('Successfully saved avatar preferences')); }, error: resp => { const avatarPhotoErrors = resp?.responseJSON?.avatar_photo || []; if (avatarPhotoErrors.length) { - avatarPhotoErrors.map(this.handleError); + avatarPhotoErrors.map(addErrorMessage); } else { - this.handleError.bind(this, t('There was an error saving your preferences.')); + addErrorMessage(t('There was an error saving your preferences.')); } }, }); }; - handleChange = (id: AvatarType) => - this.setState(state => ({ - model: { - ...state.model, - avatar: {avatarUuid: state.model.avatar?.avatarUuid ?? '', avatarType: id}, - }, - })); - - render() { - const { - allowGravatar, - allowUpload, - allowLetter, - savedDataUrl, - type, - isUser, - disabled, - title, - help, - defaultChoice, - uploadDomain, - } = this.props; - const {hasError, model, dataUrl} = this.state; - - if (hasError) { - return ; - } - if (!model) { - return ; - } - const {allowDefault, preview, choiceText: defaultChoiceText} = defaultChoice || {}; + if (hasError) { + return ; + } + if (!model) { + return ; + } + const {allowDefault, preview, choiceText: defaultChoiceText} = defaultChoice || {}; - const avatarType = model.avatar?.avatarType ?? 'letter_avatar'; - const isLetter = avatarType === 'letter_avatar'; - const isDefault = Boolean(preview && avatarType === 'default'); + const avatarType = model.avatar?.avatarType ?? 'letter_avatar'; + const isLetter = avatarType === 'letter_avatar'; + const isDefault = !!preview && avatarType === 'default'; - const isTeam = type === 'team'; - const isOrganization = type === 'organization'; - const isSentryApp = type?.startsWith('sentryApp'); + const isTeam = type === 'team'; + const isOrganization = type === 'organization'; + const isSentryApp = type?.startsWith('sentryApp'); - const choices: Array<[AvatarType, string]> = []; + const choices: Array<[AvatarType, string]> = []; - if (allowDefault && preview) { - choices.push(['default', defaultChoiceText ?? t('Use default avatar')]); - } - if (allowLetter) { - choices.push(['letter_avatar', t('Use initials')]); - } - if (allowUpload) { - choices.push(['upload', t('Upload an image')]); - } - if (allowGravatar) { - choices.push(['gravatar', t('Use Gravatar')]); - } - - const sharedAvatarProps = { - gravatar: false, - style: {width: 90, height: 90}, - }; + if (allowDefault && preview) { + choices.push(['default', defaultChoiceText ?? t('Use default avatar')]); + } + if (allowLetter) { + choices.push(['letter_avatar', t('Use initials')]); + } + if (allowUpload) { + choices.push(['upload', t('Upload an image')]); + } + if (allowGravatar) { + choices.push(['gravatar', t('Use Gravatar')]); + } - const avatar = isUser ? ( - - ) : isOrganization ? ( - - ) : isTeam ? ( - - ) : isSentryApp ? ( - - ) : null; + const sharedAvatarProps = { + gravatar: false, + style: {width: 90, height: 90}, + }; - return ( - - {title || t('Avatar')} - - - - + ) : isOrganization ? ( + + ) : isTeam ? ( + + ) : isSentryApp ? ( + + ) : null; + + return ( + + {title || t('Avatar')} + + + + + setModel(prevModel => ({ + ...prevModel, + avatar: { + avatarUuid: prevModel.avatar?.avatarUuid ?? '', + avatarType: newType, + }, + })) + } + disabled={disabled} + /> + {isLetter && avatar} + {isDefault && preview} + + + {allowGravatar && avatarType === 'gravatar' && ( + + {t('Gravatars are managed through ')} + Gravatar.com + + )} + {model.avatar && avatarType === 'upload' && ( + + setNewAvatar(newDataUrl ?? null) + } /> - {isLetter && avatar} - {isDefault && preview} - - - {allowGravatar && avatarType === 'gravatar' && ( - - {t('Gravatars are managed through ')} - Gravatar.com - - )} - {model.avatar && avatarType === 'upload' && ( - this.setState(dataState)} - /> - )} - - {help && {help}} - - - - - - - ); - } + )} + + {help && {help}} + + + + + + + ); } const AvatarHelp = styled('p')` @@ -314,4 +269,4 @@ const AvatarUploadSection = styled('div')` margin-top: ${space(1.5)}; `; -export default withApi(AvatarChooser); +export default AvatarChooser;