Allow editing Game Public Info from Game Dashboard

This commit is contained in:
Clément Pasteau
2022-01-26 18:26:52 +01:00
committed by GitHub
parent 22b117d2b3
commit 1e73122633
14 changed files with 447 additions and 154 deletions

View File

@@ -219,7 +219,12 @@ export const ExtensionOptionsEditor = ({
}}
/>
<UsersAutocomplete
userIds={eventsFunctionsExtension.getAuthorIds()}
userIds={eventsFunctionsExtension.getAuthorIds().toJSArray()}
onChange={userIds => {
const projectAuthorIds = eventsFunctionsExtension.getAuthorIds();
projectAuthorIds.clear();
userIds.forEach(userId => projectAuthorIds.push_back(userId));
}}
floatingLabelText={<Trans>Authors</Trans>}
helperText={
<Trans>

View File

@@ -25,6 +25,8 @@ import {
getGame,
updateGame,
type Game,
setGameUserAcls,
getAclsFromAuthorIds,
} from '../../Utils/GDevelopServices/Game';
import { type ExportPipeline } from '../ExportPipeline.flow';
import { GameRegistration } from '../../GameDashboard/GameRegistration';
@@ -133,12 +135,34 @@ export default class ExportLauncher extends Component<Props, State> {
});
};
tryUpdateAuthors = async () => {
const profile = this.props.authenticatedUser.profile;
if (profile) {
const authorAcls = getAclsFromAuthorIds(
this.props.project.getAuthorIds()
);
try {
await setGameUserAcls(
this.props.authenticatedUser.getAuthorizationHeader,
profile.id,
this.props.project.getProjectUuid(),
authorAcls
);
} catch (e) {
// Best effort call, do not prevent exporting the game.
console.error(e);
}
}
};
registerAndUpdateGame = async () => {
const profile = this.props.authenticatedUser.profile;
const getAuthorizationHeader = this.props.authenticatedUser
.getAuthorizationHeader;
const gameId = this.props.project.getProjectUuid();
const authorName = this.props.project.getAuthor() || 'Unspecified author';
const authorName =
this.props.project.getAuthor() || 'Unspecified publisher';
const gameName = this.props.project.getName() || 'Untitled game';
if (profile) {
const userId = profile.id;
@@ -150,6 +174,8 @@ export default class ExportLauncher extends Component<Props, State> {
authorName,
gameName,
});
// We don't await for the authors update, as it is not required for publishing.
this.tryUpdateAuthors();
this.props.onGameUpdated(game);
} catch (err) {
if (err.response.status === 404) {
@@ -159,6 +185,8 @@ export default class ExportLauncher extends Component<Props, State> {
authorName,
gameName,
});
// We don't await for the authors update, as it is not required for publishing.
this.tryUpdateAuthors();
this.props.onGameUpdated(game);
}
}

View File

