mirror of
https://github.com/4ian/GDevelop.git
synced 2025-10-15 10:19:04 +00:00
Compare commits
2 Commits
v5.5.239
...
feat/full-
Author | SHA1 | Date | |
---|---|---|---|
![]() |
7232311fa3 | ||
![]() |
48d35a50b5 |
@@ -65,7 +65,7 @@ export const AnnouncementsFeed = ({
|
||||
|
||||
const classesForClickableContainer = useStylesForClickableContainer();
|
||||
|
||||
if (error) {
|
||||
if (error && !hideLoader) {
|
||||
return (
|
||||
<PlaceholderError onRetry={fetchAnnouncementsAndPromotions}>
|
||||
<Trans>
|
||||
|
@@ -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={() => {
|
||||
|
@@ -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>
|
||||
);
|
||||
};
|
@@ -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}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@@ -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
|
||||
)}
|
||||
|
@@ -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">
|
||||
|
@@ -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>,
|
||||
|
@@ -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.
|
||||
|
@@ -207,7 +207,6 @@ const CreateAccountDialog = ({
|
||||
onChangeOptInNewsletterEmail={setGetNewsletterEmail}
|
||||
createAccountInProgress={createAccountInProgress}
|
||||
error={error}
|
||||
usernameAvailability={usernameAvailability}
|
||||
onChangeUsernameAvailability={setUsernameAvailability}
|
||||
isValidatingUsername={isValidatingUsername}
|
||||
onChangeIsValidatingUsername={setIsValidatingUsername}
|
||||
|
@@ -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,
|
||||
|
@@ -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>,
|
||||
|
@@ -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);
|
||||
|
@@ -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,
|
||||
|
@@ -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;
|
||||
|
@@ -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>
|
||||
|
@@ -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> => {
|
||||
|
@@ -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;
|
||||
};
|
||||
|
@@ -85,6 +85,7 @@ const useOpenInitialDialog = ({
|
||||
aiRequestId: null,
|
||||
paneIdentifier: 'center',
|
||||
});
|
||||
removeRouteArguments(['initial-dialog']);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
|
@@ -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>
|
||||
|
Reference in New Issue
Block a user