mirror of
https://github.com/strapi/strapi.git
synced 2025-08-06 15:53:11 +00:00
[I18N] add tabs for navigating between forms (#9364)
This commit is contained in:
parent
9fd676e62c
commit
6c23b08920
@ -0,0 +1,37 @@
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { Flex, Text } from '@buffetjs/core';
|
||||
|
||||
export const TabNavRaw = styled(props => <Flex flexDirection="column" {...props} />)`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export const TabsRaw = styled(props => (
|
||||
<Flex alignItems="center" justifyContent="flex-end" {...props} />
|
||||
))`
|
||||
width: 100%;
|
||||
margin-left: ${({ position }) => (position === 'right' ? 'auto' : 0)};
|
||||
border-bottom: 1px solid ${({ theme }) => theme.main.colors.brightGrey};
|
||||
`;
|
||||
|
||||
export const TabButton = styled(props => (
|
||||
<Text
|
||||
as="button"
|
||||
textTransform="uppercase"
|
||||
fontSize="sm"
|
||||
fontWeight={props['aria-selected'] ? 'bold' : 'semiBold'}
|
||||
color={props['aria-selected'] ? 'mediumBlue' : 'grey'}
|
||||
{...props}
|
||||
/>
|
||||
))`
|
||||
height: 3.8rem;
|
||||
letter-spacing: 0.7px;
|
||||
margin-left: 3rem;
|
||||
border-bottom: 2px solid
|
||||
${props => (props['aria-selected'] ? props.theme.main.colors.mediumBlue : 'transparent')};
|
||||
padding: 0;
|
||||
`;
|
||||
|
||||
export const TabPanelRaw = styled.div`
|
||||
padding: 2.2rem 0;
|
||||
`;
|
129
packages/strapi-helper-plugin/lib/src/components/Tabs/index.js
Normal file
129
packages/strapi-helper-plugin/lib/src/components/Tabs/index.js
Normal file
@ -0,0 +1,129 @@
|
||||
import React, { createContext, useContext, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { TabNavRaw, TabButton, TabsRaw, TabPanelRaw } from './TabComponents';
|
||||
|
||||
const TabsIndexContext = createContext({ selectedIndex: 0, setSelectedIndex: () => undefined });
|
||||
const TabsIdContext = createContext(null);
|
||||
|
||||
export const TabsNav = ({ children, defaultSelection, label, id }) => {
|
||||
const [selectedIndex, setSelectedIndex] = useState(defaultSelection);
|
||||
|
||||
return (
|
||||
<TabsIdContext.Provider value={id}>
|
||||
<TabsIndexContext.Provider value={{ selectedIndex, setSelectedIndex }}>
|
||||
<TabNavRaw role="tablist" aria-label={label}>
|
||||
{children}
|
||||
</TabNavRaw>
|
||||
</TabsIndexContext.Provider>
|
||||
</TabsIdContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
TabsNav.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
children: PropTypes.node.isRequired,
|
||||
label: PropTypes.string.isRequired,
|
||||
defaultSelection: PropTypes.number.isRequired,
|
||||
};
|
||||
|
||||
export const Tabs = ({ children, position }) => {
|
||||
const id = useContext(TabsIdContext);
|
||||
const { setSelectedIndex, selectedIndex } = useContext(TabsIndexContext);
|
||||
const childrenArray = React.Children.toArray(children);
|
||||
|
||||
return (
|
||||
<TabsRaw position={position}>
|
||||
{childrenArray.map((child, index) =>
|
||||
React.cloneElement(child, {
|
||||
onSelect: () => setSelectedIndex(index),
|
||||
selected: index === selectedIndex,
|
||||
id: `${id}-${index}`,
|
||||
})
|
||||
)}
|
||||
</TabsRaw>
|
||||
);
|
||||
};
|
||||
|
||||
Tabs.defaultProps = {
|
||||
position: 'left',
|
||||
};
|
||||
|
||||
Tabs.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
position: PropTypes.oneOf(['left', 'right']),
|
||||
};
|
||||
|
||||
export const TabsPanel = ({ children }) => {
|
||||
const { selectedIndex } = useContext(TabsIndexContext);
|
||||
const id = useContext(TabsIdContext);
|
||||
const childrenArray = React.Children.toArray(children);
|
||||
|
||||
return (
|
||||
<>
|
||||
{childrenArray.map((child, index) =>
|
||||
React.cloneElement(child, { selected: index === selectedIndex, id: `${id}-${index}` })
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
TabsPanel.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
};
|
||||
|
||||
export const Tab = ({ children, selected, onSelect, id }) => {
|
||||
const ariaControls = `${id}-tabpanel`;
|
||||
|
||||
return (
|
||||
<TabButton
|
||||
role="tab"
|
||||
id={`${id}-tab`}
|
||||
aria-selected={selected}
|
||||
aria-controls={ariaControls}
|
||||
tabIndex={-1}
|
||||
onClick={onSelect}
|
||||
type="button"
|
||||
>
|
||||
{children}
|
||||
</TabButton>
|
||||
);
|
||||
};
|
||||
|
||||
Tab.defaultProps = {
|
||||
selected: false,
|
||||
id: '',
|
||||
onSelect: () => undefined,
|
||||
};
|
||||
|
||||
Tab.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
selected: PropTypes.bool,
|
||||
onSelect: PropTypes.func,
|
||||
id: PropTypes.string,
|
||||
};
|
||||
|
||||
export const TabPanel = ({ children, selected, id }) => {
|
||||
const labelledBy = `${id}-tab`;
|
||||
|
||||
return (
|
||||
<TabPanelRaw
|
||||
role="tabpanel"
|
||||
aria-labelledby={labelledBy}
|
||||
hidden={!selected}
|
||||
id={`${id}-tabpanel`}
|
||||
>
|
||||
{children}
|
||||
</TabPanelRaw>
|
||||
);
|
||||
};
|
||||
|
||||
TabPanel.defaultProps = {
|
||||
id: '',
|
||||
selected: false,
|
||||
};
|
||||
|
||||
TabPanel.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
selected: PropTypes.bool,
|
||||
id: PropTypes.string,
|
||||
};
|
@ -25,6 +25,7 @@ export { default as HeaderSearch } from './components/HeaderSearch';
|
||||
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 { default as InputAddonWithErrors } from './components/InputAddonWithErrors';
|
||||
export { default as InputCheckbox } from './components/InputCheckbox';
|
||||
|
@ -30,12 +30,15 @@ const LocaleSettingsPage = ({ locale, onDelete, onEdit }) => {
|
||||
<FontAwesomeIcon icon="trash-alt" />
|
||||
</span>
|
||||
) : null,
|
||||
onClick: () => onDelete(locale),
|
||||
onClick: e => {
|
||||
e.stopPropagation();
|
||||
onDelete(locale);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<CustomRow>
|
||||
<CustomRow onClick={() => onEdit(locale)}>
|
||||
<td>
|
||||
<Text>{locale.code}</Text>
|
||||
</td>
|
||||
|
@ -0,0 +1,73 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Label } from '@buffetjs/core';
|
||||
import { Inputs } from '@buffetjs/custom';
|
||||
import Select from 'react-select';
|
||||
import { Col, Row } from 'reactstrap';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { useFormikContext } from 'formik';
|
||||
import { getTrad } from '../../utils';
|
||||
|
||||
const BaseForm = ({ options, defaultOption }) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const { values, handleChange } = useFormikContext();
|
||||
|
||||
return (
|
||||
<Row>
|
||||
<Col>
|
||||
<span id="locale-code">
|
||||
<Label htmlFor="">
|
||||
{formatMessage({
|
||||
id: getTrad('Settings.locales.modal.edit.locales.label'),
|
||||
})}
|
||||
</Label>
|
||||
</span>
|
||||
|
||||
<Select
|
||||
aria-labelledby="locale-code"
|
||||
options={options}
|
||||
defaultValue={defaultOption}
|
||||
isDisabled
|
||||
/>
|
||||
</Col>
|
||||
<Col>
|
||||
<Inputs
|
||||
label={formatMessage({
|
||||
id: getTrad('Settings.locales.modal.edit.locales.displayName'),
|
||||
})}
|
||||
name="displayName"
|
||||
description={formatMessage({
|
||||
id: getTrad('Settings.locales.modal.edit.locales.displayName.description'),
|
||||
})}
|
||||
type="text"
|
||||
value={values.displayName}
|
||||
onChange={handleChange}
|
||||
validations={{
|
||||
max: 50,
|
||||
}}
|
||||
translatedErrors={{
|
||||
max: formatMessage({
|
||||
id: getTrad('Settings.locales.modal.edit.locales.displayName.error'),
|
||||
}),
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
};
|
||||
|
||||
BaseForm.defaultProps = {
|
||||
defaultOption: undefined,
|
||||
};
|
||||
|
||||
BaseForm.propTypes = {
|
||||
options: PropTypes.arrayOf(
|
||||
PropTypes.exact({ value: PropTypes.number.isRequired, label: PropTypes.string.isRequired })
|
||||
).isRequired,
|
||||
defaultOption: PropTypes.exact({
|
||||
value: PropTypes.number.isRequired,
|
||||
label: PropTypes.string.isRequired,
|
||||
}),
|
||||
};
|
||||
|
||||
export default BaseForm;
|
@ -1,13 +1,25 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Modal, ModalHeader, ModalSection, ModalFooter } from 'strapi-helper-plugin';
|
||||
import {
|
||||
Modal,
|
||||
ModalHeader,
|
||||
HeaderModal,
|
||||
HeaderModalTitle,
|
||||
ModalFooter,
|
||||
ModalForm,
|
||||
Tabs,
|
||||
TabsNav,
|
||||
Tab,
|
||||
TabsPanel,
|
||||
TabPanel,
|
||||
} from 'strapi-helper-plugin';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { Button, Label, InputText } from '@buffetjs/core';
|
||||
import Select from 'react-select';
|
||||
import { Button } from '@buffetjs/core';
|
||||
import { Formik } from 'formik';
|
||||
import { object, string } from 'yup';
|
||||
import useEditLocale from '../../hooks/useEditLocale';
|
||||
import { getTrad } from '../../utils';
|
||||
import BaseForm from './BaseForm';
|
||||
|
||||
const ModalEdit = ({ localeToEdit, onClose, locales }) => {
|
||||
const { isEditing, editLocale } = useEditLocale();
|
||||
@ -31,6 +43,12 @@ const ModalEdit = ({ localeToEdit, onClose, locales }) => {
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpened} onToggle={onClose}>
|
||||
<HeaderModal>
|
||||
<ModalHeader
|
||||
headerBreadcrumbs={[formatMessage({ id: getTrad('Settings.list.actions.edit') })]}
|
||||
/>
|
||||
</HeaderModal>
|
||||
|
||||
<Formik
|
||||
initialValues={{ displayName: localeToEdit ? localeToEdit.name : '' }}
|
||||
onSubmit={handleSubmit}
|
||||
@ -38,44 +56,49 @@ const ModalEdit = ({ localeToEdit, onClose, locales }) => {
|
||||
displayName: string().max(50, 'Settings.locales.modal.edit.locales.displayName.error'),
|
||||
})}
|
||||
>
|
||||
{({ values, handleSubmit, handleChange, errors }) => (
|
||||
{({ handleSubmit, errors }) => (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<ModalHeader
|
||||
headerBreadcrumbs={[formatMessage({ id: getTrad('Settings.list.actions.edit') })]}
|
||||
/>
|
||||
<ModalSection>
|
||||
<div>
|
||||
<span id="locale-code">
|
||||
<Label htmlFor="">
|
||||
{formatMessage({ id: getTrad('Settings.locales.modal.edit.locales.label') })}
|
||||
</Label>
|
||||
</span>
|
||||
|
||||
<Select
|
||||
aria-labelledby="locale-code"
|
||||
options={options}
|
||||
defaultValue={defaultOption}
|
||||
isDisabled
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="displayName">
|
||||
<div className="container-fluid">
|
||||
<div className="container-fluid">
|
||||
<HeaderModalTitle
|
||||
style={{
|
||||
fontSize: '1.8rem',
|
||||
height: '65px',
|
||||
fontWeight: 'bold',
|
||||
alignItems: 'center',
|
||||
marginBottom: '-39px',
|
||||
paddingTop: '16px',
|
||||
}}
|
||||
>
|
||||
{formatMessage({
|
||||
id: getTrad('Settings.locales.modal.edit.locales.displayName'),
|
||||
id: getTrad('Settings.locales.modal.title'),
|
||||
})}
|
||||
</Label>
|
||||
<InputText name="displayName" value={values.displayName} onChange={handleChange} />
|
||||
</HeaderModalTitle>
|
||||
|
||||
{errors.displayName && (
|
||||
<small>
|
||||
{formatMessage({
|
||||
id: getTrad(' Settings.locales.modal.edit.locales.displayName.error'),
|
||||
<ModalForm>
|
||||
<TabsNav
|
||||
defaultSelection={0}
|
||||
label={formatMessage({
|
||||
id: getTrad('Settings.locales.modal.edit.tab.label'),
|
||||
})}
|
||||
</small>
|
||||
)}
|
||||
id="i18n-settings-tabs"
|
||||
>
|
||||
<Tabs position="right">
|
||||
<Tab>{formatMessage({ id: getTrad('Settings.locales.modal.base') })}</Tab>
|
||||
<Tab>{formatMessage({ id: getTrad('Settings.locales.modal.advanced') })}</Tab>
|
||||
</Tabs>
|
||||
|
||||
<TabsPanel>
|
||||
<TabPanel>
|
||||
<BaseForm options={options} defaultOption={defaultOption} />
|
||||
</TabPanel>
|
||||
<TabPanel>advanced</TabPanel>
|
||||
</TabsPanel>
|
||||
</TabsNav>
|
||||
</ModalForm>
|
||||
</div>
|
||||
</ModalSection>
|
||||
</div>
|
||||
|
||||
<ModalFooter>
|
||||
<section>
|
||||
<Button type="button" color="cancel" onClick={onClose}>
|
||||
|
@ -30,7 +30,15 @@ jest.mock('strapi-helper-plugin', () => ({
|
||||
ModalHeader: ({ children }) => <div>{children}</div>,
|
||||
ModalSection: ({ children }) => <div>{children}</div>,
|
||||
ModalFooter: ({ children }) => <div>{children}</div>,
|
||||
HeaderModal: ({ children }) => <div>{children}</div>,
|
||||
HeaderModalTitle: ({ children }) => <div>{children}</div>,
|
||||
ModalForm: ({ children }) => <div>{children}</div>,
|
||||
ListButton: () => <div />,
|
||||
Tabs: ({ children }) => <div>{children}</div>,
|
||||
TabsNav: ({ children }) => <div>{children}</div>,
|
||||
Tab: ({ children }) => <div>{children}</div>,
|
||||
TabsPanel: ({ children }) => <div>{children}</div>,
|
||||
TabPanel: ({ children }) => <div>{children}</div>,
|
||||
useUserPermissions: jest.fn(),
|
||||
request: jest.fn(),
|
||||
}));
|
||||
@ -166,6 +174,7 @@ describe('i18n settings page', () => {
|
||||
'a very very very very long string that has more than fifty characters in order to show a warning',
|
||||
},
|
||||
});
|
||||
fireEvent.blur(screen.getByLabelText('Settings.locales.modal.edit.locales.displayName'));
|
||||
|
||||
await waitFor(() =>
|
||||
expect(screen.getByText('Settings.locales.modal.edit.confirmation')).toBeDisabled()
|
||||
|
@ -23,6 +23,7 @@
|
||||
"Settings.locales.list.title.singular": "{number} Locale",
|
||||
"Settings.locales.list.title.plural": "{number} Locales",
|
||||
"Settings.locales.row.default-locale": "Default locale",
|
||||
"Settings.locales.modal.title": "Configurations",
|
||||
"Settings.locales.modal.delete.confirm": "Yes, delete",
|
||||
"Settings.locales.modal.delete.message": "Deleting this locale will delete all associated content. If you want to keep some content, make sure to reallocate it to another locale first.",
|
||||
"Settings.locales.modal.delete.secondMessage": "Do you want to delete this locale?",
|
||||
@ -31,5 +32,9 @@
|
||||
"Settings.locales.modal.edit.success": "Locale successfully edited",
|
||||
"Settings.locales.modal.edit.locales.label": "Locales",
|
||||
"Settings.locales.modal.edit.locales.displayName": "Locale display name",
|
||||
"Settings.locales.modal.edit.locales.displayName.error": "The locale display name can only be less than 50 characters."
|
||||
"Settings.locales.modal.edit.locales.displayName.error": "The locale display name can only be less than 50 characters.",
|
||||
"Settings.locales.modal.edit.locales.displayName.description":"Locale will be displayed under that name in the administration panel",
|
||||
"Settings.locales.modal.edit.tab.label":"Navigating between the I18N base settings and advanced settings",
|
||||
"Settings.locales.modal.base": "Base settings",
|
||||
"Settings.locales.modal.advanced": "Advanced settings"
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user