@@ -19,7 +19,6 @@ type Props = {|
onOpenDetails: () => void,
onOpenBuilds: () => void,
onOpenAnalytics: () => void,
onOpenMonetization: () => void,
|};
export const GameCard = ({
@@ -28,7 +27,6 @@ export const GameCard = ({
onOpenDetails,
onOpenBuilds,
onOpenAnalytics,
onOpenMonetization,
}: Props) => {
const openGameUrl = () => {
const url = getGameUrl(game);

View File

@@ -9,6 +9,9 @@ import {
type Game,
updateGame,
deleteGame,
getPublicGame,
setGameUserAcls,
getAclsFromAuthorIds,
} from '../Utils/GDevelopServices/Game';
import Dialog from '../UI/Dialog';
import { Tab, Tabs } from '../UI/Tabs';
@@ -23,7 +26,7 @@ import AuthenticatedUserContext from '../Profile/AuthenticatedUserContext';
import PlaceholderError from '../UI/PlaceholderError';
import SelectField from '../UI/SelectField';
import SelectOption from '../UI/SelectOption';
import { CircularProgress } from '@material-ui/core';
import { Chip, CircularProgress } from '@material-ui/core';
import { Table, TableBody, TableRow, TableRowColumn } from '../UI/Table';
import Builds from '../Export/Builds';
import AlertMessage from '../UI/AlertMessage';
@@ -31,12 +34,11 @@ import subDays from 'date-fns/subDays';
import RaisedButton from '../UI/RaisedButton';
import Window from '../Utils/Window';
import HelpButton from '../UI/HelpButton';
import { type PublicGame } from '../Utils/GDevelopServices/Game';
import PlaceholderLoader from '../UI/PlaceholderLoader';
import PublicGamePropertiesDialog from '../ProjectManager/PublicGamePropertiesDialog';
export type GamesDetailsTab =
| 'details'
| 'builds'
| 'analytics'
| 'monetization';
export type GamesDetailsTab = 'details' | 'builds' | 'analytics';
type Props = {|
game: Game,
@@ -47,16 +49,6 @@ type Props = {|
onGameDeleted: () => void,
|};
/** Check if the project has changes not refleted in the registered online game. */
const areProjectAndGameDiffering = (project: ?gdProject, game: Game) => {
if (!project) return false;
return (
project.getAuthor() !== game.authorName ||
project.getName() !== game.gameName
);
};
export const GameDetailsDialog = ({
game,
project,
@@ -83,6 +75,12 @@ export const GameDetailsDialog = ({
const [analyticsDate, setAnalyticsDate] = React.useState(yesterdayIsoDate);
const authenticatedUser = React.useContext(AuthenticatedUserContext);
const [publicGame, setPublicGame] = React.useState<?PublicGame>(null);
const [publicGameError, setPublicGameError] = React.useState<?Error>(null);
const [
isPublicGamePropertiesDialogOpen,
setIsPublicGamePropertiesDialogOpen,
] = React.useState(false);
const loadGameMetrics = React.useCallback(
async () => {
@@ -116,20 +114,40 @@ export const GameDetailsDialog = ({
[loadGameMetrics]
);
const loadPublicGame = React.useCallback(
async () => {
setPublicGameError(null);
try {
const publicGameResponse = await getPublicGame(game.id);
setPublicGame(publicGameResponse);
} catch (err) {
console.error(`Unable to load the game:`, err);
setPublicGameError(err);
}
},
[game]
);
React.useEffect(
() => {
loadPublicGame();
},
[loadPublicGame]
);
const updateGameFromProject = async () => {
if (!project || !profile) return;
const { id } = profile;
try {
const updatedGame = await updateGame(
getAuthorizationHeader,
id,
project.getProjectUuid(),
{
authorName: project.getAuthor(),
gameName: project.getName(),
}
);
setPublicGame(null); // Public game will auto update when game is updated.
const gameId = project.getProjectUuid();
const updatedGame = await updateGame(getAuthorizationHeader, id, gameId, {
authorName: project.getAuthor() || 'Unspecified publisher',
gameName: project.getName() || 'Untitle game',
});
const authorAcls = getAclsFromAuthorIds(project.getAuthorIds());
await setGameUserAcls(getAuthorizationHeader, id, gameId, authorAcls);
onGameUpdated(updatedGame);
} catch (error) {
console.error('Unable to update the game:', error);
@@ -148,6 +166,14 @@ export const GameDetailsDialog = ({
}
};
const authorUsernames =
publicGame && publicGame.authors
? publicGame.authors.map(author => author.username).filter(Boolean)
: [];
const isGameOpenedAsProject =
!!project && project.getProjectUuid() === game.id;
return (
<Dialog
title={
@@ -179,53 +205,84 @@ export const GameDetailsDialog = ({
</Tabs>
<Line>
{currentTab === 'details' ? (
<ColumnStackLayout expand>
<Text>
<Trans>
Created on{' '}
{format(game.createdAt * 1000 /* TODO */, 'yyyy-MM-dd')}.
</Trans>
</Text>
<SemiControlledTextField
fullWidth
disabled
value={game.gameName}
onChange={() => {}}
floatingLabelText={<Trans>Game name</Trans>}
/>
<SemiControlledTextField
fullWidth
disabled
value={game.authorName}
onChange={() => {}}
floatingLabelText={<Trans>Publisher name</Trans>}
/>
<Line noMargin justifyContent="space-between">
<FlatButton
onClick={() => {
const answer = Window.showConfirmDialog(
"Are you sure you want to unregister this game? You won't get access to analytics and metrics, unless you register it again."
);
if (!answer) return;
unregisterGame();
}}
label={<Trans>Unregister this game</Trans>}
publicGameError ? (
<PlaceholderError onRetry={loadPublicGame}>
<Trans>There was an issue getting the game details.</Trans>{' '}
<Trans>Verify your internet connection or try again later.</Trans>
</PlaceholderError>
) : !publicGame ? (
<PlaceholderLoader />
) : (
<ColumnStackLayout expand>
{!isGameOpenedAsProject && (
<AlertMessage kind="info">
<Trans>
In order to update these details you have to open the game's
project.
</Trans>
</AlertMessage>
)}
<Line alignItems="center">
<Line expand justifyContent="flex-start" alignItems="center">
{authorUsernames && (
<>
<Text>
<Trans>Authors:</Trans>
</Text>
<Line>
{authorUsernames.map((username, index) => (
<React.Fragment key={username}>
<Spacer />
<Chip
size="small"
label={username}
color={index === 0 ? 'primary' : 'default'}
/>
</React.Fragment>
))}
</Line>
</>
)}
</Line>
<Line expand justifyContent="flex-end">
<Text>
<Trans>
Created on{' '}
{format(game.createdAt * 1000 /* TODO */, 'yyyy-MM-dd')}
</Trans>
</Text>
</Line>
</Line>
<SemiControlledTextField
fullWidth
disabled
value={publicGame.gameName}
onChange={() => {}}
floatingLabelText={<Trans>Game name</Trans>}
/>
{areProjectAndGameDiffering(project, game) ? (
<Line noMargin justifyContent="flex-end">
<FlatButton
onClick={() => {
const answer = Window.showConfirmDialog(
"Are you sure you want to unregister this game? You won't get access to analytics and metrics, unless you register it again."
);
if (!answer) return;
unregisterGame();
}}
label={<Trans>Unregister this game</Trans>}
/>
<Spacer />
<RaisedButton
primary
onClick={() => {
updateGameFromProject();
}}
label={
<Trans>Update the game details from the project</Trans>
}
onClick={() => setIsPublicGamePropertiesDialogOpen(true)}
label={<Trans>Edit game details</Trans>}
disabled={!isGameOpenedAsProject}
/>
) : null}
</Line>
</ColumnStackLayout>
</Line>
</ColumnStackLayout>
)
) : null}
{currentTab === 'builds' ? (
<Builds
@@ -373,6 +430,18 @@ export const GameDetailsDialog = ({
)
) : null}
</Line>
{publicGame && project && (
<PublicGamePropertiesDialog
open={isPublicGamePropertiesDialogOpen}
project={project}
game={publicGame}
onApply={() => {
setIsPublicGamePropertiesDialogOpen(false);
updateGameFromProject();
}}
onClose={() => setIsPublicGamePropertiesDialogOpen(false)}
/>
)}
</Dialog>
);
};

View File

@@ -4,7 +4,7 @@ import * as React from 'react';
import CreateProfile from '../Profile/CreateProfile';
import AuthenticatedUserContext from '../Profile/AuthenticatedUserContext';
import AlertMessage from '../UI/AlertMessage';
import { Line, Spacer } from '../UI/Grid';
import { Line } from '../UI/Grid';
import { ColumnStackLayout } from '../UI/Layout';
import { showErrorBox } from '../UI/Messages/MessageBox';
import PlaceholderError from '../UI/PlaceholderError';
@@ -17,7 +17,6 @@ import {
} from '../Utils/GDevelopServices/Game';
import { type Profile } from '../Utils/GDevelopServices/Authentication';
import TimelineIcon from '@material-ui/icons/Timeline';
import MonetizationOnIcon from '@material-ui/icons/MonetizationOn';
import { GameDetailsDialog } from './GameDetailsDialog';
type Props = {|
@@ -28,7 +27,7 @@ type Props = {|
onGameRegistered?: () => void,
|};
type DetailsTab = 'details' | 'analytics' | 'monetization';
type DetailsTab = 'details' | 'analytics';
type UnavailableReason = 'unauthorized' | 'not-existing' | null;
export const GameRegistration = ({
@@ -105,7 +104,7 @@ export const GameRegistration = ({
try {
await registerGame(getAuthorizationHeader, id, {
gameId: project.getProjectUuid(),
authorName: project.getAuthor() || 'Unspecified author',
authorName: project.getAuthor() || 'Unspecified publisher',
gameName: project.getName() || 'Untitled game',
});
loadGame();
@@ -321,15 +320,6 @@ export const GameRegistrationWidget = ({
setDetailsOpened(true);
}}
/>
<Spacer />
<RaisedButton
icon={<MonetizationOnIcon />}
label={<Trans>Monetization</Trans>}
onClick={() => {
setDetailsInitialTab('monetization');
setDetailsOpened(true);
}}
/>
</Line>
{detailsOpened && (
<GameDetailsDialog

View File

@@ -107,10 +107,6 @@ export const GamesList = (props: Props) => {
setOpenedGameInitialTab('analytics');
setOpenedGame(game);
}}
onOpenMonetization={() => {
setOpenedGameInitialTab('monetization');
setOpenedGame(game);
}}
/>
))}
{openedGame && (

View File

@@ -36,7 +36,7 @@ import {
type HotReloadPreviewButtonProps,
NewPreviewIcon,
} from '../HotReload/HotReloadPreviewButton';
import { UsersAutocomplete } from '../Utils/UsersAutocomplete';
import PublicGameProperties from './PublicGameProperties';
type Props = {|
project: gdProject,
@@ -59,7 +59,7 @@ type ProjectProperties = {|
adaptGameResolutionAtRuntime: boolean,
name: string,
author: string,
authorIds: gdVectorString,
authorIds: string[],
version: string,
packageName: string,
orientation: string,
@@ -79,7 +79,7 @@ function loadPropertiesFromProject(project: gdProject): ProjectProperties {
adaptGameResolutionAtRuntime: project.getAdaptGameResolutionAtRuntime(),
name: project.getName(),
author: project.getAuthor(),
authorIds: project.getAuthorIds(),
authorIds: project.getAuthorIds().toJSArray(),
version: project.getVersion(),
packageName: project.getPackageName(),
orientation: project.getOrientation(),
@@ -103,6 +103,7 @@ function applyPropertiesToProject(
gameResolutionHeight,
adaptGameResolutionAtRuntime,
name,
authorIds,
author,
version,
packageName,
@@ -118,6 +119,9 @@ function applyPropertiesToProject(
project.setGameResolutionSize(gameResolutionWidth, gameResolutionHeight);
project.setAdaptGameResolutionAtRuntime(adaptGameResolutionAtRuntime);
project.setName(name);
const projectAuthorIds = project.getAuthorIds();
projectAuthorIds.clear();
authorIds.forEach(authorId => projectAuthorIds.push_back(authorId));
project.setAuthor(author);
project.setVersion(version);
project.setPackageName(packageName);
@@ -141,6 +145,7 @@ function ProjectPropertiesDialog(props: Props) {
[project]
);
let [name, setName] = React.useState(initialProperties.name);
let [authorIds, setAuthorIds] = React.useState(initialProperties.authorIds);
let [gameResolutionWidth, setGameResolutionWidth] = React.useState(
initialProperties.gameResolutionWidth
);
@@ -200,7 +205,7 @@ function ProjectPropertiesDialog(props: Props) {
adaptGameResolutionAtRuntime,
name,
author,
authorIds: project.getAuthorIds(),
authorIds,
version,
packageName,
orientation,
@@ -276,22 +281,19 @@ function ProjectPropertiesDialog(props: Props) {
>
{currentTab === 'properties' && (
<ColumnStackLayout expand noMargin>
<SemiControlledTextField
floatingLabelText={<Trans>Game name</Trans>}
fullWidth
type="text"
value={name}
onChange={setName}
autoFocus
/>
<SemiControlledTextField
floatingLabelText={<Trans>Version number (X.Y.Z)</Trans>}
fullWidth
hintText={defaultVersion}
type="text"
value={version}
onChange={setVersion}
<Text size="title">
<Trans>Game Info</Trans>
</Text>
<PublicGameProperties
name={name}
setName={setName}
project={project}
authorIds={authorIds}
setAuthorIds={setAuthorIds}
/>
<Text size="title">
<Trans>Packaging</Trans>
</Text>
<SemiControlledTextField
floatingLabelText={
<Trans>Package name (for iOS and Android)</Trans>
@@ -313,16 +315,24 @@ function ProjectPropertiesDialog(props: Props) {
)
}
/>
<UsersAutocomplete
userIds={project.getAuthorIds()}
floatingLabelText={<Trans>Authors</Trans>}
helperText={
<Trans>
Select the usernames of the authors of this project. They
will be displayed in the selected order, if you publish
this game as an example or in the community.
</Trans>
}
<SemiControlledTextField
floatingLabelText={<Trans>Version number (X.Y.Z)</Trans>}
fullWidth
hintText={defaultVersion}
type="text"
value={version}
onChange={setVersion}
/>
<SemiControlledTextField
floatingLabelText={<Trans>Publisher name</Trans>}
fullWidth
hintText={t`Your name`}
helperMarkdownText={i18n._(
t`This will be used when packaging and submitting your application to the stores.`
)}
type="text"
value={author}
onChange={setAuthor}
/>
{useDeprecatedZeroAsDefaultZOrder ? (
<React.Fragment>
@@ -534,20 +544,7 @@ function ProjectPropertiesDialog(props: Props) {
</Trans>
</DismissableAlertMessage>
)}
<Text size="title">
<Trans>Publishing</Trans>
</Text>
<SemiControlledTextField
floatingLabelText={<Trans>Publisher name</Trans>}
fullWidth
hintText={t`Your name`}
helperMarkdownText={i18n._(
t`This will be used when packaging and submitting your application to the stores.`
)}
type="text"
value={author}
onChange={setAuthor}
/>
<Text size="title">
<Trans>Project files</Trans>
</Text>

View File

@@ -0,0 +1,49 @@
// @flow
import React from 'react';
import { Trans } from '@lingui/macro';
import SemiControlledTextField from '../UI/SemiControlledTextField';
import { UsersAutocomplete } from '../Utils/UsersAutocomplete';
import { ColumnStackLayout } from '../UI/Layout';
type Props = {|
project: gdProject,
setName: string => void,
name: string,
setAuthorIds: (string[]) => void,
authorIds: string[],
|};
function PublicGameProperties({
project,
setName,
name,
setAuthorIds,
authorIds,
}: Props) {
return (
<ColumnStackLayout noMargin>
<SemiControlledTextField
floatingLabelText={<Trans>Game name</Trans>}
fullWidth
type="text"
value={name}
onChange={setName}
autoFocus
/>
<UsersAutocomplete
userIds={authorIds}
onChange={setAuthorIds}
floatingLabelText={<Trans>Authors</Trans>}
helperText={
<Trans>
Select the usernames of the authors of this project. They will be
displayed in the selected order, if you publish this game as an
example or in the community.
</Trans>
}
/>
</ColumnStackLayout>
);
}
export default PublicGameProperties;

View File

@@ -0,0 +1,100 @@
// @flow
import { Trans } from '@lingui/macro';
import React from 'react';
import PublicGameProperties from './PublicGameProperties';
import RaisedButton from '../UI/RaisedButton';
import {
displayProjectErrorsBox,
getProjectPropertiesErrors,
} from '../Utils/ProjectErrorsChecker';
import FlatButton from '../UI/FlatButton';
import Dialog from '../UI/Dialog';
import { type PublicGame } from '../Utils/GDevelopServices/Game';
type PublicProjectProperties = {|
name: string,
authorIds: string[],
|};
function applyPublicPropertiesToProject(
project: gdProject,
newProperties: PublicProjectProperties
) {
const t = str => str; //TODO
const { name, authorIds } = newProperties;
project.setName(name);
const projectAuthorIds = project.getAuthorIds();
projectAuthorIds.clear();
authorIds.forEach(authorId => projectAuthorIds.push_back(authorId));
return displayProjectErrorsBox(t, getProjectPropertiesErrors(t, project));
}
type Props = {|
project: gdProject,
game: PublicGame,
open: boolean,
onClose: () => void,
onApply: () => void,
|};
const PublicGamePropertiesDialog = ({
project,
game,
open,
onClose,
onApply,
}: Props) => {
const publicGameAuthorIds = game.authors.map(author => author.id);
const [name, setName] = React.useState(game.gameName);
const [authorIds, setAuthorIds] = React.useState<string[]>(
publicGameAuthorIds
);
if (!open) return null;
const onSave = () => {
if (
applyPublicPropertiesToProject(project, {
name,
authorIds,
})
)
onApply();
};
const actions = [
<FlatButton
label={<Trans>Back</Trans>}
key="back"
primary={false}
onClick={onClose}
/>,
<RaisedButton
label={<Trans>Save</Trans>}
primary
onClick={onSave}
key="save"
/>,
];
return (
<Dialog
title={<Trans>Game info</Trans>}
onRequestClose={onClose}
actions={actions}
cannotBeDismissed={false}
open={open}
>
<PublicGameProperties
name={name}
setName={setName}
project={project}
authorIds={authorIds}
setAuthorIds={setAuthorIds}
/>
</Dialog>
);
};
export default PublicGamePropertiesDialog;

View File

@@ -12,6 +12,13 @@ type Option = {|
export type DataSource = Array<?Option>;
const styles = {
chip: {
// Make the chips smaller to fit the input
height: 25,
},
};
type Props = {|
value: Array<Option>,
onChange: Option => void,
@@ -60,6 +67,10 @@ export default function SemiControlledMultiAutoComplete(props: Props) {
/>
)}
fullWidth={props.fullWidth}
disabled={props.loading}
ChipProps={{
style: styles.chip,
}}
/>
)}
</I18n>

View File

@@ -83,7 +83,7 @@ export default ({ tags, onChange, onRemove }: Props) => {
style={getChipStyle(tag)}
onBlur={() => setFocusedTag(null)}
onFocus={() => setFocusedTag(tag)}
onDelete={handleDeleteTag(tag)}
onDelete={onChange || onRemove ? handleDeleteTag(tag) : null}
label={tag}
ref={newRef}
/>

View File

@@ -2,6 +2,15 @@
import axios from 'axios';
import { GDevelopGameApi, GDevelopGamesPlatform } from './ApiConfigs';
import { type Filters } from './Filters';
import { type UserPublicProfile } from './User';
export type PublicGame = {
id: string,
gameName: string,
authorName: string, // this corresponds to the publisher name
publicWebBuildId?: ?string,
authors: Array<UserPublicProfile>,
};
export type Game = {
id: string,
@@ -49,6 +58,15 @@ export const getGameUrl = (game: ?Game) => {
return GDevelopGamesPlatform.getGameUrl(game.id);
};
export const getAclsFromAuthorIds = (
authorIds: gdVectorString
): Array<{| userId: string, feature: string, level: string |}> =>
authorIds.toJSArray().map(authorId => ({
userId: authorId,
feature: 'author',
level: 'owner',
}));
export const listAllShowcasedGames = (): Promise<AllShowcasedGames> => {
return axios
.get(`${GDevelopGameApi.baseUrl}/showcased-game`)
@@ -137,6 +155,33 @@ export const updateGame = (
.then(response => response.data);
};
export const setGameUserAcls = (
getAuthorizationHeader: () => Promise<string>,
userId: string,
gameId: string,
acls: Array<{| userId: string, feature: string, level: string |}>
): Promise<void> => {
return getAuthorizationHeader()
.then(authorizationHeader =>
axios.post(
`${GDevelopGameApi.baseUrl}/game/action/set-acls`,
{
gameId,
acls,
},
{
params: {
userId,
},
headers: {
Authorization: authorizationHeader,
},
}
)
)
.then(response => response.data);
};
export const getGame = (
getAuthorizationHeader: () => Promise<string>,
userId: string,
@@ -192,3 +237,9 @@ export const getGames = (
)
.then(response => response.data);
};
export const getPublicGame = (gameId: string): Promise<PublicGame> => {
return axios
.get(`${GDevelopGameApi.baseUrl}/public-game/${gameId}`)
.then(response => response.data);
};

View File

@@ -14,12 +14,13 @@ import {
import useForceUpdate from './UseForceUpdate';
type Props = {|
userIds: gdVectorString,
userIds: Array<string>,
onChange: (Array<string>) => void,
floatingLabelText?: React.Node,
helperText: React.Node,
|};
type Option = {|
export type AutocompleteOption = {|
text: string,
value: string,
|};
@@ -28,9 +29,14 @@ const getErrorMessage = (error: ?Error) => {
if (error) return 'Error while loading users';
};
export const UsersAutocomplete = (props: Props) => {
export const UsersAutocomplete = ({
userIds,
onChange,
floatingLabelText,
helperText,
}: Props) => {
const forceUpdate = useForceUpdate();
const [users, setUsers] = React.useState<Array<Option>>([]);
const [users, setUsers] = React.useState<Array<AutocompleteOption>>([]);
const [userInput, setUserInput] = useState('');
const [loading, setLoading] = useState(false);
const [
@@ -52,11 +58,11 @@ export const UsersAutocomplete = (props: Props) => {
userInput
);
setCompletionUserPublicProfiles(userPublicProfiles);
setLoading(false);
} catch (err) {
setLoading(false);
setError(err);
console.error('Could not load the users: ', err);
} finally {
setLoading(false);
}
}, 500);
@@ -72,7 +78,6 @@ export const UsersAutocomplete = (props: Props) => {
const getUserPublicProfilesForAutocomplete = React.useCallback(
async () => {
setError(null);
const userIds = props.userIds.toJSArray();
if (!userIds.length) {
setUsers([]);
return;
@@ -83,7 +88,7 @@ export const UsersAutocomplete = (props: Props) => {
userIds
);
setUsers(
Object.keys(userPublicProfilesByIds).map(userId => {
userIds.map(userId => {
const userPublicProfile: UserPublicProfile =
userPublicProfilesByIds[userId];
return {
@@ -92,14 +97,14 @@ export const UsersAutocomplete = (props: Props) => {
};
})
);
setLoading(false);
} catch (err) {
setLoading(false);
setError(err);
console.error('Could not load the users: ', err);
} finally {
setLoading(false);
}
},
[props.userIds]
[userIds]
);
// Do only once.
@@ -113,19 +118,15 @@ export const UsersAutocomplete = (props: Props) => {
return (
<SemiControlledMultiAutoComplete
hintText={t`Start typing a username`}
floatingLabelText={props.floatingLabelText}
helperText={props.helperText}
floatingLabelText={floatingLabelText}
helperText={helperText}
value={users}
onChange={(event, values) => {
if (!values) return;
// change users in state
setUsers(values);
// change users in project
const userIds = props.userIds;
userIds.clear();
values.forEach(({ text, value }) => {
userIds.push_back(value);
});
// call top onChange on user ids
onChange(values.map(option => option.value));
forceUpdate();
}}
inputValue={userInput}

View File

@@ -5185,7 +5185,6 @@ storiesOf('GameDashboard/GameCard', module)
onOpenDetails={action('onOpenDetails')}
onOpenBuilds={action('onOpenBuilds')}
onOpenAnalytics={action('onOpenAnalytics')}
onOpenMonetization={action('onOpenMonetization')}
/>
))
.add('current game', () => (
@@ -5195,7 +5194,6 @@ storiesOf('GameDashboard/GameCard', module)
onOpenDetails={action('onOpenDetails')}
onOpenBuilds={action('onOpenBuilds')}
onOpenAnalytics={action('onOpenAnalytics')}
onOpenMonetization={action('onOpenMonetization')}
/>
));