mirror of
https://github.com/4ian/GDevelop.git
synced 2025-10-15 10:19:04 +00:00
Allow editing Game Public Info from Game Dashboard
This commit is contained in:
@@ -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>
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
|
@@ -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);
|
||||
|
@@ -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>
|
||||
);
|
||||
};
|
||||
|
@@ -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
|
||||
|
@@ -107,10 +107,6 @@ export const GamesList = (props: Props) => {
|
||||
setOpenedGameInitialTab('analytics');
|
||||
setOpenedGame(game);
|
||||
}}
|
||||
onOpenMonetization={() => {
|
||||
setOpenedGameInitialTab('monetization');
|
||||
setOpenedGame(game);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
{openedGame && (
|
||||
|
@@ -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>
|
||||
|
49
newIDE/app/src/ProjectManager/PublicGameProperties.js
Normal file
49
newIDE/app/src/ProjectManager/PublicGameProperties.js
Normal 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;
|
100
newIDE/app/src/ProjectManager/PublicGamePropertiesDialog.js
Normal file
100
newIDE/app/src/ProjectManager/PublicGamePropertiesDialog.js
Normal 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;
|
@@ -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>
|
||||
|
@@ -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}
|
||||
/>
|
||||
|
@@ -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);
|
||||
};
|
||||
|
@@ -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}
|
||||
|
@@ -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')}
|
||||
/>
|
||||
));
|
||||
|
||||
|
Reference in New Issue
Block a user