From e4f3db71c5653f8a1361814c1cd9a7b713a1bb02 Mon Sep 17 00:00:00 2001 From: Florian Rival Date: Tue, 26 Nov 2024 17:43:10 +0100 Subject: [PATCH] Allow opening a project linked to a game directly from the game dashboard (#7167) --- .../app/src/AssetStore/ExampleStore/index.js | 2 +- newIDE/app/src/GameDashboard/GameCard.js | 123 +++++- newIDE/app/src/GameDashboard/GameHeader.js | 4 + newIDE/app/src/GameDashboard/GameThumbnail.js | 27 +- newIDE/app/src/GameDashboard/GamesList.js | 17 +- .../Monetization/UserEarnings.js | 2 +- .../GameDashboard/Widgets/ProjectsWidget.js | 36 ++ newIDE/app/src/GameDashboard/index.js | 45 +- .../HomePage/BuildSection/index.js | 314 +------------- .../MaxProjectCountAlertMessage.js | 0 .../HomePage/CreateSection/ProjectFileList.js | 385 ++++++++++++++++++ .../ProjectFileListItem.js | 20 +- .../StatusIndicator.js | 0 .../{BuildSection => CreateSection}/utils.js | 46 ++- .../EducationMarketingSection/index.js | 2 +- .../HomePage/ManageSection/index.js | 23 +- .../HomePage/SectionContainer.js | 2 +- .../TeamSection/TeamMemberProjectsView.js | 6 +- .../EditorContainers/HomePage/index.js | 4 + .../ProjectCreation/NewProjectSetupDialog.js | 2 +- .../CloudProjectWriter.js | 1 + .../SaveToStorageProviderDialog.js | 2 +- .../src/QuickCustomization/QuickPublish.js | 2 +- newIDE/app/src/UI/Carousel.js | 15 +- newIDE/app/src/UI/ErrorBoundary.js | 3 +- newIDE/app/src/UI/FlatButtonWithSplitMenu.js | 4 + .../GameDashboard/GameCard.stories.js | 5 + .../GameDashboard/GameDashboard.stories.js | 8 +- .../GameDashboard/GamesList.stories.js | 3 + .../MaxProjectCountAlertMessage.stories.js | 4 +- 30 files changed, 725 insertions(+), 382 deletions(-) create mode 100644 newIDE/app/src/GameDashboard/Widgets/ProjectsWidget.js rename newIDE/app/src/MainFrame/EditorContainers/HomePage/{BuildSection => CreateSection}/MaxProjectCountAlertMessage.js (100%) create mode 100644 newIDE/app/src/MainFrame/EditorContainers/HomePage/CreateSection/ProjectFileList.js rename newIDE/app/src/MainFrame/EditorContainers/HomePage/{BuildSection => CreateSection}/ProjectFileListItem.js (96%) rename newIDE/app/src/MainFrame/EditorContainers/HomePage/{BuildSection => CreateSection}/StatusIndicator.js (100%) rename newIDE/app/src/MainFrame/EditorContainers/HomePage/{BuildSection => CreateSection}/utils.js (89%) diff --git a/newIDE/app/src/AssetStore/ExampleStore/index.js b/newIDE/app/src/AssetStore/ExampleStore/index.js index c346b52c60e7..eacea85ddfda 100644 --- a/newIDE/app/src/AssetStore/ExampleStore/index.js +++ b/newIDE/app/src/AssetStore/ExampleStore/index.js @@ -15,7 +15,7 @@ import { type PrivateGameTemplateListingData } from '../../Utils/GDevelopService import AuthenticatedUserContext from '../../Profile/AuthenticatedUserContext'; import { PrivateGameTemplateStoreContext } from '../PrivateGameTemplates/PrivateGameTemplateStoreContext'; import GridList from '@material-ui/core/GridList'; -import { getExampleAndTemplateTiles } from '../../MainFrame/EditorContainers/HomePage/BuildSection/utils'; +import { getExampleAndTemplateTiles } from '../../MainFrame/EditorContainers/HomePage/CreateSection/utils'; import BackgroundText from '../../UI/BackgroundText'; import { ColumnStackLayout } from '../../UI/Layout'; diff --git a/newIDE/app/src/GameDashboard/GameCard.js b/newIDE/app/src/GameDashboard/GameCard.js index 07ad35110c33..9002c5d159e3 100644 --- a/newIDE/app/src/GameDashboard/GameCard.js +++ b/newIDE/app/src/GameDashboard/GameCard.js @@ -1,19 +1,20 @@ // @flow -import { Trans } from '@lingui/macro'; +import { t, Trans } from '@lingui/macro'; import { I18n } from '@lingui/react'; import * as React from 'react'; import { type I18n as I18nType } from '@lingui/core'; - import { ColumnStackLayout, LineStackLayout, ResponsiveLineStackLayout, } from '../UI/Layout'; +import { + type FileMetadataAndStorageProviderName, + type StorageProvider, +} from '../ProjectsStorage'; import FlatButton from '../UI/FlatButton'; import Text from '../UI/Text'; - import { GameThumbnail } from './GameThumbnail'; - import { getGameMainImageUrl, getGameUrl, @@ -28,8 +29,16 @@ import DollarCoin from '../UI/CustomSvgIcons/DollarCoin'; import Cross from '../UI/CustomSvgIcons/Cross'; import Messages from '../UI/CustomSvgIcons/Messages'; import GameLinkAndShareIcons from './GameLinkAndShareIcons'; +import { + getStorageProviderByInternalName, + useProjectsListFor, +} from '../MainFrame/EditorContainers/HomePage/CreateSection/utils'; +import FlatButtonWithSplitMenu from '../UI/FlatButtonWithSplitMenu'; +import useOnResize from '../Utils/UseOnResize'; +import useForceUpdate from '../Utils/UseForceUpdate'; const styles = { + buttonsContainer: { display: 'flex', flexShrink: 0 }, iconAndText: { display: 'flex', gap: 2, alignItems: 'flex-start' }, }; @@ -37,9 +46,19 @@ type Props = {| game: Game, isCurrentGame: boolean, onOpenGameManager: () => void, + storageProviders: Array, + onOpenProject: (file: FileMetadataAndStorageProviderName) => Promise, |}; -export const GameCard = ({ game, isCurrentGame, onOpenGameManager }: Props) => { +export const GameCard = ({ + storageProviders, + game, + isCurrentGame, + onOpenGameManager, + onOpenProject, +}: Props) => { + useOnResize(useForceUpdate()); + const projectsList = useProjectsListFor(game); const isPublishedOnGdGames = !!game.publicWebBuildId; const gameUrl = isPublishedOnGdGames ? getGameUrl(game) : null; @@ -47,7 +66,8 @@ export const GameCard = ({ game, isCurrentGame, onOpenGameManager }: Props) => { game, ]); - const { isMobile } = useResponsiveWindowSize(); + const { isMobile, windowSize } = useResponsiveWindowSize(); + const isWidthConstrained = windowSize === 'small' || windowSize === 'medium'; const gdevelopTheme = React.useContext(GDevelopThemeContext); const renderPublicInfo = () => { @@ -119,19 +139,86 @@ export const GameCard = ({ game, isCurrentGame, onOpenGameManager }: Props) => { gameId={game.id} thumbnailUrl={gameThumbnailUrl} background="light" + width={ + isMobile + ? undefined + : // On medium/large screens, adapt the size to the width of the window. + Math.min(272, Math.max(130, window.innerWidth / 5)) + } /> ); - const renderButtons = () => ( - - Manage game} - onClick={onOpenGameManager} - /> - - ); + const renderButtons = ({ fullWidth }: { fullWidth: boolean }) => { + return ( +
+ + Manage + ) : ( + Manage game + ) + } + onClick={onOpenGameManager} + /> + {projectsList.length === 0 ? null : projectsList.length === 1 ? ( + Opened + ) : isWidthConstrained ? ( + Open + ) : ( + Open project + ) + } + onClick={() => onOpenProject(projectsList[0])} + /> + ) : ( + Opened : Open + } + onClick={() => onOpenProject(projectsList[0])} + buildMenuTemplate={i18n => [ + ...projectsList.map(fileMetadataAndStorageProviderName => { + const name = + fileMetadataAndStorageProviderName.fileMetadata.name || '-'; + const storageProvider = getStorageProviderByInternalName( + storageProviders, + fileMetadataAndStorageProviderName.storageProviderName + ); + return { + label: i18n._( + t`${name} (${ + storageProvider ? i18n._(storageProvider.name) : '-' + })` + ), + click: () => + onOpenProject(fileMetadataAndStorageProviderName), + }; + }), + { type: 'separator' }, + { + label: i18n._(t`See all in the game dashboard`), + click: onOpenGameManager, + }, + ]} + /> + )} + +
+ ); + }; const renderShareUrl = (i18n: I18nType) => gameUrl ? : null; @@ -153,7 +240,7 @@ export const GameCard = ({ game, isCurrentGame, onOpenGameManager }: Props) => { {renderPublicInfo()} {renderShareUrl(i18n)} - {renderButtons()} + {renderButtons({ fullWidth: true })} ) : ( @@ -169,7 +256,7 @@ export const GameCard = ({ game, isCurrentGame, onOpenGameManager }: Props) => { alignItems="flex-start" > {renderTitle(i18n)} - {renderButtons()} + {renderButtons({ fullWidth: false })} {renderPublicInfo()} {renderShareUrl(i18n)} diff --git a/newIDE/app/src/GameDashboard/GameHeader.js b/newIDE/app/src/GameDashboard/GameHeader.js index 364aea9da24b..97ec7a22988b 100644 --- a/newIDE/app/src/GameDashboard/GameHeader.js +++ b/newIDE/app/src/GameDashboard/GameHeader.js @@ -24,6 +24,8 @@ import Edit from '../UI/CustomSvgIcons/Edit'; import GameLinkAndShareIcons from './GameLinkAndShareIcons'; import { CompactToggleField } from '../UI/CompactToggleField'; import { FixedHeightFlexContainer } from '../UI/Grid'; +import useOnResize from '../Utils/UseOnResize'; +import useForceUpdate from '../Utils/UseForceUpdate'; const styles = { iconAndText: { display: 'flex', gap: 2, alignItems: 'flex-start' }, @@ -42,6 +44,7 @@ const GameHeader = ({ gameUrl, onPublishOnGdGames, }: Props) => { + useOnResize(useForceUpdate()); const { isMobile } = useResponsiveWindowSize(); const gdevelopTheme = React.useContext(GDevelopThemeContext); const gameMainImageUrl = getGameMainImageUrl(game); @@ -113,6 +116,7 @@ const GameHeader = ({ gameId={game.id} thumbnailUrl={gameMainImageUrl} background="medium" + width={Math.min(272, Math.max(150, window.innerWidth / 4.5))} /> ); diff --git a/newIDE/app/src/GameDashboard/GameThumbnail.js b/newIDE/app/src/GameDashboard/GameThumbnail.js index 582a72adfef9..7064c3b80dd2 100644 --- a/newIDE/app/src/GameDashboard/GameThumbnail.js +++ b/newIDE/app/src/GameDashboard/GameThumbnail.js @@ -26,17 +26,6 @@ const styles = { aspectRatio: '16 / 9', height: 'auto', justifyContent: 'center', - overflow: 'hidden', // Keep the radius effect. - }, - thumbnailContainer: { - height: 153, - width: 272, - overflow: 'hidden', // Keep the radius effect. - }, - mobileThumbnailContainer: { - height: 84, - width: 150, - overflow: 'hidden', // Keep the radius effect. }, thumbnail: { aspectRatio: '16 / 9', @@ -62,6 +51,7 @@ type Props = {| onGameUpdated?: (updatedGame: Game) => void, onUpdatingGame?: (isUpdatingGame: boolean) => void, fullWidthOnMobile?: boolean, + width?: number, |}; export const GameThumbnail = ({ @@ -74,6 +64,7 @@ export const GameThumbnail = ({ onUpdatingGame, background = 'light', fullWidthOnMobile, + width, }: Props) => { const { isMobile, isLandscape } = useResponsiveWindowSize(); const { profile, getAuthorizationHeader } = React.useContext( @@ -168,15 +159,19 @@ export const GameThumbnail = ({ } }; + const thumbnailWidth = width ? width : isMobile && !isLandscape ? 150 : 272; + const thumbnailHeight = Math.floor(thumbnailWidth / (16 / 9)); + return ( , project: ?gdProject, games: Array, onRefreshGames: () => Promise, onOpenGameId: (gameId: ?string) => void, + onOpenProject: (file: FileMetadataAndStorageProviderName) => Promise, |}; -const GamesList = ({ project, games, onRefreshGames, onOpenGameId }: Props) => { +const GamesList = ({ + project, + games, + onRefreshGames, + onOpenGameId, + onOpenProject, + storageProviders, +}: Props) => { const { values, setGamesListOrderBy } = React.useContext(PreferencesContext); const [searchText, setSearchText] = React.useState(''); const [currentPage, setCurrentPage] = React.useState(0); @@ -211,12 +224,14 @@ const GamesList = ({ project, games, onRefreshGames, onOpenGameId }: Props) => { {displayedGames.length > 0 ? ( displayedGames.map(game => ( { onOpenGameId(game.id); }} + onOpenProject={onOpenProject} /> )) ) : !!searchText ? ( diff --git a/newIDE/app/src/GameDashboard/Monetization/UserEarnings.js b/newIDE/app/src/GameDashboard/Monetization/UserEarnings.js index ae748b32e259..07738e7df3ea 100644 --- a/newIDE/app/src/GameDashboard/Monetization/UserEarnings.js +++ b/newIDE/app/src/GameDashboard/Monetization/UserEarnings.js @@ -239,7 +239,7 @@ const UserEarnings = ({ hideTitle, margin }: Props) => { return ( <> - + {margin === 'dense' ? ( content diff --git a/newIDE/app/src/GameDashboard/Widgets/ProjectsWidget.js b/newIDE/app/src/GameDashboard/Widgets/ProjectsWidget.js new file mode 100644 index 000000000000..c5bcdf781b64 --- /dev/null +++ b/newIDE/app/src/GameDashboard/Widgets/ProjectsWidget.js @@ -0,0 +1,36 @@ +// @flow +import * as React from 'react'; +import { I18n } from '@lingui/react'; +import { Trans } from '@lingui/macro'; +import { type Game } from '../../Utils/GDevelopServices/Game'; +import { + type FileMetadataAndStorageProviderName, + type FileMetadata, + type StorageProvider, +} from '../../ProjectsStorage'; +import DashboardWidget from './DashboardWidget'; +import ProjectFileList from '../../MainFrame/EditorContainers/HomePage/CreateSection/ProjectFileList'; + +type Props = {| + game: Game, + onOpenProject: (file: FileMetadataAndStorageProviderName) => Promise, + storageProviders: Array, + + project: ?gdProject, + currentFileMetadata: ?FileMetadata, + closeProject: () => Promise, +|}; + +const ProjectsWidget = (props: Props) => { + return ( + + {({ i18n }) => ( + Projects}> + + + )} + + ); +}; + +export default ProjectsWidget; diff --git a/newIDE/app/src/GameDashboard/index.js b/newIDE/app/src/GameDashboard/index.js index 3fd1c36ca369..077090773fa3 100644 --- a/newIDE/app/src/GameDashboard/index.js +++ b/newIDE/app/src/GameDashboard/index.js @@ -20,6 +20,11 @@ import { getAclsFromUserIds, setGameUserAcls, } from '../Utils/GDevelopServices/Game'; +import { + type FileMetadataAndStorageProviderName, + type FileMetadata, + type StorageProvider, +} from '../ProjectsStorage'; import { ColumnStackLayout } from '../UI/Layout'; import GameHeader from './GameHeader'; import FeedbackWidget from './Widgets/FeedbackWidget'; @@ -51,6 +56,7 @@ import PublicGamePropertiesDialog, { } from './PublicGamePropertiesDialog'; import useAlertDialog from '../UI/Alert/useAlertDialog'; import { showErrorBox } from '../UI/Messages/MessageBox'; +import ProjectsWidget from './Widgets/ProjectsWidget'; export type GameDetailsTab = | 'details' @@ -61,26 +67,43 @@ export type GameDetailsTab = | 'leaderboards'; type Props = {| + // Project handling: project?: ?gdProject, + currentFileMetadata: ?FileMetadata, + onOpenProject: (file: FileMetadataAndStorageProviderName) => Promise, + storageProviders: Array, + closeProject: () => Promise, + + // Current game: game: Game, - analyticsSource: 'profile' | 'homepage' | 'projectManager', - currentView: GameDetailsTab, - setCurrentView: GameDetailsTab => void, - onBack: () => void, onGameUpdated: (game: Game) => void, onUnregisterGame: (i18n: I18nType) => Promise, gameUnregisterErrorText: ?React.Node, + + // Navigation: + currentView: GameDetailsTab, + setCurrentView: GameDetailsTab => void, + onBack: () => void, |}; const GameDashboard = ({ + // Project handling: project, + currentFileMetadata, + onOpenProject, + storageProviders, + closeProject, + + // Current game: game, - currentView, - setCurrentView, - onBack, onGameUpdated, onUnregisterGame, gameUnregisterErrorText, + + // Navigation: + currentView, + setCurrentView, + onBack, }: Props) => { const [ gameDetailsDialogOpen, @@ -534,6 +557,14 @@ const GameDashboard = ({ displayUnlockMoreLeaderboardsCallout } /> + setCurrentView('builds')} diff --git a/newIDE/app/src/MainFrame/EditorContainers/HomePage/BuildSection/index.js b/newIDE/app/src/MainFrame/EditorContainers/HomePage/BuildSection/index.js index b4353293189f..bec74bcfff78 100644 --- a/newIDE/app/src/MainFrame/EditorContainers/HomePage/BuildSection/index.js +++ b/newIDE/app/src/MainFrame/EditorContainers/HomePage/BuildSection/index.js @@ -2,8 +2,6 @@ import * as React from 'react'; import { type I18n as I18nType } from '@lingui/core'; import { Trans, t } from '@lingui/macro'; -import List from '@material-ui/core/List'; -import ListItem from '@material-ui/core/ListItem'; import Text from '../../../../UI/Text'; import TextButton from '../../../../UI/TextButton'; @@ -20,20 +18,16 @@ import { type FileMetadata, type StorageProvider, } from '../../../../ProjectsStorage'; -import PreferencesContext from '../../../Preferences/PreferencesContext'; import AuthenticatedUserContext from '../../../../Profile/AuthenticatedUserContext'; import SectionContainer, { SectionRow } from '../SectionContainer'; import { checkIfHasTooManyCloudProjects, MaxProjectCountAlertMessage, -} from './MaxProjectCountAlertMessage'; +} from '../CreateSection/MaxProjectCountAlertMessage'; import { ExampleStoreContext } from '../../../../AssetStore/ExampleStore/ExampleStoreContext'; import { SubscriptionSuggestionContext } from '../../../../Profile/Subscription/SubscriptionSuggestionContext'; import { type ExampleShortHeader } from '../../../../Utils/GDevelopServices/Example'; import Add from '../../../../UI/CustomSvgIcons/Add'; -import Skeleton from '@material-ui/lab/Skeleton'; -import BackgroundText from '../../../../UI/BackgroundText'; -import Paper from '../../../../UI/Paper'; import PlaceholderError from '../../../../UI/PlaceholderError'; import AlertMessage from '../../../../UI/AlertMessage'; import IconButton from '../../../../UI/IconButton'; @@ -41,31 +35,14 @@ import { type PrivateGameTemplateListingData } from '../../../../Utils/GDevelopS import { PrivateGameTemplateStoreContext } from '../../../../AssetStore/PrivateGameTemplates/PrivateGameTemplateStoreContext'; import ChevronArrowRight from '../../../../UI/CustomSvgIcons/ChevronArrowRight'; import Refresh from '../../../../UI/CustomSvgIcons/Refresh'; -import ProjectFileListItem from './ProjectFileListItem'; -import { type MenuItemTemplate } from '../../../../UI/Menu/Menu.flow'; -import { - getExampleAndTemplateTiles, - getLastModifiedInfoByProjectId, - getProjectLineHeight, - transformCloudProjectsIntoFileMetadataWithStorageProviderName, -} from './utils'; +import { getExampleAndTemplateTiles } from '../CreateSection/utils'; import ErrorBoundary from '../../../../UI/ErrorBoundary'; import InfoBar from '../../../../UI/Messages/InfoBar'; import GridList from '@material-ui/core/GridList'; import type { WindowSizeType } from '../../../../UI/Responsive/ResponsiveWindowMeasurer'; -import useAlertDialog from '../../../../UI/Alert/useAlertDialog'; -import optionalRequire from '../../../../Utils/OptionalRequire'; -import { deleteCloudProject } from '../../../../Utils/GDevelopServices/Project'; -import { extractGDevelopApiErrorStatusAndCode } from '../../../../Utils/GDevelopServices/Errors'; -import ContextMenu, { - type ContextMenuInterface, -} from '../../../../UI/Menu/ContextMenu'; -import type { ClientCoordinates } from '../../../../Utils/UseLongTouch'; import PromotionsSlideshow from '../../../../Promotions/PromotionsSlideshow'; import ExampleStore from '../../../../AssetStore/ExampleStore'; - -const electron = optionalRequire('electron'); -const path = optionalRequire('path'); +import ProjectFileList from '../CreateSection/ProjectFileList'; const cellSpacing = 2; @@ -120,13 +97,6 @@ type Props = {| closeProject: () => Promise, |}; -const locateProjectFile = (file: FileMetadataAndStorageProviderName) => { - if (!electron) return; - electron.shell.showItemInFolder( - path.resolve(file.fileMetadata.fileIdentifier) - ); -}; - const BuildSection = ({ project, currentFileMetadata, @@ -142,15 +112,7 @@ const BuildSection = ({ canManageGame, closeProject, }: Props) => { - const { getRecentProjectFiles } = React.useContext(PreferencesContext); const { exampleShortHeaders } = React.useContext(ExampleStoreContext); - const { - showDeleteConfirmation, - showConfirmation, - showAlert, - } = useAlertDialog(); - const { removeRecentProjectFile } = React.useContext(PreferencesContext); - const [pendingProject, setPendingProject] = React.useState(null); const [ showAllGameTemplates, setShowAllGameTemplates, @@ -158,7 +120,6 @@ const BuildSection = ({ const { privateGameTemplateListingDatas } = React.useContext( PrivateGameTemplateStoreContext ); - const contextMenu = React.useRef(null); const authenticatedUser = React.useContext(AuthenticatedUserContext); const { openSubscriptionDialog } = React.useContext( SubscriptionSuggestionContext @@ -170,172 +131,19 @@ const BuildSection = ({ ] = React.useState(false); const { authenticated, - profile, - cloudProjects, limits, cloudProjectsFetchingErrorLabel, onCloudProjectsChanged, onOpenLoginDialog, } = authenticatedUser; const { windowSize, isMobile, isLandscape } = useResponsiveWindowSize(); - const [ - lastModifiedInfoByProjectId, - setLastModifiedInfoByProjectId, - ] = React.useState({}); const columnsCount = getItemsColumns(windowSize, isLandscape); - let projectFiles: Array = getRecentProjectFiles().filter( - file => file.fileMetadata - ); - - if (cloudProjects) { - projectFiles = projectFiles.concat( - transformCloudProjectsIntoFileMetadataWithStorageProviderName( - cloudProjects - ) - ); - } - - // Look at projects where lastCommittedBy is not the current user, and fetch - // public profiles of the users that have modified them. - React.useEffect( - () => { - const updateModificationInfoByProjectId = async () => { - if (!cloudProjects || !profile) return; - - const _lastModifiedInfoByProjectId = await getLastModifiedInfoByProjectId( - { - cloudProjects, - profile, - } - ); - setLastModifiedInfoByProjectId(_lastModifiedInfoByProjectId); - }; - - updateModificationInfoByProjectId(); - }, - [cloudProjects, profile] - ); - const hasTooManyCloudProjects = checkIfHasTooManyCloudProjects( authenticatedUser ); - const onDeleteCloudProject = async ( - i18n: I18nType, - { fileMetadata, storageProviderName }: FileMetadataAndStorageProviderName - ) => { - if (storageProviderName !== 'Cloud') return; - const projectName = fileMetadata.name; - if (!projectName) return; // Only cloud projects can be deleted, and all cloud projects have names. - - const isCurrentProjectOpened = - !!currentFileMetadata && - currentFileMetadata.fileIdentifier === fileMetadata.fileIdentifier; - if (isCurrentProjectOpened) { - const result = await showConfirmation({ - title: t`Project is opened`, - message: t`You are about to delete the project ${projectName}, which is currently opened. If you proceed, the project will be closed and you will lose any unsaved changes. Do you want to proceed?`, - confirmButtonLabel: t`Continue`, - }); - if (!result) return; - await closeProject(); - } - - // Extract word translation to ensure it is not wrongly translated in the sentence. - const translatedConfirmText = i18n._(t`delete`); - - const deleteAnswer = await showDeleteConfirmation({ - title: t`Permanently delete the project?`, - message: t`Project ${projectName} will be deleted. You will no longer be able to access it.`, - fieldMessage: t`To confirm, type "${translatedConfirmText}"`, - confirmText: translatedConfirmText, - confirmButtonLabel: t`Delete project`, - }); - if (!deleteAnswer) return; - - try { - setPendingProject(fileMetadata.fileIdentifier); - await deleteCloudProject(authenticatedUser, fileMetadata.fileIdentifier); - authenticatedUser.onCloudProjectsChanged(); - } catch (error) { - const extractedStatusAndCode = extractGDevelopApiErrorStatusAndCode( - error - ); - const message = - extractedStatusAndCode && extractedStatusAndCode.status === 403 - ? t`You don't have permissions to delete this project.` - : t`An error occurred when saving the project. Please try again later.`; - showAlert({ - title: t`Unable to delete the project`, - message, - }); - } finally { - setPendingProject(null); - } - }; - - const buildContextMenu = ( - i18n: I18nType, - file: ?FileMetadataAndStorageProviderName - ): Array => { - if (!file) return []; - - const actions = [ - { label: i18n._(t`Open`), click: () => onOpenRecentFile(file) }, - ]; - if (file.storageProviderName === 'Cloud') { - actions.push({ - label: i18n._(t`Delete`), - click: () => onDeleteCloudProject(i18n, file), - }); - } else if (file.storageProviderName === 'LocalFile') { - actions.push( - ...[ - { - label: i18n._(t`Show in local folder`), - click: () => locateProjectFile(file), - }, - { - label: i18n._(t`Remove from list`), - click: () => removeRecentProjectFile(file), - }, - ] - ); - } else { - actions.push({ - label: i18n._(t`Remove from list`), - click: () => removeRecentProjectFile(file), - }); - } - - const gameId = file.fileMetadata.gameId; - if (gameId) { - actions.push( - ...[ - { type: 'separator' }, - { - label: i18n._(t`Manage game`), - click: () => onManageGame(gameId), - enabled: canManageGame(gameId), - }, - ] - ); - } - - return actions; - }; - - const openContextMenu = React.useCallback( - (event: ClientCoordinates, file: FileMetadataAndStorageProviderName) => { - if (contextMenu.current) { - contextMenu.current.open(event.clientX, event.clientY, { file }); - } - }, - [] - ); - const refreshCloudProjects = React.useCallback( async () => { if (isRefreshing) return; @@ -354,12 +162,6 @@ const BuildSection = ({ [onCloudProjectsChanged, isRefreshing, authenticated] ); - projectFiles.sort((a, b) => { - if (!a.fileMetadata.lastModifiedDate) return 1; - if (!b.fileMetadata.lastModifiedDate) return -1; - return b.fileMetadata.lastModifiedDate - a.fileMetadata.lastModifiedDate; - }); - const shouldDisplayPremiumGameTemplates = !authenticatedUser || !authenticatedUser.limits || @@ -398,7 +200,6 @@ const BuildSection = ({ !authenticatedUser.limits.capabilities.classrooms || !authenticatedUser.limits.capabilities.classrooms.hidePlayTab; - const skeletonLineHeight = getProjectLineHeight({ isMobile }); const pageContent = showAllGameTemplates ? ( setShowAllGameTemplates(false)} @@ -531,102 +332,15 @@ const BuildSection = ({ )} - {authenticatedUser.loginState === 'loggingIn' && - projectFiles.length === 0 ? ( // Only show skeleton on first load - new Array(10).fill(0).map((_, index) => ( - - - - - - - - )) - ) : projectFiles.length > 0 ? ( - - - {!isMobile && ( - - - - File name - - - - - Location - - - - - Last edited - - - - )} - - {projectFiles.map(file => ( - - ))} - {isMobile && limits && hasTooManyCloudProjects && ( - - openSubscriptionDialog({ - analyticsMetadata: { - reason: 'Cloud Project limit reached', - }, - }) - } - /> - )} - - - - ) : ( - - - - - - No projects yet. - - - - Create your first project using one of our templates or - start from scratch. - - - - - - - )} + @@ -659,10 +373,6 @@ const BuildSection = ({ onActionClick={onOpenLoginDialog} actionLabel={Log in} /> - buildContextMenu(_i18n, file)} - /> ); }; diff --git a/newIDE/app/src/MainFrame/EditorContainers/HomePage/BuildSection/MaxProjectCountAlertMessage.js b/newIDE/app/src/MainFrame/EditorContainers/HomePage/CreateSection/MaxProjectCountAlertMessage.js similarity index 100% rename from newIDE/app/src/MainFrame/EditorContainers/HomePage/BuildSection/MaxProjectCountAlertMessage.js rename to newIDE/app/src/MainFrame/EditorContainers/HomePage/CreateSection/MaxProjectCountAlertMessage.js diff --git a/newIDE/app/src/MainFrame/EditorContainers/HomePage/CreateSection/ProjectFileList.js b/newIDE/app/src/MainFrame/EditorContainers/HomePage/CreateSection/ProjectFileList.js new file mode 100644 index 000000000000..5c4c6b1823e4 --- /dev/null +++ b/newIDE/app/src/MainFrame/EditorContainers/HomePage/CreateSection/ProjectFileList.js @@ -0,0 +1,385 @@ +// @flow +import * as React from 'react'; +import { type I18n as I18nType } from '@lingui/core'; +import { Trans, t } from '@lingui/macro'; +import List from '@material-ui/core/List'; +import ListItem from '@material-ui/core/ListItem'; + +import Text from '../../../../UI/Text'; +import { Line, Column } from '../../../../UI/Grid'; +import { useResponsiveWindowSize } from '../../../../UI/Responsive/ResponsiveWindowMeasurer'; +import { + type FileMetadataAndStorageProviderName, + type FileMetadata, + type StorageProvider, +} from '../../../../ProjectsStorage'; +import { type Game } from '../../../../Utils/GDevelopServices/Game'; +import PreferencesContext from '../../../Preferences/PreferencesContext'; +import AuthenticatedUserContext from '../../../../Profile/AuthenticatedUserContext'; +import { + checkIfHasTooManyCloudProjects, + MaxProjectCountAlertMessage, +} from './MaxProjectCountAlertMessage'; +import { SubscriptionSuggestionContext } from '../../../../Profile/Subscription/SubscriptionSuggestionContext'; +import Skeleton from '@material-ui/lab/Skeleton'; +import BackgroundText from '../../../../UI/BackgroundText'; +import Paper from '../../../../UI/Paper'; +import PlaceholderError from '../../../../UI/PlaceholderError'; +import AlertMessage from '../../../../UI/AlertMessage'; +import ProjectFileListItem from './ProjectFileListItem'; +import { type MenuItemTemplate } from '../../../../UI/Menu/Menu.flow'; +import { + getLastModifiedInfoByProjectId, + getProjectLineHeight, + useProjectsListFor, +} from './utils'; +import ErrorBoundary from '../../../../UI/ErrorBoundary'; +import useAlertDialog from '../../../../UI/Alert/useAlertDialog'; +import optionalRequire from '../../../../Utils/OptionalRequire'; +import { deleteCloudProject } from '../../../../Utils/GDevelopServices/Project'; +import { extractGDevelopApiErrorStatusAndCode } from '../../../../Utils/GDevelopServices/Errors'; +import ContextMenu, { + type ContextMenuInterface, +} from '../../../../UI/Menu/ContextMenu'; +import type { ClientCoordinates } from '../../../../Utils/UseLongTouch'; +const electron = optionalRequire('electron'); +const path = optionalRequire('path'); + +const styles = { + listItem: { + padding: 0, + marginTop: 2, + marginBottom: 2, + borderRadius: 8, + overflowWrap: 'anywhere', // Ensure everything is wrapped on small devices. + }, + projectSkeleton: { borderRadius: 6 }, + noProjectsContainer: { padding: 10 }, + refreshIconContainer: { fontSize: 20, display: 'flex', alignItems: 'center' }, + grid: { + margin: 0, + // Remove the scroll capability of the grid, the scroll view handles it. + overflow: 'unset', + }, +}; + +type Props = {| + i18n: I18nType, + + game: ?Game, + onOpenProject: (file: FileMetadataAndStorageProviderName) => Promise, + storageProviders: Array, + + project: ?gdProject, + currentFileMetadata: ?FileMetadata, + closeProject: () => Promise, +|}; + +const locateProjectFile = (file: FileMetadataAndStorageProviderName) => { + if (!electron) return; + electron.shell.showItemInFolder( + path.resolve(file.fileMetadata.fileIdentifier) + ); +}; + +const ProjectFileList = ({ + project, + currentFileMetadata, + game, + onOpenProject, + storageProviders, + i18n, + closeProject, +}: Props) => { + const { + showDeleteConfirmation, + showConfirmation, + showAlert, + } = useAlertDialog(); + const projectFiles = useProjectsListFor(game); + const { removeRecentProjectFile } = React.useContext(PreferencesContext); + const [pendingProject, setPendingProject] = React.useState(null); + const contextMenu = React.useRef(null); + const authenticatedUser = React.useContext(AuthenticatedUserContext); + const { openSubscriptionDialog } = React.useContext( + SubscriptionSuggestionContext + ); + const { + profile, + cloudProjects, + limits, + cloudProjectsFetchingErrorLabel, + onCloudProjectsChanged, + } = authenticatedUser; + const { isMobile } = useResponsiveWindowSize(); + const [ + lastModifiedInfoByProjectId, + setLastModifiedInfoByProjectId, + ] = React.useState({}); + + // Look at projects where lastCommittedBy is not the current user, and fetch + // public profiles of the users that have modified them. + React.useEffect( + () => { + const updateModificationInfoByProjectId = async () => { + if (!cloudProjects || !profile) return; + + const _lastModifiedInfoByProjectId = await getLastModifiedInfoByProjectId( + { + cloudProjects, + profile, + } + ); + setLastModifiedInfoByProjectId(_lastModifiedInfoByProjectId); + }; + + updateModificationInfoByProjectId(); + }, + [cloudProjects, profile] + ); + + const hasTooManyCloudProjects = checkIfHasTooManyCloudProjects( + authenticatedUser + ); + + const onDeleteCloudProject = async ( + i18n: I18nType, + { fileMetadata, storageProviderName }: FileMetadataAndStorageProviderName + ) => { + if (storageProviderName !== 'Cloud') return; + const projectName = fileMetadata.name; + if (!projectName) return; // Only cloud projects can be deleted, and all cloud projects have names. + + const isCurrentProjectOpened = + !!currentFileMetadata && + currentFileMetadata.fileIdentifier === fileMetadata.fileIdentifier; + if (isCurrentProjectOpened) { + const result = await showConfirmation({ + title: t`Project is opened`, + message: t`You are about to delete the project ${projectName}, which is currently opened. If you proceed, the project will be closed and you will lose any unsaved changes. Do you want to proceed?`, + confirmButtonLabel: t`Continue`, + }); + if (!result) return; + await closeProject(); + } + + // Extract word translation to ensure it is not wrongly translated in the sentence. + const translatedConfirmText = i18n._(t`delete`); + + const deleteAnswer = await showDeleteConfirmation({ + title: t`Permanently delete the project?`, + message: t`Project ${projectName} will be deleted. You will no longer be able to access it.`, + fieldMessage: t`To confirm, type "${translatedConfirmText}"`, + confirmText: translatedConfirmText, + confirmButtonLabel: t`Delete project`, + }); + if (!deleteAnswer) return; + + try { + setPendingProject(fileMetadata.fileIdentifier); + await deleteCloudProject(authenticatedUser, fileMetadata.fileIdentifier); + authenticatedUser.onCloudProjectsChanged(); + } catch (error) { + const extractedStatusAndCode = extractGDevelopApiErrorStatusAndCode( + error + ); + const message = + extractedStatusAndCode && extractedStatusAndCode.status === 403 + ? t`You don't have permissions to delete this project.` + : t`An error occurred when saving the project. Please try again later.`; + showAlert({ + title: t`Unable to delete the project`, + message, + }); + } finally { + setPendingProject(null); + } + }; + + const buildContextMenu = ( + i18n: I18nType, + file: ?FileMetadataAndStorageProviderName + ): Array => { + if (!file) return []; + + const actions = [ + { label: i18n._(t`Open`), click: () => onOpenProject(file) }, + ]; + if (file.storageProviderName === 'Cloud') { + actions.push({ + label: i18n._(t`Delete`), + click: () => onDeleteCloudProject(i18n, file), + }); + } else if (file.storageProviderName === 'LocalFile') { + actions.push( + ...[ + { + label: i18n._(t`Show in local folder`), + click: () => locateProjectFile(file), + }, + { + label: i18n._(t`Remove from list`), + click: () => removeRecentProjectFile(file), + }, + ] + ); + } else { + actions.push({ + label: i18n._(t`Remove from list`), + click: () => removeRecentProjectFile(file), + }); + } + + return actions; + }; + + const openContextMenu = React.useCallback( + (event: ClientCoordinates, file: FileMetadataAndStorageProviderName) => { + if (contextMenu.current) { + contextMenu.current.open(event.clientX, event.clientY, { file }); + } + }, + [] + ); + + const skeletonLineHeight = getProjectLineHeight({ isMobile }); + + if (cloudProjectsFetchingErrorLabel) { + return ( + + + + {cloudProjectsFetchingErrorLabel} + + + + ); + } + + return ( + <> + {authenticatedUser.loginState === 'loggingIn' && + projectFiles.length === 0 ? ( // Only show skeleton on first load + new Array(3).fill(0).map((_, index) => ( + + + + + + + + )) + ) : projectFiles.length > 0 ? ( + + + {!isMobile && ( + + + + File name + + + + + Location + + + + + Last edited + + + + )} + + {projectFiles.map(file => ( + + ))} + {isMobile && limits && hasTooManyCloudProjects && ( + + openSubscriptionDialog({ + analyticsMetadata: { + reason: 'Cloud Project limit reached', + }, + }) + } + /> + )} + + + + ) : ( + + + + + {game ? ( + + + No projects found for this game. Open manually a local + project to have it added to the game dashboard. + + + ) : ( + <> + + No projects yet. + + + + Create your first project using one of our templates or + start from scratch. + + + + )} + + + + + )} + buildContextMenu(_i18n, file)} + /> + + ); +}; + +const ProjectFileListWithErrorBoundary = (props: Props) => ( + Project file list} + scope="project-file-list" + > + + +); + +export default ProjectFileListWithErrorBoundary; diff --git a/newIDE/app/src/MainFrame/EditorContainers/HomePage/BuildSection/ProjectFileListItem.js b/newIDE/app/src/MainFrame/EditorContainers/HomePage/CreateSection/ProjectFileListItem.js similarity index 96% rename from newIDE/app/src/MainFrame/EditorContainers/HomePage/BuildSection/ProjectFileListItem.js rename to newIDE/app/src/MainFrame/EditorContainers/HomePage/CreateSection/ProjectFileListItem.js index b9ca86c5c9f0..16b68795fd7c 100644 --- a/newIDE/app/src/MainFrame/EditorContainers/HomePage/BuildSection/ProjectFileListItem.js +++ b/newIDE/app/src/MainFrame/EditorContainers/HomePage/CreateSection/ProjectFileListItem.js @@ -26,7 +26,10 @@ import { import Avatar from '@material-ui/core/Avatar'; import Tooltip from '@material-ui/core/Tooltip'; import { getGravatarUrl } from '../../../../UI/GravatarUrl'; -import { type LastModifiedInfo } from './utils'; +import { + getStorageProviderByInternalName, + type LastModifiedInfo, +} from './utils'; import DotBadge from '../../../../UI/DotBadge'; import { type FileMetadata } from '../../../../ProjectsStorage'; import StatusIndicator from './StatusIndicator'; @@ -188,21 +191,12 @@ const PrettyBreakablePath = ({ path }: {| path: string |}) => { }, []); }; -const getStorageProviderByInternalName = ( - storageProviders: Array, - internalName: string -): ?StorageProvider => { - return storageProviders.find( - storageProvider => storageProvider.internalName === internalName - ); -}; - type ProjectFileListItemProps = {| file: FileMetadataAndStorageProviderName, currentFileMetadata: ?FileMetadata, lastModifiedInfo?: LastModifiedInfo | null, storageProviders: Array, - onOpenRecentFile: (file: FileMetadataAndStorageProviderName) => Promise, + onOpenProject: (file: FileMetadataAndStorageProviderName) => Promise, isWindowSizeMediumOrLarger: boolean, isLoading: boolean, onOpenContextMenu: ( @@ -216,7 +210,7 @@ export const ProjectFileListItem = ({ currentFileMetadata, lastModifiedInfo, // If null, the project has been modified last by the current user. storageProviders, - onOpenRecentFile, + onOpenProject, isWindowSizeMediumOrLarger, isLoading, onOpenContextMenu, @@ -244,7 +238,7 @@ export const ProjectFileListItem = ({ button key={file.fileMetadata.fileIdentifier} onClick={() => { - onOpenRecentFile(file); + onOpenProject(file); }} style={styles.listItem} onContextMenu={event => onOpenContextMenu(event, file)} diff --git a/newIDE/app/src/MainFrame/EditorContainers/HomePage/BuildSection/StatusIndicator.js b/newIDE/app/src/MainFrame/EditorContainers/HomePage/CreateSection/StatusIndicator.js similarity index 100% rename from newIDE/app/src/MainFrame/EditorContainers/HomePage/BuildSection/StatusIndicator.js rename to newIDE/app/src/MainFrame/EditorContainers/HomePage/CreateSection/StatusIndicator.js diff --git a/newIDE/app/src/MainFrame/EditorContainers/HomePage/BuildSection/utils.js b/newIDE/app/src/MainFrame/EditorContainers/HomePage/CreateSection/utils.js similarity index 89% rename from newIDE/app/src/MainFrame/EditorContainers/HomePage/BuildSection/utils.js rename to newIDE/app/src/MainFrame/EditorContainers/HomePage/CreateSection/utils.js index eb4b97aa37c0..c9bbd5f4b926 100644 --- a/newIDE/app/src/MainFrame/EditorContainers/HomePage/BuildSection/utils.js +++ b/newIDE/app/src/MainFrame/EditorContainers/HomePage/CreateSection/utils.js @@ -4,7 +4,10 @@ import { type I18n as I18nType } from '@lingui/core'; import { getUserPublicProfilesByIds } from '../../../../Utils/GDevelopServices/User'; import { type Profile } from '../../../../Utils/GDevelopServices/Authentication'; import { type CloudProjectWithUserAccessInfo } from '../../../../Utils/GDevelopServices/Project'; -import { type FileMetadataAndStorageProviderName } from '../../../../ProjectsStorage'; +import { + type FileMetadataAndStorageProviderName, + type StorageProvider, +} from '../../../../ProjectsStorage'; import { marginsSize } from '../../../../UI/Grid'; import { sendGameTemplateInformationOpened } from '../../../../Utils/Analytics/EventSender'; import { getProductPriceOrOwnedLabel } from '../../../../AssetStore/ProductPriceTag'; @@ -12,10 +15,13 @@ import { type PrivateGameTemplateListingData } from '../../../../Utils/GDevelopS import { type ExampleShortHeader } from '../../../../Utils/GDevelopServices/Example'; import { type PrivateGameTemplate } from '../../../../Utils/GDevelopServices/Asset'; import { type CarouselThumbnail } from '../../../../UI/Carousel'; +import { type Game } from '../../../../Utils/GDevelopServices/Game'; import { ExampleTile, PrivateGameTemplateTile, } from '../../../../AssetStore/ShopTiles'; +import PreferencesContext from '../../../Preferences/PreferencesContext'; +import AuthenticatedUserContext from '../../../../Profile/AuthenticatedUserContext'; export type LastModifiedInfo = {| lastModifiedByUsername: ?string, @@ -86,6 +92,44 @@ export const getLastModifiedInfoByProjectId = async ({ } }; +export const getStorageProviderByInternalName = ( + storageProviders: Array, + internalName: string +): ?StorageProvider => { + return storageProviders.find( + storageProvider => storageProvider.internalName === internalName + ); +}; + +export const useProjectsListFor = (game: ?Game) => { + const { getRecentProjectFiles } = React.useContext(PreferencesContext); + const authenticatedUser = React.useContext(AuthenticatedUserContext); + + const { cloudProjects } = authenticatedUser; + + let projectFiles: Array = getRecentProjectFiles().filter( + file => !game || (file.fileMetadata && file.fileMetadata.gameId === game.id) + ); + + if (cloudProjects) { + projectFiles = projectFiles.concat( + transformCloudProjectsIntoFileMetadataWithStorageProviderName( + cloudProjects.filter( + cloudProject => !game || cloudProject.gameId === game.id + ) + ) + ); + } + + projectFiles.sort((a, b) => { + if (!a.fileMetadata.lastModifiedDate) return 1; + if (!b.fileMetadata.lastModifiedDate) return -1; + return b.fileMetadata.lastModifiedDate - a.fileMetadata.lastModifiedDate; + }); + + return projectFiles; +}; + export const transformCloudProjectsIntoFileMetadataWithStorageProviderName = ( cloudProjects: Array, ownerId?: string diff --git a/newIDE/app/src/MainFrame/EditorContainers/HomePage/EducationMarketingSection/index.js b/newIDE/app/src/MainFrame/EditorContainers/HomePage/EducationMarketingSection/index.js index 484efa7efc42..f5d7473c14c3 100644 --- a/newIDE/app/src/MainFrame/EditorContainers/HomePage/EducationMarketingSection/index.js +++ b/newIDE/app/src/MainFrame/EditorContainers/HomePage/EducationMarketingSection/index.js @@ -303,7 +303,7 @@ const EducationMarketingSection = ({ {({ i18n }) => ( - + {!isMobile && educationTutorials && ( Promise, + storageProviders: Array, + closeProject: () => Promise, + games: ?Array, onRefreshGames: () => Promise, onGameUpdated: (game: Game) => void, @@ -58,6 +67,11 @@ type Props = {| const ManageSection = ({ project, + currentFileMetadata, + onOpenProject, + storageProviders, + closeProject, + games, onRefreshGames, onGameUpdated, @@ -255,10 +269,13 @@ const ManageSection = ({ ) : ( ) ) : gamesFetchingError ? ( diff --git a/newIDE/app/src/MainFrame/EditorContainers/HomePage/SectionContainer.js b/newIDE/app/src/MainFrame/EditorContainers/HomePage/SectionContainer.js index ca7947aaaadd..01c13bbdb808 100644 --- a/newIDE/app/src/MainFrame/EditorContainers/HomePage/SectionContainer.js +++ b/newIDE/app/src/MainFrame/EditorContainers/HomePage/SectionContainer.js @@ -11,7 +11,7 @@ import { LineStackLayout } from '../../../UI/Layout'; import { AnnouncementsFeed } from '../../../AnnouncementsFeed'; import { AnnouncementsFeedContext } from '../../../AnnouncementsFeed/AnnouncementsFeedContext'; -export const SECTION_PADDING = 30; +export const SECTION_PADDING = 20; const styles = { title: { overflowWrap: 'anywhere', textWrap: 'wrap' }, diff --git a/newIDE/app/src/MainFrame/EditorContainers/HomePage/TeamSection/TeamMemberProjectsView.js b/newIDE/app/src/MainFrame/EditorContainers/HomePage/TeamSection/TeamMemberProjectsView.js index f0bece513815..eb7484e46a05 100644 --- a/newIDE/app/src/MainFrame/EditorContainers/HomePage/TeamSection/TeamMemberProjectsView.js +++ b/newIDE/app/src/MainFrame/EditorContainers/HomePage/TeamSection/TeamMemberProjectsView.js @@ -24,8 +24,8 @@ import Text from '../../../../UI/Text'; import { getProjectLineHeight, transformCloudProjectsIntoFileMetadataWithStorageProviderName, -} from '../BuildSection/utils'; -import ProjectFileListItem from '../BuildSection/ProjectFileListItem'; +} from '../CreateSection/utils'; +import ProjectFileListItem from '../CreateSection/ProjectFileListItem'; import ContextMenu, { type ContextMenuInterface, } from '../../../../UI/Menu/ContextMenu'; @@ -169,7 +169,7 @@ const TeamMemberProjectsView = ({ key={file.fileMetadata.fileIdentifier} isLoading={false} onOpenContextMenu={openContextMenu} - onOpenRecentFile={onOpenRecentFile} + onOpenProject={onOpenRecentFile} storageProviders={storageProviders} isWindowSizeMediumOrLarger={!isMobile} /> diff --git a/newIDE/app/src/MainFrame/EditorContainers/HomePage/index.js b/newIDE/app/src/MainFrame/EditorContainers/HomePage/index.js index e1e69e4d0eaa..d99e79e3790e 100644 --- a/newIDE/app/src/MainFrame/EditorContainers/HomePage/index.js +++ b/newIDE/app/src/MainFrame/EditorContainers/HomePage/index.js @@ -463,6 +463,10 @@ export const HomePage = React.memo( {activeTab === 'manage' && ( ({ ); return ( - - - {title} - + + + + {title} + + {additionalAction && ( <> {additionalAction} @@ -605,7 +608,7 @@ const Carousel = ({ )} - + ); }; diff --git a/newIDE/app/src/UI/ErrorBoundary.js b/newIDE/app/src/UI/ErrorBoundary.js index 51a320155f12..e7396ffe53f1 100644 --- a/newIDE/app/src/UI/ErrorBoundary.js +++ b/newIDE/app/src/UI/ErrorBoundary.js @@ -70,7 +70,8 @@ type ErrorBoundaryScope = | 'project-icons' | 'box-search-result' | 'list-search-result' - | 'custom-object-editor-canvas'; + | 'custom-object-editor-canvas' + | 'project-file-list'; export const getEditorErrorBoundaryProps = ( editorKey: string diff --git a/newIDE/app/src/UI/FlatButtonWithSplitMenu.js b/newIDE/app/src/UI/FlatButtonWithSplitMenu.js index 7fc0b588c096..3bc3e7dc1c2a 100644 --- a/newIDE/app/src/UI/FlatButtonWithSplitMenu.js +++ b/newIDE/app/src/UI/FlatButtonWithSplitMenu.js @@ -25,6 +25,7 @@ type Props = {| margin?: number, flexShrink?: 0, |}, + fullWidth?: boolean, |}; const shouldNeverBeCalled = () => { @@ -39,6 +40,7 @@ const styles = { // Reduce the size forced by Material UI to avoid making the arrow // too big. minWidth: 30, + maxWidth: 30, paddingLeft: 0, paddingRight: 0, }, @@ -58,6 +60,7 @@ const FlatButtonWithSplitMenu = (props: Props) => { primary, icon, disabled, + fullWidth, } = props; // In theory, focus ripple is only shown after a keyboard interaction @@ -74,6 +77,7 @@ const FlatButtonWithSplitMenu = (props: Props) => { disabled={disabled} size="small" style={props.style} + fullWidth={fullWidth} >