mirror of
https://github.com/4ian/GDevelop.git
synced 2025-10-15 10:19:04 +00:00
Add new settings to leaderboards
* Add parameter to control number of entries to display * Add possibility to set a default leaderboard, displayed on Liluo game page by default
This commit is contained in:
@@ -24,6 +24,7 @@ import {
|
||||
unitToNextSeparator,
|
||||
} from '../../Leaderboard/LeaderboardScoreFormatter';
|
||||
import AlertMessage from '../../UI/AlertMessage';
|
||||
import HelpButton from '../../UI/HelpButton';
|
||||
|
||||
const unitToAbbreviation = {
|
||||
hour: 'HH',
|
||||
@@ -32,6 +33,9 @@ const unitToAbbreviation = {
|
||||
millisecond: 'ms',
|
||||
};
|
||||
|
||||
const isWholeNumber = (value: any): boolean =>
|
||||
!isNaN(value) && Number.isInteger(value);
|
||||
|
||||
const getIdentifierFromUnits = (units: {|
|
||||
smallestUnit: LeaderboardScoreFormattingTimeUnit,
|
||||
biggestUnit: LeaderboardScoreFormattingTimeUnit,
|
||||
@@ -76,6 +80,8 @@ type Props = {
|
||||
const scorePreviewMaxValue = 999999999;
|
||||
const precisionMinValue = -3;
|
||||
const precisionMaxValue = 3;
|
||||
const displayedEntriesMinNumber = 1;
|
||||
const displayedEntriesMaxNumber = 25;
|
||||
|
||||
function LeaderboardAppearanceDialog({
|
||||
open,
|
||||
@@ -85,6 +91,18 @@ function LeaderboardAppearanceDialog({
|
||||
}: Props) {
|
||||
const [isLoading, setIsLoading] = React.useState<boolean>(false);
|
||||
const [scoreTitleError, setScoreTitleError] = React.useState<?string>(null);
|
||||
const [
|
||||
defaultDisplayedEntriesNumber,
|
||||
setDefaultDisplayedEntriesNumber,
|
||||
] = React.useState<number>(
|
||||
(leaderboardCustomizationSettings &&
|
||||
leaderboardCustomizationSettings.defaultDisplayedEntriesNumber) ||
|
||||
20
|
||||
);
|
||||
const [
|
||||
defaultDisplayedEntriesNumberError,
|
||||
setDefaultDisplayedEntriesNumberError,
|
||||
] = React.useState<?string>(null);
|
||||
const [scoreTitle, setScoreTitle] = React.useState<string>(
|
||||
leaderboardCustomizationSettings
|
||||
? leaderboardCustomizationSettings.scoreTitle
|
||||
@@ -113,6 +131,7 @@ function LeaderboardAppearanceDialog({
|
||||
? leaderboardCustomizationSettings.scoreFormatting.precision
|
||||
: 0
|
||||
);
|
||||
const [precisionError, setPrecisionError] = React.useState<?string>(null);
|
||||
const [timeUnits, setTimeUnits] = React.useState<string>(
|
||||
leaderboardCustomizationSettings &&
|
||||
leaderboardCustomizationSettings.scoreFormatting.type === 'time'
|
||||
@@ -134,8 +153,25 @@ function LeaderboardAppearanceDialog({
|
||||
setScoreTitleError(i18n._(t`Title cannot be empty.`));
|
||||
return;
|
||||
}
|
||||
if (!isWholeNumber(defaultDisplayedEntriesNumber)) {
|
||||
setDefaultDisplayedEntriesNumberError(
|
||||
i18n._(
|
||||
t`The number of displayed entries must be a whole value between ${displayedEntriesMinNumber} and ${displayedEntriesMaxNumber}`
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (!isWholeNumber(precision)) {
|
||||
setPrecisionError(
|
||||
i18n._(
|
||||
t`The number of decimal places must be a whole value between ${precisionMinValue} and ${precisionMaxValue}`
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
setIsLoading(true);
|
||||
const customizationSettings = {
|
||||
defaultDisplayedEntriesNumber,
|
||||
scoreTitle,
|
||||
scoreFormatting:
|
||||
scoreType === 'custom'
|
||||
@@ -159,6 +195,12 @@ function LeaderboardAppearanceDialog({
|
||||
onRequestClose={() => {
|
||||
if (!isLoading) onClose();
|
||||
}}
|
||||
secondaryActions={[
|
||||
<HelpButton
|
||||
helpPagePath="/interface/games-dashboard/leaderboard-administration"
|
||||
anchor="change_the_appearance_of_a_leaderboard"
|
||||
/>,
|
||||
]}
|
||||
actions={[
|
||||
<FlatButton
|
||||
label={<Trans>Cancel</Trans>}
|
||||
@@ -174,7 +216,40 @@ function LeaderboardAppearanceDialog({
|
||||
key={'save'}
|
||||
/>,
|
||||
]}
|
||||
onApply={() => {
|
||||
onSaveSettings(i18n);
|
||||
}}
|
||||
>
|
||||
<Text size="title">
|
||||
<Trans>Table settings</Trans>
|
||||
</Text>
|
||||
<Line>
|
||||
<TextField
|
||||
fullWidth
|
||||
type="number"
|
||||
floatingLabelText={<Trans>Number of entries to display</Trans>}
|
||||
value={
|
||||
isNaN(defaultDisplayedEntriesNumber)
|
||||
? ''
|
||||
: defaultDisplayedEntriesNumber
|
||||
}
|
||||
errorText={defaultDisplayedEntriesNumberError}
|
||||
min={displayedEntriesMinNumber}
|
||||
max={displayedEntriesMaxNumber}
|
||||
onChange={(e, newValue) => {
|
||||
if (!!defaultDisplayedEntriesNumberError && !!newValue) {
|
||||
setDefaultDisplayedEntriesNumberError(null);
|
||||
}
|
||||
|
||||
setDefaultDisplayedEntriesNumber(
|
||||
Math.max(
|
||||
displayedEntriesMinNumber,
|
||||
Math.min(displayedEntriesMaxNumber, parseFloat(newValue))
|
||||
)
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Line>
|
||||
<Text size="title">
|
||||
<Trans>Score column settings</Trans>
|
||||
</Text>
|
||||
@@ -259,10 +334,14 @@ function LeaderboardAppearanceDialog({
|
||||
floatingLabelText={
|
||||
<Trans>Round to X decimal point</Trans>
|
||||
}
|
||||
errorText={precisionError}
|
||||
value={isNaN(precision) ? '' : precision}
|
||||
min={precisionMinValue}
|
||||
max={precisionMaxValue}
|
||||
onChange={(e, newValue) => {
|
||||
if (!!precisionError && !!newValue) {
|
||||
setPrecisionError(null);
|
||||
}
|
||||
setPrecision(
|
||||
Math.max(
|
||||
precisionMinValue,
|
||||
|
@@ -64,6 +64,7 @@ import { shouldValidate } from '../../UI/KeyboardShortcuts/InteractionKeys';
|
||||
import Text from '../../UI/Text';
|
||||
import { GameRegistration } from '../GameRegistration';
|
||||
import LeaderboardAppearanceDialog from './LeaderboardAppearanceDialog';
|
||||
import FlatButton from '../../UI/FlatButton';
|
||||
|
||||
type Props = {| onLoading: boolean => void, project?: gdProject |};
|
||||
type ContainerProps = {| ...Props, gameId: string |};
|
||||
@@ -76,6 +77,7 @@ type ApiError = {|
|
||||
| 'leaderboardNameUpdate'
|
||||
| 'leaderboardSortUpdate'
|
||||
| 'leaderboardVisibilityUpdate'
|
||||
| 'leaderboardPrimaryUpdate'
|
||||
| 'leaderboardAppearanceUpdate'
|
||||
| 'leaderboardPlayerUnicityDisplayChoiceUpdate'
|
||||
| 'leaderboardCreation'
|
||||
@@ -103,13 +105,15 @@ const styles = {
|
||||
leaderboardNameTextField: { width: 125, fontSize: 14 },
|
||||
};
|
||||
|
||||
const getApiError = (payload: LeaderboardUpdatePayload) => ({
|
||||
const getApiError = (payload: LeaderboardUpdatePayload): ApiError => ({
|
||||
action: payload.name
|
||||
? 'leaderboardNameUpdate'
|
||||
: payload.sort
|
||||
? 'leaderboardSortUpdate'
|
||||
: payload.visibility
|
||||
? 'leaderboardVisibilityUpdate'
|
||||
: payload.primary
|
||||
? 'leaderboardPrimaryUpdate'
|
||||
: payload.customizationSettings
|
||||
? 'leaderboardAppearanceUpdate'
|
||||
: 'leaderboardPlayerUnicityDisplayChoiceUpdate',
|
||||
@@ -128,6 +132,11 @@ const getApiError = (payload: LeaderboardUpdatePayload) => ({
|
||||
An error occurred when updating the visibility of the leaderboard, please
|
||||
close the dialog, come back and try again.
|
||||
</Trans>
|
||||
) : payload.primary ? (
|
||||
<Trans>
|
||||
An error occurred when setting the leaderboard as default, please close
|
||||
the dialog, come back and try again.
|
||||
</Trans>
|
||||
) : payload.customizationSettings ? (
|
||||
<Trans>
|
||||
An error occurred when updating the appearance of the leaderboard, please
|
||||
@@ -792,7 +801,11 @@ export const LeaderboardAdmin = ({ onLoading, project }: Props) => {
|
||||
<SelectOption
|
||||
key={leaderboard.id}
|
||||
value={leaderboard.id}
|
||||
primaryText={leaderboard.name}
|
||||
primaryText={
|
||||
leaderboard.primary
|
||||
? t`${leaderboard.name} (default)`
|
||||
: leaderboard.name
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</SelectField>
|
||||
@@ -837,15 +850,34 @@ export const LeaderboardAdmin = ({ onLoading, project }: Props) => {
|
||||
</React.Fragment>
|
||||
))}
|
||||
</List>
|
||||
<Line>
|
||||
<RaisedButton
|
||||
<Line justifyContent="space-between">
|
||||
<FlatButton
|
||||
icon={<Delete />}
|
||||
label={<Trans>Delete</Trans>}
|
||||
disabled={isRequestPending || isEditingName}
|
||||
onClick={() => onDeleteLeaderboard(i18n)}
|
||||
/>
|
||||
<RaisedButton
|
||||
label={
|
||||
currentLeaderboard.primary ? (
|
||||
<Trans>Default</Trans>
|
||||
) : (
|
||||
<Trans>Set as default</Trans>
|
||||
)
|
||||
}
|
||||
disabled={
|
||||
isRequestPending ||
|
||||
isEditingName ||
|
||||
currentLeaderboard.primary
|
||||
}
|
||||
onClick={() =>
|
||||
onUpdateLeaderboard(i18n, { primary: true })
|
||||
}
|
||||
/>
|
||||
</Line>
|
||||
{apiError && apiError.action === 'leaderboardDeletion' ? (
|
||||
{apiError &&
|
||||
(apiError.action === 'leaderboardDeletion' ||
|
||||
apiError.action === 'leaderboardPrimaryUpdate') ? (
|
||||
<PlaceholderError kind="error">
|
||||
{apiError.message}
|
||||
</PlaceholderError>
|
||||
|
@@ -17,7 +17,7 @@ import {
|
||||
extractExtremeScoreDisplayData,
|
||||
extractEntryDisplayData,
|
||||
listLeaderboardEntries,
|
||||
listGameLeaderboards,
|
||||
listGameActiveLeaderboards,
|
||||
} from '../Utils/GDevelopServices/Play';
|
||||
import AuthenticatedUserContext from '../Profile/AuthenticatedUserContext';
|
||||
|
||||
@@ -67,8 +67,11 @@ const reducer = (state: ReducerState, action: ReducerAction): ReducerState => {
|
||||
}, {});
|
||||
const shouldDefineCurrentLeaderboardIfNoneSelected =
|
||||
!state.currentLeaderboard && leaderboards && leaderboards.length > 0;
|
||||
const primaryLeaderboard = leaderboards.find(
|
||||
leaderboard => leaderboard.primary
|
||||
);
|
||||
const newCurrentLeaderboard = shouldDefineCurrentLeaderboardIfNoneSelected
|
||||
? leaderboards[0]
|
||||
? primaryLeaderboard || leaderboards[0]
|
||||
: state.currentLeaderboard;
|
||||
return {
|
||||
...state,
|
||||
@@ -122,13 +125,25 @@ const reducer = (state: ReducerState, action: ReducerAction): ReducerState => {
|
||||
displayOnlyBestEntry: action.payload,
|
||||
};
|
||||
case 'UPDATE_OR_CREATE_LEADERBOARD':
|
||||
const leaderboardsByIdsWithUpdatedPrimaryFlags = {};
|
||||
if (state.leaderboardsByIds) {
|
||||
Object.entries(state.leaderboardsByIds).forEach(
|
||||
([leaderboardId, leaderboard]) => {
|
||||
leaderboardsByIdsWithUpdatedPrimaryFlags[leaderboardId] = {
|
||||
...leaderboard,
|
||||
// $FlowFixMe: known error where Flow returns mixed for object value https://github.com/facebook/flow/issues/2221
|
||||
primary: action.payload.primary ? undefined : leaderboard.primary,
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
leaderboardsByIdsWithUpdatedPrimaryFlags[action.payload.id] =
|
||||
action.payload;
|
||||
|
||||
return {
|
||||
...state,
|
||||
displayOnlyBestEntry: shouldDisplayOnlyBestEntries(action.payload),
|
||||
leaderboardsByIds: {
|
||||
...state.leaderboardsByIds,
|
||||
[action.payload.id]: action.payload,
|
||||
},
|
||||
leaderboardsByIds: leaderboardsByIdsWithUpdatedPrimaryFlags,
|
||||
currentLeaderboardId: action.payload.id,
|
||||
currentLeaderboard: action.payload,
|
||||
};
|
||||
@@ -192,7 +207,7 @@ const LeaderboardProvider = ({ gameId, children }: Props) => {
|
||||
isListingLeaderboards.current = true;
|
||||
try {
|
||||
dispatch({ type: 'SET_LEADERBOARDS', payload: null });
|
||||
const fetchedLeaderboards = await listGameLeaderboards(
|
||||
const fetchedLeaderboards = await listGameActiveLeaderboards(
|
||||
authenticatedUser,
|
||||
gameId
|
||||
);
|
||||
|
@@ -9,6 +9,7 @@ import { Trans } from '@lingui/macro';
|
||||
type PropsType = {
|
||||
helpPagePath: ?string,
|
||||
label?: React.Node,
|
||||
anchor?: string,
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -16,7 +17,7 @@ type PropsType = {
|
||||
*/
|
||||
const HelpButton = (props: PropsType) => {
|
||||
if (!props.helpPagePath) return null;
|
||||
const helpLink = getHelpLink(props.helpPagePath);
|
||||
const helpLink = getHelpLink(props.helpPagePath, props.anchor);
|
||||
if (!helpLink) return null;
|
||||
|
||||
return (
|
||||
|
@@ -35,6 +35,7 @@ export type LeaderboardScoreFormatting =
|
||||
| LeaderboardScoreFormattingTime;
|
||||
|
||||
export type LeaderboardCustomizationSettings = {|
|
||||
defaultDisplayedEntriesNumber?: number,
|
||||
scoreTitle: string,
|
||||
scoreFormatting: LeaderboardScoreFormatting,
|
||||
|};
|
||||
@@ -49,6 +50,7 @@ export type Leaderboard = {|
|
||||
playerUnicityDisplayChoice: LeaderboardPlayerUnicityDisplayOption,
|
||||
visibility: LeaderboardVisibilityOption,
|
||||
customizationSettings?: LeaderboardCustomizationSettings,
|
||||
primary?: boolean,
|
||||
|};
|
||||
|
||||
export type LeaderboardUpdatePayload = {|
|
||||
@@ -57,6 +59,7 @@ export type LeaderboardUpdatePayload = {|
|
||||
playerUnicityDisplayChoice?: LeaderboardPlayerUnicityDisplayOption,
|
||||
visibility?: LeaderboardVisibilityOption,
|
||||
customizationSettings?: LeaderboardCustomizationSettings,
|
||||
primary?: boolean,
|
||||
|};
|
||||
|
||||
export type LeaderboardEntry = {|
|
||||
@@ -112,7 +115,7 @@ export const extractExtremeScoreDisplayData = ({
|
||||
|
||||
export const breakUuid = (uuid: string): string => `${uuid.split('-')[0]}-...`;
|
||||
|
||||
export const listGameLeaderboards = async (
|
||||
export const listGameActiveLeaderboards = async (
|
||||
authenticatedUser: AuthenticatedUser,
|
||||
gameId: string
|
||||
): Promise<?Array<Leaderboard>> => {
|
||||
@@ -121,14 +124,14 @@ export const listGameLeaderboards = async (
|
||||
|
||||
const { uid: userId } = firebaseUser;
|
||||
const authorizationHeader = await getAuthorizationHeader();
|
||||
return axios
|
||||
.get(`${GDevelopPlayApi.baseUrl}/game/${gameId}/leaderboard`, {
|
||||
const response = await axios.get(
|
||||
`${GDevelopPlayApi.baseUrl}/game/${gameId}/leaderboard?deleted=false`,
|
||||
{
|
||||
headers: { Authorization: authorizationHeader },
|
||||
params: { userId },
|
||||
})
|
||||
.then(response =>
|
||||
response.data.filter(leaderboard => !leaderboard.deletedAt)
|
||||
);
|
||||
}
|
||||
);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const extractNextPageUriFromLinkHeader = (
|
||||
|
@@ -8,9 +8,11 @@ export const isDocumentationAbsoluteUrl = (path: string) => {
|
||||
return path.startsWith('http://') || path.startsWith('https://');
|
||||
};
|
||||
|
||||
export const getHelpLink = (path: string): string => {
|
||||
export const getHelpLink = (path: string, anchor: string = ''): string => {
|
||||
if (isRelativePathToDocumentationRoot(path))
|
||||
return `http://wiki.compilgames.net/doku.php/gdevelop5${path}?utm_source=gdevelop&utm_medium=help-link`;
|
||||
return `http://wiki.compilgames.net/doku.php/gdevelop5${path}?utm_source=gdevelop&utm_medium=help-link${
|
||||
anchor ? `#${anchor}` : ''
|
||||
}`;
|
||||
|
||||
if (isDocumentationAbsoluteUrl(path)) return path;
|
||||
|
||||
|
@@ -18,6 +18,7 @@ export default {
|
||||
decorators: [paperDecorator, muiDecorator],
|
||||
};
|
||||
|
||||
const primaryLeaderboardIndex = 2;
|
||||
const mockedLeaderboards = Array(5)
|
||||
.fill(0)
|
||||
.map((_, index) => ({
|
||||
@@ -28,6 +29,7 @@ const mockedLeaderboards = Array(5)
|
||||
startDatetime: '2021-11-18T10:19:50.417Z',
|
||||
playerUnicityDisplayChoice: index % 2 === 0 ? 'PREFER_UNIQUE' : 'FREE',
|
||||
visibility: index % 3 === 0 ? 'HIDDEN' : 'PUBLIC',
|
||||
primary: index === primaryLeaderboardIndex ? true : undefined,
|
||||
}));
|
||||
const mockedEntries = Array(8)
|
||||
.fill(0)
|
||||
@@ -96,7 +98,7 @@ const MockLeaderboardProvider = ({ children }: {| children: React.Node |}) => {
|
||||
|
||||
export const WithErrors = () => (
|
||||
<MockLeaderboardProvider>
|
||||
<FixedHeightFlexContainer height={500}>
|
||||
<FixedHeightFlexContainer height={600}>
|
||||
<LeaderboardAdmin onLoading={() => action('onLoading')} />
|
||||
</FixedHeightFlexContainer>
|
||||
</MockLeaderboardProvider>
|
||||
|
Reference in New Issue
Block a user