Compare commits

...

14 Commits

Author SHA1 Message Date
AlexandreSi
6efb481505 Prettier 2021-10-06 15:45:13 +02:00
AlexandreSi
24116fd3f3 Open public profile dialog when clicking on user chip 2021-10-06 15:43:51 +02:00
AlexandreSi
9b5633bfee Add public profil provider 2021-10-06 15:33:04 +02:00
AlexandreSi
8808d6c7f0 Set Profile type on dummy user profile in storybook 2021-10-06 14:41:59 +02:00
AlexandreSi
7cf166e7d4 Fix flow error 2021-10-06 14:33:41 +02:00
AlexandreSi
951e98c525 Remove email from user public profile type 2021-10-06 11:49:01 +02:00
AlexandreSi
ca4b517f6d Remove email from user public profile display 2021-10-06 10:53:09 +02:00
AlexandreSi
502876d130 Prettier 2021-10-05 18:31:06 +02:00
AlexandreSi
f9df0cb02a Remove exact props typing for ProfileDetails 2021-10-05 18:16:12 +02:00
AlexandreSi
187be83575 Use generic displayed profile for ProfileDetails component 2021-10-05 18:08:06 +02:00
AlexandreSi
160753c605 Use UserPublicProfileSearch type matching API one 2021-10-05 18:07:20 +02:00
AlexandreSi
aaf9e9c8e0 Adapt ProfileDetails for another user profile 2021-10-05 18:07:20 +02:00
AlexandreSi
9bbbed71f0 Fix typing 2021-10-05 18:05:33 +02:00
AlexandreSi
91fd83dd7e Move profile details story in dedicated file 2021-10-05 18:05:33 +02:00
15 changed files with 263 additions and 94 deletions

View File

