mirror of
https://github.com/strapi/strapi.git
synced 2025-09-08 16:16:21 +00:00
Merge branch 'main' of github.com:strapi/strapi into features/deits
This commit is contained in:
commit
516963a080
@ -1,18 +1,9 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { Typography } from '@strapi/design-system/Typography';
|
||||
|
||||
export const Hint = ({ id, error, name, description }) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const hint = description
|
||||
? formatMessage(
|
||||
{ id: description.id, defaultMessage: description.defaultMessage },
|
||||
{ ...description.values }
|
||||
)
|
||||
: '';
|
||||
|
||||
if (!hint || error) {
|
||||
export const Hint = ({ id, error, name, hint }) => {
|
||||
if (hint.length === 0 || error) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -25,16 +16,12 @@ export const Hint = ({ id, error, name, description }) => {
|
||||
|
||||
Hint.defaultProps = {
|
||||
id: undefined,
|
||||
description: undefined,
|
||||
error: undefined,
|
||||
hint: '',
|
||||
};
|
||||
|
||||
Hint.propTypes = {
|
||||
description: PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
defaultMessage: PropTypes.string.isRequired,
|
||||
values: PropTypes.object,
|
||||
}),
|
||||
hint: PropTypes.oneOfType([PropTypes.string, PropTypes.array]),
|
||||
error: PropTypes.string,
|
||||
id: PropTypes.string,
|
||||
name: PropTypes.string.isRequired,
|
||||
|
@ -23,7 +23,7 @@ import {
|
||||
const InputUID = ({
|
||||
attribute,
|
||||
contentTypeUID,
|
||||
description,
|
||||
hint,
|
||||
disabled,
|
||||
error,
|
||||
intlLabel,
|
||||
@ -54,13 +54,6 @@ const InputUID = ({
|
||||
)
|
||||
: name;
|
||||
|
||||
const hint = description
|
||||
? formatMessage(
|
||||
{ id: description.id, defaultMessage: description.defaultMessage },
|
||||
{ ...description.values }
|
||||
)
|
||||
: '';
|
||||
|
||||
const formattedPlaceholder = placeholder
|
||||
? formatMessage(
|
||||
{ id: placeholder.id, defaultMessage: placeholder.defaultMessage },
|
||||
@ -251,11 +244,6 @@ InputUID.propTypes = {
|
||||
required: PropTypes.bool,
|
||||
}).isRequired,
|
||||
contentTypeUID: PropTypes.string.isRequired,
|
||||
description: PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
defaultMessage: PropTypes.string.isRequired,
|
||||
values: PropTypes.object,
|
||||
}),
|
||||
disabled: PropTypes.bool,
|
||||
error: PropTypes.string,
|
||||
intlLabel: PropTypes.shape({
|
||||
@ -273,16 +261,17 @@ InputUID.propTypes = {
|
||||
values: PropTypes.object,
|
||||
}),
|
||||
required: PropTypes.bool,
|
||||
hint: PropTypes.oneOfType([PropTypes.string, PropTypes.array]),
|
||||
};
|
||||
|
||||
InputUID.defaultProps = {
|
||||
description: undefined,
|
||||
disabled: false,
|
||||
error: undefined,
|
||||
labelAction: undefined,
|
||||
placeholder: undefined,
|
||||
value: '',
|
||||
required: false,
|
||||
hint: '',
|
||||
};
|
||||
|
||||
export default InputUID;
|
||||
|
@ -31,7 +31,7 @@ const TypographyAsterisk = styled(Typography)`
|
||||
`;
|
||||
|
||||
const Wysiwyg = ({
|
||||
description,
|
||||
hint,
|
||||
disabled,
|
||||
error,
|
||||
intlLabel,
|
||||
@ -167,7 +167,7 @@ const Wysiwyg = ({
|
||||
|
||||
{!isExpandMode && <WysiwygFooter onToggleExpand={handleToggleExpand} />}
|
||||
</EditorLayout>
|
||||
<Hint description={description} name={name} error={error} />
|
||||
<Hint hint={hint} name={name} error={error} />
|
||||
</Stack>
|
||||
|
||||
{error && (
|
||||
@ -186,21 +186,17 @@ const Wysiwyg = ({
|
||||
};
|
||||
|
||||
Wysiwyg.defaultProps = {
|
||||
description: null,
|
||||
disabled: false,
|
||||
error: '',
|
||||
labelAction: undefined,
|
||||
placeholder: null,
|
||||
required: false,
|
||||
value: '',
|
||||
hint: '',
|
||||
};
|
||||
|
||||
Wysiwyg.propTypes = {
|
||||
description: PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
defaultMessage: PropTypes.string.isRequired,
|
||||
values: PropTypes.object,
|
||||
}),
|
||||
hint: PropTypes.oneOfType([PropTypes.string, PropTypes.array]),
|
||||
disabled: PropTypes.bool,
|
||||
error: PropTypes.string,
|
||||
intlLabel: PropTypes.shape({
|
||||
|
@ -1,25 +1,40 @@
|
||||
import React, { useRef } from 'react';
|
||||
import propTypes from 'prop-types';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { useCMEditViewDataManager } from '@strapi/helper-plugin';
|
||||
import { Box } from '@strapi/design-system/Box';
|
||||
import { Divider } from '@strapi/design-system/Divider';
|
||||
import { Typography } from '@strapi/design-system/Typography';
|
||||
import { Flex } from '@strapi/design-system/Flex';
|
||||
import { Stack } from '@strapi/design-system/Stack';
|
||||
import { Box, Divider, Flex, Stack, Typography } from '@strapi/design-system';
|
||||
|
||||
import { getTrad } from '../../../utils';
|
||||
import getUnits from './utils/getUnits';
|
||||
import { getFullName } from '../../../../utils';
|
||||
|
||||
const Informations = () => {
|
||||
const KeyValuePair = ({ label, value }) => {
|
||||
return (
|
||||
<Flex justifyContent="space-between">
|
||||
<Typography as="dt" fontWeight="bold" textColor="neutral600">
|
||||
{label}
|
||||
</Typography>
|
||||
<Typography as="dd">{value}</Typography>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
KeyValuePair.propTypes = {
|
||||
label: propTypes.string.isRequired,
|
||||
value: propTypes.string.isRequired,
|
||||
};
|
||||
|
||||
const Information = () => {
|
||||
const { formatMessage, formatRelativeTime } = useIntl();
|
||||
const { initialData, isCreatingEntry } = useCMEditViewDataManager();
|
||||
const currentTime = useRef(Date.now());
|
||||
|
||||
const getFieldInfo = (atField, byField) => {
|
||||
const userFirstname = initialData[byField]?.firstname || '';
|
||||
const userLastname = initialData[byField]?.lastname || '';
|
||||
const userUsername = initialData[byField]?.username;
|
||||
const user = userUsername || getFullName(userFirstname, userLastname);
|
||||
const { firstname, lastname, username } = initialData[byField] ?? {};
|
||||
|
||||
const userFirstname = firstname ?? '';
|
||||
const userLastname = lastname ?? '';
|
||||
const user = username ?? getFullName(userFirstname, userLastname);
|
||||
const timestamp = initialData[atField] ? new Date(initialData[atField]).getTime() : Date.now();
|
||||
const elapsed = timestamp - currentTime.current;
|
||||
const { unit, value } = getUnits(-elapsed);
|
||||
@ -34,56 +49,57 @@ const Informations = () => {
|
||||
const created = getFieldInfo('createdAt', 'createdBy');
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Typography variant="sigma" textColor="neutral600" id="additional-informations">
|
||||
<Stack spacing={2}>
|
||||
<Typography variant="sigma" textColor="neutral600" id="additional-information">
|
||||
{formatMessage({
|
||||
id: getTrad('containers.Edit.information'),
|
||||
defaultMessage: 'Information',
|
||||
})}
|
||||
</Typography>
|
||||
<Box paddingTop={2} paddingBottom={6}>
|
||||
|
||||
<Box paddingBottom={4}>
|
||||
<Divider />
|
||||
</Box>
|
||||
|
||||
<Stack spacing={4}>
|
||||
<Flex justifyContent="space-between">
|
||||
<Typography fontWeight="bold">
|
||||
{formatMessage({
|
||||
<Stack spacing={2} as="dl">
|
||||
<KeyValuePair
|
||||
label={formatMessage({
|
||||
id: getTrad('containers.Edit.information.created'),
|
||||
defaultMessage: 'Created',
|
||||
})}
|
||||
</Typography>
|
||||
<Typography>{created.at}</Typography>
|
||||
</Flex>
|
||||
<Flex justifyContent="space-between">
|
||||
<Typography fontWeight="bold">
|
||||
{formatMessage({
|
||||
value={created.at}
|
||||
/>
|
||||
|
||||
<KeyValuePair
|
||||
label={formatMessage({
|
||||
id: getTrad('containers.Edit.information.by'),
|
||||
defaultMessage: 'By',
|
||||
})}
|
||||
</Typography>
|
||||
<Typography>{created.by}</Typography>
|
||||
</Flex>
|
||||
<Flex justifyContent="space-between">
|
||||
<Typography fontWeight="bold">
|
||||
{formatMessage({
|
||||
value={created.by}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
<Stack spacing={2} as="dl">
|
||||
<KeyValuePair
|
||||
label={formatMessage({
|
||||
id: getTrad('containers.Edit.information.lastUpdate'),
|
||||
defaultMessage: 'Last update',
|
||||
})}
|
||||
</Typography>
|
||||
<Typography>{updated.at}</Typography>
|
||||
</Flex>
|
||||
<Flex justifyContent="space-between">
|
||||
<Typography fontWeight="bold">
|
||||
{formatMessage({
|
||||
value={updated.at}
|
||||
/>
|
||||
|
||||
<KeyValuePair
|
||||
label={formatMessage({
|
||||
id: getTrad('containers.Edit.information.by'),
|
||||
defaultMessage: 'By',
|
||||
})}
|
||||
</Typography>
|
||||
<Typography>{updated.by}</Typography>
|
||||
</Flex>
|
||||
value={updated.by}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default Informations;
|
||||
export default Information;
|
@ -0,0 +1,113 @@
|
||||
/**
|
||||
*
|
||||
* Tests for Information
|
||||
*
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
import { useCMEditViewDataManager } from '@strapi/helper-plugin';
|
||||
import { lightTheme, darkTheme } from '@strapi/design-system';
|
||||
import Theme from '../../../../../components/Theme';
|
||||
import ThemeToggleProvider from '../../../../../components/ThemeToggleProvider';
|
||||
import Information from '../index';
|
||||
|
||||
jest.mock('@strapi/helper-plugin', () => ({
|
||||
...jest.requireActual('@strapi/helper-plugin'),
|
||||
useCMEditViewDataManager: jest.fn(),
|
||||
}));
|
||||
|
||||
const makeApp = () => {
|
||||
return (
|
||||
<IntlProvider
|
||||
locale="en"
|
||||
defaultLocale="en"
|
||||
messages={{ 'containers.Edit.information': 'Information' }}
|
||||
>
|
||||
<ThemeToggleProvider themes={{ light: lightTheme, dark: darkTheme }}>
|
||||
<Theme>
|
||||
<Information />
|
||||
</Theme>
|
||||
</ThemeToggleProvider>
|
||||
</IntlProvider>
|
||||
);
|
||||
};
|
||||
|
||||
describe('CONTENT MANAGER | EditView | Information', () => {
|
||||
const RealNow = Date.now;
|
||||
|
||||
beforeAll(() => {
|
||||
global.Date.now = jest.fn(() => new Date('2022-09-20').getTime());
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
global.Date.now = RealNow;
|
||||
});
|
||||
|
||||
it('renders and matches the snaphsot in case an entry is created', () => {
|
||||
useCMEditViewDataManager.mockImplementationOnce(() => ({
|
||||
initialData: {},
|
||||
isCreatingEntry: true,
|
||||
}));
|
||||
|
||||
const { getByText, getAllByText } = render(makeApp());
|
||||
|
||||
expect(getByText('Created')).toBeInTheDocument();
|
||||
expect(getByText('Last update')).toBeInTheDocument();
|
||||
expect(getAllByText('By').length).toBe(2);
|
||||
expect(getAllByText('now').length).toBe(2);
|
||||
expect(getAllByText('-').length).toBe(2);
|
||||
});
|
||||
|
||||
it('renders and matches the snaphsot in case an entry is edited', () => {
|
||||
useCMEditViewDataManager.mockImplementationOnce(() => ({
|
||||
initialData: {
|
||||
updatedAt: 'Fri Jan 13 2022 13:10:14 GMT+0100',
|
||||
updatedBy: {
|
||||
firstname: 'First name',
|
||||
lastname: 'Last name',
|
||||
},
|
||||
|
||||
createdAt: 'Fri Jan 13 2022 12:10:14 GMT+0100',
|
||||
createdBy: {
|
||||
firstname: 'First name',
|
||||
lastname: 'Last name',
|
||||
},
|
||||
},
|
||||
isCreatingEntry: false,
|
||||
}));
|
||||
|
||||
const { getAllByText } = render(makeApp());
|
||||
|
||||
expect(getAllByText('8 months ago').length).toBe(2);
|
||||
expect(getAllByText('First name Last name').length).toBe(2);
|
||||
});
|
||||
|
||||
it('renders and matches the snaphsot in case a username is set', () => {
|
||||
useCMEditViewDataManager.mockImplementationOnce(() => ({
|
||||
initialData: {
|
||||
updatedAt: 'Fri Jan 13 2022 13:10:14 GMT+0100',
|
||||
updatedBy: {
|
||||
firstname: 'First name',
|
||||
lastname: 'Last name',
|
||||
username: 'user@strapi.io',
|
||||
},
|
||||
|
||||
createdAt: 'Fri Jan 13 2022 12:10:14 GMT+0100',
|
||||
createdBy: {
|
||||
firstname: 'First name',
|
||||
lastname: 'Last name',
|
||||
username: 'user@strapi.io',
|
||||
},
|
||||
},
|
||||
isCreatingEntry: false,
|
||||
}));
|
||||
|
||||
const { queryByText, getAllByText } = render(makeApp());
|
||||
|
||||
expect(getAllByText('user@strapi.io').length).toBe(2);
|
||||
expect(queryByText('First name')).toBeNull();
|
||||
expect(queryByText('Last name')).toBeNull();
|
||||
});
|
||||
});
|
@ -1,219 +0,0 @@
|
||||
/**
|
||||
*
|
||||
* Tests for Informations
|
||||
*
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
import { useCMEditViewDataManager } from '@strapi/helper-plugin';
|
||||
import { lightTheme, darkTheme } from '@strapi/design-system';
|
||||
import Theme from '../../../../../components/Theme';
|
||||
import ThemeToggleProvider from '../../../../../components/ThemeToggleProvider';
|
||||
import Informations from '../index';
|
||||
|
||||
jest.mock('@strapi/helper-plugin', () => ({
|
||||
useCMEditViewDataManager: jest.fn(),
|
||||
wrapAxiosInstance: jest.fn(() => {}),
|
||||
}));
|
||||
|
||||
const makeApp = () => {
|
||||
return (
|
||||
<IntlProvider
|
||||
locale="en"
|
||||
defaultLocale="en"
|
||||
messages={{ 'containers.Edit.information': 'Information' }}
|
||||
>
|
||||
<ThemeToggleProvider themes={{ light: lightTheme, dark: darkTheme }}>
|
||||
<Theme>
|
||||
<Informations />
|
||||
</Theme>
|
||||
</ThemeToggleProvider>
|
||||
</IntlProvider>
|
||||
);
|
||||
};
|
||||
|
||||
describe('CONTENT MANAGER | EditView | Header', () => {
|
||||
const RealNow = Date.now;
|
||||
|
||||
beforeAll(() => {
|
||||
global.Date.now = jest.fn(() => new Date('2021-09-20').getTime());
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
global.Date.now = RealNow;
|
||||
});
|
||||
|
||||
it('renders and matches the snaphsot', () => {
|
||||
useCMEditViewDataManager.mockImplementationOnce(() => ({
|
||||
initialData: {},
|
||||
isCreatingEntry: true,
|
||||
}));
|
||||
|
||||
const {
|
||||
container: { firstChild },
|
||||
} = render(makeApp());
|
||||
|
||||
expect(firstChild).toMatchInlineSnapshot(`
|
||||
.c1 {
|
||||
padding-top: 8px;
|
||||
padding-bottom: 24px;
|
||||
}
|
||||
|
||||
.c2 {
|
||||
background: #eaeaef;
|
||||
}
|
||||
|
||||
.c3 {
|
||||
height: 1px;
|
||||
border: none;
|
||||
-webkit-flex-shrink: 0;
|
||||
-ms-flex-negative: 0;
|
||||
flex-shrink: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.c0 {
|
||||
font-weight: 600;
|
||||
font-size: 0.6875rem;
|
||||
line-height: 1.45;
|
||||
text-transform: uppercase;
|
||||
color: #666687;
|
||||
}
|
||||
|
||||
.c7 {
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.43;
|
||||
font-weight: 600;
|
||||
color: #32324d;
|
||||
}
|
||||
|
||||
.c8 {
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.43;
|
||||
color: #32324d;
|
||||
}
|
||||
|
||||
.c4 {
|
||||
-webkit-align-items: stretch;
|
||||
-webkit-box-align: stretch;
|
||||
-ms-flex-align: stretch;
|
||||
align-items: stretch;
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-flex-direction: column;
|
||||
-ms-flex-direction: column;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.c6 {
|
||||
-webkit-align-items: center;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-flex-direction: row;
|
||||
-ms-flex-direction: row;
|
||||
flex-direction: row;
|
||||
-webkit-box-pack: justify;
|
||||
-webkit-justify-content: space-between;
|
||||
-ms-flex-pack: justify;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.c5 > * {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.c5 > * + * {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
<div
|
||||
class=""
|
||||
>
|
||||
<span
|
||||
class="c0"
|
||||
id="additional-informations"
|
||||
>
|
||||
Information
|
||||
</span>
|
||||
<div
|
||||
class="c1"
|
||||
>
|
||||
<hr
|
||||
class="c2 c3"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="c4 c5"
|
||||
spacing="4"
|
||||
>
|
||||
<div
|
||||
class="c6"
|
||||
>
|
||||
<span
|
||||
class="c7"
|
||||
>
|
||||
Created
|
||||
</span>
|
||||
<span
|
||||
class="c8"
|
||||
>
|
||||
now
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="c6"
|
||||
>
|
||||
<span
|
||||
class="c7"
|
||||
>
|
||||
By
|
||||
</span>
|
||||
<span
|
||||
class="c8"
|
||||
>
|
||||
-
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="c6"
|
||||
>
|
||||
<span
|
||||
class="c7"
|
||||
>
|
||||
Last update
|
||||
</span>
|
||||
<span
|
||||
class="c8"
|
||||
>
|
||||
now
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="c6"
|
||||
>
|
||||
<span
|
||||
class="c7"
|
||||
>
|
||||
By
|
||||
</span>
|
||||
<span
|
||||
class="c8"
|
||||
>
|
||||
-
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
});
|
||||
});
|
@ -25,7 +25,7 @@ import SingleTypeFormWrapper from '../../components/SingleTypeFormWrapper';
|
||||
import { getTrad } from '../../utils';
|
||||
import useLazyComponents from '../../hooks/useLazyComponents';
|
||||
import DraftAndPublishBadge from './DraftAndPublishBadge';
|
||||
import Informations from './Informations';
|
||||
import Information from './Information';
|
||||
import Header from './Header';
|
||||
import { getFieldsActionMatchingPermissions } from './utils';
|
||||
import DeleteLink from './DeleteLink';
|
||||
@ -175,7 +175,7 @@ const EditView = ({ allowedActions, isSingleType, goBack, slug, id, origin, user
|
||||
<DraftAndPublishBadge />
|
||||
<Box
|
||||
as="aside"
|
||||
aria-labelledby="additional-informations"
|
||||
aria-labelledby="additional-information"
|
||||
background="neutral0"
|
||||
borderColor="neutral150"
|
||||
hasRadius
|
||||
@ -185,7 +185,7 @@ const EditView = ({ allowedActions, isSingleType, goBack, slug, id, origin, user
|
||||
paddingTop={6}
|
||||
shadow="tableShadow"
|
||||
>
|
||||
<Informations />
|
||||
<Information />
|
||||
<InjectionZone area="contentManager.editView.informations" />
|
||||
</Box>
|
||||
<Box as="aside" aria-labelledby="links">
|
||||
|
@ -465,7 +465,7 @@
|
||||
"clearLabel": "Clear",
|
||||
"selectButtonTitle": "Select",
|
||||
"coming.soon": "This content is currently under construction and will be back in a few weeks!",
|
||||
"component.Input.error.validation.integer": "The value must be an integer",
|
||||
"component.Input.error.validation.integer": "Value must be an integer",
|
||||
"components.AutoReloadBlocker.description": "Run Strapi with one of the following commands:",
|
||||
"components.AutoReloadBlocker.header": "Reload feature is required for this plugin.",
|
||||
"components.ErrorBoundary.title": "Something went wrong...",
|
||||
@ -481,26 +481,26 @@
|
||||
"components.FilterOptions.FILTER_TYPES.$notNull": "is not null",
|
||||
"components.FilterOptions.FILTER_TYPES.$null": "is null",
|
||||
"components.FilterOptions.FILTER_TYPES.$startsWith": "starts with",
|
||||
"components.Input.error.attribute.key.taken": "This value already exists",
|
||||
"components.Input.error.attribute.key.taken": "Value already exists",
|
||||
"components.Input.error.attribute.sameKeyAndName": "Can't be equal",
|
||||
"components.Input.error.attribute.taken": "This field name already exists",
|
||||
"components.Input.error.attribute.taken": "Field name already exists",
|
||||
"components.Input.error.contain.lowercase": "Password must contain at least one lowercase character",
|
||||
"components.Input.error.contain.number": "Password must contain at least one number",
|
||||
"components.Input.error.contain.uppercase": "Password must contain at least one uppercase character",
|
||||
"components.Input.error.contentTypeName.taken": "This name already exists",
|
||||
"components.Input.error.contentTypeName.taken": "Name already exists",
|
||||
"components.Input.error.custom-error": "{errorMessage} ",
|
||||
"components.Input.error.password.noMatch": "Passwords do not match",
|
||||
"components.Input.error.validation.email": "This is an invalid email",
|
||||
"components.Input.error.validation.json": "This doesn't match the JSON format",
|
||||
"components.Input.error.validation.email": "Value is an invalid email",
|
||||
"components.Input.error.validation.json": "Value is invalid JSON",
|
||||
"components.Input.error.validation.lowercase": "The value must be a lowercase string",
|
||||
"components.Input.error.validation.max": "The value is too high.",
|
||||
"components.Input.error.validation.maxLength": "The value is too long.",
|
||||
"components.Input.error.validation.min": "The value is too low.",
|
||||
"components.Input.error.validation.minLength": "The value is too short.",
|
||||
"components.Input.error.validation.minSupMax": "Can't be superior",
|
||||
"components.Input.error.validation.regex": "The value does not match the regex.",
|
||||
"components.Input.error.validation.required": "This value is required.",
|
||||
"components.Input.error.validation.unique": "This value is already used.",
|
||||
"components.Input.error.validation.max": "Value is larger than the maximum",
|
||||
"components.Input.error.validation.maxLength": "Value is longer than the maximum",
|
||||
"components.Input.error.validation.min": "Value is smaller than the minimum",
|
||||
"components.Input.error.validation.minLength": "Value is shorter than the minimum",
|
||||
"components.Input.error.validation.minSupMax": "Value cannot be superior",
|
||||
"components.Input.error.validation.regex": "Value does not match the required pattern",
|
||||
"components.Input.error.validation.required": "Value is required",
|
||||
"components.Input.error.validation.unique": "Value is already used",
|
||||
"components.InputSelect.option.placeholder": "Choose here",
|
||||
"components.ListRow.empty": "There is no data to be shown.",
|
||||
"components.NotAllowedInput.text": "No permissions to see this field",
|
||||
@ -699,6 +699,9 @@
|
||||
"content-manager.form.Input.sort.field": "Enable sort on this field",
|
||||
"content-manager.form.Input.sort.order": "Default sort order",
|
||||
"content-manager.form.Input.wysiwyg": "Display as WYSIWYG",
|
||||
"content-manager.form.Input.hint.text": "{min, select, undefined {} other {min. {min}}}{divider}{max, select, undefined {} other {max. {max}}}{unit}{br}{description}",
|
||||
"content-manager.form.Input.hint.minMaxDivider": " / ",
|
||||
"content-manager.form.Input.hint.character.unit": "{maxValue, plural, one { character} other { characters}}",
|
||||
"content-manager.global.displayedFields": "Displayed Fields",
|
||||
"content-manager.groups": "Groups",
|
||||
"content-manager.groups.numbered": "Groups ({number})",
|
||||
|
@ -36,8 +36,9 @@ function createDefaultMetadata(schema, name) {
|
||||
editable: true,
|
||||
};
|
||||
|
||||
if (isRelation(schema.attributes[name])) {
|
||||
const { targetModel } = schema.attributes[name];
|
||||
const fieldAttributes = schema.attributes[name];
|
||||
if (isRelation(fieldAttributes)) {
|
||||
const { targetModel } = fieldAttributes;
|
||||
|
||||
const targetSchema = getTargetSchema(targetModel);
|
||||
|
||||
|
@ -16,8 +16,16 @@ class PostgresDialect extends Dialect {
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
this.db.connection.client.driver.types.setTypeParser(1082, 'text', (v) => v); // Don't cast DATE string to Date()
|
||||
this.db.connection.client.driver.types.setTypeParser(1700, 'text', parseFloat);
|
||||
this.db.connection.client.driver.types.setTypeParser(
|
||||
this.db.connection.client.driver.types.builtins.DATE,
|
||||
'text',
|
||||
(v) => v
|
||||
); // Don't cast DATE string to Date()
|
||||
this.db.connection.client.driver.types.setTypeParser(
|
||||
this.db.connection.client.driver.types.builtins.NUMERIC,
|
||||
'text',
|
||||
parseFloat
|
||||
);
|
||||
}
|
||||
|
||||
usesForeignKeys() {
|
||||
|
@ -34,7 +34,7 @@
|
||||
"date-fns": "2.29.3",
|
||||
"debug": "4.3.4",
|
||||
"fs-extra": "10.0.0",
|
||||
"knex": "1.0.7",
|
||||
"knex": "2.4.0",
|
||||
"lodash": "4.17.21",
|
||||
"semver": "7.3.8",
|
||||
"umzug": "3.1.1"
|
||||
|
@ -9,6 +9,7 @@ import PropTypes from 'prop-types';
|
||||
import parseISO from 'date-fns/parseISO';
|
||||
import formatISO from 'date-fns/formatISO';
|
||||
import { useIntl } from 'react-intl';
|
||||
|
||||
import {
|
||||
Checkbox,
|
||||
DatePicker,
|
||||
@ -24,7 +25,9 @@ import {
|
||||
import { Option } from '@strapi/design-system/Select';
|
||||
import EyeStriked from '@strapi/icons/EyeStriked';
|
||||
import Eye from '@strapi/icons/Eye';
|
||||
|
||||
import NotSupported from './NotSupported';
|
||||
import useFieldHint from '../../hooks/useFieldHint';
|
||||
|
||||
const GenericInput = ({
|
||||
autoComplete,
|
||||
@ -43,9 +46,16 @@ const GenericInput = ({
|
||||
type,
|
||||
value: defaultValue,
|
||||
isNullable,
|
||||
attribute,
|
||||
...rest
|
||||
}) => {
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
const { hint } = useFieldHint({
|
||||
description,
|
||||
fieldSchema: attribute,
|
||||
type: attribute?.type || type,
|
||||
});
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
|
||||
const CustomInput = customInputs ? customInputs[type] : null;
|
||||
@ -91,7 +101,9 @@ const GenericInput = ({
|
||||
return (
|
||||
<CustomInput
|
||||
{...rest}
|
||||
attribute={attribute}
|
||||
description={description}
|
||||
hint={hint}
|
||||
disabled={disabled}
|
||||
intlLabel={intlLabel}
|
||||
labelAction={labelAction}
|
||||
@ -114,13 +126,6 @@ const GenericInput = ({
|
||||
)
|
||||
: name;
|
||||
|
||||
const hint = description
|
||||
? formatMessage(
|
||||
{ id: description.id, defaultMessage: description.defaultMessage },
|
||||
{ ...description.values }
|
||||
)
|
||||
: '';
|
||||
|
||||
const formattedPlaceholder = placeholder
|
||||
? formatMessage(
|
||||
{ id: placeholder.id, defaultMessage: placeholder.defaultMessage },
|
||||
@ -210,7 +215,10 @@ const GenericInput = ({
|
||||
required={required}
|
||||
value={value && new Date(value)}
|
||||
selectedDateLabel={(formattedDate) => `Date picker, current is ${formattedDate}`}
|
||||
selectButtonTitle={formatMessage({ id: 'selectButtonTitle', defaultMessage: 'Select' })}
|
||||
selectButtonTitle={formatMessage({
|
||||
id: 'selectButtonTitle',
|
||||
defaultMessage: 'Select',
|
||||
})}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -451,6 +459,7 @@ GenericInput.defaultProps = {
|
||||
options: [],
|
||||
step: 1,
|
||||
value: undefined,
|
||||
attribute: null,
|
||||
};
|
||||
|
||||
GenericInput.propTypes = {
|
||||
@ -461,6 +470,7 @@ GenericInput.propTypes = {
|
||||
defaultMessage: PropTypes.string.isRequired,
|
||||
values: PropTypes.object,
|
||||
}),
|
||||
attribute: PropTypes.object,
|
||||
disabled: PropTypes.bool,
|
||||
error: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
|
@ -0,0 +1,75 @@
|
||||
import React from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { getFieldUnits, getMinMax } from './utils';
|
||||
|
||||
/**
|
||||
* @description
|
||||
* A hook for generating the hint for a field
|
||||
* @type {
|
||||
* ({ description: { id: string, defaultMessage: string },
|
||||
* type: string,
|
||||
* fieldSchema: { minLength?: number|string; maxLength?: number|string; max?: number|string; min?: number|string }
|
||||
* })
|
||||
* => { hint: ''|Array }
|
||||
* }
|
||||
*/
|
||||
const useFieldHint = ({ description, fieldSchema, type }) => {
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
/**
|
||||
* @returns {String}
|
||||
*/
|
||||
const buildDescription = () =>
|
||||
description?.id
|
||||
? formatMessage(
|
||||
{ id: description.id, defaultMessage: description.defaultMessage },
|
||||
{ ...description.values }
|
||||
)
|
||||
: '';
|
||||
|
||||
/**
|
||||
* @returns {''|Array}
|
||||
*/
|
||||
const buildHint = () => {
|
||||
const { maximum, minimum } = getMinMax(fieldSchema);
|
||||
const units = getFieldUnits({
|
||||
type,
|
||||
minimum,
|
||||
maximum,
|
||||
});
|
||||
|
||||
const minIsNumber = typeof minimum === 'number';
|
||||
const maxIsNumber = typeof maximum === 'number';
|
||||
const hasMinAndMax = maxIsNumber && minIsNumber;
|
||||
const hasMinOrMax = maxIsNumber || minIsNumber;
|
||||
|
||||
if (!description?.id && !hasMinOrMax) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return formatMessage(
|
||||
{
|
||||
id: 'content-manager.form.Input.hint.text',
|
||||
defaultMessage:
|
||||
'{min, select, undefined {} other {min. {min}}}{divider}{max, select, undefined {} other {max. {max}}}{unit}{br}{description}',
|
||||
},
|
||||
{
|
||||
min: minimum,
|
||||
max: maximum,
|
||||
description: buildDescription(),
|
||||
unit: units?.message && hasMinOrMax ? formatMessage(units.message, units.values) : null,
|
||||
divider: hasMinAndMax
|
||||
? formatMessage({
|
||||
id: 'content-manager.form.Input.hint.minMaxDivider',
|
||||
defaultMessage: ' / ',
|
||||
})
|
||||
: null,
|
||||
br: hasMinOrMax ? <br /> : null,
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
return { hint: buildHint() };
|
||||
};
|
||||
|
||||
export default useFieldHint;
|
@ -0,0 +1,110 @@
|
||||
import React from 'react';
|
||||
import { renderHook, act } from '@testing-library/react-hooks';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
|
||||
import useFieldHint from '../index';
|
||||
|
||||
const messages = { 'message.id': 'response' };
|
||||
const knownDescription = { id: 'message.id', defaultMessage: '' };
|
||||
|
||||
// eslint-disable-next-line react/prop-types
|
||||
export const IntlWrapper = ({ children }) => (
|
||||
<IntlProvider locale="en" messages={messages} textComponent="span">
|
||||
{children}
|
||||
</IntlProvider>
|
||||
);
|
||||
|
||||
function setup(args) {
|
||||
return new Promise((resolve) => {
|
||||
act(() => {
|
||||
resolve(renderHook(() => useFieldHint(args), { wrapper: IntlWrapper }));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
describe('useFieldHint', () => {
|
||||
describe('descriptions', () => {
|
||||
test('generates a known description', async () => {
|
||||
const { result } = await setup({
|
||||
description: knownDescription,
|
||||
});
|
||||
|
||||
expect(result.current.hint).toEqual('response');
|
||||
});
|
||||
|
||||
test('fails to generate an unknown description', async () => {
|
||||
const { result } = await setup({
|
||||
description: {},
|
||||
});
|
||||
|
||||
expect(result.current.hint).toEqual('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('minimum/maximum limits', () => {
|
||||
test('generates a minimum limit', async () => {
|
||||
const minimum = 1;
|
||||
const fieldSchema = { min: minimum };
|
||||
|
||||
const { result } = await setup({
|
||||
fieldSchema,
|
||||
});
|
||||
|
||||
expect(result.current.hint.length).toEqual(3);
|
||||
|
||||
expect(result.current.hint[0]).toEqual(`min. ${minimum} character`);
|
||||
expect(result.current.hint[2]).toEqual('');
|
||||
});
|
||||
|
||||
test('generates a maximum limit', async () => {
|
||||
const maximum = 5;
|
||||
const fieldSchema = { max: maximum };
|
||||
|
||||
const { result } = await setup({
|
||||
fieldSchema,
|
||||
});
|
||||
|
||||
expect(result.current.hint.length).toEqual(3);
|
||||
|
||||
expect(result.current.hint[0]).toEqual(`max. ${maximum} characters`);
|
||||
expect(result.current.hint[2]).toEqual('');
|
||||
});
|
||||
|
||||
test('generates a minimum/maximum limits', async () => {
|
||||
const minimum = 1;
|
||||
const maximum = 5;
|
||||
const fieldSchema = { minLength: minimum, maxLength: maximum };
|
||||
|
||||
const { result } = await setup({
|
||||
fieldSchema,
|
||||
});
|
||||
|
||||
expect(result.current.hint.length).toEqual(3);
|
||||
|
||||
expect(result.current.hint).toContain(`min. ${minimum} / max. ${maximum} characters`);
|
||||
expect(result.current.hint[2]).toEqual('');
|
||||
});
|
||||
});
|
||||
|
||||
test('returns an empty string when there is no description or minimum and maximum limits', async () => {
|
||||
const { result } = await setup({});
|
||||
|
||||
expect(result.current.hint).toEqual('');
|
||||
});
|
||||
|
||||
test('generates the description and min max hint', async () => {
|
||||
const minimum = 1;
|
||||
const maximum = 5;
|
||||
const fieldSchema = { minLength: minimum, maxLength: maximum };
|
||||
|
||||
const { result } = await setup({
|
||||
description: knownDescription,
|
||||
fieldSchema,
|
||||
});
|
||||
|
||||
expect(result.current.hint.length).toEqual(3);
|
||||
|
||||
expect(result.current.hint[0]).toEqual(`min. ${minimum} / max. ${maximum} characters`);
|
||||
expect(result.current.hint[2]).toEqual('response');
|
||||
});
|
||||
});
|
@ -0,0 +1,22 @@
|
||||
/**
|
||||
* @type { ({ type?: string; minimum?: number; maximum: number; } ) => {
|
||||
* message?: {id: string, defaultMessage: string}; values?: {maxValue: number} } }
|
||||
*/
|
||||
const getFieldUnits = ({ type, minimum, maximum }) => {
|
||||
if (['biginteger', 'integer', 'number'].includes(type)) {
|
||||
return {};
|
||||
}
|
||||
const maxValue = Math.max(minimum || 0, maximum || 0);
|
||||
|
||||
return {
|
||||
message: {
|
||||
id: 'content-manager.form.Input.hint.character.unit',
|
||||
defaultMessage: '{maxValue, plural, one { character} other { characters}}',
|
||||
},
|
||||
values: {
|
||||
maxValue,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default getFieldUnits;
|
@ -0,0 +1,38 @@
|
||||
/**
|
||||
* Get the minimum and maximum limits for an input
|
||||
* @type {
|
||||
* (fieldSchema: { minLength?: number|string; maxLength?: number|string; max?: number|string; min?: number|string } )
|
||||
* => { maximum: number; minimum: number } }
|
||||
*/
|
||||
const getMinMax = (fieldSchema) => {
|
||||
if (!fieldSchema) {
|
||||
return { maximum: undefined, minimum: undefined };
|
||||
}
|
||||
|
||||
const { minLength, maxLength, max, min } = fieldSchema;
|
||||
|
||||
let minimum;
|
||||
let maximum;
|
||||
|
||||
const parsedMin = parseInt(min, 10);
|
||||
const parsedMinLength = parseInt(minLength, 10);
|
||||
|
||||
if (!Number.isNaN(parsedMin)) {
|
||||
minimum = parsedMin;
|
||||
} else if (!Number.isNaN(parsedMinLength)) {
|
||||
minimum = parsedMinLength;
|
||||
}
|
||||
|
||||
const parsedMax = parseInt(max, 10);
|
||||
const parsedMaxLength = parseInt(maxLength, 10);
|
||||
|
||||
if (!Number.isNaN(parsedMax)) {
|
||||
maximum = parsedMax;
|
||||
} else if (!Number.isNaN(parsedMaxLength)) {
|
||||
maximum = parsedMaxLength;
|
||||
}
|
||||
|
||||
return { maximum, minimum };
|
||||
};
|
||||
|
||||
export default getMinMax;
|
@ -0,0 +1,2 @@
|
||||
export { default as getFieldUnits } from './getFieldUnits';
|
||||
export { default as getMinMax } from './getMinMax';
|
@ -0,0 +1,29 @@
|
||||
import { getFieldUnits } from '../index';
|
||||
|
||||
describe('Content Manager | Inputs | Utils', () => {
|
||||
describe('getFieldUnits', () => {
|
||||
it('returns <empty> for number types', () => {
|
||||
expect(getFieldUnits({ type: 'number' })).toEqual({});
|
||||
});
|
||||
|
||||
it('returns <empty> for biginteger types', () => {
|
||||
expect(getFieldUnits({ type: 'biginteger' })).toEqual({});
|
||||
});
|
||||
|
||||
it('returns <empty> for integer types', () => {
|
||||
expect(getFieldUnits({ type: 'integer' })).toEqual({});
|
||||
});
|
||||
|
||||
it('correctly returns units translation object', () => {
|
||||
expect(getFieldUnits({ type: 'text', minimum: 1, maximum: 5 })).toEqual({
|
||||
message: {
|
||||
id: 'content-manager.form.Input.hint.character.unit',
|
||||
defaultMessage: '{maxValue, plural, one { character} other { characters}}',
|
||||
},
|
||||
values: {
|
||||
maxValue: 5,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,50 @@
|
||||
import getMinMax from '../getMinMax';
|
||||
|
||||
describe('Content Manager | Inputs | Utils', () => {
|
||||
describe('getMinMax', () => {
|
||||
it('ignores a blank schema', () => {
|
||||
expect(getMinMax({})).toEqual({ maximum: undefined, minimium: undefined });
|
||||
});
|
||||
|
||||
it('ignores a null schema', () => {
|
||||
expect(getMinMax(null)).toEqual({ maximum: undefined, minimium: undefined });
|
||||
});
|
||||
|
||||
it('ignores values provided as strings that cannot be parsed to integers', () => {
|
||||
const notANumber = 'NOT_A_NUMBER';
|
||||
const fieldSchema = {
|
||||
min: notANumber,
|
||||
max: notANumber,
|
||||
minLength: notANumber,
|
||||
maxLength: notANumber,
|
||||
};
|
||||
expect(getMinMax(fieldSchema)).toEqual({ maximum: undefined, minimum: undefined });
|
||||
});
|
||||
|
||||
it('correctly parses integer values from strings', () => {
|
||||
const fieldSchema = {
|
||||
min: '2',
|
||||
max: '5',
|
||||
};
|
||||
expect(getMinMax(fieldSchema)).toEqual({ maximum: 5, minimum: 2 });
|
||||
});
|
||||
|
||||
it('returns based on minLength and maxLength values', () => {
|
||||
const fieldSchema = {
|
||||
minLength: 10,
|
||||
maxLength: 20,
|
||||
};
|
||||
|
||||
expect(getMinMax(fieldSchema)).toEqual({ maximum: 20, minimum: 10 });
|
||||
});
|
||||
|
||||
it('returns based on min and max values', () => {
|
||||
const fieldSchema = {
|
||||
min: 10,
|
||||
max: 20,
|
||||
};
|
||||
|
||||
expect(getMinMax(fieldSchema)).toEqual({ maximum: 20, minimum: 10 });
|
||||
});
|
||||
});
|
||||
});
|
@ -85,7 +85,15 @@ const loadPlugins = async (strapi) => {
|
||||
for (const pluginName of Object.keys(enabledPlugins)) {
|
||||
const enabledPlugin = enabledPlugins[pluginName];
|
||||
|
||||
const serverEntrypointPath = join(enabledPlugin.pathToPlugin, 'strapi-server.js');
|
||||
let serverEntrypointPath;
|
||||
|
||||
try {
|
||||
serverEntrypointPath = join(enabledPlugin.pathToPlugin, 'strapi-server.js');
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
`Error loading the plugin ${pluginName} because ${pluginName} is not installed. Please either install the plugin or remove it's configuration.`
|
||||
);
|
||||
}
|
||||
|
||||
// only load plugins with a server entrypoint
|
||||
if (!(await fse.pathExists(serverEntrypointPath))) {
|
||||
|
@ -27,7 +27,8 @@ module.exports = (app) => {
|
||||
[chalk.blue('Environment'), app.config.environment],
|
||||
[chalk.blue('Process PID'), process.pid],
|
||||
[chalk.blue('Version'), `${app.config.info.strapi} (node ${process.version})`],
|
||||
[chalk.blue('Edition'), isEE ? 'Enterprise' : 'Community']
|
||||
[chalk.blue('Edition'), isEE ? 'Enterprise' : 'Community'],
|
||||
[chalk.blue('Database'), app.db.dialect.client]
|
||||
);
|
||||
|
||||
console.log(infoTable.toString());
|
||||
|
@ -1,151 +0,0 @@
|
||||
{
|
||||
"Analytics": "Analytics",
|
||||
"Content Manager": "Content Manager",
|
||||
"Content Type Builder": " Content Types Builder",
|
||||
"Email": "Email",
|
||||
"Files Upload": "Files Upload",
|
||||
"HomePage.notification.newsLetter.success": "Successfully subscribed to the newsletter",
|
||||
"New entry": "New entry",
|
||||
"Password": "Password",
|
||||
"Provider": "Provider",
|
||||
"ResetPasswordToken": "Reset Password Token",
|
||||
"Role": "Role",
|
||||
"Roles & Permissions": "Roles & Permission",
|
||||
"Settings Manager": "Settings Manager",
|
||||
"Username": "Username",
|
||||
"Users": "Users",
|
||||
"Users & Permissions": "Users & Permissions",
|
||||
"app.components.BlockLink.code": "Code examples",
|
||||
"app.components.BlockLink.code.content": "Learn by testing real projects developed the community.",
|
||||
"app.components.BlockLink.documentation": "Read the documentation",
|
||||
"app.components.BlockLink.documentation.content": "Discover the concepts, reference guides and tutorials.",
|
||||
"app.components.Button.cancel": "Cancel",
|
||||
"app.components.Button.save": "Save",
|
||||
"app.components.ComingSoonPage.comingSoon": "Coming soon",
|
||||
"app.components.ComingSoonPage.featuresNotAvailable": "This feature is still under active development.",
|
||||
"app.components.DownloadInfo.download": "Download in progress...",
|
||||
"app.components.DownloadInfo.text": "This could take a minute. Thanks for your patience.",
|
||||
"app.components.EmptyAttributes.title": "There are no fields yet",
|
||||
"app.components.HomePage.button.blog": "SEE MORE ON THE BLOG",
|
||||
"app.components.HomePage.button.quickStart": "START THE QUICK START TUTORIAL",
|
||||
"app.components.HomePage.community": "Find the community on the web",
|
||||
"app.components.HomePage.community.content": "Discuss with team members, contributors and developers on different channels.",
|
||||
"app.components.HomePage.createBlock.content.first": "The ",
|
||||
"app.components.HomePage.createBlock.content.second": " plugin will help you to define the data structure of your models. If you’re new here, we highly recommend you to follow our ",
|
||||
"app.components.HomePage.createBlock.content.tutorial": " tutorial.",
|
||||
"app.components.HomePage.cta": "CONFIRM",
|
||||
"app.components.HomePage.newsLetter": "Subscribe to the newsletter to get in touch about Strapi",
|
||||
"app.components.HomePage.support": "SUPPORT US",
|
||||
"app.components.HomePage.support.content": "By buying the T-shirt, it will allow us to continue our work on the project to give you the best possible experience!",
|
||||
"app.components.HomePage.support.link": "GET YOUR T-SHIRT NOW",
|
||||
"app.components.HomePage.welcome": "Welcome on board!",
|
||||
"app.components.HomePage.welcome.again": "Welcome ",
|
||||
"app.components.HomePage.welcomeBlock.content": "We are happy to have you as part of the community. We are constantly looking for feedback so feel free to send us DM on ",
|
||||
"app.components.HomePage.welcomeBlock.content.again": "We hope you are making progress on your project... Feel free to read the latest new about Strapi. We are giving our best to improve the product based on your feedback.",
|
||||
"app.components.HomePage.welcomeBlock.content.issues": "issues.",
|
||||
"app.components.HomePage.welcomeBlock.content.raise": " or raise ",
|
||||
"app.components.ImgPreview.hint": "Drag & drop your file into this area or {browse} for a file to upload",
|
||||
"app.components.ImgPreview.hint.browse": "browse",
|
||||
"app.components.InputFile.newFile": "Add new file",
|
||||
"app.components.InputFileDetails.open": "Open in a new tab",
|
||||
"app.components.InputFileDetails.originalName": "Original name:",
|
||||
"app.components.InputFileDetails.remove": "Remove this file",
|
||||
"app.components.InputFileDetails.size": "Size:",
|
||||
"app.components.InstallPluginPage.Download.title": "Downloading...",
|
||||
"app.components.InstallPluginPage.Download.description": "It might take a few seconds to download and install the plugin.",
|
||||
"app.components.InstallPluginPage.InputSearch.label": " ",
|
||||
"app.components.InstallPluginPage.InputSearch.placeholder": "Search for a plugin... (ex: authentication)",
|
||||
"app.components.InstallPluginPage.description": "Extend your app effortlessly.",
|
||||
"app.components.InstallPluginPage.helmet": "Marketplace - Plugins",
|
||||
"app.components.InstallPluginPage.plugin.support-us.description": "Support us by buying the Strapi T-shirt. That will allow us to keep working on the project and try giving you the best possible experience!",
|
||||
"app.components.InstallPluginPage.title": "Marketplace - Plugins",
|
||||
"app.components.InstallPluginPopup.downloads": "download",
|
||||
"app.components.InstallPluginPopup.navLink.avis": "avis",
|
||||
"app.components.InstallPluginPopup.navLink.changelog": "changelog",
|
||||
"app.components.InstallPluginPopup.navLink.description": "Description",
|
||||
"app.components.InstallPluginPopup.navLink.faq": "faq",
|
||||
"app.components.InstallPluginPopup.navLink.screenshots": "Screenshots",
|
||||
"app.components.InstallPluginPopup.noDescription": "No description available",
|
||||
"app.components.LeftMenuFooter.documentation": "Documentation",
|
||||
"app.components.LeftMenuFooter.help": "Help",
|
||||
"app.components.LeftMenuFooter.poweredBy": "Powered by ",
|
||||
"app.components.LeftMenuLinkContainer.configuration": "Configurations",
|
||||
"app.components.LeftMenuLinkContainer.general": "General",
|
||||
"app.components.LeftMenuLinkContainer.installNewPlugin": "Marketplace",
|
||||
"app.components.LeftMenuLinkContainer.listPlugins": "Plugins",
|
||||
"app.components.LeftMenuLinkContainer.noPluginsInstalled": "No plugins installed yet",
|
||||
"app.components.LeftMenuLinkContainer.plugins": "Plugins",
|
||||
"app.components.ListPluginsPage.description": "List of the installed plugins in the project.",
|
||||
"app.components.ListPluginsPage.helmet.title": "List plugins",
|
||||
"app.components.ListPluginsPage.title": "Plugins",
|
||||
"app.components.Logout.profile": "Profile",
|
||||
"app.components.Logout.logout": "Logout",
|
||||
"app.components.NotFoundPage.back": "Back to homepage",
|
||||
"app.components.NotFoundPage.description": "Not Found",
|
||||
"app.components.Official": "Official",
|
||||
"app.components.Onboarding.label.completed": "% completed",
|
||||
"app.components.Onboarding.title": "Get Started Videos",
|
||||
"app.components.PluginCard.Button.label.download": "Download",
|
||||
"app.components.PluginCard.Button.label.install": "Already installed",
|
||||
"app.components.PluginCard.Button.label.support": "Support us",
|
||||
"app.components.PluginCard.compatible": "Compatible with your app",
|
||||
"app.components.PluginCard.compatibleCommunity": "Compatible with the community",
|
||||
"app.components.PluginCard.more-details": "More details",
|
||||
"app.components.PluginCard.price.free": "Free",
|
||||
"app.components.PluginCard.settings": "Settings",
|
||||
"app.components.listPlugins.button": "Add New Plugin",
|
||||
"app.components.listPlugins.title.none": "No plugins installed",
|
||||
"app.components.listPlugins.title.plural": "{number} plugins are installed",
|
||||
"app.components.listPlugins.title.singular": "{number} plugin is installed",
|
||||
"app.components.listPluginsPage.deletePlugin.error": "An error occurred while uninstalling the plugin",
|
||||
"app.utils.SelectOption.defaultMessage": " ",
|
||||
"app.utils.defaultMessage": " ",
|
||||
"app.utils.placeholder.defaultMessage": " ",
|
||||
"components.AutoReloadBlocker.description": "Open the following file and enable the feature.",
|
||||
"components.AutoReloadBlocker.header": "Reload feature is required for this plugin.",
|
||||
"components.ErrorBoundary.title": "Something went wrong...",
|
||||
"components.Input.error.attribute.key.taken": "This value already exists",
|
||||
"components.Input.error.attribute.sameKeyAndName": "Can't be equal",
|
||||
"components.Input.error.attribute.taken": "This field name already exists",
|
||||
"components.Input.error.contentTypeName.taken": "This name already exists",
|
||||
"components.Input.error.custom-error": "{errorMessage} ",
|
||||
"components.Input.error.validation.email": "This is an invalid email",
|
||||
"components.Input.error.validation.json": "This doesn't match the JSON format",
|
||||
"components.Input.error.validation.max": "The value is too high.",
|
||||
"components.Input.error.validation.maxLength": "The value is too long.",
|
||||
"components.Input.error.validation.min": "The value is too low.",
|
||||
"components.Input.error.validation.minLength": "The value is too short.",
|
||||
"components.Input.error.validation.minSupMax": "Can't be superior",
|
||||
"components.Input.error.validation.regex": "The value does not match the regex.",
|
||||
"components.Input.error.validation.required": "This value is required.",
|
||||
"components.ListRow.empty": "There is no data to be shown.",
|
||||
"components.OverlayBlocker.description": "You're using a feature that needs the server to restart. Please wait until the server is up.",
|
||||
"components.OverlayBlocker.description.serverError": "The server should have restarted, please check your logs in the terminal.",
|
||||
"components.OverlayBlocker.title": "Waiting for restart...",
|
||||
"components.OverlayBlocker.title.serverError": "The restart is taking longer than expected",
|
||||
"components.PageFooter.select": "entries per page",
|
||||
"components.ProductionBlocker.description": "For safety purposes we have to disable this plugin in other environments.",
|
||||
"components.ProductionBlocker.header": "This plugin is only available in development.",
|
||||
"components.Wysiwyg.ToggleMode.markdown": "Switch to markdown",
|
||||
"components.Wysiwyg.ToggleMode.preview": "Switch to preview",
|
||||
"components.Wysiwyg.collapse": "Collapse",
|
||||
"components.Wysiwyg.selectOptions.H1": "Title H1",
|
||||
"components.Wysiwyg.selectOptions.H2": "Title H2",
|
||||
"components.Wysiwyg.selectOptions.H3": "Title H3",
|
||||
"components.Wysiwyg.selectOptions.H4": "Title H4",
|
||||
"components.Wysiwyg.selectOptions.H5": "Title H5",
|
||||
"components.Wysiwyg.selectOptions.H6": "Title H6",
|
||||
"components.Wysiwyg.selectOptions.title": "Add a title",
|
||||
"components.WysiwygBottomControls.charactersIndicators": "characters",
|
||||
"components.WysiwygBottomControls.fullscreen": "Expand",
|
||||
"components.WysiwygBottomControls.uploadFiles": "Drag & drop files, paste from the clipboard or {browse}.",
|
||||
"components.WysiwygBottomControls.uploadFiles.browse": "select them",
|
||||
"components.popUpWarning.button.cancel": "Cancel",
|
||||
"components.popUpWarning.button.confirm": "Confirm",
|
||||
"components.popUpWarning.message": "Are you sure you want to delete this?",
|
||||
"components.popUpWarning.title": "Please confirm",
|
||||
"notification.error": "An error occurred",
|
||||
"notification.error.layout": "Couldn't retrieve the layout",
|
||||
"request.error.model.unknown": "This model doesn't exist",
|
||||
"app.utils.delete": "Delete"
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
import commonTrads from './commonTrads.json';
|
||||
|
||||
const formatMessagesWithPluginId = (pluginId, messages) => {
|
||||
return Object.keys(messages).reduce((acc, current) => {
|
||||
acc[`${pluginId}.${current}`] = messages[current];
|
||||
|
||||
return acc;
|
||||
}, commonTrads);
|
||||
};
|
||||
|
||||
export default formatMessagesWithPluginId;
|
23
yarn.lock
23
yarn.lock
@ -8939,21 +8939,16 @@ color@^4.2.3:
|
||||
color-convert "^2.0.1"
|
||||
color-string "^1.9.0"
|
||||
|
||||
colorette@2.0.16:
|
||||
version "2.0.16"
|
||||
resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.16.tgz#713b9af84fdb000139f04546bd4a93f62a5085da"
|
||||
integrity sha512-hUewv7oMjCp+wkBv5Rm0v87eJhq4woh5rSR+42YSQJKecCqgIqNkZ6lAlQms/BwHPJA5NKMRlpxPRv0n8HQW6g==
|
||||
colorette@2.0.19, colorette@^2.0.10, colorette@^2.0.14, colorette@^2.0.16, colorette@^2.0.17:
|
||||
version "2.0.19"
|
||||
resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.19.tgz#cdf044f47ad41a0f4b56b3a0d5b4e6e1a2d5a798"
|
||||
integrity sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==
|
||||
|
||||
colorette@^1.2.2:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.4.0.tgz#5190fbb87276259a86ad700bff2c6d6faa3fca40"
|
||||
integrity sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==
|
||||
|
||||
colorette@^2.0.10, colorette@^2.0.14, colorette@^2.0.16, colorette@^2.0.17:
|
||||
version "2.0.19"
|
||||
resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.19.tgz#cdf044f47ad41a0f4b56b3a0d5b4e6e1a2d5a798"
|
||||
integrity sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==
|
||||
|
||||
colors@~1.2.1:
|
||||
version "1.2.5"
|
||||
resolved "https://registry.yarnpkg.com/colors/-/colors-1.2.5.tgz#89c7ad9a374bc030df8013241f68136ed8835afc"
|
||||
@ -14786,12 +14781,12 @@ klona@^2.0.4:
|
||||
resolved "https://registry.yarnpkg.com/klona/-/klona-2.0.5.tgz#d166574d90076395d9963aa7a928fabb8d76afbc"
|
||||
integrity sha512-pJiBpiXMbt7dkzXe8Ghj/u4FfXOOa98fPW+bihOJ4SjnoijweJrNThJfd3ifXpXhREjpoF2mZVH1GfS9LV3kHQ==
|
||||
|
||||
knex@1.0.7:
|
||||
version "1.0.7"
|
||||
resolved "https://registry.yarnpkg.com/knex/-/knex-1.0.7.tgz#965f4490efc451b140aac4c5c6efa39fd877597b"
|
||||
integrity sha512-89jxuRATt4qJMb9ZyyaKBy0pQ4d5h7eOFRqiNFnUvsgU+9WZ2eIaZKrAPG1+F3mgu5UloPUnkVE5Yo2sKZUs6Q==
|
||||
knex@2.4.0:
|
||||
version "2.4.0"
|
||||
resolved "https://registry.yarnpkg.com/knex/-/knex-2.4.0.tgz#7d33cc36f320cdac98741010544b4c6a98b8b19e"
|
||||
integrity sha512-i0GWwqYp1Hs2yvc2rlDO6nzzkLhwdyOZKRdsMTB8ZxOs2IXQyL5rBjSbS1krowCh6V65T4X9CJaKtuIfkaPGSA==
|
||||
dependencies:
|
||||
colorette "2.0.16"
|
||||
colorette "2.0.19"
|
||||
commander "^9.1.0"
|
||||
debug "4.3.4"
|
||||
escalade "^3.1.1"
|
||||
|
Loading…
x
Reference in New Issue
Block a user