Improve Guided lessons (#6446)

* Lessons have been slightly reworked overall
* Most languages are now supported
* A new option allows to fill a value automatically with a button, speeding up the typing part
This commit is contained in:
Clément Pasteau
2024-03-14 14:40:12 +01:00
committed by GitHub
parent 99f7e55044
commit 6b7af0474f
28 changed files with 337 additions and 197 deletions

View File

@@ -341,8 +341,9 @@
"description": {
"messageByLocale": {
"en": "Let's choose an **object** from the asset store",
"fr": "Nous allons choisir un objet dans le **magasin de ressources**.",
"fr": "Nous allons choisir un **objet** dans le magasin de ressources.",
"es": "Vamos a elegir un **objeto** de la tienda de recursos.",
"pt": "Vamos escolher um objeto da loja de recursos.",
"th": "เลือก **วัตถุ** จาก ร้านค้า asset",
"ar": "هيّا نقوم باختيار **كائن** من متجر العناصر"
}
@@ -2180,6 +2181,7 @@
"en": "Add a few $(obstacle) to the **scene** to protect $(target).",
"fr": "Ajoutez quelques $(obstacle) à la **scène** pour protéger $(target).",
"es": "Agrega algunos $(obstacle) a la **escena** para proteger a $(target).",
"pt": "Adicione alguns $(obstacle) à **cena** para proteger $(target).",
"th": "เพิ่ม $(obstacle) ปริมาณนิดหน่อยลงใน **scene** เพื่อป้องกัน $(target)",
"ar": "إضافة بضعة $(obstacle) إلى **المشهد** لحماية $(target)."
}

View File

@@ -2,7 +2,24 @@
{
"id": "cameraParallax",
"contentUrl": "https://resources.gdevelop-app.com/in-app-tutorials/cameraParallax.json",
"availableLocales": ["en", "fr", "es", "pt", "th", "ar"],
"availableLocales": [
"en",
"fr",
"ar",
"de",
"es",
"it",
"ja",
"ko",
"pl",
"pt",
"th",
"ru",
"sl",
"sq",
"uk",
"zh"
],
"initialTemplateUrl": "https://resources.gdevelop-app.com/in-app-tutorials/templates/cameraParallax/game.json",
"initialProjectData": {
"cameraScene": "CameraScene",
@@ -19,14 +36,48 @@
{
"id": "healthBar",
"contentUrl": "https://resources.gdevelop-app.com/in-app-tutorials/healthBar.json",
"availableLocales": ["en", "fr", "es", "pt", "th", "ar"],
"availableLocales": [
"en",
"fr",
"ar",
"de",
"es",
"it",
"ja",
"ko",
"pl",
"pt",
"th",
"ru",
"sl",
"sq",
"uk",
"zh"
],
"initialTemplateUrl": "https://resources.gdevelop-app.com/in-app-tutorials/templates/healthBar/game.json",
"initialProjectData": { "level": "Level", "player": "Player" }
},
{
"id": "joystick",
"contentUrl": "https://resources.gdevelop-app.com/in-app-tutorials/joystick.json",
"availableLocales": ["en", "fr", "es", "pt", "th", "ar", "sq"],
"availableLocales": [
"en",
"fr",
"ar",
"de",
"es",
"it",
"ja",
"ko",
"pl",
"pt",
"th",
"ru",
"sl",
"sq",
"uk",
"zh"
],
"initialTemplateUrl": "https://resources.gdevelop-app.com/in-app-tutorials/templates/joystick/game.json",
"initialProjectData": {
"gameScene": "GameScene",
@@ -36,14 +87,48 @@
{
"id": "object3d",
"contentUrl": "https://resources.gdevelop-app.com/in-app-tutorials/object3d.json",
"availableLocales": ["en", "fr", "es", "pt", "th", "ar"],
"availableLocales": [
"en",
"fr",
"ar",
"de",
"es",
"it",
"ja",
"ko",
"pl",
"pt",
"th",
"ru",
"sl",
"sq",
"uk",
"zh"
],
"initialTemplateUrl": "https://resources.gdevelop-app.com/in-app-tutorials/templates/object3d/game.json",
"initialProjectData": { "gameScene": "GameScene" }
"initialProjectData": { "gameScene": "GameScene", "platform": "Platform" }
},
{
"id": "plinkoMultiplier",
"contentUrl": "https://resources.gdevelop-app.com/in-app-tutorials/plinkoMultiplier.json",
"availableLocales": ["en", "fr", "es", "pt", "th", "ar"],
"availableLocales": [
"en",
"fr",
"ar",
"de",
"es",
"it",
"ja",
"ko",
"pl",
"pt",
"th",
"ru",
"sl",
"sq",
"uk",
"zh"
],
"initialTemplateUrl": "https://resources.gdevelop-app.com/in-app-tutorials/templates/plinkoMultiplier/game.json",
"initialProjectData": {
"gameScene": "GameScene",
@@ -56,7 +141,24 @@
{
"id": "timer",
"contentUrl": "https://resources.gdevelop-app.com/in-app-tutorials/timer.json",
"availableLocales": ["en", "fr", "es", "pt", "th", "ar"],
"availableLocales": [
"en",
"fr",
"ar",
"de",
"es",
"it",
"ja",
"ko",
"pl",
"pt",
"th",
"ru",
"sl",
"sq",
"uk",
"zh"
],
"initialTemplateUrl": "https://resources.gdevelop-app.com/in-app-tutorials/templates/timer/game.json",
"initialProjectData": { "gameScene": "GameScene" }
}

View File

@@ -93,7 +93,6 @@ export const CustomObjectPackResults = ({
<TextButton
icon={<ChevronArrowLeft />}
label={<Trans>Back</Trans>}
primary={false}
onClick={onBack}
disabled={isAssetBeingInstalled}
/>

View File

@@ -652,7 +652,6 @@ export const AssetStore = React.forwardRef<Props, AssetStoreInterface>(
<TextButton
icon={<ChevronArrowLeft />}
label={<Trans>Back</Trans>}
primary={false}
onClick={async () => {
const page = shopNavigationState.backToPreviousPage();
const isUpdatingSearchtext = reApplySearchTextIfNeeded(

View File

@@ -43,6 +43,7 @@ import useAlertDialog from '../UI/Alert/useAlertDialog';
import PasteIcon from '../UI/CustomSvgIcons/Clipboard';
import CopyIcon from '../UI/CustomSvgIcons/Copy';
import ResponsiveFlatButton from '../UI/ResponsiveFlatButton';
import { useResponsiveWindowSize } from '../UI/Responsive/ResponsiveWindowMeasurer';
const gd: libGDevelop = global.gd;
@@ -281,6 +282,7 @@ type Props = {|
|};
const BehaviorsEditor = (props: Props) => {
const { isMobile } = useResponsiveWindowSize();
const scrollView = React.useRef<?ScrollViewInterface>(null);
const [
justAddedBehaviorName,
@@ -598,7 +600,9 @@ const BehaviorsEditor = (props: Props) => {
helpPagePath="/behaviors"
tutorialId="intro-behaviors-and-functions"
actionButtonId="add-behavior-button"
actionLabel={<Trans>Add a behavior</Trans>}
actionLabel={
isMobile ? <Trans>Add</Trans> : <Trans>Add a behavior</Trans>
}
onAction={() => setNewBehaviorDialogOpen(true)}
secondaryActionIcon={<PasteIcon />}
secondaryActionLabel={
@@ -645,7 +649,13 @@ const BehaviorsEditor = (props: Props) => {
<ResponsiveFlatButton
key={'copy-all-behaviors'}
leftIcon={<CopyIcon />}
label={<Trans>Copy all behaviors</Trans>}
label={
isMobile ? (
<Trans>Copy all</Trans>
) : (
<Trans>Copy all behaviors</Trans>
)
}
onClick={() => {
copyAllBehaviors();
}}
@@ -663,7 +673,13 @@ const BehaviorsEditor = (props: Props) => {
<LineStackLayout justifyContent="flex-end" expand>
<RaisedButton
key="add-behavior-line"
label={<Trans>Add a behavior</Trans>}
label={
isMobile ? (
<Trans>Add</Trans>
) : (
<Trans>Add a behavior</Trans>
)
}
primary
onClick={() => setNewBehaviorDialogOpen(true)}
icon={<Add />}

View File

@@ -370,7 +370,6 @@ const PublishHome = ({
<TextButton
icon={<ChevronArrowLeft />}
label={<Trans>Back</Trans>}
primary={false}
onClick={onBack}
disabled={isNavigationDisabled}
/>

View File

@@ -1039,20 +1039,20 @@ const InAppTutorialOrchestrator = React.forwardRef<
let formattedStepTrigger;
const stepTrigger = currentStep.nextStepTrigger;
if (stepTrigger) {
if (stepTrigger.clickOnTooltipButton) {
const formattedButtonLabel = translateAndInterpolateText({
text: stepTrigger.clickOnTooltipButton,
data,
i18n,
project,
});
formattedStepTrigger = formattedButtonLabel
? {
clickOnTooltipButton: formattedButtonLabel,
}
: undefined;
}
if (stepTrigger && stepTrigger.clickOnTooltipButton) {
const formattedButtonLabel = translateAndInterpolateText({
text: stepTrigger.clickOnTooltipButton,
data,
i18n,
project,
});
formattedStepTrigger = formattedButtonLabel
? {
clickOnTooltipButton: formattedButtonLabel,
}
: undefined;
} else {
formattedStepTrigger = stepTrigger;
}
const formattedStep: InAppTutorialFlowFormattedStep = {
...currentStep,

View File

@@ -100,7 +100,10 @@ const getWrongEditorTooltip = (
export const queryElementOrItsMostVisuallySignificantParent = (
elementToHighlightId: string
) => {
): {|
elementToHighlight: ?HTMLElement,
elementWithId: ?HTMLElement,
|} => {
let foundElement = document.querySelector(elementToHighlightId);
if (foundElement instanceof HTMLTextAreaElement) {
// In this case, the element to highlight is a Material UI multiline text field
@@ -108,7 +111,7 @@ export const queryElementOrItsMostVisuallySignificantParent = (
// to highlight the parent div.
const parentDiv = foundElement.closest('div');
if (parentDiv instanceof HTMLElement && isElementAMuiInput(parentDiv)) {
foundElement = parentDiv;
return { elementToHighlight: parentDiv, elementWithId: foundElement };
}
} else if (
foundElement instanceof HTMLInputElement &&
@@ -116,10 +119,13 @@ export const queryElementOrItsMostVisuallySignificantParent = (
) {
const containerDiv = foundElement.closest('div[data-search-bar-container]');
if (containerDiv instanceof HTMLElement) {
foundElement = containerDiv;
return { elementToHighlight: containerDiv, elementWithId: foundElement };
}
}
return foundElement;
return {
elementToHighlight: foundElement,
elementWithId: foundElement,
};
};
type Props = {|
@@ -149,6 +155,7 @@ function InAppTutorialStepDisplayer({
elementToHighlight,
setElementToHighlight,
] = React.useState<?HTMLElement>(null);
const [elementWithId, setElementWithId] = React.useState<?HTMLElement>(null);
const [
hideBehindOtherDialog,
setHideBehindOtherDialog,
@@ -169,9 +176,12 @@ function InAppTutorialStepDisplayer({
() => {
if (!elementToHighlightId) return;
setElementToHighlight(
queryElementOrItsMostVisuallySignificantParent(elementToHighlightId)
);
const {
elementToHighlight,
elementWithId,
} = queryElementOrItsMostVisuallySignificantParent(elementToHighlightId);
setElementToHighlight(elementToHighlight);
setElementWithId(elementWithId);
},
[elementToHighlightId]
);
@@ -262,6 +272,36 @@ function InAppTutorialStepDisplayer({
);
};
const getFillAutomaticallyFunction = React.useCallback(
() => {
if (!nextStepTrigger || !nextStepTrigger.valueEquals) {
return undefined;
}
if (
!(elementWithId instanceof HTMLInputElement) &&
!(elementWithId instanceof HTMLTextAreaElement)
) {
return undefined;
}
const valuePropertyDescriptor = Object.getOwnPropertyDescriptor(
elementWithId.constructor.prototype,
'value'
);
if (!valuePropertyDescriptor) return undefined;
const valueSetter = valuePropertyDescriptor.set;
if (!valueSetter) return undefined;
return () => {
valueSetter.call(elementWithId, nextStepTrigger.valueEquals);
// Trigger blur to make sure the value is taken into account
// by the React input.
elementWithId.dispatchEvent(new Event('blur', { bubbles: true }));
};
},
[nextStepTrigger, elementWithId]
);
const renderTooltip = (i18n: I18nType) => {
if (tooltip && !expectedEditor) {
const anchorElement = tooltip.standalone
@@ -281,6 +321,7 @@ function InAppTutorialStepDisplayer({
? nextStepTrigger.clickOnTooltipButton
: undefined
}
fillAutomatically={getFillAutomaticallyFunction()}
/>
);
}
@@ -290,7 +331,7 @@ function InAppTutorialStepDisplayer({
return (
<InAppTutorialTooltipDisplayer
endTutorial={endTutorial}
showQuitButton={!isOnClosableDialog}
showQuitButton // Always show the quit button when the user is on the wrong editor
anchorElement={assistantImage}
tooltip={wrongEditorTooltip}
progress={progress}

View File

@@ -21,6 +21,7 @@ import { LineStackLayout } from '../UI/Layout';
import ChevronArrowTop from '../UI/CustomSvgIcons/ChevronArrowTop';
import { textEllipsisStyle } from '../UI/TextEllipsis';
import { useResponsiveWindowSize } from '../UI/Responsive/ResponsiveWindowMeasurer';
import TextButton from '../UI/TextButton';
const themeColors = {
grey10: '#EBEBED',
@@ -129,12 +130,14 @@ type TooltipBodyProps = {|
tooltip: InAppTutorialFormattedTooltip,
buttonLabel?: string,
goToNextStep: () => void,
fillAutomatically?: () => void,
|};
const TooltipBody = ({
tooltip,
buttonLabel,
goToNextStep,
fillAutomatically,
}: TooltipBodyProps) => {
const { isMobile } = useResponsiveWindowSize();
const titleAndDescription = (
@@ -182,10 +185,18 @@ const TooltipBody = ({
</Column>
);
const fillAutomaticallyButton = fillAutomatically && (
<TextButton
onClick={fillAutomatically}
label={<Trans>Fill automatically</Trans>}
primary
/>
);
return (
<Column noMargin>
{titleAndDescription}
{imageAndButton}
{fillAutomaticallyButton}
</Column>
);
};
@@ -287,6 +298,7 @@ type Props = {|
progress: number,
endTutorial: () => void,
goToNextStep: () => void,
fillAutomatically?: () => void,
|};
const InAppTutorialTooltipDisplayer = ({
@@ -297,6 +309,7 @@ const InAppTutorialTooltipDisplayer = ({
progress,
endTutorial,
goToNextStep,
fillAutomatically,
}: Props) => {
const { isMobile } = useResponsiveWindowSize();
const {
@@ -388,6 +401,7 @@ const InAppTutorialTooltipDisplayer = ({
tooltip={tooltip}
buttonLabel={buttonLabel}
goToNextStep={goToNextStep}
fillAutomatically={fillAutomatically}
/>
)}
</Column>

View File

@@ -500,7 +500,7 @@ const BuildSection = ({
</Text>
<Spacer />
<TextButton
primary
secondary
label={
isMobile ? (
<Trans>Open</Trans>

View File

@@ -99,85 +99,63 @@ const GuidedLessons = ({ selectInAppTutorial, lessonsIds }: Props) => {
return tutorialProgress.progress[0]; // guided lessons only have one part.
};
const lessonsCompleted = guidedLessonsIds.reduce((acc, tutorialId) => {
const tutorialProgress = getTutorialPartProgress({ tutorialId }) || 0;
return tutorialProgress === 100 ? acc + 1 : acc;
}, 0);
const displayedGuidedLessonsIds = lessonsIds || guidedLessonsIds;
const lessonsCompleted = displayedGuidedLessonsIds.reduce(
(acc, tutorialId) => {
const tutorialProgress = getTutorialPartProgress({ tutorialId }) || 0;
return tutorialProgress === 100 ? acc + 1 : acc;
},
0
);
const lessonsProgress = Math.round(
(lessonsCompleted / guidedLessonsIds.length) * 100
(lessonsCompleted / displayedGuidedLessonsIds.length) * 100
);
const guidedLessonCards = [
{
id: JOYSTICK_IN_APP_TUTORIAL_ID,
title: t`Add Joystick controls`,
title: t`Joystick controls`,
description: t`Learn how to add a joystick to control the player.`,
keyPoints: [
t`Add a layer`,
t`Download and use a prefab`,
t`Use a behavior`,
],
durationInMinutes: 1,
renderImage: props => <Joystick {...props} />,
},
{
id: HEALTH_BAR_IN_APP_TUTORIAL_ID,
title: t`Display a Health bar for the player`,
title: t`Health bar`,
description: t`Learn how to display the health of a player on the foreground.`,
keyPoints: [t`Add a layer`, t`Download and use a prefab`],
durationInMinutes: 2,
renderImage: props => <HealthBar {...props} />,
},
{
id: OBJECT_3D_IN_APP_TUTORIAL_ID,
title: t`Add a 3D object`,
description: t`Learn how to add a 3D object to your game.`,
keyPoints: [
t`Add a 3D box`,
t`Add a behavior`,
t`Update the elevation of a 3D box`,
],
title: t`3D box`,
description: t`Learn how to add a 3D box to your game.`,
durationInMinutes: 2,
renderImage: props => <Object3D {...props} />,
},
{
id: CAMERA_PARALLAX_IN_APP_TUTORIAL_ID,
title: t`Improve background and camera`,
title: t`Background`,
description: t`Learn how to create a parallax background as well as a camera that follows the player.`,
keyPoints: [
t`Add an extension`,
t`Add a layer`,
t`Use a tiled sprite`,
t`Control the camera`,
],
durationInMinutes: 2,
renderImage: props => <Parallax {...props} />,
},
{
id: TIMER_IN_APP_TUTORIAL_ID,
title: t`Use a timer`,
title: t`Timer`,
description: t`Learn how to use a timer to count a score.`,
keyPoints: [
t`Create and use a timer`,
t`Create and modify a text`,
t`Build an expression`,
],
durationInMinutes: 2,
renderImage: props => <Timer {...props} />,
},
{
id: PLINKO_MULTIPLIER_IN_APP_TUTORIAL_ID,
title: t`Add score multiplier`,
title: t`Score multiplier`,
description: t`Learn how to manipulate a score by adding collectibles.`,
keyPoints: [
t`Create a variable`,
t`Use & manipulate a variable`,
t`Build an expression`,
],
durationInMinutes: 3,
renderImage: props => <MultiplierScore {...props} />,
},
].filter(item => (lessonsIds ? lessonsIds.includes(item.id) : true));
].filter(item => displayedGuidedLessonsIds.includes(item.id));
return (
<Line>
@@ -191,20 +169,18 @@ const GuidedLessons = ({ selectInAppTutorial, lessonsIds }: Props) => {
</PlaceholderError>
) : (
<ColumnStackLayout noMargin>
{!lessonsIds && (
<Column>
<LineStackLayout alignItems="center">
{lessonsProgress !== 100 ? (
<Text displayInlineAsSpan noMargin size="body2">
{lessonsProgress}%
</Text>
) : (
<Trophy />
)}
<ColoredLinearProgress value={lessonsProgress} />
</LineStackLayout>
</Column>
)}
<Column>
<LineStackLayout alignItems="center">
{lessonsProgress !== 100 ? (
<Text displayInlineAsSpan noMargin size="body2">
{lessonsProgress}%
</Text>
) : (
<Trophy />
)}
<ColoredLinearProgress value={lessonsProgress} />
</LineStackLayout>
</Column>
<GridList
cols={getColumnsFromWindowSize(windowSize, isLandscape)}
style={styles.grid}
@@ -217,7 +193,6 @@ const GuidedLessons = ({ selectInAppTutorial, lessonsIds }: Props) => {
title={item.title}
description={item.description}
durationInMinutes={item.durationInMinutes}
keyPoints={item.keyPoints}
renderImage={item.renderImage}
progress={getTutorialPartProgress({ tutorialId: item.id })}
onClick={() => selectInAppTutorial(item.id)}

View File

@@ -4,10 +4,8 @@ import { Trans } from '@lingui/macro';
import * as React from 'react';
import Dialog, { DialogPrimaryButton } from '../../../../UI/Dialog';
import FlatButton from '../../../../UI/FlatButton';
import { Line, Column } from '../../../../UI/Grid';
import { Line } from '../../../../UI/Grid';
import { ColumnStackLayout } from '../../../../UI/Layout';
import InAppTutorialContext from '../../../../InAppTutorial/InAppTutorialContext';
import { getLanguageLabelForLocale } from '../../../../Utils/i18n/MessageByLocale';
import Text from '../../../../UI/Text';
import {
FLING_GAME_IN_APP_TUTORIAL_ID,
@@ -26,15 +24,6 @@ const styles = {
},
};
type Props = {|
open: boolean,
tutorialId: string,
onClose: () => void,
tutorialCompletionStatus: 'notStarted' | 'started' | 'complete',
isProjectOpened?: boolean,
startTutorial: (scenario: 'resume' | 'startOver' | 'start') => Promise<void>,
|};
const getGuidedLessonContent = ({
learningKeys,
}: {
@@ -44,12 +33,6 @@ const getGuidedLessonContent = ({
<Text>
<Trans>You're about to start this guided lesson.</Trans>
</Text>
<Text>
<Trans>
A new project will be opened, so before beginning please ensure you have
closed and saved your current project.
</Trans>
</Text>
<Text>
<Trans>In this tutorial you will learn:</Trans>
</Text>
@@ -105,7 +88,6 @@ const titleAndContentByKey = {
content: getGuidedLessonContent({
learningKeys: [
<Trans>Add a background with parallax effect</Trans>,
<Trans>Add a new layer</Trans>,
<Trans>Add an extension</Trans>,
<Trans>Use basic camera movements to follow the player</Trans>,
],
@@ -117,8 +99,8 @@ const titleAndContentByKey = {
),
content: getGuidedLessonContent({
learningKeys: [
<Trans>Add a new layer</Trans>,
<Trans>Use a prefab to display the player's health bar</Trans>,
<Trans>Use a prefab for a health bar</Trans>,
<Trans>Update the health bar based on the player's health</Trans>,
],
}),
},
@@ -126,7 +108,6 @@ const titleAndContentByKey = {
title: <Trans>Let's add mobile controls to our game</Trans>,
content: getGuidedLessonContent({
learningKeys: [
<Trans>Add a new layer</Trans>,
<Trans>Add a joystick prefab</Trans>,
<Trans>Add a behavior</Trans>,
],
@@ -138,12 +119,21 @@ const titleAndContentByKey = {
learningKeys: [
<Trans>Add a 3D Box</Trans>,
<Trans>Add a behavior</Trans>,
<Trans>Update the elevation of a 3D box</Trans>,
],
}),
},
};
type Props = {|
open: boolean,
tutorialId: string,
onClose: () => void,
tutorialCompletionStatus: 'notStarted' | 'started' | 'complete',
isProjectOpened?: boolean,
isProjectOpening: boolean,
startTutorial: (scenario: 'resume' | 'startOver' | 'start') => Promise<void>,
|};
const StartInAppTutorialDialog = ({
open,
tutorialId,
@@ -151,33 +141,16 @@ const StartInAppTutorialDialog = ({
tutorialCompletionStatus,
isProjectOpened,
startTutorial,
isProjectOpening,
}: Props) => {
const resumeTutorial = () => startTutorial('resume');
const startOverTutorial = () => startTutorial('startOver');
const startTutorialForFirstTime = () => startTutorial('start');
const { getInAppTutorialShortHeader } = React.useContext(
InAppTutorialContext
);
const selectedInAppTutorialShortHeader = getInAppTutorialShortHeader(
tutorialId
);
const availableLocales = selectedInAppTutorialShortHeader
? selectedInAppTutorialShortHeader.availableLocales
: null;
const dialogContentByCompletionStatus = {
notStarted: {
title: titleAndContentByKey[tutorialId].title,
content: titleAndContentByKey[tutorialId].content,
availableLocalesLabels: availableLocales
? availableLocales.map(locale => [
locale,
getLanguageLabelForLocale(locale),
])
: null,
primaryAction: {
label: <Trans>Yes</Trans>,
onClick: startTutorialForFirstTime,
@@ -248,12 +221,14 @@ const StartInAppTutorialDialog = ({
key="close"
label={secondaryAction.label}
onClick={secondaryAction.onClick}
disabled={isProjectOpening}
/>,
<DialogPrimaryButton
key="start"
label={primaryAction.label}
primary
onClick={primaryAction.onClick}
disabled={isProjectOpening}
/>,
];
const secondaryActions = tertiaryAction
@@ -262,6 +237,7 @@ const StartInAppTutorialDialog = ({
key="other"
label={tertiaryAction.label}
onClick={tertiaryAction.onClick}
disabled={isProjectOpening}
/>,
]
: undefined;
@@ -284,20 +260,6 @@ const StartInAppTutorialDialog = ({
</div>
</Line>
{content}
{dialogContent.availableLocalesLabels ? (
<Column noMargin>
<Text>
<Trans>
This tutorial is available in the following languages:
</Trans>
</Text>
{dialogContent.availableLocalesLabels.map(([locale, label]) => (
<Text displayAsListItem noMargin key={locale}>
{label}
</Text>
))}
</Column>
) : null}
<Text>
<Trans>Are you ready?</Trans>
</Text>

View File

@@ -3480,6 +3480,7 @@ const MainFrame = (props: Props) => {
onClose={() => {
setSelectedInAppTutorialInfo(null);
}}
isProjectOpening={isProjectOpening}
/>
)}
{state.gdjsDevelopmentWatcherEnabled &&

View File

@@ -269,7 +269,6 @@ const EditProfileDialog = ({
label={<Trans>Delete my account</Trans>}
disabled={actionInProgress}
key="delete"
primary={false}
onClick={onDeleteAccount}
/>,
];

View File

@@ -78,6 +78,12 @@ export class ExtensionTreeViewItemContent implements TreeViewItemContent {
return `extension-item-${index}`;
}
getDataSet(): { [string]: string } {
return {
extension: this.eventsFunctionsExtension.getName(),
};
}
getThumbnail(): ?string {
return (
this.eventsFunctionsExtension.getIconUrl() ||

View File

@@ -73,6 +73,12 @@ export class ExternalEventsTreeViewItemContent implements TreeViewItemContent {
return `external-events-item-${index}`;
}
getDataSet(): { [string]: string } {
return {
'external-events': this.externalEvents.getName(),
};
}
getThumbnail(): ?string {
return 'res/icons_default/external_events_black.svg';
}

View File

@@ -73,6 +73,12 @@ export class ExternalLayoutTreeViewItemContent implements TreeViewItemContent {
return `external-layout-item-${index}`;
}
getDataSet(): { [string]: string } {
return {
'external-layout': this.externalLayout.getName(),
};
}
getThumbnail(): ?string {
return 'res/icons_default/external_layout_black.svg';
}

View File

@@ -72,6 +72,12 @@ export class SceneTreeViewItemContent implements TreeViewItemContent {
return `scene-item-${index}`;
}
getDataSet(): { [string]: string } {
return {
scene: this.scene.getName(),
};
}
getThumbnail(): ?string {
return 'res/icons_default/scene_black.svg';
}

View File

@@ -116,6 +116,7 @@ export interface TreeViewItemContent {
getName(): string | React.Node;
getId(): string;
getHtmlId(index: number): ?string;
getDataSet(): { [string]: string };
getThumbnail(): ?string;
onClick(): void;
buildMenuTemplate(i18n: I18nType, index: number): Array<MenuItemTemplate>;
@@ -182,6 +183,7 @@ class PlaceHolderTreeViewItem implements TreeViewItem {
class LabelTreeViewItemContent implements TreeViewItemContent {
id: string;
label: string | React.Node;
dataSet: { [string]: string };
buildMenuTemplateFunction: (
i18n: I18nType,
index: number
@@ -199,6 +201,7 @@ class LabelTreeViewItemContent implements TreeViewItemContent {
rightButton
? [
{
id: rightButton.id,
label: rightButton.label,
click: rightButton.click,
},
@@ -223,6 +226,10 @@ class LabelTreeViewItemContent implements TreeViewItemContent {
return this.id;
}
getDataSet(): { [string]: string } {
return {};
}
getThumbnail(): ?string {
return null;
}
@@ -307,6 +314,10 @@ class ActionTreeViewItemContent implements TreeViewItemContent {
return this.id;
}
getDataSet(): { [string]: string } {
return {};
}
getThumbnail(): ?string {
return this.thumbnail;
}
@@ -358,7 +369,8 @@ const getTreeViewItemChildren = (i18n: I18nType) => (item: TreeViewItem) =>
item.getChildren(i18n);
const getTreeViewItemThumbnail = (item: TreeViewItem) =>
item.content.getThumbnail();
const getTreeViewItemData = (item: TreeViewItem) => null;
const getTreeViewItemDataSet = (item: TreeViewItem) =>
item.content.getDataSet();
const buildMenuTemplate = (i18n: I18nType) => (
item: TreeViewItem,
index: number
@@ -1016,6 +1028,7 @@ const ProjectManager = React.forwardRef<Props, ProjectManagerInterface>(
const index = project.getLayoutsCount() - 1;
addNewScene(index, i18n);
},
id: 'add-new-scene-button',
}
),
getChildren(i18n: I18nType): ?Array<TreeViewItem> {
@@ -1049,6 +1062,7 @@ const ProjectManager = React.forwardRef<Props, ProjectManagerInterface>(
icon: <Add />,
label: i18n._(t`Create or search for new extensions`),
click: openSearchExtensionDialog,
id: 'project-manager-extension-search-or-create',
}
),
getChildren(i18n: I18nType): ?Array<TreeViewItem> {
@@ -1086,6 +1100,7 @@ const ProjectManager = React.forwardRef<Props, ProjectManagerInterface>(
const index = project.getExternalEventsCount() - 1;
addExternalEvents(index, i18n);
},
id: 'add-new-external-events-button',
}
),
getChildren(i18n: I18nType): ?Array<TreeViewItem> {
@@ -1123,6 +1138,7 @@ const ProjectManager = React.forwardRef<Props, ProjectManagerInterface>(
const index = project.getExternalLayoutsCount() - 1;
addExternalLayout(index, i18n);
},
id: 'add-new-external-layout-button',
}
),
getChildren(i18n: I18nType): ?Array<TreeViewItem> {
@@ -1275,7 +1291,7 @@ const ProjectManager = React.forwardRef<Props, ProjectManagerInterface>(
multiSelect={false}
getItemId={getTreeViewItemId}
getItemHtmlId={getTreeViewItemHtmlId}
getItemDataset={getTreeViewItemData}
getItemDataset={getTreeViewItemDataSet}
onEditItem={editItem}
onCollapseItem={onCollapseItem}
selectedItems={selectedItems}

View File

@@ -9,6 +9,7 @@ import { type Node as ReactNode } from 'react';
export type MenuItemTemplate =
// "Classic" menu item
| {|
id?: string,
label: string,
visible?: boolean,
enabled?: boolean,
@@ -18,11 +19,13 @@ export type MenuItemTemplate =
|}
// Sub menu
| {|
id?: string,
label: string,
submenu: Array<MenuItemTemplate>,
|}
// Checkbox
| {|
id?: string,
type: 'checkbox',
label: string,
visible?: boolean,

View File

@@ -109,7 +109,7 @@ const NotificationList = ({
</List>
{shouldShowLoadMoreButton && (
<TextButton
primary
secondary
label={<Trans>Load more...</Trans>}
onClick={() => setShowAll(true)}
/>

View File

@@ -8,6 +8,7 @@ type Props = {|
label: React.Node,
onClick: ?(ev: any) => void | Promise<void>,
primary?: boolean,
secondary?: boolean,
allowBrowserAutoTranslate?: boolean,
disabled?: boolean,
keyboardFocused?: boolean,
@@ -33,6 +34,7 @@ const TextButton = React.forwardRef<Props, ButtonInterface>(
{
label,
primary,
secondary,
icon,
keyboardFocused,
disabled,
@@ -53,7 +55,7 @@ const TextButton = React.forwardRef<Props, ButtonInterface>(
variant="text"
size="small"
translate={allowBrowserAutoTranslate ? undefined : 'no'}
color={primary ? 'secondary' : 'default'}
color={primary ? 'primary' : secondary ? 'secondary' : 'default'}
autoFocus={keyboardFocused}
focusRipple={focusRipple}
disabled={disabled}

View File

@@ -418,6 +418,7 @@ const TreeViewRow = <Item: ItemBaseAttributes>(props: Props<Item>) => {
{node.rightComponent}
{rightButton && (
<IconButton
id={rightButton.id}
size="small"
onClick={e => {
e.stopPropagation();

View File

@@ -27,6 +27,7 @@ export type ItemBaseAttributes = {
};
export type MenuButton = {|
id?: string,
icon: React.Node,
label: string,
click: ?() => void | Promise<void>,

View File

@@ -73,8 +73,9 @@ export type InAppTutorialFlowStepTrigger =
export type InAppTutorialFlowStepFormattedTrigger =
| InAppTutorialFlowStepDOMChangeTrigger
| {| valueEquals: string |}
| {| editorIsActive: string |}
| {| valueHasChanged: true |}
| {| valueEquals: string |}
| {| instanceAddedOnScene: string, instancesCount?: number |}
| {| objectAddedInLayout: true |}
| {| previewLaunched: true |}

View File

@@ -1,7 +1,5 @@
// @flow
import * as React from 'react';
import { type I18n as I18nType } from '@lingui/core';
import { Trans } from '@lingui/macro';
export type MessageByLocale = { [string]: string };
export const selectMessageByLocale = (
@@ -27,45 +25,3 @@ export const selectMessageByLocale = (
return '';
};
export const getLanguageLabelForLocale = (locale: string): React.Node => {
const languageCode = locale.split(/[-_]/)[0];
switch (languageCode) {
case 'en':
return <Trans>English</Trans>;
case 'fr':
return <Trans>French</Trans>;
case 'pt':
return <Trans>Portuguese</Trans>;
case 'es':
return <Trans>Spanish</Trans>;
case 'it':
return <Trans>Italian</Trans>;
case 'ar':
return <Trans>Arabic</Trans>;
case 'id':
return <Trans>Indonesian</Trans>;
case 'ja':
return <Trans>Japanese</Trans>;
case 'ko':
return <Trans>Korean</Trans>;
case 'zh':
return <Trans>Chinese</Trans>;
case 'pl':
return <Trans>Polish</Trans>;
case 'ru':
return <Trans>Russian</Trans>;
case 'sl':
return <Trans>Slovene</Trans>;
case 'si':
return <Trans>Sinhala</Trans>;
case 'th':
return <Trans>Thai</Trans>;
case 'tr':
return <Trans>Turkish</Trans>;
case 'uk':
return <Trans>Ukrainian</Trans>;
default:
return null;
}
};

View File

@@ -21,6 +21,20 @@ export const Default = () => {
tutorialCompletionStatus={'notStarted'}
startTutorial={action('Start tutorial')}
onClose={() => action('On close dialog')()}
isProjectOpening={false}
/>
);
};
export const Opening = () => {
return (
<StartInAppTutorialDialog
open
tutorialId="flingGame"
tutorialCompletionStatus={'notStarted'}
startTutorial={action('Start tutorial')}
onClose={() => action('On close dialog')()}
isProjectOpening
/>
);
};
@@ -33,6 +47,7 @@ export const WithTutorialAlreadyStarted = () => {
tutorialCompletionStatus={'started'}
startTutorial={action('Start tutorial')}
onClose={() => action('On close dialog')()}
isProjectOpening={false}
/>
);
};
@@ -44,6 +59,7 @@ export const WithTutorialCompleted = () => {
tutorialCompletionStatus={'complete'}
startTutorial={action('Start tutorial')}
onClose={() => action('On close dialog')()}
isProjectOpening={false}
/>
);
};

View File

@@ -107,6 +107,17 @@ export const Default = () => (
label="Primary Text button"
onClick={action('onClick')()}
/>
<TextButton
secondary
label="Secondary Text button"
onClick={action('onClick')()}
/>
<TextButton
icon={<Download />}
secondary
label="Secondary Text button"
onClick={action('onClick')()}
/>
</LineStackLayout>
<LineStackLayout noMargin>
<Text size="block-title">Buttons with split menus:</Text>