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:
AlexandreS
2022-05-04 10:37:01 +02:00
committed by GitHub
parent 7cdc92c776
commit c37049cd72
7 changed files with 157 additions and 23 deletions

View File

@@ -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,

View File

@@ -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>

View File

@@ -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
);

View File

@@ -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 (

View File

@@ -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 = (

View File

@@ -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;

View File

@@ -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>