Merge branch 'main' of github.com:strapi/strapi into features/deits

This commit is contained in:
Convly 2023-01-24 15:07:37 +01:00
commit 516963a080
25 changed files with 577 additions and 505 deletions

View File

@ -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,

View File

@ -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;

View File

@ -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({

View File

@ -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>
</Box>
</Stack>
);
};
export default Informations;
export default Information;

View File

@ -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();
});
});

View File

@ -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>
`);
});
});

View File

@ -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">

View File

@ -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})",

View File

@ -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);

View File

@ -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() {

View File

@ -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"

View File

@ -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,

View File

@ -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;

View File

@ -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');
});
});

View File

@ -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;

View File

@ -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;

View File

@ -0,0 +1,2 @@
export { default as getFieldUnits } from './getFieldUnits';
export { default as getMinMax } from './getMinMax';

View File

@ -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,
},
});
});
});
});

View File

@ -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 });
});
});
});

View File

@ -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))) {

View File

@ -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());

View File

@ -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 youre 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"
}

View File

@ -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;

View File

@ -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"