Compare commits

...

1 Commits

Author SHA1 Message Date
Florian Rival
7232311fa3 Add support for setting a full name for a student account 2025-08-25 19:08:55 +02:00
18 changed files with 381 additions and 142 deletions

View File

@@ -65,7 +65,7 @@ export const AnnouncementsFeed = ({
const classesForClickableContainer = useStylesForClickableContainer();
if (error) {
if (error && !hideLoader) {
return (
<PlaceholderError onRetry={fetchAnnouncementsAndPromotions}>
<Trans>

View File

@@ -19,7 +19,10 @@ import {
} from '../../Utils/GDevelopServices/Project';
import PlaceholderLoader from '../../UI/PlaceholderLoader';
import PlaceholderError from '../../UI/PlaceholderError';
import { getUserPublicProfilesByIds } from '../../Utils/GDevelopServices/User';
import {
getUserPublicProfilesByIds,
type UserPublicProfileByIds,
} from '../../Utils/GDevelopServices/User';
import Dialog, { DialogPrimaryButton } from '../../UI/Dialog';
import FlatButton from '../../UI/FlatButton';
import LeftLoader from '../../UI/LeftLoader';
@@ -84,9 +87,10 @@ const InviteHome = ({ cloudProjectId }: Props) => {
| 'unexpected'
| null
>(null);
const [userPublicProfileByIds, setUserPublicProfileByIds] = React.useState(
{}
);
const [
userPublicProfileByIds,
setUserPublicProfileByIds,
] = React.useState<UserPublicProfileByIds>({});
const [
showCollaboratorAddDialog,
setShowCollaboratorAddDialog,
@@ -312,6 +316,7 @@ const InviteHome = ({ cloudProjectId }: Props) => {
<ColumnStackLayout expand noMargin>
<UserLine
username={profile.username}
fullName={profile.fullName}
email={profile.email}
level={currentUserLevel}
/>
@@ -365,6 +370,7 @@ const InviteHome = ({ cloudProjectId }: Props) => {
projectUserAcls.map(projectUserAcl => (
<UserLine
username={getCollaboratorUsername(projectUserAcl.userId)}
fullName={null}
email={projectUserAcl.email}
level={projectUserAcl.level}
onDelete={() => {

View File

@@ -0,0 +1,119 @@
// @flow
import * as React from 'react';
import { t, Trans } from '@lingui/macro';
import FlatButton from '../../../../../UI/FlatButton';
import Dialog, { DialogPrimaryButton } from '../../../../../UI/Dialog';
import { ColumnStackLayout } from '../../../../../UI/Layout';
import {
type User,
type EditUserChanges,
type UsernameAvailability,
} from '../../../../../Utils/GDevelopServices/User';
import LeftLoader from '../../../../../UI/LeftLoader';
import SemiControlledTextField from '../../../../../UI/SemiControlledTextField';
import { UsernameField } from '../../../../../Profile/UsernameField';
import AlertMessage from '../../../../../UI/AlertMessage';
type Props = {|
member: User,
onApply: (changes: EditUserChanges) => Promise<void>,
onClose: () => void,
isSaving: boolean,
error: ?Error,
|};
export const EditStudentDialog = ({
member,
onApply,
onClose,
isSaving,
error,
}: Props) => {
const [changes, setChanges] = React.useState<EditUserChanges | null>(null);
const [
usernameAvailability,
setUsernameAvailability,
] = React.useState<?UsernameAvailability>(null);
const [
isValidatingUsername,
setIsValidatingUsername,
] = React.useState<boolean>(false);
const canSave =
!usernameAvailability || (usernameAvailability.isAvailable && !!changes);
const triggerApply = React.useCallback(
async () => {
if (!canSave || !changes) return;
await onApply(changes);
},
[changes, canSave, onApply]
);
return (
<Dialog
open
title={<Trans>Edit student</Trans>}
actions={[
<FlatButton
key="close"
label={<Trans>Close</Trans>}
primary={false}
onClick={onClose}
disabled={isSaving}
id="close-button"
/>,
<LeftLoader isLoading={isSaving}>
<DialogPrimaryButton
key="apply"
primary
label={<Trans>Apply</Trans>}
onClick={triggerApply}
disabled={isSaving || !canSave}
id="apply-button"
/>
</LeftLoader>,
]}
onRequestClose={onClose}
onApply={triggerApply}
maxWidth="sm"
>
<ColumnStackLayout expand noMargin>
{error && (
<AlertMessage kind="error">
<Trans>
An error occurred while editing the student. Please try again.
</Trans>
</AlertMessage>
)}
<SemiControlledTextField
onChange={fullName => setChanges({ ...changes, fullName })}
value={
changes && typeof changes.fullName !== 'undefined'
? changes.fullName
: member.fullName || ''
}
disabled={isSaving}
fullWidth
commitOnBlur
floatingLabelText={<Trans>Full name</Trans>}
/>
<UsernameField
initialUsername={member.username}
value={
changes && typeof changes.username !== 'undefined'
? changes.username
: member.username || ''
}
onChange={(_, username) => setChanges({ ...changes, username })}
disabled={isSaving}
allowEmpty
onAvailabilityChecked={setUsernameAvailability}
onAvailabilityCheckLoading={setIsValidatingUsername}
isValidatingUsername={isValidatingUsername}
/>
</ColumnStackLayout>
</Dialog>
);
};

View File

@@ -3,7 +3,10 @@
import * as React from 'react';
import { t, Trans } from '@lingui/macro';
import Grid from '@material-ui/core/Grid';
import { type User } from '../../../../../Utils/GDevelopServices/User';
import {
type User,
type EditUserChanges,
} from '../../../../../Utils/GDevelopServices/User';
import Text from '../../../../../UI/Text';
import { LineStackLayout } from '../../../../../UI/Layout';
import Checkbox from '../../../../../UI/Checkbox';
@@ -12,9 +15,11 @@ import CheckboxChecked from '../../../../../UI/CustomSvgIcons/CheckboxChecked';
import AsyncSemiControlledTextField from '../../../../../UI/AsyncSemiControlledTextField';
import IconButton from '../../../../../UI/IconButton';
import Key from '../../../../../UI/CustomSvgIcons/Key';
import Edit from '../../../../../UI/CustomSvgIcons/Edit';
import { Line } from '../../../../../UI/Grid';
import { useResponsiveWindowSize } from '../../../../../UI/Responsive/ResponsiveWindowMeasurer';
import { textEllipsisStyle } from '../../../../../UI/TextEllipsis';
import { EditStudentDialog } from './EditStudentDialog';
const primaryTextArchivedOpacity = 0.6;
const primaryTextPlaceholderOpacity = 0.7;
@@ -47,6 +52,10 @@ type Props = {|
userId: string,
newPassword: string,
|}) => Promise<void>,
onEdit: ({|
editedUserId: string,
changes: EditUserChanges,
|}) => Promise<void>,
|};
const ManageStudentRow = ({
@@ -55,11 +64,18 @@ const ManageStudentRow = ({
isArchived,
onSelect,
onChangePassword,
onEdit,
}: Props) => {
const { isMobile } = useResponsiveWindowSize();
const [isEditingPassword, setIsEditingPassword] = React.useState<boolean>(
false
);
const [isSavingUser, setIsSavingUser] = React.useState<boolean>(false);
const [isEditingUser, setIsEditingUser] = React.useState<boolean>(false);
const [editedUserError, setEditedUserError] = React.useState<Error | null>(
null
);
const [
passwordEditionError,
setPasswordEditionError,
@@ -110,15 +126,30 @@ const ManageStudentRow = ({
uncheckedIcon={<CheckboxUnchecked />}
checkedIcon={<CheckboxChecked />}
/>
{member.username ? (
{member.username || member.fullName ? (
<Text
allowSelection
style={{
...textEllipsisStyle,
opacity: isArchived ? primaryTextArchivedOpacity : undefined,
}}
tooltip={[member.fullName, member.username]
.filter(Boolean)
.join(' - ')}
>
<b>{member.username}</b>
<b>
{member.fullName ? (
member.username ? (
<Trans>
{member.fullName} ({member.username})
</Trans>
) : (
member.fullName
)
) : (
member.username
)}
</b>
</Text>
) : (
<Text
@@ -136,6 +167,15 @@ const ManageStudentRow = ({
</i>
</Text>
)}
{!isArchived && (
<IconButton
size="small"
onClick={() => setIsEditingUser(true)}
tooltip={t`Change username or full name`}
>
<Edit fontSize="small" />
</IconButton>
)}
</LineStackLayout>
);
@@ -205,6 +245,31 @@ const ManageStudentRow = ({
</LineStackLayout>
);
const editDialog = isEditingUser && (
<EditStudentDialog
member={member}
isSaving={isSavingUser}
error={editedUserError}
onApply={async (changes: EditUserChanges) => {
try {
setEditedUserError(null);
setIsSavingUser(true);
await onEdit({ editedUserId: member.id, changes });
setIsEditingUser(false);
} catch (error) {
console.error('An error occurred while editing user:', error);
setEditedUserError(error);
} finally {
setIsSavingUser(false);
}
}}
onClose={() => {
setIsEditingUser(false);
setEditedUserError(null);
}}
/>
);
if (isMobile) {
return (
<>
@@ -217,21 +282,23 @@ const ManageStudentRow = ({
<Grid item xs={4} style={isMobile ? styles.mobileCell : styles.cell}>
{passwordCell}
</Grid>
{editDialog}
</>
);
}
return (
<>
<Grid item xs={5} style={isMobile ? styles.mobileCell : styles.cell}>
<Grid item xs={9} style={isMobile ? styles.mobileCell : styles.cell}>
<LineStackLayout noMargin alignItems="center">
{usernameCell}
{emailCell}
</LineStackLayout>
</Grid>
<Grid item xs={7} style={isMobile ? styles.mobileCell : styles.cell}>
<Grid item xs={3} style={isMobile ? styles.mobileCell : styles.cell}>
{passwordCell}
</Grid>
{editDialog}
</>
);
};

View File

@@ -56,6 +56,7 @@ import { selectMessageByLocale } from '../../../../../Utils/i18n/MessageByLocale
import TextButton from '../../../../../UI/TextButton';
import Chip from '../../../../../UI/Chip';
import { SubscriptionSuggestionContext } from '../../../../../Profile/Subscription/SubscriptionSuggestionContext';
import { type EditUserChanges } from '../../../../../Utils/GDevelopServices/User';
const styles = {
selectedMembersControlsContainer: {
@@ -324,6 +325,7 @@ const ManageEducationAccountDialog = ({ onClose }: Props) => {
onActivateMembers,
onSetAdmin,
onRefreshAdmins,
onEditUser,
} = React.useContext(TeamContext);
React.useEffect(
@@ -337,6 +339,20 @@ const ManageEducationAccountDialog = ({ onClose }: Props) => {
[credentialsCopySuccess]
);
const onEditTeamMember = React.useCallback(
async ({
editedUserId,
changes,
}: {|
editedUserId: string,
changes: EditUserChanges,
|}) => {
await onEditUser(editedUserId, changes);
await onRefreshMembers();
},
[onEditUser, onRefreshMembers]
);
const onChangeTeamMemberPassword = React.useCallback(
async ({
userId,
@@ -354,7 +370,7 @@ const ManageEducationAccountDialog = ({ onClose }: Props) => {
const onCopyActiveCredentials = React.useCallback(
() => {
if (!members) return;
let content = 'Username,Email,Password';
let content = 'Username,Full Name,Email,Password';
let membersToConsider = [];
if (selectedUserIds.length === 0) {
membersToConsider = members.filter(member => !member.deactivatedAt);
@@ -364,7 +380,7 @@ const ManageEducationAccountDialog = ({ onClose }: Props) => {
);
}
membersToConsider.forEach(member => {
content += `\n${member.username || ''},${
content += `\n${member.username || ''},${member.fullName || ''},${
member.email
},${member.password || ''}`;
});
@@ -673,12 +689,13 @@ const ManageEducationAccountDialog = ({ onClose }: Props) => {
<UserLine
key={adminUser.id}
username={adminUser.username}
fullName={adminUser.fullName}
email={adminUser.email}
level={null}
onDelete={() => onRemoveAdmin(adminUser.email)}
disabled={
(profile && adminUser.id === profile.id) ||
(adminEmailBeingRemoved &&
(!!adminEmailBeingRemoved &&
adminEmailBeingRemoved === adminUser.email)
}
/>
@@ -822,10 +839,10 @@ const ManageEducationAccountDialog = ({ onClose }: Props) => {
size="small"
tooltip={
selectedUserIds.length === 0
? t`Copy active credentials`
? t`Copy active credentials to CSV`
: t`Copy ${
selectedUserIds.length
} credentials`
} credentials to CSV`
}
disabled={
hasNoActiveTeamMembers &&
@@ -847,12 +864,12 @@ const ManageEducationAccountDialog = ({ onClose }: Props) => {
<Column>
{!isMobile && (
<GridList cols={2} cellHeight={'auto'}>
<Grid item xs={5}>
<Grid item xs={9}>
<Text style={{ opacity: 0.7 }}>
<Trans>Student</Trans>
</Text>
</Grid>
<Grid item xs={7}>
<Grid item xs={3}>
<Text style={{ opacity: 0.7 }}>
<Trans>Password</Trans>
</Text>
@@ -885,6 +902,7 @@ const ManageEducationAccountDialog = ({ onClose }: Props) => {
onChangePassword={
onChangeTeamMemberPassword
}
onEdit={onEditTeamMember}
isSelected={selectedUserIds.includes(
member.id
)}
@@ -966,6 +984,7 @@ const ManageEducationAccountDialog = ({ onClose }: Props) => {
onChangePassword={
onChangeTeamMemberPassword
}
onEdit={onEditTeamMember}
isSelected={selectedUserIds.includes(
member.id
)}

View File

@@ -117,9 +117,19 @@ const TeamMemberRow = ({
/>
) : (
<LineStackLayout noMargin alignItems="center">
{member.username && (
{(member.username || member.fullName) && (
<Text allowSelection noMargin>
{member.username}
{member.fullName ? (
member.username ? (
<Trans>
{member.fullName} ({member.username})
</Trans>
) : (
member.fullName
)
) : (
member.username
)}
</Text>
)}
<Text allowSelection noMargin color="secondary">

View File

@@ -4,7 +4,6 @@ import {
type Profile,
type LoginForm,
type RegisterForm,
type PatchUserPayload,
type ForgotPasswordForm,
type AuthError,
type IdentityProvider,
@@ -13,7 +12,10 @@ import { type PreferencesValues } from '../MainFrame/Preferences/PreferencesCont
import { type CloudProjectWithUserAccessInfo } from '../Utils/GDevelopServices/Project';
import { User as FirebaseUser } from 'firebase/auth';
import { type Badge, type Achievement } from '../Utils/GDevelopServices/Badge';
import { type Recommendation } from '../Utils/GDevelopServices/User';
import {
type Recommendation,
type EditUserChanges,
} from '../Utils/GDevelopServices/User';
import { type Notification } from '../Utils/GDevelopServices/Notification';
import {
type Limits,
@@ -62,7 +64,7 @@ export type AuthenticatedUser = {|
preferences: PreferencesValues
) => Promise<void>,
onEditProfile: (
payload: PatchUserPayload,
changes: EditUserChanges,
preferences: PreferencesValues
) => Promise<void>,
onResetPassword: ForgotPasswordForm => Promise<void>,

View File

@@ -8,6 +8,8 @@ import {
getUserEarningsBalance,
} from '../Utils/GDevelopServices/Usage';
import {
editUser,
type EditUserChanges,
getUserBadges,
listDefaultRecommendations,
listRecommendations,
@@ -17,7 +19,6 @@ import { getAchievements } from '../Utils/GDevelopServices/Badge';
import Authentication, {
type LoginForm,
type RegisterForm,
type PatchUserPayload,
type ChangeEmailForm,
type AuthError,
type ForgotPasswordForm,
@@ -622,10 +623,11 @@ export default class AuthenticatedUserProvider extends React.Component<
if (!userProfile.isCreator) {
// If the user is not a creator, then update the profile to say they now are.
try {
await authentication.editUserProfile(
authentication.getAuthorizationHeader,
{ isCreator: true }
);
await editUser(authentication.getAuthorizationHeader, {
editedUserId: userProfile.id,
userId: userProfile.id,
changes: { isCreator: true },
});
} catch (error) {
// Catch the error so that the user profile is still fetched.
console.error('Error while updating the user profile:', error);
@@ -1206,11 +1208,13 @@ export default class AuthenticatedUserProvider extends React.Component<
};
_doEdit = async (
payload: PatchUserPayload,
payload: EditUserChanges,
preferences: PreferencesValues
) => {
const { authentication } = this.props;
if (!authentication) return;
const { profile } = this.state.authenticatedUser;
if (!profile) return;
this.setState({
editInProgress: true,
@@ -1218,9 +1222,10 @@ export default class AuthenticatedUserProvider extends React.Component<
});
this._automaticallyUpdateUserProfile = false;
try {
await authentication.editUserProfile(
authentication.getAuthorizationHeader,
{
await editUser(authentication.getAuthorizationHeader, {
editedUserId: profile.id,
userId: profile.id,
changes: {
username: payload.username,
description: payload.description,
getGameStatsEmail: payload.getGameStatsEmail,
@@ -1231,8 +1236,8 @@ export default class AuthenticatedUserProvider extends React.Component<
githubUsername: payload.githubUsername,
communityLinks: payload.communityLinks,
survey: payload.survey,
}
);
},
});
await this._fetchUserProfileWithoutThrowingErrors();
} catch (apiCallError) {
this.setState({ apiCallError });
@@ -1625,9 +1630,9 @@ export default class AuthenticatedUserProvider extends React.Component<
badges={this.state.authenticatedUser.badges}
subscription={this.state.authenticatedUser.subscription}
onClose={() => this.openEditProfileDialog(false)}
onEdit={async form => {
onEdit={async changes => {
try {
await this._doEdit(form, this.props.preferencesValues);
await this._doEdit(changes, this.props.preferencesValues);
this.openEditProfileDialog(false);
} catch (error) {
// Ignore errors, we will let the user retry in their profile.

View File

@@ -207,7 +207,6 @@ const CreateAccountDialog = ({
onChangeOptInNewsletterEmail={setGetNewsletterEmail}
createAccountInProgress={createAccountInProgress}
error={error}
usernameAvailability={usernameAvailability}
onChangeUsernameAvailability={setUsernameAvailability}
isValidatingUsername={isValidatingUsername}
onChangeIsValidatingUsername={setIsValidatingUsername}

View File

@@ -60,7 +60,6 @@ type Props = {|
onChangeUsername: string => void,
optInNewsletterEmail: boolean,
onChangeOptInNewsletterEmail: boolean => void,
usernameAvailability: ?UsernameAvailability,
onChangeUsernameAvailability: (?UsernameAvailability) => void,
isValidatingUsername: boolean,
onChangeIsValidatingUsername: boolean => void,
@@ -80,7 +79,6 @@ const CreateAccountForm = ({
onChangeUsername,
optInNewsletterEmail,
onChangeOptInNewsletterEmail,
usernameAvailability,
onChangeUsernameAvailability,
isValidatingUsername,
onChangeIsValidatingUsername,

View File

@@ -7,7 +7,6 @@ import { type MessageDescriptor } from '../Utils/i18n/MessageDescriptor.flow';
import FlatButton from '../UI/FlatButton';
import Dialog, { DialogPrimaryButton } from '../UI/Dialog';
import {
type EditForm,
type AuthError,
type Profile,
type UpdateGitHubStarResponse,
@@ -16,6 +15,7 @@ import {
type UpdateYoutubeSubscriptionResponse,
} from '../Utils/GDevelopServices/Authentication';
import {
type EditUserChanges,
communityLinksConfig,
donateLinkConfig,
discordUsernameConfig,
@@ -59,7 +59,7 @@ export type EditProfileDialogProps = {|
badges: ?Array<Badge>,
subscription: ?Subscription,
onClose: () => void,
onEdit: (form: EditForm) => Promise<void>,
onEdit: (form: EditUserChanges) => Promise<void>,
onUpdateGitHubStar: (
githubUsername: string
) => Promise<UpdateGitHubStarResponse>,

View File

@@ -5,6 +5,7 @@ import {
type TeamGroup,
type User,
type TeamMembership,
type EditUserChanges,
} from '../../Utils/GDevelopServices/User';
import { type CloudProjectWithUserAccessInfo } from '../../Utils/GDevelopServices/Project';
@@ -31,6 +32,7 @@ export type TeamState = {|
userId: string,
newPassword: string
) => Promise<void>,
onEditUser: (editedUserId: string, changes: EditUserChanges) => Promise<void>,
|};
export const initialTeamState = {
@@ -51,6 +53,7 @@ export const initialTeamState = {
onRefreshAdmins: async () => {},
onSetAdmin: async () => {},
onChangeMemberPassword: async () => {},
onEditUser: async () => {},
};
const TeamContext = React.createContext<TeamState>(initialTeamState);

View File

@@ -20,6 +20,8 @@ import {
changeTeamMemberPassword,
activateTeamMembers,
setUserAsAdmin,
editUser,
type EditUserChanges,
} from '../../Utils/GDevelopServices/User';
import AuthenticatedUserContext from '../../Profile/AuthenticatedUserContext';
import { listOtherUserCloudProjects } from '../../Utils/GDevelopServices/Project';
@@ -150,6 +152,18 @@ const TeamProvider = ({ children }: Props) => {
[team, getAuthorizationHeader, adminUserId]
);
const onEditUser = React.useCallback(
async (editedUserId: string, changes: EditUserChanges) => {
if (!adminUserId) return;
await editUser(getAuthorizationHeader, {
userId: adminUserId,
editedUserId,
changes,
});
},
[getAuthorizationHeader, adminUserId]
);
const onChangeMemberPassword = React.useCallback(
async (userId: string, newPassword: string) => {
if (!adminUserId) return;
@@ -357,6 +371,7 @@ const TeamProvider = ({ children }: Props) => {
admins,
members,
memberships,
onEditUser,
onChangeGroupName,
onChangeUserGroup,
onListUserProjects,

View File

@@ -226,11 +226,7 @@ type DialogProps = {|
export const DialogPrimaryButton = RaisedButton;
/**
* A enhanced material-ui Dialog that can have optional secondary actions
* and no margins if required.
*/
const Dialog = ({
const DialogWithoutWindowSizeProvider = ({
onApply,
secondaryActions,
dangerLevel,
@@ -481,12 +477,16 @@ const Dialog = ({
);
};
const DialogWithTopLevelWindowSize = (props: DialogProps) => {
/**
* A enhanced material-ui Dialog that can have optional secondary actions
* and no margins if required.
*/
const Dialog = (props: DialogProps) => {
return (
<TopLevelWindowSizeProvider>
<Dialog {...props} />
<DialogWithoutWindowSizeProvider {...props} />
</TopLevelWindowSizeProvider>
);
};
export default DialogWithTopLevelWindowSize;
export default Dialog;

View File

@@ -1,3 +1,6 @@
// @flow
import * as React from 'react';
import { Trans } from '@lingui/macro';
import { I18n } from '@lingui/react';
import Avatar from '@material-ui/core/Avatar';
import { getGravatarUrl } from '../../UI/GravatarUrl';
@@ -6,18 +9,21 @@ import Text from '../Text';
import IconButton from '../IconButton';
import Trash from '../CustomSvgIcons/Trash';
import { getTranslatableLevel } from '../../Utils/AclUtils';
import { type Level } from '../../Utils/GDevelopServices/Project';
const UserLine = ({
username,
fullName,
email,
level,
onDelete,
disabled,
}: {|
username: ?string,
fullName: ?string,
email: string,
level: ?Level,
onDelete?: () => void,
onDelete?: () => Promise<void> | void,
disabled?: boolean,
|}) => (
<I18n>
@@ -26,7 +32,21 @@ const UserLine = ({
<Line noMargin expand>
<Avatar src={getGravatarUrl(email, { size: 40 })} />
<Column expand justifyContent="flex-end">
{username && <Text noMargin>{username}</Text>}
{(username || fullName) && (
<Text noMargin>
{fullName ? (
username ? (
<Trans>
{fullName} ({username})
</Trans>
) : (
fullName
)
) : (
username
)}
</Text>
)}
<Text noMargin color="secondary">
{email}
</Text>

View File

@@ -35,6 +35,7 @@ export type Profile = {|
donateLink: ?string,
discordUsername: ?string,
githubUsername?: ?string,
fullName?: ?string,
communityLinks?: CommunityLinks,
survey?: UserSurvey,
@@ -62,31 +63,6 @@ export type RegisterForm = {|
getNewsletterEmail: boolean,
|};
export type EditForm = {|
+username: string,
+description: string,
+getGameStatsEmail: boolean,
+getNewsletterEmail: boolean,
+donateLink: string,
+discordUsername: string,
+githubUsername: string,
+communityLinks: CommunityLinks,
|};
export type PatchUserPayload = {
+username?: string,
+description?: string,
+getGameStatsEmail?: boolean,
+getNewsletterEmail?: boolean,
+appLanguage?: string,
+isCreator?: boolean,
+donateLink?: string,
+discordUsername?: string,
+githubUsername?: string,
+communityLinks?: CommunityLinks,
+survey?: UserSurvey,
};
export type ChangeEmailForm = {|
email: string,
|};
@@ -388,60 +364,6 @@ export default class Authentication {
});
};
editUserProfile = async (
getAuthorizationHeader: () => Promise<string>,
{
username,
description,
getGameStatsEmail,
getNewsletterEmail,
appLanguage,
isCreator,
donateLink,
discordUsername,
githubUsername,
communityLinks,
survey,
}: PatchUserPayload
) => {
const { currentUser } = this.auth;
if (!currentUser)
throw new Error('Tried to edit user profile while not authenticated.');
return getAuthorizationHeader()
.then(authorizationHeader => {
return axios.patch(
`${GDevelopUserApi.baseUrl}/user/${currentUser.uid}`,
{
username,
description,
getGameStatsEmail,
getNewsletterEmail,
appLanguage,
isCreator,
donateLink,
discordUsername,
githubUsername,
communityLinks,
survey,
},
{
params: {
userId: currentUser.uid,
},
headers: {
Authorization: authorizationHeader,
},
}
);
})
.then(response => response.data)
.catch(error => {
console.error('Error while editing user:', error);
throw error;
});
};
updateGitHubStar = async (
getAuthorizationHeader: () => Promise<string>
): Promise<UpdateGitHubStarResponse> => {

View File

@@ -142,6 +142,21 @@ export type UserPublicProfile = {|
iconUrl: string,
|};
export type EditUserChanges = {|
username?: string,
fullName?: string,
description?: string,
getGameStatsEmail?: boolean,
getNewsletterEmail?: boolean,
appLanguage?: string,
isCreator?: boolean,
donateLink?: string,
discordUsername?: string,
githubUsername?: string,
communityLinks?: CommunityLinks,
survey?: UserSurvey,
|};
export type UserPublicProfileByIds = {|
[key: string]: UserPublicProfile,
|};
@@ -801,3 +816,24 @@ export const generateCustomAuthToken = async (
);
return response.data.customAuthToken;
};
export const editUser = async (
getAuthorizationHeader: () => Promise<string>,
{
editedUserId,
userId,
changes,
}: {|
editedUserId: string,
userId: string,
changes: EditUserChanges,
|}
): Promise<User> => {
const authorizationHeader = await getAuthorizationHeader();
const response = await client.patch(`/user/${editedUserId}`, changes, {
headers: { Authorization: authorizationHeader },
params: { userId },
});
return response.data;
};

View File

@@ -9,6 +9,7 @@ import {
type TeamGroup,
type TeamMembership,
type User,
type EditUserChanges,
} from '../../../Utils/GDevelopServices/User';
import { type CloudProjectWithUserAccessInfo } from '../../../Utils/GDevelopServices/Project';
import { testProject } from '../../GDevelopJsInitializerDecorator';
@@ -57,6 +58,7 @@ const initialMembers: Array<User> = [
email: 'user1@hotmail.com',
username: null,
password: 'blue-chair-34',
fullName: 'John Doe',
},
// $FlowIgnore - the whole user object is not needed for this component
{
@@ -94,6 +96,7 @@ const initialMembers: Array<User> = [
id: 'user7',
email: 'user7@mail.ru',
username: 'Bayonetta',
fullName: 'Jane Smith',
},
// $FlowIgnore - the whole user object is not needed for this component
{
@@ -435,6 +438,18 @@ const MockTeamProvider = ({
setMembers(newMembers);
};
const editUser = async (userId: string, changes: EditUserChanges) => {
if (!members) return;
const newMembers = [...members];
const memberIndex = newMembers.findIndex(member => member.id === userId);
if (memberIndex === -1) return;
newMembers.splice(memberIndex, 1, {
...newMembers[memberIndex],
...changes,
});
setMembers(newMembers);
};
return (
<AlertProvider>
<DragAndDropContextProvider>
@@ -460,6 +475,7 @@ const MockTeamProvider = ({
onActivateMembers: action('activateMembers'),
onChangeMemberPassword: changeMemberPassword,
onSetAdmin: setAdmin,
onEditUser: editUser,
}}
>
<Text allowSelection>
@@ -474,19 +490,21 @@ const MockTeamProvider = ({
);
};
export const Default = () => (
<MockTeamProvider loading={false} teamSize={12}>
<FixedHeightFlexContainer height={600}>
<TeamSection
project={testProject.project}
currentFileMetadata={null}
onOpenRecentFile={action('onOpenRecentFile')}
storageProviders={[CloudStorageProvider]}
onOpenTeachingResources={action('onOpenTeachingResources')}
/>
</FixedHeightFlexContainer>
</MockTeamProvider>
);
export const Default = () => {
return (
<MockTeamProvider loading={false} teamSize={12}>
<FixedHeightFlexContainer height={600}>
<TeamSection
project={testProject.project}
currentFileMetadata={null}
onOpenRecentFile={action('onOpenRecentFile')}
storageProviders={[CloudStorageProvider]}
onOpenTeachingResources={action('onOpenTeachingResources')}
/>
</FixedHeightFlexContainer>
</MockTeamProvider>
);
};
export const WithNoGroupsYet = () => (
<MockTeamProvider loading={false} noGroups>