Splitting the profile page and adding the language toggle (#9421)

This commit is contained in:
Marvin Frachet 2021-02-19 09:30:13 +01:00 committed by GitHub
parent 8695853ea0
commit 28f8182bdf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 353 additions and 32 deletions

View File

@ -31,7 +31,7 @@ import Header from '../../components/Header/index';
import NavTopRightWrapper from '../../components/NavTopRightWrapper';
import LeftMenu from '../LeftMenu';
import InstalledPluginsPage from '../InstalledPluginsPage';
import LocaleToggle from '../LocaleToggle';
import HomePage from '../HomePage';
import MarketplacePage from '../MarketplacePage';
import NotFoundPage from '../NotFoundPage';
@ -292,7 +292,6 @@ export class Admin extends React.Component {
<NavTopRightWrapper>
{/* Injection zone not ready yet */}
<Logout />
<LocaleToggle isLogged />
</NavTopRightWrapper>
<div className="adminPageRightWrapper">
<Header />

View File

@ -0,0 +1,15 @@
import { useSelector, useDispatch } from 'react-redux';
import { changeLocale } from '../actions';
const languageSelector = state => state.get('language').toJS();
const useLanguages = () => {
const { locale } = useSelector(languageSelector);
const dispatch = useDispatch();
const selectLanguage = nextLocale => dispatch(changeLocale(nextLocale));
return { currentLanguage: locale, selectLanguage };
};
export default useLanguages;

View File

@ -0,0 +1,11 @@
import styled from 'styled-components';
import { Label, Text } from '@buffetjs/core';
export const Title = styled(Text)`
text-transform: uppercase;
color: ${({ theme }) => theme.main.colors.grey};
`;
export const ProfilePageLabel = styled(Label)`
margin-bottom: 1rem;
`;

View File

@ -1,22 +1,32 @@
import React, { useMemo } from 'react';
import { BackHeader, BaselineAlignment, auth } from 'strapi-helper-plugin';
import { BackHeader, BaselineAlignment, auth, Select, Option, Row } from 'strapi-helper-plugin';
import { Padded, Text } from '@buffetjs/core';
import { Col } from 'reactstrap';
import { useHistory } from 'react-router-dom';
import { get } from 'lodash';
import { useIntl } from 'react-intl';
import ContainerFluid from '../../components/ContainerFluid';
import FormBloc from '../../components/FormBloc';
import PageTitle from '../../components/PageTitle';
import SizedInput from '../../components/SizedInput';
import { Header } from '../../components/Settings';
import { useSettingsForm } from '../../hooks';
import { form, schema } from './utils';
import useLanguages from '../LanguageProvider/hooks/useLanguages';
import { languages, languageNativeNames } from '../../i18n';
import { Title, ProfilePageLabel } from './components';
import Bloc from '../../components/Bloc';
const ProfilePage = () => {
const { goBack } = useHistory();
const { currentLanguage, selectLanguage } = useLanguages();
const { formatMessage } = useIntl();
const onSubmitSuccessCb = data => auth.setUserInfo(data);
const [
{ formErrors, initialData, isLoading, modifiedData, showHeaderLoader, showHeaderButtonLoader },
// eslint-disable-next-line no-unused-vars
dispatch,
_,
{ handleCancel, handleChange, handleSubmit },
] = useSettingsForm('/admin/users/me', schema, onSubmitSuccessCb, [
'email',
@ -37,9 +47,13 @@ const ProfilePage = () => {
return (
<>
<PageTitle title="User profile" />
<BackHeader onClick={goBack} />
<BaselineAlignment top size="2px" />
<form onSubmit={handleSubmit}>
<ContainerFluid>
<ContainerFluid padding="18px 30px 0 30px">
<Header
isLoading={showHeaderLoader}
initialData={initialData}
@ -48,22 +62,132 @@ const ProfilePage = () => {
onCancel={handleCancel}
showHeaderButtonLoader={showHeaderButtonLoader}
/>
<BaselineAlignment top size="3px" />
<FormBloc isLoading={isLoading}>
{Object.keys(form).map(key => {
return (
<SizedInput
{...form[key]}
key={key}
error={formErrors[key]}
name={key}
onChange={handleChange}
value={get(modifiedData, key, '')}
/>
);
})}
</FormBloc>
</ContainerFluid>
<BaselineAlignment top size="5px" />
{/* Experience block */}
<Padded size="md" left right bottom>
<Bloc isLoading={isLoading}>
<Padded size="sm" top left right bottom>
<Col>
<Padded size="sm" top bottom>
<Title>
{formatMessage({ id: 'Settings.profile.form.section.profile.title' })}
</Title>
</Padded>
</Col>
<BaselineAlignment top size="9px" />
<Row>
{Object.keys(form).map(key => (
<SizedInput
{...form[key]}
key={key}
error={formErrors[key]}
name={key}
onChange={handleChange}
value={get(modifiedData, key, '')}
/>
))}
</Row>
</Padded>
</Bloc>
</Padded>
<BaselineAlignment top size="13px" />
{/* Password block */}
<Padded size="md" left right bottom>
<Bloc>
<Padded size="sm" top left right bottom>
<Col>
<Padded size="sm" top bottom>
<Title>
{formatMessage({ id: 'Settings.profile.form.section.password.title' })}
</Title>
</Padded>
</Col>
<BaselineAlignment top size="9px" />
<Row>
<SizedInput
label="Auth.form.password.label"
type="password"
autoComplete="new-password"
validations={{}}
error={formErrors.password}
name="password"
onChange={handleChange}
value={get(modifiedData, 'password', '')}
/>
<SizedInput
label="Auth.form.confirmPassword.label"
type="password"
validations={{}}
error={formErrors.confirmPassword}
name="confirmPassword"
onChange={handleChange}
value={get(modifiedData, 'confirmPassword', '')}
/>
</Row>
</Padded>
</Bloc>
</Padded>
<BaselineAlignment top size="13px" />
{/* Interface block */}
<Padded size="md" left right bottom>
<Bloc>
<Padded size="sm" top left right bottom>
<Col>
<Padded size="sm" top bottom>
<Title>
{formatMessage({ id: 'Settings.profile.form.section.experience.title' })}
</Title>
</Padded>
</Col>
<BaselineAlignment top size="7px" />
<div className="col-6">
<ProfilePageLabel htmlFor="">
{formatMessage({
id: 'Settings.profile.form.section.experience.interfaceLanguage',
})}
</ProfilePageLabel>
<Select
aria-labelledby="interface-language"
selectedValue={currentLanguage}
onChange={selectLanguage}
>
{languages.map(language => {
const langName = languageNativeNames[language];
return (
<Option value={language} key={language}>
{langName}
</Option>
);
})}
</Select>
<Padded size="sm" top bottom>
<Text color="grey">
{formatMessage({
id: 'Settings.profile.form.section.experience.interfaceLanguage.hint',
})}
</Text>
</Padded>
</div>
</Padded>
</Bloc>
</Padded>
</form>
</>
);

View File

@ -31,17 +31,6 @@ const form = {
autoComplete: 'no',
validations: {},
},
password: {
label: 'Auth.form.password.label',
type: 'password',
autoComplete: 'new-password',
validations: {},
},
confirmPassword: {
label: 'Auth.form.confirmPassword.label',
type: 'password',
validations: {},
},
};
export default form;

View File

@ -99,7 +99,9 @@ const EditPage = ({ canUpdate }) => {
);
})}
</FormBloc>
<BaselineAlignment top size="2px" />
<Padded top size="md">
{!isLoading && (
<FormBloc

View File

@ -105,6 +105,11 @@
"Settings.permissions.users.listview.header.description.plural": "{number} users found",
"Settings.permissions.users.listview.header.description.singular": "{number} user found",
"Settings.permissions.users.listview.header.title": "Users",
"Settings.profile.form.section.profile.title":"Profile",
"Settings.profile.form.section.password.title": "Change password",
"Settings.profile.form.section.experience.title": "Experience",
"Settings.profile.form.section.experience.interfaceLanguage": "Interface language",
"Settings.profile.form.section.experience.interfaceLanguage.hint": "This will only display your own interface in the chosen language.",
"Settings.roles.create.description": "Define the rights given to the role",
"Settings.roles.create.title": "Create a role",
"Settings.roles.created": "Role created",

View File

@ -0,0 +1,59 @@
import React from 'react';
import ReactSelect, { components } from 'react-select';
import PropTypes from 'prop-types';
import { Carret } from '@buffetjs/icons';
import { useTheme } from 'styled-components';
import getStyles from './styles';
const DropdownIndicator = props => {
const theme = useTheme();
return (
<components.DropdownIndicator {...props}>
<Carret fill={theme.main.colors.grey} />
</components.DropdownIndicator>
);
};
export const Select = ({ children, onChange, selectedValue, ...props }) => {
const theme = useTheme();
const selectStyles = getStyles(theme);
const childrenArray = React.Children.toArray(children);
const options = childrenArray.map(child => ({
value: child.props.value,
label: child.props.children,
}));
const selectedOption = options.find(({ value }) => value === selectedValue);
return (
<ReactSelect
{...props}
options={options}
onChange={({ value }) => onChange(value)}
components={{ DropdownIndicator }}
styles={selectStyles}
value={selectedOption}
/>
);
};
/**
* Do not remove this component.
* The Select component is a mimic of the select HTML element:
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/select
* The Select component will map over its "Option" components and verify their
* "value" in order to pass them down to react-select
*/
export const Option = () => <></>;
Select.defaultProps = {
selectedValue: undefined,
};
Select.propTypes = {
children: PropTypes.node.isRequired,
onChange: PropTypes.func.isRequired,
selectedValue: PropTypes.string,
};

View File

@ -0,0 +1,116 @@
/* eslint-disable indent */
/* eslint-disable no-nested-ternary */
const getStyles = theme => {
const { colors, fontWeights, sizes } = theme.main;
// Colors that does not exist in the theme.main.colors
const unknownLightGrey = `#f6f6f6`;
const unknownGrey = `#aaa`;
const unknownLightblue = `#78caff`;
// Sizes that does not exist in the theme.main.sizes
const unknownBorderSize1 = `1px`;
const optionHeight = `36px`;
const controlMinHeight = `34px`;
return {
container: base => ({
...base,
width: '100%',
}),
control: (base, state) => {
const {
selectProps: { error, value },
} = state;
let border;
let borderBottom;
let backgroundColor;
if (state.isFocused) {
border = `${unknownBorderSize1} solid ${unknownLightblue} !important`;
} else if (error && !value.length) {
border = `${unknownBorderSize1} solid ${colors.lightOrange} !important`;
} else {
border = `${unknownBorderSize1} solid ${colors.border} !important`;
}
if (state.menuIsOpen === true) {
borderBottom = `${unknownBorderSize1} solid ${colors.border} !important`;
}
if (state.isDisabled) {
backgroundColor = `${colors.content.background} !important`;
}
return {
...base,
fontSize: sizes.fonts.md,
minHeight: controlMinHeight,
border,
outline: 0,
boxShadow: 0,
borderRadius: `${sizes.borderRadius} !important`,
borderBottom,
backgroundColor,
width: '100%',
};
},
menu: base => ({
...base,
width: '100%',
margin: '0',
paddingTop: 0,
borderRadius: `${sizes.borderRadius} !important`,
borderTopLeftRadius: '0 !important',
borderTopRightRadius: '0 !important',
border: `${unknownBorderSize1} solid ${unknownLightblue} !important`,
boxShadow: 0,
borderTop: '0 !important',
fontSize: sizes.fonts.md,
}),
menuList: base => ({
...base,
maxHeight: '112px',
paddingTop: sizes.borderRadius,
}),
option: (base, state) => ({
...base,
height: optionHeight,
backgroundColor: state.isFocused ? unknownLightGrey : colors.white,
':active': {
...base[':active'],
backgroundColor: unknownLightGrey,
},
WebkitFontSmoothing: 'antialiased',
color: colors.black,
fontWeight: state.isFocused ? fontWeights.bold : fontWeights.regular,
cursor: 'pointer',
}),
placeholder: base => ({
...base,
marginTop: 0,
color: unknownGrey,
}),
valueContainer: base => ({
...base,
padding: '2px 4px 4px 4px', // These value don't exist in the theme
fontSize: sizes.fonts.md,
}),
indicatorsContainer: base => ({
...base,
width: '32px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
background: colors.content.background,
}),
indicatorSeparator: () => ({
display: 'none',
}),
};
};
export default getStyles;

View File

@ -26,6 +26,7 @@ export { default as IcoContainer } from './components/IcoContainer';
export { default as InputAddon } from './components/InputAddon';
export { default as EmptyState } from './components/EmptyState';
export * from './components/Tabs';
export * from './components/Select';
export { default as InputAddonWithErrors } from './components/InputAddonWithErrors';
export { default as InputCheckbox } from './components/InputCheckbox';