mirror of
https://github.com/strapi/strapi.git
synced 2025-12-03 10:32:10 +00:00
Splitting the profile page and adding the language toggle (#9421)
This commit is contained in:
parent
8695853ea0
commit
28f8182bdf
@ -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 />
|
||||
|
||||
@ -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;
|
||||
@ -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;
|
||||
`;
|
||||
@ -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>
|
||||
</>
|
||||
);
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -99,7 +99,9 @@ const EditPage = ({ canUpdate }) => {
|
||||
);
|
||||
})}
|
||||
</FormBloc>
|
||||
|
||||
<BaselineAlignment top size="2px" />
|
||||
|
||||
<Padded top size="md">
|
||||
{!isLoading && (
|
||||
<FormBloc
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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,
|
||||
};
|
||||
@ -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;
|
||||
@ -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';
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user