Improve communication around cloud projects

Do not show in changelog
This commit is contained in:
AlexandreS
2022-08-03 15:42:20 +02:00
committed by GitHub
parent 3e246e3cf0
commit 1787b3c5c8
17 changed files with 263 additions and 101 deletions

View File

@@ -5,6 +5,7 @@ import {
type ResourceSource,
type ChooseResourceFunction,
} from '../../ResourcesList/ResourceSource';
import type { StorageProvider } from '../../ProjectsStorage';
import { type PreviewDebuggerServer } from '../../Export/PreviewLauncher.flow';
import { type HotReloadPreviewButtonProps } from '../../HotReload/HotReloadPreviewButton';
import { type ResourceExternalEditor } from '../../ResourcesList/ResourceExternalEditor.flow';
@@ -16,8 +17,12 @@ import {
import { type FileMetadataAndStorageProviderName } from '../../ProjectsStorage';
export type EditorContainerExtraProps = {|
// Events function extension editor
initiallyFocusedFunctionName?: ?string,
initiallyFocusedBehaviorName?: ?string,
// Homepage
storageProviders?: Array<StorageProvider>,
|};
export type RenderEditorContainerProps = {|

View File

@@ -3,19 +3,25 @@ import * as React from 'react';
import { Trans, t } from '@lingui/macro';
import { I18n } from '@lingui/react';
import { type I18n as I18nType } from '@lingui/core';
import { makeStyles } from '@material-ui/core/styles';
import List from '@material-ui/core/List';
import ListItem from '@material-ui/core/ListItem';
import ListItemText from '@material-ui/core/ListItemText';
import ListItemAvatar from '@material-ui/core/ListItemAvatar';
import AddIcon from '@material-ui/icons/Add';
import Text from '../../../UI/Text';
import TextButton from '../../../UI/TextButton';
import RaisedButton from '../../../UI/RaisedButton';
import AlertMessage from '../../../UI/AlertMessage';
import { Line, Column, Spacer } from '../../../UI/Grid';
import { useResponsiveWindowWidth } from '../../../UI/Reponsive/ResponsiveWindowMeasurer';
import { LineStackLayout, ResponsiveLineStackLayout } from '../../../UI/Layout';
import { type FileMetadataAndStorageProviderName } from '../../../ProjectsStorage';
import {
type FileMetadataAndStorageProviderName,
type StorageProvider,
} from '../../../ProjectsStorage';
import PreferencesContext from '../../Preferences/PreferencesContext';
import AuthenticatedUserContext from '../../../Profile/AuthenticatedUserContext';
import SectionContainer, { SectionRow } from './SectionContainer';
@@ -25,7 +31,10 @@ import ContextMenu, {
import CircularProgress from '../../../UI/CircularProgress';
import { type MenuItemTemplate } from '../../../UI/Menu/Menu.flow';
import useConfirmDialog from '../../../UI/Confirm/useConfirmDialog';
import { deleteCloudProject } from '../../../Utils/GDevelopServices/Project';
import {
CLOUD_PROJECT_MAX_COUNT,
deleteCloudProject,
} from '../../../Utils/GDevelopServices/Project';
import optionalRequire from '../../../Utils/OptionalRequire';
import { showErrorBox } from '../../../UI/Messages/MessageBox';
import { getRelativeOrAbsoluteDisplayDate } from '../../../Utils/DateDisplay';
@@ -51,6 +60,7 @@ type Props = {|
onOpen: () => void,
onOpenRecentFile: (file: FileMetadataAndStorageProviderName) => void,
onCreateProject: () => void,
storageProviders: Array<StorageProvider>,
|};
export type BuildSectionInterface = {|
@@ -71,8 +81,33 @@ const PrettyBreakablePath = ({ path }: {| path: string |}) => {
}, []);
};
const getStorageProviderByInternalName = (
storageProviders: Array<StorageProvider>,
internalName: string
): ?StorageProvider => {
return storageProviders.find(
storageProvider => storageProvider.internalName === internalName
);
};
const useStylesForListItemIcon = makeStyles({
root: {
minWidth: 0,
},
});
const BuildSection = React.forwardRef<Props, BuildSectionInterface>(
({ project, canOpen, onOpen, onCreateProject, onOpenRecentFile }, ref) => {
(
{
project,
canOpen,
onOpen,
onCreateProject,
onOpenRecentFile,
storageProviders,
},
ref
) => {
const { getRecentProjectFiles, removeRecentProjectFile } = React.useContext(
PreferencesContext
);
@@ -83,6 +118,8 @@ const BuildSection = React.forwardRef<Props, BuildSectionInterface>(
const windowWidth = useResponsiveWindowWidth();
const forceUpdate = useForceUpdate();
const iconClasses = useStylesForListItemIcon();
React.useImperativeHandle(ref, () => ({
forceUpdate,
}));
@@ -91,6 +128,8 @@ const BuildSection = React.forwardRef<Props, BuildSectionInterface>(
file => file.fileMetadata
);
let hasTooManyCloudProjects = false;
// Show cloud projects on the web app only.
if (isWebApp) {
const { cloudProjects } = authenticatedUser;
@@ -112,6 +151,9 @@ const BuildSection = React.forwardRef<Props, BuildSectionInterface>(
})
.filter(Boolean)
);
hasTooManyCloudProjects =
cloudProjects.filter(cloudProject => !cloudProject.deletedAt)
.length >= CLOUD_PROJECT_MAX_COUNT;
}
}
@@ -213,7 +255,33 @@ const BuildSection = React.forwardRef<Props, BuildSectionInterface>(
<I18n>
{({ i18n }) => (
<>
<SectionContainer title={<Trans>My projects</Trans>}>
<SectionContainer
title={<Trans>My projects</Trans>}
renderFooter={() =>
isWebApp &&
hasTooManyCloudProjects && (
<Line>
<Column expand>
<AlertMessage kind="warning">
<Text size="block-title">
<Trans>
You've reached your maximum storage of{' '}
{CLOUD_PROJECT_MAX_COUNT}
cloud-based projects
</Trans>
</Text>
<Text>
<Trans>
To keep using GDevelop cloud, consider deleting one
of your projects.
</Trans>
</Text>
</AlertMessage>
</Column>
</Line>
)
}
>
<SectionRow>
<Line noMargin>
<ResponsiveLineStackLayout
@@ -272,6 +340,11 @@ const BuildSection = React.forwardRef<Props, BuildSectionInterface>(
<Trans>File name</Trans>
</Text>
</Column>
<Column expand>
<Text color="secondary">
<Trans>Location</Trans>
</Text>
</Column>
<Column expand>
<Text color="secondary">
<Trans>Last edited</Trans>
@@ -280,93 +353,136 @@ const BuildSection = React.forwardRef<Props, BuildSectionInterface>(
</LineStackLayout>
)}
<List>
{projectFiles.map(file => (
<ListItem
button
key={file.fileMetadata.fileIdentifier}
onClick={() => {
onOpenRecentFile(file);
}}
style={styles.listItem}
onContextMenu={event =>
openContextMenu(event, file)
}
>
{isWindowWidthMediumOrLarger ? (
<LineStackLayout
justifyContent="flex-start"
expand
>
<Column expand>
<Line noMargin alignItems="center">
<Text noMargin>
{file.fileMetadata.name || (
<PrettyBreakablePath
path={
file.fileMetadata.fileIdentifier
}
/>
)}
</Text>
{pendingProject ===
file.fileMetadata.fileIdentifier && (
<>
<Spacer />
<CircularProgress size={16} />
</>
)}
</Line>
</Column>
<Column expand>
{file.fileMetadata.lastModifiedDate && (
<Text noMargin>
{getRelativeOrAbsoluteDisplayDate(
i18n,
file.fileMetadata.lastModifiedDate
)}
</Text>
{projectFiles.map(file => {
const storageProvider = getStorageProviderByInternalName(
storageProviders,
file.storageProviderName
);
return (
<ListItem
button
key={file.fileMetadata.fileIdentifier}
onClick={() => {
onOpenRecentFile(file);
}}
style={styles.listItem}
onContextMenu={event =>
openContextMenu(event, file)
}
>
<>
{storageProvider &&
storageProvider.renderIcon &&
!isWindowWidthMediumOrLarger && (
<ListItemAvatar
classes={iconClasses}
style={{
marginTop: 8,
alignSelf: 'flex-start',
}}
>
{storageProvider.renderIcon({
size: 'small',
})}
</ListItemAvatar>
)}
</Column>
</LineStackLayout>
) : (
<Column expand>
<Line
noMargin
alignItems="center"
justifyContent="space-between"
>
<ListItemText
primary={
file.fileMetadata.name || (
<PrettyBreakablePath
path={
file.fileMetadata.fileIdentifier
}
/>
)
}
secondary={
file.fileMetadata.lastModifiedDate
? getRelativeOrAbsoluteDisplayDate(
{isWindowWidthMediumOrLarger ? (
<LineStackLayout
justifyContent="flex-start"
expand
>
<Column expand>
<Line noMargin alignItems="center">
{storageProvider &&
storageProvider.renderIcon && (
<>
{storageProvider.renderIcon({
size: 'small',
})}
<Spacer />
</>
)}
<Text noMargin>
{file.fileMetadata.name || (
<PrettyBreakablePath
path={
file.fileMetadata
.fileIdentifier
}
/>
)}
</Text>
{pendingProject ===
file.fileMetadata
.fileIdentifier && (
<>
<Spacer />
<CircularProgress size={16} />
</>
)}
</Line>
</Column>
<Column expand>
<Text noMargin>
{storageProvider
? i18n._(storageProvider.name)
: ''}
</Text>
</Column>
<Column expand>
{file.fileMetadata.lastModifiedDate && (
<Text noMargin>
{getRelativeOrAbsoluteDisplayDate(
i18n,
file.fileMetadata.lastModifiedDate
)}
</Text>
)}
</Column>
</LineStackLayout>
) : (
<Column expand>
<Line
noMargin
alignItems="center"
justifyContent="space-between"
>
<ListItemText
primary={
file.fileMetadata.name || (
<PrettyBreakablePath
path={
file.fileMetadata
.fileIdentifier
}
/>
)
: undefined
}
onContextMenu={event =>
openContextMenu(event, file)
}
/>
{pendingProject ===
file.fileMetadata.fileIdentifier && (
<CircularProgress size={24} />
)}
</Line>
</Column>
)}
</ListItem>
))}
}
secondary={
file.fileMetadata.lastModifiedDate
? getRelativeOrAbsoluteDisplayDate(
i18n,
file.fileMetadata
.lastModifiedDate
)
: undefined
}
onContextMenu={event =>
openContextMenu(event, file)
}
/>
{pendingProject ===
file.fileMetadata.fileIdentifier && (
<CircularProgress size={24} />
)}
</Line>
</Column>
)}
</>
</ListItem>
);
})}
</List>
</Column>
</Line>

View File

@@ -20,6 +20,12 @@ const styles = {
paddingLeft: SECTION_PADDING,
paddingRight: SECTION_PADDING,
},
mobileFooter: {
padding: 5,
},
desktopFooter: {
paddingLeft: SECTION_PADDING,
},
rowContainer: {
display: 'flex',
flexDirection: 'column',
@@ -37,6 +43,7 @@ type Props = {|
subtitle?: React.Node,
backAction?: () => void,
flexBody?: boolean,
renderFooter?: () => React.Node,
|};
const SectionContainer = ({
@@ -45,12 +52,14 @@ const SectionContainer = ({
subtitle,
backAction,
flexBody,
renderFooter,
}: Props) => {
const windowWidth = useResponsiveWindowWidth();
const GDevelopTheme = React.useContext(GDevelopThemeContext);
return (
<Column useFullHeight noMargin expand>
<Paper
elevation={0}
style={{
...styles.scrollContainer,
display: flexBody ? 'flex' : 'block',
@@ -86,6 +95,20 @@ const SectionContainer = ({
{children}
</Column>
</Paper>
{renderFooter && (
<Paper
elevation={0}
style={{
borderLeft: `1px solid ${GDevelopTheme.home.separator.color}`,
...(windowWidth === 'small'
? styles.mobileFooter
: styles.desktopFooter),
}}
square
>
{renderFooter()}
</Paper>
)}
</Column>
);
};

View File

@@ -8,7 +8,10 @@ import {
type OnCreateBlankFunction,
type OnOpenProjectAfterCreationFunction,
} from '../../../ProjectCreation/CreateProjectDialog';
import { type FileMetadataAndStorageProviderName } from '../../../ProjectsStorage';
import {
type FileMetadataAndStorageProviderName,
type StorageProvider,
} from '../../../ProjectsStorage';
import GetStartedSection from './GetStartedSection';
import BuildSection, { type BuildSectionInterface } from './BuildSection';
import LearnSection from './LearnSection';
@@ -29,6 +32,7 @@ type Props = {|
projectItemName: ?string,
project: ?gdProject,
setToolbar: (?React.Node) => void,
storageProviders: Array<StorageProvider>,
// Project opening
canOpen: boolean,
@@ -74,6 +78,7 @@ export const HomePage = React.memo<Props>(
setToolbar,
onOpenOnboardingDialog,
isActive,
storageProviders,
}: Props,
ref
) => {
@@ -179,6 +184,7 @@ export const HomePage = React.memo<Props>(
onOpen={onOpen}
onCreateProject={onCreateProject}
onOpenRecentFile={onOpenRecentFile}
storageProviders={storageProviders}
/>
)}
{activeTab === 'learn' && (
@@ -225,5 +231,8 @@ export const renderHomePageContainer = (
onOpenLanguageDialog={props.onOpenLanguageDialog}
onOpenProfile={props.onOpenProfile}
onOpenOnboardingDialog={props.onOpenOnboardingDialog}
storageProviders={
(props.extraEditorProps && props.extraEditorProps.storageProviders) || []
}
/>
);

View File

@@ -1468,11 +1468,14 @@ const MainFrame = (props: Props) => {
projectItemName: null,
renderEditorContainer: renderHomePageContainer,
key: 'start page',
extraEditorProps: {
storageProviders: props.storageProviders,
},
closable: false,
}),
}));
},
[setState, i18n]
[setState, i18n, props.storageProviders]
);
const _openDebugger = React.useCallback(

View File

@@ -18,8 +18,8 @@ import Cloud from '../../UI/CustomSvgIcons/Cloud';
export default ({
internalName: 'Cloud',
name: t`GDevelop cloud storage`,
renderIcon: () => <Cloud />,
name: t`GDevelop Cloud`,
renderIcon: props => <Cloud fontSize={props.size} />,
hiddenInOpenDialog: true,
needUserAuthentication: true,
getFileMetadataFromAppArguments: (appArguments: AppArguments) => {

View File

@@ -12,7 +12,7 @@ import SaveAlt from '@material-ui/icons/SaveAlt';
export default ({
internalName: 'DownloadFile',
name: t`Download a copy`,
renderIcon: () => <SaveAlt />,
renderIcon: props => <SaveAlt fontSize={props.size} />,
hiddenInOpenDialog: true,
createOperations: ({ setDialog, closeDialog }) => ({
onSaveProjectAs: (project: gdProject, fileMetadata: ?FileMetadata) => {

View File

@@ -56,7 +56,7 @@ export default ({
internalName: 'Dropbox',
name: t`Dropbox (coming soon)`,
disabled: true,
renderIcon: () => <Dropbox />,
renderIcon: props => <Dropbox fontSize={props.size} />,
createOperations: ({ setDialog, closeDialog }) => {
initializeApis().catch(() => {});

View File

@@ -11,7 +11,7 @@ const FakeCloudStorageProvider = ({
internalName: 'FakeCloud',
name: t`GDevelop cloud storage (coming soon)`,
disabled: true,
renderIcon: () => <Cloud />,
renderIcon: props => <Cloud fontSize={props.size} />,
createOperations: () => {
return {
doesInitialOpenRequireUserInteraction: true,

View File

@@ -276,7 +276,7 @@ const showFilePicker = ({
export default ({
internalName: 'GoogleDrive',
name: t`Google Drive`,
renderIcon: () => <GoogleDrive />,
renderIcon: props => <GoogleDrive fontSize={props.size} />,
getFileMetadataFromAppArguments: (appArguments: AppArguments) => {
if (appArguments.state) {
try {

View File

@@ -28,7 +28,7 @@ import Computer from '../../UI/CustomSvgIcons/Computer';
export default ({
internalName: 'LocalFile',
name: t`Your computer`,
renderIcon: () => <Computer />,
renderIcon: props => <Computer fontSize={props.size} />,
getFileMetadataFromAppArguments: (appArguments: AppArguments) => {
if (!appArguments[POSITIONAL_ARGUMENTS_KEY]) return null;
if (!appArguments[POSITIONAL_ARGUMENTS_KEY].length) return null;

View File

@@ -56,7 +56,7 @@ export default ({
internalName: 'OneDrive',
name: t`OneDrive (coming soon)`,
disabled: true,
renderIcon: () => <OneDrive />,
renderIcon: props => <OneDrive fontSize={props.size} />,
createOperations: ({ setDialog, closeDialog }) => {
initializeApis().catch(() => {});

View File

@@ -50,7 +50,7 @@ const OpenFromStorageProviderDialog = ({
primaryText={i18n._(storageProvider.name)}
leftIcon={
storageProvider.renderIcon
? storageProvider.renderIcon()
? storageProvider.renderIcon({})
: undefined
}
onClick={() => onChooseProvider(storageProvider)}

View File

@@ -60,12 +60,15 @@ const StorageProviderListItem = ({
>
<ListItemIcon>
{storageProvider.renderIcon
? storageProvider.renderIcon()
? storageProvider.renderIcon({})
: undefined}
</ListItemIcon>
<ListItemText>
<Line justifyContent="space-between" alignItems="center">
<Text noMargin>{i18n._(storageProvider.name)}</Text>
<Text noMargin>
{i18n._(storageProvider.name)}{' '}
{storageProvider.internalName === 'Cloud' && i18n._(t`(Beta)`)}
</Text>
{shouldDisplayAuthenticationButtons && (
<Line noMargin>
<FlatButton

View File

@@ -85,7 +85,7 @@ export type StorageProvider = {|
hiddenInOpenDialog?: boolean,
hiddenInSaveDialog?: boolean,
disabled?: boolean,
renderIcon?: () => React.Node,
renderIcon?: ({| size?: 'small' | 'medium' |}) => React.Node,
getFileMetadataFromAppArguments?: AppArguments => ?FileMetadata,
createOperations: ({
/** Open a dialog (a render function) */

View File

@@ -7,6 +7,7 @@ import {
import { type AuthenticatedUser } from '../../Profile/AuthenticatedUserContext';
export const CLOUD_PROJECT_NAME_MAX_LENGTH = 50;
export const CLOUD_PROJECT_MAX_COUNT = 10;
const projectResourcesClient = axios.create({
baseURL: GDevelopProjectResourcesStorage.baseUrl,

View File

@@ -18,6 +18,7 @@ import AuthenticatedUserContext, {
initialAuthenticatedUser,
type AuthenticatedUser,
} from '../../Profile/AuthenticatedUserContext';
import CloudStorageProvider from '../../ProjectsStorage/CloudStorageProvider';
import {
fakeIndieAuthenticatedUser,
indieUserProfile,
@@ -83,6 +84,7 @@ const WrappedHomePage = ({
projectItemName={null}
setToolbar={() => {}}
canOpen={true}
storageProviders={[CloudStorageProvider]}
onOpen={() => action('onOpen')()}
onOpenRecentFile={() => action('onOpenRecentFile')()}
onCreateProject={() => action('onCreateProject')()}