@@ -1,5 +1,5 @@
module.exports = {
stories: ['../src/stories/index.js'],
stories: ['../src/stories/index.js', '../src/stories/**/*.stories.js'],
addons: [{
name: '@storybook/addon-essentials',
options: {

View File

@@ -139,7 +139,11 @@ export function ExampleDialog({
{exampleShortHeader.authors && (
<Line>
{exampleShortHeader.authors.map(author => (
<UserPublicProfileChip user={author} key={author.id} />
<UserPublicProfileChip
user={author}
key={author.id}
isClickable
/>
))}
</Line>
)}

View File

@@ -149,7 +149,11 @@ export default class ExtensionInstallDialog extends Component<Props, State> {
<Line>
{extensionShortHeader.authors &&
extensionShortHeader.authors.map(author => (
<UserPublicProfileChip user={author} key={author.id} />
<UserPublicProfileChip
user={author}
key={author.id}
isClickable
/>
))}
</Line>
</Column>

View File

@@ -5,6 +5,7 @@ import { ThemeProvider } from '@material-ui/styles';
import { StylesProvider, jssPreset } from '@material-ui/core/styles';
import { getTheme } from '../UI/Theme';
import AuthenticatedUserProvider from '../Profile/AuthenticatedUserProvider';
import PublicProfileProvider from '../Profile/PublicProfileProvider';
import Authentication from '../Utils/GDevelopServices/Authentication';
import PreferencesProvider from './Preferences/PreferencesProvider';
import PreferencesContext from './Preferences/PreferencesContext';
@@ -85,40 +86,42 @@ export default class Providers extends React.Component<Props, {||}> {
<AuthenticatedUserProvider
authentication={authentication}
>
<I18n update>
{({ i18n }) => (
<EventsFunctionsExtensionsProvider
i18n={i18n}
makeEventsFunctionCodeWriter={
makeEventsFunctionCodeWriter
}
eventsFunctionsExtensionWriter={
eventsFunctionsExtensionWriter
}
eventsFunctionsExtensionOpener={
eventsFunctionsExtensionOpener
}
>
<CommandsContextProvider>
<AssetStoreStateProvider>
<ResourceStoreStateProvider>
<ExampleStoreStateProvider>
<ExtensionStoreStateProvider>
<GamesShowcaseStateProvider>
<ResourceFetcherContext.Provider
value={resourceFetcher}
>
{children({ i18n })}
</ResourceFetcherContext.Provider>
</GamesShowcaseStateProvider>
</ExtensionStoreStateProvider>
</ExampleStoreStateProvider>
</ResourceStoreStateProvider>
</AssetStoreStateProvider>
</CommandsContextProvider>
</EventsFunctionsExtensionsProvider>
)}
</I18n>
<PublicProfileProvider>
<I18n update>
{({ i18n }) => (
<EventsFunctionsExtensionsProvider
i18n={i18n}
makeEventsFunctionCodeWriter={
makeEventsFunctionCodeWriter
}
eventsFunctionsExtensionWriter={
eventsFunctionsExtensionWriter
}
eventsFunctionsExtensionOpener={
eventsFunctionsExtensionOpener
}
>
<CommandsContextProvider>
<AssetStoreStateProvider>
<ResourceStoreStateProvider>
<ExampleStoreStateProvider>
<ExtensionStoreStateProvider>
<GamesShowcaseStateProvider>
<ResourceFetcherContext.Provider
value={resourceFetcher}
>
{children({ i18n })}
</ResourceFetcherContext.Provider>
</GamesShowcaseStateProvider>
</ExtensionStoreStateProvider>
</ExampleStoreStateProvider>
</ResourceStoreStateProvider>
</AssetStoreStateProvider>
</CommandsContextProvider>
</EventsFunctionsExtensionsProvider>
)}
</I18n>
</PublicProfileProvider>
</AuthenticatedUserProvider>
</ThemeProvider>
</StylesProvider>

View File

@@ -4,7 +4,6 @@ import { Trans, t } from '@lingui/macro';
import * as React from 'react';
import Avatar from '@material-ui/core/Avatar';
import { Column, Line, Spacer } from '../UI/Grid';
import { type Profile } from '../Utils/GDevelopServices/Authentication';
import PlaceholderLoader from '../UI/PlaceholderLoader';
import { getGravatarUrl } from '../UI/GravatarUrl';
import Text from '../UI/Text';
@@ -12,12 +11,19 @@ import RaisedButton from '../UI/RaisedButton';
import TextField from '../UI/TextField';
import { I18n } from '@lingui/react';
type Props = {|
profile: ?Profile,
onEditProfile: Function,
|};
type DisplayedProfile = {
+email?: string,
description: ?string,
username: ?string,
};
export default ({ profile, onEditProfile }: Props) => {
type Props = {
profile: ?DisplayedProfile,
onEditProfile?: Function,
isPrivate?: boolean,
};
export default ({ profile, onEditProfile, isPrivate }: Props) => {
return profile ? (
<I18n>
{({ i18n }) => (
@@ -32,18 +38,22 @@ export default ({ profile, onEditProfile }: Props) => {
}}
>
{profile.username ||
i18n._(t`Edit your profile to pick a username!`)}
(isPrivate
? i18n._(t`Edit your profile to pick a username!`)
: i18n._(t`No username`))}
</Text>
</Line>
<Line>
<TextField
value={profile.email}
readOnly
fullWidth
floatingLabelText={<Trans>Email</Trans>}
floatingLabelFixed={true}
/>
</Line>
{isPrivate && profile.email && (
<Line>
<TextField
value={profile.email}
readOnly
fullWidth
floatingLabelText={<Trans>Email</Trans>}
floatingLabelFixed={true}
/>
</Line>
)}
<Line>
<TextField
value={profile.description || ''}
@@ -52,18 +62,24 @@ export default ({ profile, onEditProfile }: Props) => {
multiline
floatingLabelText={<Trans>Bio</Trans>}
floatingLabelFixed={true}
hintText={t`No bio defined. Edit your profile to tell us what you are using GDevelop for!`}
hintText={
isPrivate
? t`No bio defined. Edit your profile to tell us what you are using GDevelop for!`
: t`No bio defined`
}
rows={3}
rowsMax={5}
/>
</Line>
<Line justifyContent="flex-end">
<RaisedButton
label={<Trans>Edit my profile</Trans>}
primary
onClick={onEditProfile}
/>
</Line>
{isPrivate && (
<Line justifyContent="flex-end">
<RaisedButton
label={<Trans>Edit my profile</Trans>}
primary
onClick={onEditProfile}
/>
</Line>
)}
</Column>
)}
</I18n>

View File

@@ -0,0 +1,40 @@
// @flow
import { Trans } from '@lingui/macro';
import React from 'react';
import Dialog from '../UI/Dialog';
import FlatButton from '../UI/FlatButton';
import { getUserPublicProfile } from '../Utils/GDevelopServices/User';
import ProfileDetails from './ProfileDetails';
type Props = {|
userId: string,
onClose: () => void,
|};
export default ({ userId, onClose }: Props) => {
const [profile, setProfile] = React.useState(null);
React.useEffect(
() => {
setProfile(null);
getUserPublicProfile(userId).then(profile => setProfile(profile));
},
[userId]
);
return (
<Dialog
open={true}
actions={[
<FlatButton
key="close"
label={<Trans>Close</Trans>}
primary={false}
onClick={onClose}
/>,
]}
>
<ProfileDetails profile={profile} />
</Dialog>
);
};

View File

@@ -0,0 +1,12 @@
// @flow
import * as React from 'react';
export type PublicProfileOpenerType = string => void;
export const initialPublicProfileOpener = (userId: string) => {};
const PublicProfileContext = React.createContext<PublicProfileOpenerType>(
initialPublicProfileOpener
);
export default PublicProfileContext;

View File

@@ -0,0 +1,36 @@
// @flow
import * as React from 'react';
import PublicProfile from './PublicProfile';
import PublicProfileDataContext from './PublicProfileContext';
type Props = {|
children: React.Node,
|};
export default ({ children }: Props) => {
const [open, setOpen] = React.useState(false);
const [userId, setUserId] = React.useState(null);
const setUserAndOpenProfile = (userId: string): void => {
setUserId(userId);
setOpen(true);
};
const closeProfile = (): void => {
setUserId(null);
setOpen(false);
};
return (
<React.Fragment>
<PublicProfileDataContext.Provider value={setUserAndOpenProfile}>
{children}
</PublicProfileDataContext.Provider>
{open && userId && (
<PublicProfile userId={userId} onClose={closeProfile} />
)}
</React.Fragment>
);
};

View File

@@ -2,7 +2,10 @@
import * as React from 'react';
import Chip from '@material-ui/core/Chip';
import FaceIcon from '@material-ui/icons/Face';
import { type UserPublicProfile } from '../Utils/GDevelopServices/User';
import { type UserPublicProfileSearch } from '../Utils/GDevelopServices/User';
import PublicProfileContext, {
type PublicProfileOpenerType,
} from '../Profile/PublicProfileContext';
const styles = {
chip: {
@@ -11,17 +14,23 @@ const styles = {
};
type Props = {|
user: UserPublicProfile,
user: UserPublicProfileSearch,
isClickable?: boolean,
|};
export const UserPublicProfileChip = ({ user }: Props) => {
export const UserPublicProfileChip = ({ user, isClickable = false }: Props) => {
return (
<Chip
icon={<FaceIcon />}
size="small"
style={styles.chip}
label={user.username}
key={user.username}
/>
<PublicProfileContext.Consumer>
{(openPublicProfile: PublicProfileOpenerType) => (
<Chip
icon={<FaceIcon />}
size="small"
style={styles.chip}
label={user.username}
key={user.username}
onClick={isClickable ? () => openPublicProfile(user.id) : null}
/>
)}
</PublicProfileContext.Consumer>
);
};

View File

@@ -2,7 +2,7 @@
import axios from 'axios';
import { GDevelopAssetApi } from './ApiConfigs';
import { type Filters } from './Filters';
import { type UserPublicProfile } from './User';
import { type UserPublicProfileSearch } from './User';
export type ExampleShortHeader = {|
id: string,
@@ -10,8 +10,8 @@ export type ExampleShortHeader = {|
shortDescription: string,
license: string,
tags: Array<string>,
authors?: Array<UserPublicProfile>,
authorIds?: Array<UserPublicProfile>,
authors?: Array<UserPublicProfileSearch>,
authorIds?: Array<UserPublicProfileSearch>,
previewImageUrls: Array<string>,
gdevelopVersion: string,
|};

View File

@@ -2,11 +2,11 @@
import axios from 'axios';
import { GDevelopAssetApi } from './ApiConfigs';
import semverSatisfies from 'semver/functions/satisfies';
import { type UserPublicProfile } from './User';
import { type UserPublicProfileSearch } from './User';
export type ExtensionShortHeader = {|
shortDescription: string,
authors?: Array<UserPublicProfile>,
authors?: Array<UserPublicProfileSearch>,
extensionNamespace: string,
fullName: string,
name: string,

View File

@@ -4,7 +4,13 @@ import { GDevelopUserApi } from './ApiConfigs';
export type UserPublicProfile = {|
id: string,
username?: string,
username: ?string,
description: ?string,
|};
export type UserPublicProfileSearch = {|
id: string,
username: ?string,
|};
export type UserPublicProfileByIds = {|
@@ -13,7 +19,7 @@ export type UserPublicProfileByIds = {|
export const searchUserPublicProfilesByUsername = (
searchString: string
): Promise<Array<UserPublicProfile>> => {
): Promise<Array<UserPublicProfileSearch>> => {
return axios
.get(`${GDevelopUserApi.baseUrl}/user-public-profile/search`, {
params: {

View File

@@ -7,6 +7,7 @@ import SemiControlledMultiAutoComplete from '../UI/SemiControlledMultiAutoComple
import {
searchUserPublicProfilesByUsername,
type UserPublicProfile,
type UserPublicProfileSearch,
getUserPublicProfilesByIds,
} from './GDevelopServices/User';
@@ -35,7 +36,7 @@ export const UsersAutocomplete = (props: Props) => {
const [
completionUserPublicProfiles,
setCompletionUserPublicProfiles,
] = React.useState<Array<UserPublicProfile>>([]);
] = React.useState<Array<UserPublicProfileSearch>>([]);
const [error, setError] = React.useState(null);
// Recalculate if the userInput has changed.
@@ -132,7 +133,7 @@ export const UsersAutocomplete = (props: Props) => {
setUserInput(value);
}}
dataSource={completionUserPublicProfiles
.map((userPublicProfile: UserPublicProfile) => {
.map((userPublicProfile: UserPublicProfileSearch) => {
if (userPublicProfile.username && userPublicProfile.id) {
return {
text: userPublicProfile.username,

View File

@@ -0,0 +1,52 @@
// @flow
import * as React from 'react';
import { action } from '@storybook/addon-actions';
import muiDecorator from '../ThemeDecorator';
import paperDecorator from '../PaperDecorator';
import ProfileDetails from '../../Profile/ProfileDetails';
import { indieUserProfile } from '../../fixtures/GDevelopServicesTestData';
import { type Profile as ProfileType } from '../../Utils/GDevelopServices/Authentication';
const indieUserWithoutUsernameNorDescriptionProfile: ProfileType = {
...indieUserProfile,
username: null,
description: null,
};
export default {
title: 'ProfileDetails',
component: ProfileDetails,
decorators: [paperDecorator, muiDecorator],
argTypes: {
profile: {
control: { type: 'radio' },
options: ['Complete profile', 'Without username nor bio'],
defaultValue: 'Complete profile',
mapping: {
'Complete profile': indieUserProfile,
'Without username nor bio': indieUserWithoutUsernameNorDescriptionProfile,
},
},
onEditProfile: { action: 'edit profile' },
},
};
type ArgsTypes = {|
profile: ProfileType,
onEditProfile: () => void,
|};
export const MyProfile = (args: ArgsTypes) => (
<ProfileDetails {...args} isPrivate={true} />
);
export const OtherUserProfile = (args: ArgsTypes) => (
<ProfileDetails {...args} />
);
export const Loading = (args: ArgsTypes) => (
<ProfileDetails {...args} profile={null} />
);
Loading.argTypes = {
profile: { control: { disable: true } },
};

View File

@@ -86,7 +86,6 @@ import {
noSubscription,
usagesForIndieUser,
indieFirebaseUser,
indieUserProfile,
fakeNoSubscriptionAuthenticatedUser,
fakeIndieAuthenticatedUser,
fakeNotAuthenticatedAuthenticatedUser,
@@ -3871,19 +3870,6 @@ storiesOf('LimitDisplayer', module)
/>
));
storiesOf('ProfileDetails', module)
.addDecorator(paperDecorator)
.addDecorator(muiDecorator)
.add('profile', () => (
<ProfileDetails
profile={indieUserProfile}
onEditProfile={action('edit profile')}
/>
))
.add('loading', () => (
<ProfileDetails profile={null} onEditProfile={action('edit profile')} />
));
storiesOf('SubscriptionDetails', module)
.addDecorator(paperDecorator)
.addDecorator(muiDecorator)