(false);
+
+ const onClickShare = React.useCallback(
+ () => {
+ if (!!profile) {
+ onShareProject();
+ } else {
+ onOpenLoginDialog();
+ }
+ },
+ [profile, onShareProject, onOpenLoginDialog]
+ );
+
+ const onCloseTooltip = React.useCallback(
+ () => {
+ setDisplayTooltipDelayed(false);
+ acknowledgeNewFeature({ featureId: 'gamesDashboardInProjectManager' });
+ },
+ [acknowledgeNewFeature]
+ );
+
+ const shouldDisplayTooltip = shouldDisplayNewFeatureHighlighting({
+ featureId: 'gamesDashboardInProjectManager',
+ });
+
+ const displayTooltip =
+ canDisplayTooltip && shouldDisplayTooltip && gameDashboardItemContainer;
+
+ React.useEffect(
+ () => {
+ if (displayTooltip) {
+ const timeoutId = setTimeout(() => {
+ setDisplayTooltipDelayed(true);
+ }, 500);
+ return () => clearTimeout(timeoutId);
+ }
+ },
+ // Delay display of tooltip because the project manager opening is animated
+ // and the popper does not follow the item.
+ [displayTooltip]
+ );
+
+ if (onOpenGamesDashboardDialog) {
+ return (
+ setGameDashboardItemContainer(ref)}>
+ Game Dashboard}
+ leftIcon={ }
+ onClick={onOpenGamesDashboardDialog}
+ noPadding
+ />
+ {displayTooltipDelayed && (
+ Game Dashboard}
+ content={[
+
+
+ Follow your game’s online performance, manage published
+ versions, and collect player feedback.
+
+ ,
+
+
+ Window.openExternalURL(gamesDashboardWikiArticle)
+ }
+ >
+ Learn more
+
+ ,
+ ]}
+ placement={isMobile ? 'bottom' : 'right'}
+ onClose={onCloseTooltip}
+ closeWithBackdropClick
+ />
+ )}
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+
+ Games Dashboard
+
+
+
+
+ Share your project online to unlock player engagement analytics,
+ player feedback and other functionalities.
+
+
+
+
+
+ Window.openExternalURL(publishingWikiArticle)}
+ >
+ Learn more
+
+
+ Share}
+ icon={ }
+ onClick={onClickShare}
+ />
+
+
+ );
+};
+
+export default GamesDashboardInfo;
diff --git a/newIDE/app/src/ProjectManager/ShortcutsReminder.js b/newIDE/app/src/ProjectManager/ShortcutsReminder.js
deleted file mode 100644
index 574c57cd360b..000000000000
--- a/newIDE/app/src/ProjectManager/ShortcutsReminder.js
+++ /dev/null
@@ -1,74 +0,0 @@
-// @flow
-import * as React from 'react';
-import { Trans } from '@lingui/macro';
-import { useResponsiveWindowWidth } from '../UI/Reponsive/ResponsiveWindowMeasurer';
-import optionalRequire from '../Utils/OptionalRequire';
-import AlertMessage from '../UI/AlertMessage';
-import { Line, Spacer } from '../UI/Grid';
-import Text from '../UI/Text';
-import { adaptAcceleratorString } from '../UI/AcceleratorString';
-import { getElectronAccelerator } from '../KeyboardShortcuts';
-import { type ShortcutMap } from '../KeyboardShortcuts/DefaultShortcuts';
-import { Column } from '../UI/Grid';
-const electron = optionalRequire('electron');
-
-const styles = {
- shortcutReminders: {
- opacity: 0.7,
- },
-};
-
-const shortcuts = [
- {
- label: Save ,
- shortcutMapKey: 'SAVE_PROJECT',
- },
- {
- label: Save as... ,
- shortcutMapKey: 'SAVE_PROJECT_AS',
- },
- {
- label: Export ,
- shortcutMapKey: 'EXPORT_GAME',
- },
- {
- label: Close ,
- shortcutMapKey: 'CLOSE_PROJECT',
- },
-];
-
-export const ShortcutsReminder = ({
- shortcutMap,
-}: {|
- shortcutMap: ShortcutMap,
-|}) => {
- const windowWidth = useResponsiveWindowWidth();
- const isMobileScreen = windowWidth === 'small';
-
- if (isMobileScreen) return null;
- if (!!electron) return null;
-
- return (
-
-
- Find these actions on the Menu close to the “Home” tab.
-
-
-
- {shortcuts.map(({ label, shortcutMapKey }, index) => (
-
-
- {label}
-
-
- {adaptAcceleratorString(
- getElectronAccelerator(shortcutMap[shortcutMapKey])
- )}
-
-
- ))}
-
-
-
- );
-};
diff --git a/newIDE/app/src/ProjectManager/index.js b/newIDE/app/src/ProjectManager/index.js
index 29ba4b7ea288..15a4293c3e73 100644
--- a/newIDE/app/src/ProjectManager/index.js
+++ b/newIDE/app/src/ProjectManager/index.js
@@ -25,10 +25,6 @@ import {
} from '../Utils/Serializer';
import ExtensionsSearchDialog from '../AssetStore/ExtensionStore/ExtensionsSearchDialog';
import Flag from '@material-ui/icons/Flag';
-import SettingsApplications from '@material-ui/icons/SettingsApplications';
-import PhotoLibrary from '@material-ui/icons/PhotoLibrary';
-import VariableTree from '../UI/CustomSvgIcons/VariableTree';
-import ArtTrack from '@material-ui/icons/ArtTrack';
import ScenePropertiesDialog from '../SceneEditor/ScenePropertiesDialog';
import SceneVariablesDialog from '../SceneEditor/SceneVariablesDialog';
import { isExtensionNameTaken } from './EventFunctionExtensionNameVerifier';
@@ -48,13 +44,20 @@ import Tooltip from '@material-ui/core/Tooltip';
import SceneIcon from '../UI/CustomSvgIcons/Scene';
import ExternalLayoutIcon from '../UI/CustomSvgIcons/ExternalLayout';
import ExternalEventsIcon from '../UI/CustomSvgIcons/ExternalEvents';
-import { type ShortcutMap } from '../KeyboardShortcuts/DefaultShortcuts';
-import { ShortcutsReminder } from './ShortcutsReminder';
import Paper from '../UI/Paper';
import { makeDragSourceAndDropTarget } from '../UI/DragAndDrop/DragSourceAndDropTarget';
-import { useScreenType } from '../UI/Reponsive/ScreenTypeMeasurer';
+import { useShouldAutofocusInput } from '../UI/Reponsive/ScreenTypeMeasurer';
import { addDefaultLightToAllLayers } from '../ProjectCreation/CreateProject';
import ErrorBoundary from '../UI/ErrorBoundary';
+import Settings from '../UI/CustomSvgIcons/Settings';
+import Picture from '../UI/CustomSvgIcons/Picture';
+import Publish from '../UI/CustomSvgIcons/Publish';
+import ProjectResources from '../UI/CustomSvgIcons/ProjectResources';
+import GamesDashboardInfo from './GamesDashboardInfo';
+import useForceUpdate from '../Utils/UseForceUpdate';
+import useGamesList from '../GameDashboard/UseGamesList';
+import AuthenticatedUserContext from '../Profile/AuthenticatedUserContext';
+import { GameDetailsDialog } from '../GameDashboard/GameDetailsDialog';
const LAYOUT_CLIPBOARD_KIND = 'Layout';
const EXTERNAL_LAYOUT_CLIPBOARD_KIND = 'External layout';
@@ -82,7 +85,7 @@ const styles = {
overflowY: 'scroll',
scrollbarWidth: 'thin', // For Firefox, to avoid having a very large scrollbar.
marginTop: 16,
- padding: '0 16px 16px 16px',
+ padding: '0 8px 12px 12px',
position: 'relative',
},
searchBarContainer: {
@@ -99,7 +102,8 @@ type ProjectItemKind =
| 'external-layout'
| 'events-functions-extension';
-const getTabId = (identifier: string) => `project-manager-tab-${identifier}`;
+export const getProjectManagerItemId = (identifier: string) =>
+ `project-manager-tab-${identifier}`;
type Props = {|
project: gdProject,
@@ -125,608 +129,692 @@ type Props = {|
unsavedChanges?: UnsavedChanges,
hotReloadPreviewButtonProps: HotReloadPreviewButtonProps,
onInstallExtension: ExtensionShortHeader => void,
- shortcutMap: ShortcutMap,
+ onShareProject: () => void,
// For resources:
resourceManagementProps: ResourceManagementProps,
|};
-type State = {|
- editedPropertiesLayout: ?gdLayout,
- editedVariablesLayout: ?gdLayout,
- renamedItemKind: ?ProjectItemKind,
- renamedItemName: string,
- searchText: string,
- projectPropertiesDialogOpen: boolean,
- projectPropertiesDialogInitialTab: 'properties' | 'loading-screen',
- projectVariablesEditorOpen: boolean,
- extensionsSearchDialogOpen: boolean,
- openedExtensionShortHeader: ?ExtensionShortHeader,
- openedExtensionName: ?string,
- isInstallingExtension: boolean,
- layoutPropertiesDialogOpen: boolean,
- layoutVariablesDialogOpen: boolean,
-|};
-
-class ProjectManager extends React.Component {
- _searchBar: ?SearchBarInterface;
- _draggedLayoutIndex: number | null = null;
- _draggedExternalLayoutIndex: number | null = null;
- _draggedExternalEventsIndex: number | null = null;
- _draggedExtensionIndex: number | null = null;
-
- state = {
- editedPropertiesLayout: null,
- editedVariablesLayout: null,
- renamedItemKind: null,
- renamedItemName: '',
- searchText: '',
- projectPropertiesDialogOpen: false,
- projectPropertiesDialogInitialTab: 'properties',
- projectVariablesEditorOpen: false,
- extensionsSearchDialogOpen: false,
- openedExtensionShortHeader: null,
- openedExtensionName: null,
- isInstallingExtension: false,
- layoutPropertiesDialogOpen: false,
- layoutVariablesDialogOpen: false,
- };
-
- shouldComponentUpdate(nextProps: Props, nextState: State) {
- if (
- nextState.projectPropertiesDialogOpen !==
- this.state.projectPropertiesDialogOpen ||
- nextState.projectVariablesEditorOpen !==
- this.state.projectVariablesEditorOpen ||
- nextState.extensionsSearchDialogOpen !==
- this.state.extensionsSearchDialogOpen ||
- nextState.openedExtensionShortHeader !==
- this.state.openedExtensionShortHeader
- )
- return true;
-
- // Rendering the component is (super) costly (~20ms) as it iterates over
- // every project layouts/external layouts/external events,
- // so the prop freezeUpdate allow to ask the component to stop
- // updating, for example when hidden.
- return !nextProps.freezeUpdate;
- }
-
- componentDidUpdate(prevProps: Props) {
- // Typical usage (don't forget to compare props):
- if (!this.props.freezeUpdate && prevProps.freezeUpdate) {
- // TODO: When this component is refactored into a functional component,
- // use useShouldAutofocusInput.
- // eslint-disable-next-line react-hooks/rules-of-hooks
- if (useScreenType() === 'normal' && this._searchBar)
- this._searchBar.focus();
- }
- }
-
- _openProjectProperties = () => {
- this.setState({
- projectPropertiesDialogOpen: true,
- projectPropertiesDialogInitialTab: 'properties',
- });
- };
-
- _openProjectLoadingScreen = () => {
- this.setState({
- projectPropertiesDialogOpen: true,
- projectPropertiesDialogInitialTab: 'loading-screen',
- });
- };
-
- _openProjectVariables = () => {
- this.setState({
- projectVariablesEditorOpen: true,
- });
- };
-
- _openSearchExtensionDialog = () => {
- this.setState({ extensionsSearchDialogOpen: true });
- };
+const ProjectManager = React.memo(
+ ({
+ project,
+ onChangeProjectName,
+ onSaveProjectProperties,
+ onDeleteLayout,
+ onDeleteExternalEvents,
+ onDeleteExternalLayout,
+ onDeleteEventsFunctionsExtension,
+ onRenameLayout,
+ onRenameExternalEvents,
+ onRenameExternalLayout,
+ onRenameEventsFunctionsExtension,
+ onOpenLayout,
+ onOpenExternalEvents,
+ onOpenExternalLayout,
+ onOpenEventsFunctionsExtension,
+ onOpenResources,
+ onOpenPlatformSpecificAssets,
+ eventsFunctionsExtensionsError,
+ onReloadEventsFunctionsExtensions,
+ freezeUpdate,
+ unsavedChanges,
+ hotReloadPreviewButtonProps,
+ onInstallExtension,
+ onShareProject,
+ resourceManagementProps,
+ }: Props) => {
+ const forceUpdate = useForceUpdate();
+ const shouldAutofocusInput = useShouldAutofocusInput();
+ const { profile } = React.useContext(AuthenticatedUserContext);
+ const userId = profile ? profile.id : null;
+ const { games, fetchGames } = useGamesList();
+
+ const searchBarRef = React.useRef(null);
+ const draggedLayoutIndexRef = React.useRef(null);
+ const draggedExternalLayoutIndexRef = React.useRef(null);
+ const draggedExternalEventsIndexRef = React.useRef(null);
+ const draggedExtensionIndexRef = React.useRef(null);
+
+ const [editedPropertiesLayout, setEditedPropertiesLayout] = React.useState(
+ null
+ );
+ const [editedVariablesLayout, setEditedVariablesLayout] = React.useState(
+ null
+ );
+ const [renamedItemKind, setRenamedItemKind] = React.useState(null);
+ const [renamedItemName, setRenamedItemName] = React.useState('');
+ const [searchText, setSearchText] = React.useState('');
+ const [
+ projectPropertiesDialogOpen,
+ setProjectPropertiesDialogOpen,
+ ] = React.useState(false);
+ const [
+ projectPropertiesDialogInitialTab,
+ setProjectPropertiesDialogInitialTab,
+ ] = React.useState('properties');
+ const [
+ projectVariablesEditorOpen,
+ setProjectVariablesEditorOpen,
+ ] = React.useState(false);
+ const [
+ extensionsSearchDialogOpen,
+ setExtensionsSearchDialogOpen,
+ ] = React.useState(false);
+ const [
+ openedExtensionShortHeader,
+ setOpenedExtensionShortHeader,
+ ] = React.useState(null);
+ const [openedExtensionName, setOpenedExtensionName] = React.useState(null);
+ const [openGameDetails, setOpenGameDetails] = React.useState(
+ false
+ );
- _onEditName = (kind: ?ProjectItemKind, name: string) => {
- this.setState({
- renamedItemKind: kind,
- renamedItemName: name,
- });
- };
+ const projectUuid = project.getProjectUuid();
+ const gameMatchingProjectUuid = games
+ ? games.find(game => game.id === projectUuid)
+ : null;
- _copyLayout = (layout: gdLayout) => {
- Clipboard.set(LAYOUT_CLIPBOARD_KIND, {
- layout: serializeToJSObject(layout),
- name: layout.getName(),
+ React.useEffect(() => {
+ if (!freezeUpdate && shouldAutofocusInput && searchBarRef.current) {
+ searchBarRef.current.focus();
+ }
});
- };
- _cutLayout = (layout: gdLayout) => {
- this._copyLayout(layout);
- this.props.onDeleteLayout(layout);
- };
+ React.useEffect(
+ () => {
+ fetchGames();
+ },
+ [fetchGames, userId]
+ );
- _pasteLayout = (index: number) => {
- if (!Clipboard.has(LAYOUT_CLIPBOARD_KIND)) return;
+ const onProjectItemModified = React.useCallback(
+ () => {
+ forceUpdate();
+ if (unsavedChanges) unsavedChanges.triggerUnsavedChanges();
+ },
+ [forceUpdate, unsavedChanges]
+ );
- const clipboardContent = Clipboard.get(LAYOUT_CLIPBOARD_KIND);
- const copiedLayout = SafeExtractor.extractObjectProperty(
- clipboardContent,
- 'layout'
+ const openProjectProperties = React.useCallback(() => {
+ setProjectPropertiesDialogOpen(true);
+ setProjectPropertiesDialogInitialTab('properties');
+ }, []);
+
+ const openProjectLoadingScreen = React.useCallback(() => {
+ setProjectPropertiesDialogOpen(true);
+ setProjectPropertiesDialogInitialTab('loading-screen');
+ }, []);
+
+ const openProjectVariables = React.useCallback(() => {
+ setProjectVariablesEditorOpen(true);
+ }, []);
+
+ const openSearchExtensionDialog = React.useCallback(() => {
+ setExtensionsSearchDialogOpen(true);
+ }, []);
+
+ const onEditName = React.useCallback(
+ (kind: ?ProjectItemKind, name: string) => {
+ setRenamedItemKind(kind);
+ setRenamedItemName(name);
+ },
+ []
);
- const name = SafeExtractor.extractStringProperty(clipboardContent, 'name');
- if (!name || !copiedLayout) return;
- const { project } = this.props;
+ const copyLayout = React.useCallback((layout: gdLayout) => {
+ Clipboard.set(LAYOUT_CLIPBOARD_KIND, {
+ layout: serializeToJSObject(layout),
+ name: layout.getName(),
+ });
+ }, []);
+
+ const cutLayout = React.useCallback(
+ (layout: gdLayout) => {
+ copyLayout(layout);
+ onDeleteLayout(layout);
+ },
+ [onDeleteLayout, copyLayout]
+ );
- const newName = newNameGenerator(name, name =>
- project.hasLayoutNamed(name)
+ const pasteLayout = React.useCallback(
+ (index: number) => {
+ if (!Clipboard.has(LAYOUT_CLIPBOARD_KIND)) return;
+
+ const clipboardContent = Clipboard.get(LAYOUT_CLIPBOARD_KIND);
+ const copiedLayout = SafeExtractor.extractObjectProperty(
+ clipboardContent,
+ 'layout'
+ );
+ const name = SafeExtractor.extractStringProperty(
+ clipboardContent,
+ 'name'
+ );
+ if (!name || !copiedLayout) return;
+
+ const newName = newNameGenerator(name, name =>
+ project.hasLayoutNamed(name)
+ );
+
+ const newLayout = project.insertNewLayout(newName, index);
+
+ unserializeFromJSObject(
+ newLayout,
+ copiedLayout,
+ 'unserializeFrom',
+ project
+ );
+ newLayout.setName(newName); // Unserialization has overwritten the name.
+ newLayout.updateBehaviorsSharedData(project);
+
+ onProjectItemModified();
+ },
+ [project, onProjectItemModified]
);
- const newLayout = project.insertNewLayout(newName, index);
+ const duplicateLayout = React.useCallback(
+ (layout: gdLayout, index: number) => {
+ const newName = newNameGenerator(layout.getName(), name =>
+ project.hasLayoutNamed(name)
+ );
+
+ const newLayout = project.insertNewLayout(newName, index);
+
+ unserializeFromJSObject(
+ newLayout,
+ serializeToJSObject(layout),
+ 'unserializeFrom',
+ project
+ );
+ newLayout.setName(newName); // Unserialization has overwritten the name.
+ newLayout.updateBehaviorsSharedData(project);
+
+ onProjectItemModified();
+ },
+ [project, onProjectItemModified]
+ );
- unserializeFromJSObject(
- newLayout,
- copiedLayout,
- 'unserializeFrom',
- project
+ const addLayout = React.useCallback(
+ (index: number, i18n: I18nType) => {
+ const newName = newNameGenerator(i18n._(t`Untitled scene`), name =>
+ project.hasLayoutNamed(name)
+ );
+ const newLayout = project.insertNewLayout(newName, index + 1);
+ newLayout.setName(newName);
+ newLayout.updateBehaviorsSharedData(project);
+ addDefaultLightToAllLayers(newLayout);
+
+ onProjectItemModified();
+
+ // Trigger an edit of the name, so that the user can rename the layout easily.
+ onEditName('layout', newName);
+ },
+ [project, onProjectItemModified, onEditName]
);
- newLayout.setName(newName); // Unserialization has overwritten the name.
- newLayout.updateBehaviorsSharedData(project);
- this._onProjectItemModified();
- };
+ const onOpenLayoutProperties = React.useCallback((layout: ?gdLayout) => {
+ setEditedPropertiesLayout(layout);
+ }, []);
+
+ const onOpenLayoutVariables = React.useCallback((layout: ?gdLayout) => {
+ setEditedVariablesLayout(layout);
+ }, []);
+
+ const addExternalEvents = React.useCallback(
+ (index: number, i18n: I18nType) => {
+ const newName = newNameGenerator(
+ i18n._(t`Untitled external events`),
+ name => project.hasExternalEventsNamed(name)
+ );
+ project.insertNewExternalEvents(newName, index + 1);
+ onProjectItemModified();
+
+ // Trigger an edit of the name, so that the user can rename the external events easily.
+ onEditName('external-events', newName);
+ },
+ [project, onProjectItemModified, onEditName]
+ );
- _duplicateLayout = (layout: gdLayout, index: number) => {
- const { project } = this.props;
+ const addExternalLayout = React.useCallback(
+ (index: number, i18n: I18nType) => {
+ const newName = newNameGenerator(
+ i18n._(t`Untitled external layout`),
+ name => project.hasExternalLayoutNamed(name)
+ );
+ project.insertNewExternalLayout(newName, index + 1);
+ onProjectItemModified();
+
+ // Trigger an edit of the name, so that the user can rename the external layout easily.
+ onEditName('external-layout', newName);
+ },
+ [project, onEditName, onProjectItemModified]
+ );
- const newName = newNameGenerator(layout.getName(), name =>
- project.hasLayoutNamed(name)
+ const addEventsFunctionsExtension = React.useCallback(
+ (index: number, i18n: I18nType) => {
+ const newName = newNameGenerator(i18n._(t`UntitledExtension`), name =>
+ isExtensionNameTaken(name, project)
+ );
+ project.insertNewEventsFunctionsExtension(newName, index + 1);
+ onProjectItemModified();
+ return newName;
+ },
+ [project, onProjectItemModified]
);
- const newLayout = project.insertNewLayout(newName, index);
+ const moveUpLayout = React.useCallback(
+ (index: number) => {
+ if (index <= 0) return;
- unserializeFromJSObject(
- newLayout,
- serializeToJSObject(layout),
- 'unserializeFrom',
- project
+ project.swapLayouts(index, index - 1);
+ onProjectItemModified();
+ },
+ [project, onProjectItemModified]
);
- newLayout.setName(newName); // Unserialization has overwritten the name.
- newLayout.updateBehaviorsSharedData(project);
- this._onProjectItemModified();
- };
+ const moveDownLayout = React.useCallback(
+ (index: number) => {
+ if (index >= project.getLayoutsCount() - 1) return;
- _addLayout = (index: number, i18n: I18nType) => {
- const { project } = this.props;
+ project.swapLayouts(index, index + 1);
+ onProjectItemModified();
+ },
+ [project, onProjectItemModified]
+ );
- const newName = newNameGenerator(i18n._(t`Untitled scene`), name =>
- project.hasLayoutNamed(name)
+ const dropOnLayout = React.useCallback(
+ (targetLayoutIndex: number) => {
+ const { current: draggedLayoutIndex } = draggedLayoutIndexRef;
+ if (draggedLayoutIndex === null) return;
+
+ if (targetLayoutIndex !== draggedLayoutIndex) {
+ project.moveLayout(
+ draggedLayoutIndex,
+ targetLayoutIndex > draggedLayoutIndex
+ ? targetLayoutIndex - 1
+ : targetLayoutIndex
+ );
+ onProjectItemModified();
+ }
+ draggedLayoutIndexRef.current = null;
+ },
+ [project, onProjectItemModified]
);
- const newLayout = project.insertNewLayout(newName, index + 1);
- newLayout.setName(newName);
- newLayout.updateBehaviorsSharedData(project);
- addDefaultLightToAllLayers(newLayout);
- this._onProjectItemModified();
+ const dropOnExternalLayout = React.useCallback(
+ (targetExternalLayoutIndex: number) => {
+ const {
+ current: draggedExternalLayoutIndex,
+ } = draggedExternalLayoutIndexRef;
+ if (draggedExternalLayoutIndex === null) return;
+
+ if (targetExternalLayoutIndex !== draggedExternalLayoutIndex) {
+ project.moveExternalLayout(
+ draggedExternalLayoutIndex,
+ targetExternalLayoutIndex > draggedExternalLayoutIndex
+ ? targetExternalLayoutIndex - 1
+ : targetExternalLayoutIndex
+ );
+ onProjectItemModified();
+ }
+ draggedExternalLayoutIndexRef.current = null;
+ },
+ [project, onProjectItemModified]
+ );
- // Trigger an edit of the name, so that the user can rename the layout easily.
- this._onEditName('layout', newName);
- };
+ const dropOnExternalEvents = React.useCallback(
+ (targetExternalEventsIndex: number) => {
+ const {
+ current: draggedExternalEventsIndex,
+ } = draggedExternalEventsIndexRef;
+ if (draggedExternalEventsIndex === null) return;
+
+ if (targetExternalEventsIndex !== draggedExternalEventsIndex) {
+ project.moveExternalEvents(
+ draggedExternalEventsIndex,
+ targetExternalEventsIndex > draggedExternalEventsIndex
+ ? targetExternalEventsIndex - 1
+ : targetExternalEventsIndex
+ );
+ onProjectItemModified();
+ }
+ draggedExternalEventsIndexRef.current = null;
+ },
+ [project, onProjectItemModified]
+ );
- _onOpenLayoutProperties = (layout: ?gdLayout) => {
- this.setState({ editedPropertiesLayout: layout });
- };
+ const dropOnExtension = React.useCallback(
+ (targetExtensionIndex: number) => {
+ const { current: draggedExtensionIndex } = draggedExtensionIndexRef;
+ if (draggedExtensionIndex === null) return;
+
+ if (targetExtensionIndex !== draggedExtensionIndex) {
+ project.moveEventsFunctionsExtension(
+ draggedExtensionIndex,
+ targetExtensionIndex > draggedExtensionIndex
+ ? targetExtensionIndex - 1
+ : targetExtensionIndex
+ );
+ onProjectItemModified();
+ }
+ draggedExtensionIndexRef.current = null;
+ },
+ [project, onProjectItemModified]
+ );
- _onOpenLayoutVariables = (layout: ?gdLayout) => {
- this.setState({ editedVariablesLayout: layout });
- };
+ const copyExternalEvents = React.useCallback(
+ (externalEvents: gdExternalEvents) => {
+ Clipboard.set(EXTERNAL_EVENTS_CLIPBOARD_KIND, {
+ externalEvents: serializeToJSObject(externalEvents),
+ name: externalEvents.getName(),
+ });
+ },
+ []
+ );
- _addExternalEvents = (index: number, i18n: I18nType) => {
- const { project } = this.props;
+ const cutExternalEvents = React.useCallback(
+ (externalEvents: gdExternalEvents) => {
+ copyExternalEvents(externalEvents);
+ onDeleteExternalEvents(externalEvents);
+ },
+ [copyExternalEvents, onDeleteExternalEvents]
+ );
- const newName = newNameGenerator(
- i18n._(t`Untitled external events`),
- name => project.hasExternalEventsNamed(name)
+ const pasteExternalEvents = React.useCallback(
+ (index: number) => {
+ if (!Clipboard.has(EXTERNAL_EVENTS_CLIPBOARD_KIND)) return;
+
+ const clipboardContent = Clipboard.get(EXTERNAL_EVENTS_CLIPBOARD_KIND);
+ const copiedExternalEvents = SafeExtractor.extractObjectProperty(
+ clipboardContent,
+ 'externalEvents'
+ );
+ const name = SafeExtractor.extractStringProperty(
+ clipboardContent,
+ 'name'
+ );
+ if (!name || !copiedExternalEvents) return;
+
+ const newName = newNameGenerator(name, name =>
+ project.hasExternalEventsNamed(name)
+ );
+
+ const newExternalEvents = project.insertNewExternalEvents(
+ newName,
+ index
+ );
+
+ unserializeFromJSObject(
+ newExternalEvents,
+ copiedExternalEvents,
+ 'unserializeFrom',
+ project
+ );
+ newExternalEvents.setName(newName); // Unserialization has overwritten the name.
+
+ onProjectItemModified();
+ },
+ [project, onProjectItemModified]
);
- project.insertNewExternalEvents(newName, index + 1);
- this._onProjectItemModified();
- // Trigger an edit of the name, so that the user can rename the external events easily.
- this._onEditName('external-events', newName);
- };
+ const duplicateExternalEvents = React.useCallback(
+ (externalEvents: gdExternalEvents, index: number) => {
+ copyExternalEvents(externalEvents);
+ pasteExternalEvents(index);
+ },
+ [copyExternalEvents, pasteExternalEvents]
+ );
- _addExternalLayout = (index: number, i18n: I18nType) => {
- const { project } = this.props;
+ const moveUpExternalEvents = React.useCallback(
+ (index: number) => {
+ if (index <= 0) return;
- const newName = newNameGenerator(
- i18n._(t`Untitled external layout`),
- name => project.hasExternalLayoutNamed(name)
+ project.swapExternalEvents(index, index - 1);
+ onProjectItemModified();
+ },
+ [project, onProjectItemModified]
);
- project.insertNewExternalLayout(newName, index + 1);
- this._onProjectItemModified();
-
- // Trigger an edit of the name, so that the user can rename the external layout easily.
- this._onEditName('external-layout', newName);
- };
- _addEventsFunctionsExtension = (index: number, i18n: I18nType) => {
- const { project } = this.props;
+ const moveDownExternalEvents = React.useCallback(
+ (index: number) => {
+ if (index >= project.getExternalEventsCount() - 1) return;
- const newName = newNameGenerator(i18n._(t`UntitledExtension`), name =>
- isExtensionNameTaken(name, project)
+ project.swapExternalEvents(index, index + 1);
+ onProjectItemModified();
+ },
+ [project, onProjectItemModified]
);
- project.insertNewEventsFunctionsExtension(newName, index + 1);
- this._onProjectItemModified();
- return newName;
- };
-
- _moveUpLayout = (index: number) => {
- const { project } = this.props;
- if (index <= 0) return;
-
- project.swapLayouts(index, index - 1);
- this._onProjectItemModified();
- };
-
- _moveDownLayout = (index: number) => {
- const { project } = this.props;
- if (index >= project.getLayoutsCount() - 1) return;
-
- project.swapLayouts(index, index + 1);
- this._onProjectItemModified();
- };
-
- _dropOnLayout = (targetLayoutIndex: number) => {
- const { _draggedLayoutIndex } = this;
- if (_draggedLayoutIndex === null) return;
-
- if (targetLayoutIndex !== _draggedLayoutIndex) {
- this.props.project.moveLayout(
- _draggedLayoutIndex,
- targetLayoutIndex > _draggedLayoutIndex
- ? targetLayoutIndex - 1
- : targetLayoutIndex
- );
- this._onProjectItemModified();
- }
- this._draggedLayoutIndex = null;
- };
-
- _dropOnExternalLayout = (targetExternalLayoutIndex: number) => {
- const { _draggedExternalLayoutIndex } = this;
- if (_draggedExternalLayoutIndex === null) return;
-
- if (targetExternalLayoutIndex !== _draggedExternalLayoutIndex) {
- this.props.project.moveExternalLayout(
- _draggedExternalLayoutIndex,
- targetExternalLayoutIndex > _draggedExternalLayoutIndex
- ? targetExternalLayoutIndex - 1
- : targetExternalLayoutIndex
- );
- this._onProjectItemModified();
- }
- this._draggedExternalLayoutIndex = null;
- };
-
- _dropOnExternalEvents = (targetExternalEventsIndex: number) => {
- const { _draggedExternalEventsIndex } = this;
- if (_draggedExternalEventsIndex === null) return;
-
- if (targetExternalEventsIndex !== _draggedExternalEventsIndex) {
- this.props.project.moveExternalEvents(
- _draggedExternalEventsIndex,
- targetExternalEventsIndex > _draggedExternalEventsIndex
- ? targetExternalEventsIndex - 1
- : targetExternalEventsIndex
- );
- this._onProjectItemModified();
- }
- this._draggedExternalEventsIndex = null;
- };
-
- _dropOnExtension = (targetExtensionIndex: number) => {
- const { _draggedExtensionIndex } = this;
- if (_draggedExtensionIndex === null) return;
-
- if (targetExtensionIndex !== _draggedExtensionIndex) {
- this.props.project.moveEventsFunctionsExtension(
- _draggedExtensionIndex,
- targetExtensionIndex > _draggedExtensionIndex
- ? targetExtensionIndex - 1
- : targetExtensionIndex
- );
- this._onProjectItemModified();
- }
- this._draggedExtensionIndex = null;
- };
-
- _copyExternalEvents = (externalEvents: gdExternalEvents) => {
- Clipboard.set(EXTERNAL_EVENTS_CLIPBOARD_KIND, {
- externalEvents: serializeToJSObject(externalEvents),
- name: externalEvents.getName(),
- });
- };
- _cutExternalEvents = (externalEvents: gdExternalEvents) => {
- this._copyExternalEvents(externalEvents);
- this.props.onDeleteExternalEvents(externalEvents);
- };
-
- _pasteExternalEvents = (index: number) => {
- if (!Clipboard.has(EXTERNAL_EVENTS_CLIPBOARD_KIND)) return;
+ const copyExternalLayout = React.useCallback(
+ (externalLayout: gdExternalLayout) => {
+ Clipboard.set(EXTERNAL_LAYOUT_CLIPBOARD_KIND, {
+ externalLayout: serializeToJSObject(externalLayout),
+ name: externalLayout.getName(),
+ });
+ },
+ []
+ );
- const clipboardContent = Clipboard.get(EXTERNAL_EVENTS_CLIPBOARD_KIND);
- const copiedExternalEvents = SafeExtractor.extractObjectProperty(
- clipboardContent,
- 'externalEvents'
+ const cutExternalLayout = React.useCallback(
+ (externalLayout: gdExternalLayout) => {
+ copyExternalLayout(externalLayout);
+ onDeleteExternalLayout(externalLayout);
+ },
+ [copyExternalLayout, onDeleteExternalLayout]
);
- const name = SafeExtractor.extractStringProperty(clipboardContent, 'name');
- if (!name || !copiedExternalEvents) return;
- const { project } = this.props;
+ const pasteExternalLayout = React.useCallback(
+ (index: number) => {
+ if (!Clipboard.has(EXTERNAL_LAYOUT_CLIPBOARD_KIND)) return;
+
+ const clipboardContent = Clipboard.get(EXTERNAL_LAYOUT_CLIPBOARD_KIND);
+ const copiedExternalLayout = SafeExtractor.extractObjectProperty(
+ clipboardContent,
+ 'externalLayout'
+ );
+ const name = SafeExtractor.extractStringProperty(
+ clipboardContent,
+ 'name'
+ );
+ if (!name || !copiedExternalLayout) return;
+
+ const newName = newNameGenerator(name, name =>
+ project.hasExternalLayoutNamed(name)
+ );
+
+ const newExternalLayout = project.insertNewExternalLayout(
+ newName,
+ index
+ );
+
+ unserializeFromJSObject(newExternalLayout, copiedExternalLayout);
+ newExternalLayout.setName(newName); // Unserialization has overwritten the name.
+ onProjectItemModified();
+ },
+ [project, onProjectItemModified]
+ );
- const newName = newNameGenerator(name, name =>
- project.hasExternalEventsNamed(name)
+ const duplicateExternalLayout = React.useCallback(
+ (externalLayout: gdExternalLayout, index: number) => {
+ copyExternalLayout(externalLayout);
+ pasteExternalLayout(index);
+ },
+ [copyExternalLayout, pasteExternalLayout]
);
- const newExternalEvents = project.insertNewExternalEvents(newName, index);
+ const moveUpExternalLayout = React.useCallback(
+ (index: number) => {
+ if (index <= 0) return;
- unserializeFromJSObject(
- newExternalEvents,
- copiedExternalEvents,
- 'unserializeFrom',
- project
+ project.swapExternalLayouts(index, index - 1);
+ onProjectItemModified();
+ },
+ [project, onProjectItemModified]
);
- newExternalEvents.setName(newName); // Unserialization has overwritten the name.
-
- this._onProjectItemModified();
- };
-
- _duplicateExternalEvents = (
- externalEvents: gdExternalEvents,
- index: number
- ) => {
- this._copyExternalEvents(externalEvents);
- this._pasteExternalEvents(index);
- };
-
- _moveUpExternalEvents = (index: number) => {
- const { project } = this.props;
- if (index <= 0) return;
-
- project.swapExternalEvents(index, index - 1);
- this._onProjectItemModified();
- };
-
- _moveDownExternalEvents = (index: number) => {
- const { project } = this.props;
- if (index >= project.getExternalEventsCount() - 1) return;
-
- project.swapExternalEvents(index, index + 1);
- this._onProjectItemModified();
- };
-
- _copyExternalLayout = (externalLayout: gdExternalLayout) => {
- Clipboard.set(EXTERNAL_LAYOUT_CLIPBOARD_KIND, {
- externalLayout: serializeToJSObject(externalLayout),
- name: externalLayout.getName(),
- });
- };
-
- _cutExternalLayout = (externalLayout: gdExternalLayout) => {
- this._copyExternalLayout(externalLayout);
- this.props.onDeleteExternalLayout(externalLayout);
- };
- _pasteExternalLayout = (index: number) => {
- if (!Clipboard.has(EXTERNAL_LAYOUT_CLIPBOARD_KIND)) return;
+ const moveDownExternalLayout = React.useCallback(
+ (index: number) => {
+ if (index >= project.getExternalLayoutsCount() - 1) return;
- const clipboardContent = Clipboard.get(EXTERNAL_LAYOUT_CLIPBOARD_KIND);
- const copiedExternalLayout = SafeExtractor.extractObjectProperty(
- clipboardContent,
- 'externalLayout'
+ project.swapExternalLayouts(index, index + 1);
+ onProjectItemModified();
+ },
+ [project, onProjectItemModified]
);
- const name = SafeExtractor.extractStringProperty(clipboardContent, 'name');
- if (!name || !copiedExternalLayout) return;
- const { project } = this.props;
+ const copyEventsFunctionsExtension = React.useCallback(
+ (eventsFunctionsExtension: gdEventsFunctionsExtension) => {
+ Clipboard.set(EVENTS_FUNCTIONS_EXTENSION_CLIPBOARD_KIND, {
+ eventsFunctionsExtension: serializeToJSObject(
+ eventsFunctionsExtension
+ ),
+ name: eventsFunctionsExtension.getName(),
+ });
+ },
+ []
+ );
- const newName = newNameGenerator(name, name =>
- project.hasExternalLayoutNamed(name)
+ const cutEventsFunctionsExtension = React.useCallback(
+ (eventsFunctionsExtension: gdEventsFunctionsExtension) => {
+ copyEventsFunctionsExtension(eventsFunctionsExtension);
+ onDeleteEventsFunctionsExtension(eventsFunctionsExtension);
+ },
+ [copyEventsFunctionsExtension, onDeleteEventsFunctionsExtension]
);
- const newExternalLayout = project.insertNewExternalLayout(newName, index);
-
- unserializeFromJSObject(newExternalLayout, copiedExternalLayout);
- newExternalLayout.setName(newName); // Unserialization has overwritten the name.
- this._onProjectItemModified();
- };
-
- _duplicateExternalLayout = (
- externalLayout: gdExternalLayout,
- index: number
- ) => {
- this._copyExternalLayout(externalLayout);
- this._pasteExternalLayout(index);
- };
-
- _moveUpExternalLayout = (index: number) => {
- const { project } = this.props;
- if (index <= 0) return;
-
- project.swapExternalLayouts(index, index - 1);
- this._onProjectItemModified();
- };
-
- _moveDownExternalLayout = (index: number) => {
- const { project } = this.props;
- if (index >= project.getExternalLayoutsCount() - 1) return;
-
- project.swapExternalLayouts(index, index + 1);
- this._onProjectItemModified();
- };
-
- _copyEventsFunctionsExtension = (
- eventsFunctionsExtension: gdEventsFunctionsExtension
- ) => {
- Clipboard.set(EVENTS_FUNCTIONS_EXTENSION_CLIPBOARD_KIND, {
- eventsFunctionsExtension: serializeToJSObject(eventsFunctionsExtension),
- name: eventsFunctionsExtension.getName(),
- });
- };
-
- _cutEventsFunctionsExtension = (
- eventsFunctionsExtension: gdEventsFunctionsExtension
- ) => {
- this._copyEventsFunctionsExtension(eventsFunctionsExtension);
- this.props.onDeleteEventsFunctionsExtension(eventsFunctionsExtension);
- };
-
- _duplicateEventsFunctionsExtension = (
- eventsFunctionsExtension: gdEventsFunctionsExtension,
- index: number
- ) => {
- this._copyEventsFunctionsExtension(eventsFunctionsExtension);
- this._pasteEventsFunctionsExtension(index);
- };
-
- _pasteEventsFunctionsExtension = (index: number) => {
- if (!Clipboard.has(EVENTS_FUNCTIONS_EXTENSION_CLIPBOARD_KIND)) return;
-
- const clipboardContent = Clipboard.get(
- EVENTS_FUNCTIONS_EXTENSION_CLIPBOARD_KIND
+ const pasteEventsFunctionsExtension = React.useCallback(
+ (index: number) => {
+ if (!Clipboard.has(EVENTS_FUNCTIONS_EXTENSION_CLIPBOARD_KIND)) return;
+
+ const clipboardContent = Clipboard.get(
+ EVENTS_FUNCTIONS_EXTENSION_CLIPBOARD_KIND
+ );
+ const copiedEventsFunctionsExtension = SafeExtractor.extractObjectProperty(
+ clipboardContent,
+ 'eventsFunctionsExtension'
+ );
+ const name = SafeExtractor.extractStringProperty(
+ clipboardContent,
+ 'name'
+ );
+ if (!name || !copiedEventsFunctionsExtension) return;
+
+ const newName = newNameGenerator(name, name =>
+ isExtensionNameTaken(name, project)
+ );
+
+ const newEventsFunctionsExtension = project.insertNewEventsFunctionsExtension(
+ newName,
+ index
+ );
+
+ unserializeFromJSObject(
+ newEventsFunctionsExtension,
+ copiedEventsFunctionsExtension,
+ 'unserializeFrom',
+ project
+ );
+ newEventsFunctionsExtension.setName(newName); // Unserialization has overwritten the name.
+
+ onProjectItemModified();
+ onReloadEventsFunctionsExtensions();
+ },
+ [project, onProjectItemModified, onReloadEventsFunctionsExtensions]
);
- const copiedEventsFunctionsExtension = SafeExtractor.extractObjectProperty(
- clipboardContent,
- 'eventsFunctionsExtension'
+
+ const duplicateEventsFunctionsExtension = React.useCallback(
+ (eventsFunctionsExtension: gdEventsFunctionsExtension, index: number) => {
+ copyEventsFunctionsExtension(eventsFunctionsExtension);
+ pasteEventsFunctionsExtension(index);
+ },
+ [copyEventsFunctionsExtension, pasteEventsFunctionsExtension]
);
- const name = SafeExtractor.extractStringProperty(clipboardContent, 'name');
- if (!name || !copiedEventsFunctionsExtension) return;
- const { project } = this.props;
+ const moveUpEventsFunctionsExtension = React.useCallback(
+ (index: number) => {
+ if (index <= 0) return;
- const newName = newNameGenerator(name, name =>
- isExtensionNameTaken(name, project)
+ project.swapEventsFunctionsExtensions(index, index - 1);
+ onProjectItemModified();
+ },
+ [project, onProjectItemModified]
);
- const newEventsFunctionsExtension = project.insertNewEventsFunctionsExtension(
- newName,
- index
- );
+ const moveDownEventsFunctionsExtension = React.useCallback(
+ (index: number) => {
+ if (index >= project.getEventsFunctionsExtensionsCount() - 1) return;
- unserializeFromJSObject(
- newEventsFunctionsExtension,
- copiedEventsFunctionsExtension,
- 'unserializeFrom',
- project
+ project.swapEventsFunctionsExtensions(index, index + 1);
+ onProjectItemModified();
+ },
+ [project, onProjectItemModified]
);
- newEventsFunctionsExtension.setName(newName); // Unserialization has overwritten the name.
-
- this._onProjectItemModified();
- this.props.onReloadEventsFunctionsExtensions();
- };
-
- _moveUpEventsFunctionsExtension = (index: number) => {
- const { project } = this.props;
- if (index <= 0) return;
-
- project.swapEventsFunctionsExtensions(index, index - 1);
- this._onProjectItemModified();
- };
-
- _moveDownEventsFunctionsExtension = (index: number) => {
- const { project } = this.props;
- if (index >= project.getEventsFunctionsExtensionsCount() - 1) return;
-
- project.swapEventsFunctionsExtensions(index, index + 1);
- this._onProjectItemModified();
- };
-
- _onEditEventsFunctionExtensionOrSeeDetails = (
- extensionShortHeadersByName: { [string]: ExtensionShortHeader },
- eventsFunctionsExtension: gdEventsFunctionsExtension,
- name: string
- ) => {
- // If the extension is coming from the store, open its details.
- // If that's not the case, or if it cannot be found in the store, edit it directly.
- const originName = eventsFunctionsExtension.getOriginName();
- if (originName !== 'gdevelop-extension-store') {
- this.props.onOpenEventsFunctionsExtension(name);
- return;
- }
- const originIdentifier = eventsFunctionsExtension.getOriginIdentifier();
- const extensionShortHeader = extensionShortHeadersByName[originIdentifier];
- if (!extensionShortHeader) {
- console.warn(
- `This extension was downloaded from the store but its reference ${originIdentifier} couldn't be found in the store. Opening the extension in the editor...`
- );
- this.props.onOpenEventsFunctionsExtension(name);
- return;
- }
- this.setState({
- openedExtensionShortHeader: extensionShortHeader,
- openedExtensionName: name,
- });
- };
- _onProjectPropertiesApplied = (options: { newName?: string }) => {
- if (this.props.unsavedChanges) {
- this.props.unsavedChanges.triggerUnsavedChanges();
- }
+ const onEditEventsFunctionExtensionOrSeeDetails = React.useCallback(
+ (
+ extensionShortHeadersByName: { [string]: ExtensionShortHeader },
+ eventsFunctionsExtension: gdEventsFunctionsExtension,
+ name: string
+ ) => {
+ // If the extension is coming from the store, open its details.
+ // If that's not the case, or if it cannot be found in the store, edit it directly.
+ const originName = eventsFunctionsExtension.getOriginName();
+ if (originName !== 'gdevelop-extension-store') {
+ onOpenEventsFunctionsExtension(name);
+ return;
+ }
+ const originIdentifier = eventsFunctionsExtension.getOriginIdentifier();
+ const extensionShortHeader =
+ extensionShortHeadersByName[originIdentifier];
+ if (!extensionShortHeader) {
+ console.warn(
+ `This extension was downloaded from the store but its reference ${originIdentifier} couldn't be found in the store. Opening the extension in the editor...`
+ );
+ onOpenEventsFunctionsExtension(name);
+ return;
+ }
+ setOpenedExtensionShortHeader(extensionShortHeader);
+ setOpenedExtensionName(name);
+ },
+ [onOpenEventsFunctionsExtension]
+ );
- if (options.newName) {
- this.props.onChangeProjectName(options.newName);
- }
+ const onProjectPropertiesApplied = React.useCallback(
+ (options: { newName?: string }) => {
+ if (unsavedChanges) {
+ unsavedChanges.triggerUnsavedChanges();
+ }
+
+ if (options.newName) {
+ onChangeProjectName(options.newName);
+ }
+ setProjectPropertiesDialogOpen(false);
+ },
+ [unsavedChanges, onChangeProjectName]
+ );
- this.setState({ projectPropertiesDialogOpen: false });
- };
+ const onRequestSearch = () => {
+ /* Do nothing for now, but we could open the first result. */
+ };
- _onSearchChange = (text: string) =>
- this.setState({
- searchText: text,
- });
+ const setProjectFirstLayout = React.useCallback(
+ (layoutName: string) => {
+ project.setFirstLayout(layoutName);
+ forceUpdate();
+ },
+ [project, forceUpdate]
+ );
- _onRequestSearch = () => {
- /* Do nothing for now, but we could open the first result. */
- };
-
- _onProjectItemModified = () => {
- this.forceUpdate();
- if (this.props.unsavedChanges)
- this.props.unsavedChanges.triggerUnsavedChanges();
- };
-
- _setProjectFirstLayout = (layoutName: string) => {
- this.props.project.setFirstLayout(layoutName);
- this.forceUpdate();
- };
-
- _onCreateNewExtension = (project: gdProject, i18n: I18nType) => {
- const newExtensionName = this._addEventsFunctionsExtension(
- project.getEventsFunctionsExtensionsCount(),
- i18n
+ const onCreateNewExtension = React.useCallback(
+ (project: gdProject, i18n: I18nType) => {
+ const newExtensionName = addEventsFunctionsExtension(
+ project.getEventsFunctionsExtensionsCount(),
+ i18n
+ );
+ onOpenEventsFunctionsExtension(newExtensionName);
+ setExtensionsSearchDialogOpen(false);
+ },
+ [addEventsFunctionsExtension, onOpenEventsFunctionsExtension]
);
- this.props.onOpenEventsFunctionsExtension(newExtensionName);
- this.setState({ extensionsSearchDialogOpen: false });
- };
-
- render() {
- const {
- project,
- eventsFunctionsExtensionsError,
- onReloadEventsFunctionsExtensions,
- onInstallExtension,
- shortcutMap,
- } = this.props;
- const {
- renamedItemKind,
- renamedItemName,
- searchText,
- openedExtensionShortHeader,
- openedExtensionName,
- } = this.state;
const firstLayoutName = project.getFirstLayout();
@@ -747,74 +835,87 @@ class ProjectManager extends React.Component {
searchText
);
+ const onOpenGamesDashboardDialog = gameMatchingProjectUuid
+ ? () => setOpenGameDetails(true)
+ : null;
+
return (
{({ i18n }) => (
(this._searchBar = searchBar)}
+ ref={searchBarRef}
value={searchText}
- onRequestSearch={this._onRequestSearch}
- onChange={this._onSearchChange}
+ onRequestSearch={onRequestSearch}
+ onChange={setSearchText}
placeholder={t`Search in project`}
/>
-
Game settings}
renderNestedItems={() => [
Properties}
- leftIcon={ }
- onClick={this._openProjectProperties}
+ leftIcon={ }
+ onClick={openProjectProperties}
noPadding
/>,
Global variables}
- leftIcon={ }
- onClick={this._openProjectVariables}
+ id={getProjectManagerItemId('game-icons')}
+ key="icons"
+ primaryText={Icons and thumbnail }
+ leftIcon={ }
+ onClick={onOpenPlatformSpecificAssets}
noPadding
/>,
+ ,
+ ]}
+ />
+ Project settings}
+ renderNestedItems={() => [
Icons and thumbnail}
- leftIcon={ }
- onClick={this.props.onOpenPlatformSpecificAssets}
+ id={getProjectManagerItemId('global-variables')}
+ key="global-variables"
+ primaryText={Global variables }
+ leftIcon={ }
+ onClick={openProjectVariables}
noPadding
/>,
Resources}
- leftIcon={ }
- onClick={this.props.onOpenResources}
+ leftIcon={ }
+ onClick={onOpenResources}
noPadding
/>,
]}
/>
Scenes}
renderNestedItems={() => [
...displayedScenes.map((layout: gdLayout, i: number) => {
@@ -848,48 +949,48 @@ class ProjectManager extends React.Component {
renamedItemKind === 'layout' &&
renamedItemName === name
}
- onEdit={() => this.props.onOpenLayout(name)}
- onDelete={() => this.props.onDeleteLayout(layout)}
+ onEdit={() => onOpenLayout(name)}
+ onDelete={() => onDeleteLayout(layout)}
addLabel={t`Add a new scene`}
- onAdd={() => this._addLayout(i, i18n)}
+ onAdd={() => addLayout(i, i18n)}
onRename={newName => {
- this.props.onRenameLayout(name, newName);
- this._onEditName(null, '');
+ onRenameLayout(name, newName);
+ onEditName(null, '');
}}
- onEditName={() => this._onEditName('layout', name)}
- onCopy={() => this._copyLayout(layout)}
- onCut={() => this._cutLayout(layout)}
- onPaste={() => this._pasteLayout(i)}
- onDuplicate={() => this._duplicateLayout(layout, i)}
+ onEditName={() => onEditName('layout', name)}
+ onCopy={() => copyLayout(layout)}
+ onCut={() => cutLayout(layout)}
+ onPaste={() => pasteLayout(i)}
+ onDuplicate={() => duplicateLayout(layout, i)}
canPaste={() => Clipboard.has(LAYOUT_CLIPBOARD_KIND)}
canMoveUp={i !== 0}
- onMoveUp={() => this._moveUpLayout(i)}
+ onMoveUp={() => moveUpLayout(i)}
canMoveDown={i !== project.getLayoutsCount() - 1}
- onMoveDown={() => this._moveDownLayout(i)}
+ onMoveDown={() => moveDownLayout(i)}
dragAndDropProps={{
DragSourceAndDropTarget: DragSourceAndDropTargetForScenes,
onBeginDrag: () => {
- this._draggedLayoutIndex = i;
+ draggedLayoutIndexRef.current = i;
},
onDrop: () => {
- this._dropOnLayout(i);
+ dropOnLayout(i);
},
}}
buildExtraMenuTemplate={(i18n: I18nType) => [
{
label: i18n._(t`Edit scene properties`),
enabled: true,
- click: () => this._onOpenLayoutProperties(layout),
+ click: () => onOpenLayoutProperties(layout),
},
{
label: i18n._(t`Edit scene variables`),
enabled: true,
- click: () => this._onOpenLayoutVariables(layout),
+ click: () => onOpenLayoutVariables(layout),
},
{
label: i18n._(t`Set as start scene`),
enabled: name !== firstLayoutName,
- click: () => this._setProjectFirstLayout(name),
+ click: () => setProjectFirstLayout(name),
},
]}
/>
@@ -903,7 +1004,7 @@ class ProjectManager extends React.Component {
id="add-new-scene-button"
key={'add-scene'}
onClick={() =>
- this._addLayout(project.getLayoutsCount(), i18n)
+ addLayout(project.getLayoutsCount(), i18n)
}
primaryText={Add scene }
/>,
@@ -911,7 +1012,7 @@ class ProjectManager extends React.Component {
]}
/>
Extensions}
error={eventsFunctionsExtensionsError}
onRefresh={onReloadEventsFunctionsExtensions}
@@ -928,43 +1029,36 @@ class ProjectManager extends React.Component {
renamedItemName === name
}
onEdit={extensionShortHeadersByName =>
- this._onEditEventsFunctionExtensionOrSeeDetails(
+ onEditEventsFunctionExtensionOrSeeDetails(
extensionShortHeadersByName,
eventsFunctionsExtension,
name
)
}
onDelete={() =>
- this.props.onDeleteEventsFunctionsExtension(
+ onDeleteEventsFunctionsExtension(
eventsFunctionsExtension
)
}
onAdd={() => {
- this._addEventsFunctionsExtension(i, i18n);
+ addEventsFunctionsExtension(i, i18n);
}}
onRename={newName => {
- this.props.onRenameEventsFunctionsExtension(
- name,
- newName
- );
- this._onEditName(null, '');
+ onRenameEventsFunctionsExtension(name, newName);
+ onEditName(null, '');
}}
onEditName={() =>
- this._onEditName('events-functions-extension', name)
+ onEditName('events-functions-extension', name)
}
onCopy={() =>
- this._copyEventsFunctionsExtension(
- eventsFunctionsExtension
- )
+ copyEventsFunctionsExtension(eventsFunctionsExtension)
}
onCut={() =>
- this._cutEventsFunctionsExtension(
- eventsFunctionsExtension
- )
+ cutEventsFunctionsExtension(eventsFunctionsExtension)
}
- onPaste={() => this._pasteEventsFunctionsExtension(i)}
+ onPaste={() => pasteEventsFunctionsExtension(i)}
onDuplicate={() =>
- this._duplicateEventsFunctionsExtension(
+ duplicateEventsFunctionsExtension(
eventsFunctionsExtension,
i
)
@@ -975,20 +1069,18 @@ class ProjectManager extends React.Component {
)
}
canMoveUp={i !== 0}
- onMoveUp={() => this._moveUpEventsFunctionsExtension(i)}
+ onMoveUp={() => moveUpEventsFunctionsExtension(i)}
canMoveDown={
i !== project.getEventsFunctionsExtensionsCount() - 1
}
- onMoveDown={() =>
- this._moveDownEventsFunctionsExtension(i)
- }
+ onMoveDown={() => moveDownEventsFunctionsExtension(i)}
dragAndDropProps={{
DragSourceAndDropTarget: DragSourceAndDropTargetForExtensions,
onBeginDrag: () => {
- this._draggedExtensionIndex = i;
+ draggedExtensionIndexRef.current = i;
},
onDrop: () => {
- this._dropOnExtension(i);
+ dropOnExtension(i);
},
}}
/>
@@ -1005,13 +1097,13 @@ class ProjectManager extends React.Component {
primaryText={
Create or search for new extensions
}
- onClick={this._openSearchExtensionDialog}
+ onClick={openSearchExtensionDialog}
/>,
]),
]}
/>
External events}
renderNestedItems={() => [
...displayedExternalEvents.map((externalEvents, i) => {
@@ -1026,39 +1118,35 @@ class ProjectManager extends React.Component {
renamedItemKind === 'external-events' &&
renamedItemName === name
}
- onEdit={() => this.props.onOpenExternalEvents(name)}
- onDelete={() =>
- this.props.onDeleteExternalEvents(externalEvents)
- }
+ onEdit={() => onOpenExternalEvents(name)}
+ onDelete={() => onDeleteExternalEvents(externalEvents)}
addLabel={t`Add new external events`}
- onAdd={() => this._addExternalEvents(i, i18n)}
+ onAdd={() => addExternalEvents(i, i18n)}
onRename={newName => {
- this.props.onRenameExternalEvents(name, newName);
- this._onEditName(null, '');
+ onRenameExternalEvents(name, newName);
+ onEditName(null, '');
}}
- onEditName={() =>
- this._onEditName('external-events', name)
- }
- onCopy={() => this._copyExternalEvents(externalEvents)}
- onCut={() => this._cutExternalEvents(externalEvents)}
- onPaste={() => this._pasteExternalEvents(i)}
+ onEditName={() => onEditName('external-events', name)}
+ onCopy={() => copyExternalEvents(externalEvents)}
+ onCut={() => cutExternalEvents(externalEvents)}
+ onPaste={() => pasteExternalEvents(i)}
onDuplicate={() =>
- this._duplicateExternalEvents(externalEvents, i)
+ duplicateExternalEvents(externalEvents, i)
}
canPaste={() =>
Clipboard.has(EXTERNAL_EVENTS_CLIPBOARD_KIND)
}
canMoveUp={i !== 0}
- onMoveUp={() => this._moveUpExternalEvents(i)}
+ onMoveUp={() => moveUpExternalEvents(i)}
canMoveDown={i !== project.getExternalEventsCount() - 1}
- onMoveDown={() => this._moveDownExternalEvents(i)}
+ onMoveDown={() => moveDownExternalEvents(i)}
dragAndDropProps={{
DragSourceAndDropTarget: DragSourceAndDropTargetForExternalEvents,
onBeginDrag: () => {
- this._draggedExternalEventsIndex = i;
+ draggedExternalEventsIndexRef.current = i;
},
onDrop: () => {
- this._dropOnExternalEvents(i);
+ dropOnExternalEvents(i);
},
}}
/>
@@ -1072,7 +1160,7 @@ class ProjectManager extends React.Component {
key={'add-external-events'}
primaryText={Add external events }
onClick={() =>
- this._addExternalEvents(
+ addExternalEvents(
project.getExternalEventsCount(),
i18n
)
@@ -1082,7 +1170,7 @@ class ProjectManager extends React.Component {
]}
/>
External layouts}
renderNestedItems={() => [
...displayedExternalLayouts.map((externalLayout, i) => {
@@ -1097,41 +1185,37 @@ class ProjectManager extends React.Component {
renamedItemKind === 'external-layout' &&
renamedItemName === name
}
- onEdit={() => this.props.onOpenExternalLayout(name)}
- onDelete={() =>
- this.props.onDeleteExternalLayout(externalLayout)
- }
+ onEdit={() => onOpenExternalLayout(name)}
+ onDelete={() => onDeleteExternalLayout(externalLayout)}
addLabel={t`Add a new external layout`}
- onAdd={() => this._addExternalLayout(i, i18n)}
+ onAdd={() => addExternalLayout(i, i18n)}
onRename={newName => {
- this.props.onRenameExternalLayout(name, newName);
- this._onEditName(null, '');
+ onRenameExternalLayout(name, newName);
+ onEditName(null, '');
}}
- onEditName={() =>
- this._onEditName('external-layout', name)
- }
- onCopy={() => this._copyExternalLayout(externalLayout)}
- onCut={() => this._cutExternalLayout(externalLayout)}
- onPaste={() => this._pasteExternalLayout(i)}
+ onEditName={() => onEditName('external-layout', name)}
+ onCopy={() => copyExternalLayout(externalLayout)}
+ onCut={() => cutExternalLayout(externalLayout)}
+ onPaste={() => pasteExternalLayout(i)}
onDuplicate={() =>
- this._duplicateExternalLayout(externalLayout, i)
+ duplicateExternalLayout(externalLayout, i)
}
canPaste={() =>
Clipboard.has(EXTERNAL_LAYOUT_CLIPBOARD_KIND)
}
canMoveUp={i !== 0}
- onMoveUp={() => this._moveUpExternalLayout(i)}
+ onMoveUp={() => moveUpExternalLayout(i)}
canMoveDown={
i !== project.getExternalLayoutsCount() - 1
}
- onMoveDown={() => this._moveDownExternalLayout(i)}
+ onMoveDown={() => moveDownExternalLayout(i)}
dragAndDropProps={{
DragSourceAndDropTarget: DragSourceAndDropTargetForExternalLayouts,
onBeginDrag: () => {
- this._draggedExternalLayoutIndex = i;
+ draggedExternalLayoutIndexRef.current = i;
},
onDrop: () => {
- this._dropOnExternalLayout(i);
+ dropOnExternalLayout(i);
},
}}
/>
@@ -1145,7 +1229,7 @@ class ProjectManager extends React.Component {
key={'add-external-layout'}
primaryText={Add external layout }
onClick={() =>
- this._addExternalLayout(
+ addExternalLayout(
project.getExternalLayoutsCount(),
i18n
)
@@ -1155,19 +1239,16 @@ class ProjectManager extends React.Component {
]}
/>
- {this.state.projectVariablesEditorOpen && (
+ {projectVariablesEditorOpen && (
Global Variables}
open
variablesContainer={project.getVariables()}
- onCancel={() =>
- this.setState({ projectVariablesEditorOpen: false })
- }
+ onCancel={() => setProjectVariablesEditorOpen(false)}
onApply={() => {
- if (this.props.unsavedChanges)
- this.props.unsavedChanges.triggerUnsavedChanges();
- this.setState({ projectVariablesEditorOpen: false });
+ if (unsavedChanges) unsavedChanges.triggerUnsavedChanges();
+ setProjectVariablesEditorOpen(false);
}}
emptyPlaceholderTitle={
Add your first global variable
@@ -1178,9 +1259,7 @@ class ProjectManager extends React.Component {
}
helpPagePath={'/all-features/variables/global-variables'}
- hotReloadPreviewButtonProps={
- this.props.hotReloadPreviewButtonProps
- }
+ hotReloadPreviewButtonProps={hotReloadPreviewButtonProps}
onComputeAllVariableNames={() =>
EventsRootVariablesFinder.findAllGlobalVariables(
project.getCurrentPlatform(),
@@ -1189,94 +1268,94 @@ class ProjectManager extends React.Component {
}
/>
)}
- {this.state.projectPropertiesDialogOpen && (
+ {projectPropertiesDialogOpen && (
- this.setState({ projectPropertiesDialogOpen: false })
- }
- onApply={this.props.onSaveProjectProperties}
- onPropertiesApplied={this._onProjectPropertiesApplied}
- resourceManagementProps={this.props.resourceManagementProps}
- hotReloadPreviewButtonProps={
- this.props.hotReloadPreviewButtonProps
- }
+ onClose={() => setProjectPropertiesDialogOpen(false)}
+ onApply={onSaveProjectProperties}
+ onPropertiesApplied={onProjectPropertiesApplied}
+ resourceManagementProps={resourceManagementProps}
+ hotReloadPreviewButtonProps={hotReloadPreviewButtonProps}
i18n={i18n}
/>
)}
- {!!this.state.editedPropertiesLayout && (
+ {!!editedPropertiesLayout && (
{
- if (this.props.unsavedChanges)
- this.props.unsavedChanges.triggerUnsavedChanges();
- this._onOpenLayoutProperties(null);
+ if (unsavedChanges) unsavedChanges.triggerUnsavedChanges();
+ onOpenLayoutProperties(null);
}}
- onClose={() => this._onOpenLayoutProperties(null)}
+ onClose={() => onOpenLayoutProperties(null)}
onEditVariables={() => {
- this._onOpenLayoutVariables(
- this.state.editedPropertiesLayout
- );
- this._onOpenLayoutProperties(null);
+ onOpenLayoutVariables(editedPropertiesLayout);
+ onOpenLayoutProperties(null);
}}
- resourceManagementProps={this.props.resourceManagementProps}
+ resourceManagementProps={resourceManagementProps}
/>
)}
- {!!this.state.editedVariablesLayout && (
+ {!!editedVariablesLayout && (
this._onOpenLayoutVariables(null)}
+ layout={editedVariablesLayout}
+ onClose={() => onOpenLayoutVariables(null)}
onApply={() => {
- if (this.props.unsavedChanges)
- this.props.unsavedChanges.triggerUnsavedChanges();
- this._onOpenLayoutVariables(null);
+ if (unsavedChanges) unsavedChanges.triggerUnsavedChanges();
+ onOpenLayoutVariables(null);
}}
- hotReloadPreviewButtonProps={
- this.props.hotReloadPreviewButtonProps
- }
+ hotReloadPreviewButtonProps={hotReloadPreviewButtonProps}
/>
)}
- {this.state.extensionsSearchDialogOpen && (
+ {extensionsSearchDialogOpen && (
- this.setState({ extensionsSearchDialogOpen: false })
- }
+ onClose={() => setExtensionsSearchDialogOpen(false)}
onInstallExtension={onInstallExtension}
onCreateNew={() => {
- this._onCreateNewExtension(project, i18n);
+ onCreateNewExtension(project, i18n);
}}
/>
)}
{openedExtensionShortHeader && openedExtensionName && (
- this.setState({
- openedExtensionShortHeader: null,
- openedExtensionName: null,
- })
- }
- onOpenEventsFunctionsExtension={
- this.props.onOpenEventsFunctionsExtension
- }
+ onClose={() => {
+ setOpenedExtensionShortHeader(null);
+ setOpenedExtensionName(null);
+ }}
+ onOpenEventsFunctionsExtension={onOpenEventsFunctionsExtension}
extensionShortHeader={openedExtensionShortHeader}
extensionName={openedExtensionName}
onInstallExtension={onInstallExtension}
/>
)}
+ {openGameDetails && gameMatchingProjectUuid && (
+ setOpenGameDetails(false)}
+ onGameDeleted={() => {
+ setOpenGameDetails(false);
+ fetchGames();
+ }}
+ onGameUpdated={() => {
+ fetchGames();
+ }}
+ />
+ )}
)}
);
- }
-}
+ },
+ (prevProps, nextProps) => nextProps.freezeUpdate
+);
const ProjectManagerWithErrorBoundary = (props: Props) => (
(
+
+
+
+
+
+
+));
diff --git a/newIDE/app/src/UI/CustomSvgIcons/Picture.js b/newIDE/app/src/UI/CustomSvgIcons/Picture.js
new file mode 100644
index 000000000000..f0b1dcf5ac63
--- /dev/null
+++ b/newIDE/app/src/UI/CustomSvgIcons/Picture.js
@@ -0,0 +1,13 @@
+import React from 'react';
+import SvgIcon from '@material-ui/core/SvgIcon';
+
+export default React.memo(props => (
+
+
+
+));
diff --git a/newIDE/app/src/UI/CustomSvgIcons/ProjectResources.js b/newIDE/app/src/UI/CustomSvgIcons/ProjectResources.js
new file mode 100644
index 000000000000..2a5f937c80c6
--- /dev/null
+++ b/newIDE/app/src/UI/CustomSvgIcons/ProjectResources.js
@@ -0,0 +1,19 @@
+import React from 'react';
+import SvgIcon from '@material-ui/core/SvgIcon';
+
+export default React.memo(props => (
+
+
+
+
+));
diff --git a/newIDE/app/src/UI/CustomSvgIcons/Settings.js b/newIDE/app/src/UI/CustomSvgIcons/Settings.js
new file mode 100644
index 000000000000..592826e0aa00
--- /dev/null
+++ b/newIDE/app/src/UI/CustomSvgIcons/Settings.js
@@ -0,0 +1,11 @@
+import React from 'react';
+import SvgIcon from '@material-ui/core/SvgIcon';
+
+export default React.memo(props => (
+
+
+
+));
diff --git a/newIDE/app/src/UI/ErrorBoundary.js b/newIDE/app/src/UI/ErrorBoundary.js
index 4745fd14a03e..a10beaa0f0e9 100644
--- a/newIDE/app/src/UI/ErrorBoundary.js
+++ b/newIDE/app/src/UI/ErrorBoundary.js
@@ -44,6 +44,7 @@ type ErrorBoundaryScope =
| 'start-page-play'
| 'start-page-community'
| 'start-page-team'
+ | 'start-page-manage'
| 'about'
| 'preferences'
| 'profile'
diff --git a/newIDE/app/src/UI/HighlightingTooltip.js b/newIDE/app/src/UI/HighlightingTooltip.js
new file mode 100644
index 000000000000..e472afd83416
--- /dev/null
+++ b/newIDE/app/src/UI/HighlightingTooltip.js
@@ -0,0 +1,198 @@
+// @flow
+
+import * as React from 'react';
+import Text from './Text';
+import { Line } from './Grid';
+import { getDisplayZIndexForHighlighter } from '../InAppTutorial/HTMLUtils';
+import ClickAwayListener from '@material-ui/core/ClickAwayListener';
+import Fade from '@material-ui/core/Fade';
+import Paper from '@material-ui/core/Paper';
+import Popper from '@material-ui/core/Popper';
+import { makeStyles } from '@material-ui/core/styles';
+import { CorsAwareImage } from './CorsAwareImage';
+import IconButton from './IconButton';
+import Cross from './CustomSvgIcons/Cross';
+import { ColumnStackLayout } from './Layout';
+import GDevelopThemeContext from './Theme/GDevelopThemeContext';
+import InAppTutorialContext from '../InAppTutorial/InAppTutorialContext';
+
+const styles = {
+ paper: {
+ padding: '8px 10px',
+ minWidth: 180,
+ },
+};
+
+const useClasses = makeStyles({
+ popper: {
+ '&[x-placement*="bottom"] #new-feature-popper-arrow': {
+ top: 0,
+ left: 0,
+ marginTop: '-0.71em',
+ marginLeft: 4,
+ marginRight: 4,
+ '&::before': {
+ transformOrigin: '0 100%',
+ },
+ },
+ '&[x-placement*="top"] #new-feature-popper-arrow': {
+ bottom: 0,
+ left: 0,
+ marginBottom: '-0.71em',
+ marginLeft: 4,
+ marginRight: 4,
+ '&::before': {
+ transformOrigin: '100% 0',
+ },
+ },
+ '&[x-placement*="right"] #new-feature-popper-arrow': {
+ left: 0,
+ marginLeft: '-0.71em',
+ height: '1em',
+ width: '0.71em',
+ marginTop: 4,
+ marginBottom: 4,
+ '&::before': {
+ transformOrigin: '100% 100%',
+ },
+ },
+ '&[x-placement*="left"] #new-feature-popper-arrow': {
+ right: 0,
+ marginRight: '-0.71em',
+ height: '1em',
+ width: '0.71em',
+ marginTop: 4,
+ marginBottom: 4,
+ '&::before': {
+ transformOrigin: '0 0',
+ },
+ },
+ },
+ arrow: {
+ overflow: 'hidden',
+ position: 'absolute',
+ width: '1em',
+ /* = width / sqrt(2) = (length of the hypotenuse) */
+ height: '0.71em',
+ boxSizing: 'border-box',
+ '&::before': {
+ content: '""',
+ margin: 'auto',
+ display: 'block',
+ width: '100%',
+ height: '100%',
+ backgroundColor: 'currentColor',
+ transform: 'rotate(45deg)',
+ },
+ },
+});
+
+type Props = {|
+ title: React.Node,
+ thumbnailSource?: string,
+ thumbnailAlt?: string,
+ content: React.Node,
+ anchorElement: HTMLElement,
+ onClose: () => void,
+ placement: 'left' | 'top' | 'bottom' | 'right',
+ closeWithBackdropClick: boolean,
+|};
+
+const HighlightingTooltip = ({
+ title,
+ thumbnailSource,
+ thumbnailAlt,
+ content,
+ anchorElement,
+ onClose,
+ placement,
+ closeWithBackdropClick,
+}: Props) => {
+ const classes = useClasses();
+ const gdevelopTheme = React.useContext(GDevelopThemeContext);
+ const { currentlyRunningInAppTutorial } = React.useContext(
+ InAppTutorialContext
+ );
+ if (currentlyRunningInAppTutorial) return null;
+
+ const popper = (
+
+ {({ TransitionProps }) => (
+
+
+
+
+
+ {title}
+
+
+
+
+
+ {thumbnailSource && thumbnailAlt && (
+
+ )}
+ {content}
+
+
+
+
+ )}
+
+ );
+
+ if (closeWithBackdropClick) {
+ return (
+ {
+ event.preventDefault();
+ event.stopPropagation();
+ onClose();
+ }}
+ >
+ {popper}
+
+ );
+ }
+ return popper;
+};
+
+export default HighlightingTooltip;
diff --git a/newIDE/app/src/Utils/Analytics/EventSender.js b/newIDE/app/src/Utils/Analytics/EventSender.js
index 905ab675487b..97f17cbadcdb 100644
--- a/newIDE/app/src/Utils/Analytics/EventSender.js
+++ b/newIDE/app/src/Utils/Analytics/EventSender.js
@@ -192,6 +192,12 @@ export const sendExportLaunched = (exportKind: string) => {
});
};
+export const sendGameDetailsOpened = (options: {
+ from: 'profile' | 'homepage' | 'projectManager',
+}) => {
+ recordEvent('game_details_opened', options);
+};
+
export const sendExampleDetailsOpened = (slug: string) => {
recordEvent('example-details-opened', { slug });
};
diff --git a/newIDE/app/src/Utils/UseDisplayNewFeature.js b/newIDE/app/src/Utils/UseDisplayNewFeature.js
new file mode 100644
index 000000000000..f4eb8fb300c8
--- /dev/null
+++ b/newIDE/app/src/Utils/UseDisplayNewFeature.js
@@ -0,0 +1,69 @@
+// @flow
+
+import * as React from 'react';
+import PreferencesContext from '../MainFrame/Preferences/PreferencesContext';
+
+const featuresDisplaySettings = {
+ gamesDashboardInProjectManager: { count: 2, intervalInDays: 7 },
+ gamesDashboardInHomePage: { count: 2, intervalInDays: 7 },
+};
+
+const ONE_DAY = 24 * 3600 * 1000;
+
+type Feature = string;
+
+const useDisplayNewFeature = () => {
+ const {
+ values: { newFeaturesAcknowledgements },
+ setNewFeaturesAcknowledgements,
+ } = React.useContext(PreferencesContext);
+
+ const shouldDisplayNewFeatureHighlighting = React.useCallback(
+ ({ featureId }: { featureId: Feature }): boolean => {
+ const settings = featuresDisplaySettings[featureId];
+ if (!settings) return false;
+
+ const acknowledgments = newFeaturesAcknowledgements[featureId];
+ if (!acknowledgments) return true;
+
+ const { count, intervalInDays } = settings;
+ const { dates } = acknowledgments;
+ if (dates.length >= count) return false;
+
+ const lastDate = dates[dates.length - 1];
+
+ return Date.now() > lastDate + intervalInDays * ONE_DAY;
+ },
+ [newFeaturesAcknowledgements]
+ );
+
+ const acknowledgeNewFeature = React.useCallback(
+ ({ featureId }: { featureId: Feature }) => {
+ if (!featuresDisplaySettings[featureId]) return;
+
+ const acknowledgments = newFeaturesAcknowledgements[featureId];
+ if (!acknowledgments) {
+ setNewFeaturesAcknowledgements({
+ ...newFeaturesAcknowledgements,
+ [featureId]: { dates: [Date.now()] },
+ });
+ return;
+ }
+ setNewFeaturesAcknowledgements({
+ ...newFeaturesAcknowledgements,
+ [featureId]: {
+ ...acknowledgments,
+ dates: [...acknowledgments.dates, Date.now()],
+ },
+ });
+ },
+ [newFeaturesAcknowledgements, setNewFeaturesAcknowledgements]
+ );
+
+ return {
+ shouldDisplayNewFeatureHighlighting,
+ acknowledgeNewFeature,
+ };
+};
+
+export default useDisplayNewFeature;
diff --git a/newIDE/app/src/Utils/UseOpenInitialDialog.js b/newIDE/app/src/Utils/UseOpenInitialDialog.js
index a5d2590ae8a1..d4a8d98706b7 100644
--- a/newIDE/app/src/Utils/UseOpenInitialDialog.js
+++ b/newIDE/app/src/Utils/UseOpenInitialDialog.js
@@ -59,9 +59,8 @@ const useOpenInitialDialog = ({
removeRouteArguments(['initial-dialog', 'tutorial-id']);
break;
case 'games-dashboard':
- openProfileDialog(true);
- // As the games dashboard is not a dialog in itself, we don't remove the argument
- // and let the ProfileDialog do it once the tab is opened.
+ // Do nothing as it should open the games dashboard on the homepage
+ // in the manage tab. So the homepage handles the route arguments itself.
break;
default:
break;
diff --git a/newIDE/app/src/stories/componentStories/GameDashboard/GamesList.stories.js b/newIDE/app/src/stories/componentStories/GameDashboard/GamesList.stories.js
index aa6d8a4f2c30..e2ddb2f46b7d 100644
--- a/newIDE/app/src/stories/componentStories/GameDashboard/GamesList.stories.js
+++ b/newIDE/app/src/stories/componentStories/GameDashboard/GamesList.stories.js
@@ -3,18 +3,16 @@
import * as React from 'react';
import muiDecorator from '../../ThemeDecorator';
-
import paperDecorator from '../../PaperDecorator';
+
+import { action } from '@storybook/addon-actions';
import {
fakeSilverAuthenticatedUser,
game1,
game2,
} from '../../../fixtures/GDevelopServicesTestData';
-import MockAdapter from 'axios-mock-adapter';
-import axios from 'axios';
-import { GDevelopGameApi } from '../../../Utils/GDevelopServices/ApiConfigs';
import AuthenticatedUserContext from '../../../Profile/AuthenticatedUserContext';
-import { GamesList } from '../../../GameDashboard/GamesList';
+import GamesList from '../../../GameDashboard/GamesList';
export default {
title: 'GameDashboard/GamesList',
@@ -23,54 +21,15 @@ export default {
};
export const WithoutAProjectOpened = () => {
- const mock = new MockAdapter(axios);
- mock
- .onGet(`${GDevelopGameApi.baseUrl}/game`)
- .reply(200, [game1, game2])
- .onAny()
- .reply(config => {
- console.error(`Unexpected call to ${config.url} (${config.method})`);
- return [504, null];
- });
-
- return (
-
-
-
- );
-};
-
-export const WithoutAProjectOpenedLongLoading = () => {
- const mock = new MockAdapter(axios, { delayResponse: 2500 });
- mock
- .onGet(`${GDevelopGameApi.baseUrl}/game`)
- .reply(200, [game1, game2])
- .onAny()
- .reply(config => {
- console.error(`Unexpected call to ${config.url} (${config.method})`);
- return [504, null];
- });
-
- return (
-
-
-
- );
-};
-
-export const WithAnError = () => {
- const mock = new MockAdapter(axios);
- mock
- .onGet(`${GDevelopGameApi.baseUrl}/game`)
- .reply(500)
- .onAny()
- .reply(config => {
- console.error(`Unexpected call to ${config.url} (${config.method})`);
- return [504, null];
- });
return (
-
+
);
};
diff --git a/newIDE/app/src/stories/componentStories/ProjectManager/ProjectManager.stories.js b/newIDE/app/src/stories/componentStories/ProjectManager/ProjectManager.stories.js
index 455f8bce7aa5..0c90489068a4 100644
--- a/newIDE/app/src/stories/componentStories/ProjectManager/ProjectManager.stories.js
+++ b/newIDE/app/src/stories/componentStories/ProjectManager/ProjectManager.stories.js
@@ -9,7 +9,6 @@ import GDevelopJsInitializerDecorator, {
testProject,
} from '../../GDevelopJsInitializerDecorator';
import fakeHotReloadPreviewButtonProps from '../../FakeHotReloadPreviewButtonProps';
-import defaultShortcuts from '../../../KeyboardShortcuts/DefaultShortcuts';
export default {
title: 'Project Creation/ProjectManager',
@@ -19,7 +18,6 @@ export default {
export const Default = () => (
true}
onChangeProjectName={action('onChangeProjectName')}
onOpenExternalEvents={action('onOpenExternalEvents')}
@@ -45,6 +43,7 @@ export const Default = () => (
onReloadEventsFunctionsExtensions={action(
'onReloadEventsFunctionsExtensions'
)}
+ onShareProject={action('onShareProject')}
freezeUpdate={false}
hotReloadPreviewButtonProps={fakeHotReloadPreviewButtonProps}
resourceManagementProps={fakeResourceManagementProps}
@@ -54,7 +53,6 @@ export const Default = () => (
export const ErrorsInFunctions = () => (
true}
onChangeProjectName={action('onChangeProjectName')}
onOpenExternalEvents={action('onOpenExternalEvents')}
@@ -82,6 +80,7 @@ export const ErrorsInFunctions = () => (
onReloadEventsFunctionsExtensions={action(
'onReloadEventsFunctionsExtensions'
)}
+ onShareProject={action('onShareProject')}
freezeUpdate={false}
hotReloadPreviewButtonProps={fakeHotReloadPreviewButtonProps}
resourceManagementProps={fakeResourceManagementProps}
diff --git a/newIDE/app/src/stories/componentStories/UI/HighlightingTooltip.stories.js b/newIDE/app/src/stories/componentStories/UI/HighlightingTooltip.stories.js
new file mode 100644
index 000000000000..88d394efd0ff
--- /dev/null
+++ b/newIDE/app/src/stories/componentStories/UI/HighlightingTooltip.stories.js
@@ -0,0 +1,112 @@
+// @flow
+import * as React from 'react';
+import { action } from '@storybook/addon-actions';
+
+import muiDecorator from '../../ThemeDecorator';
+import paperDecorator from '../../PaperDecorator';
+
+import HighlightingTooltip from '../../../UI/HighlightingTooltip';
+import FixedHeightFlexContainer from '../../FixedHeightFlexContainer';
+import Text from '../../../UI/Text';
+import Link from '../../../UI/Link';
+import Window from '../../../Utils/Window';
+import TreeLeaves from '../../../UI/CustomSvgIcons/TreeLeaves';
+
+export default {
+ title: 'UI Building Blocks/HighlightingTooltip',
+ component: HighlightingTooltip,
+ decorators: [paperDecorator, muiDecorator],
+};
+
+export const WithThumbnailSetByHref = () => {
+ const [anchorEl, setAnchorEl] = React.useState(null);
+ return (
+
+ setAnchorEl(ref)}
+ >
+ Anchor
+
+ {anchorEl && (
+
+ Follow your game’s online performance, manage published versions,
+ and collect player feedback.
+ ,
+
+ Window.openExternalURL('https://gdevelop.io')}
+ >
+ Learn more
+
+ ,
+ ]}
+ thumbnailSource="https://resources.gdevelop-app.com/tutorials/images/best-practices-when-making-games.png?gdUsage=img"
+ onClose={action('onClose')}
+ closeWithBackdropClick={false}
+ />
+ )}
+
+ );
+};
+
+export const WithThumbnailSetInContent = () => {
+ const [anchorEl, setAnchorEl] = React.useState(null);
+ return (
+
+ setAnchorEl(ref)}
+ >
+ Anchor
+
+ {anchorEl && (
+ ,
+
+
+ Follow your game’s online performance, manage published versions,
+ and collect player feedback.
+ ,
+
+ Window.openExternalURL('https://gdevelop.io')}
+ >
+ Learn more
+
+ ,
+ ]}
+ onClose={action('onClose')}
+ closeWithBackdropClick={false}
+ />
+ )}
+
+ );
+};