diff --git a/packages/core/admin/admin/src/content-manager/components/Hint/index.js b/packages/core/admin/admin/src/content-manager/components/Hint/index.js index 92822a9be9..a4d7daaf0e 100644 --- a/packages/core/admin/admin/src/content-manager/components/Hint/index.js +++ b/packages/core/admin/admin/src/content-manager/components/Hint/index.js @@ -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, diff --git a/packages/core/admin/admin/src/content-manager/components/InputUID/index.js b/packages/core/admin/admin/src/content-manager/components/InputUID/index.js index ea443a1382..2c79c74a24 100644 --- a/packages/core/admin/admin/src/content-manager/components/InputUID/index.js +++ b/packages/core/admin/admin/src/content-manager/components/InputUID/index.js @@ -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; diff --git a/packages/core/admin/admin/src/content-manager/components/Wysiwyg/index.js b/packages/core/admin/admin/src/content-manager/components/Wysiwyg/index.js index d8fdbe8e3c..cdf094b577 100644 --- a/packages/core/admin/admin/src/content-manager/components/Wysiwyg/index.js +++ b/packages/core/admin/admin/src/content-manager/components/Wysiwyg/index.js @@ -31,7 +31,7 @@ const TypographyAsterisk = styled(Typography)` `; const Wysiwyg = ({ - description, + hint, disabled, error, intlLabel, @@ -167,7 +167,7 @@ const Wysiwyg = ({ {!isExpandMode && } - + {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({ diff --git a/packages/core/admin/admin/src/content-manager/pages/EditView/Informations/index.js b/packages/core/admin/admin/src/content-manager/pages/EditView/Information/index.js similarity index 53% rename from packages/core/admin/admin/src/content-manager/pages/EditView/Informations/index.js rename to packages/core/admin/admin/src/content-manager/pages/EditView/Information/index.js index 3e3aa43616..092a48fa04 100644 --- a/packages/core/admin/admin/src/content-manager/pages/EditView/Informations/index.js +++ b/packages/core/admin/admin/src/content-manager/pages/EditView/Information/index.js @@ -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 ( + + + {label} + + {value} + + ); +}; + +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 ( - - + + {formatMessage({ id: getTrad('containers.Edit.information'), defaultMessage: 'Information', })} - + + + - - - {formatMessage({ + + - {created.at} - - - - {formatMessage({ + value={created.at} + /> + + - {created.by} - - - - {formatMessage({ + value={created.by} + /> + + + + - {updated.at} - - - - {formatMessage({ + value={updated.at} + /> + + - {updated.by} - + value={updated.by} + /> + - + ); }; -export default Informations; +export default Information; diff --git a/packages/core/admin/admin/src/content-manager/pages/EditView/Information/tests/index.test.js b/packages/core/admin/admin/src/content-manager/pages/EditView/Information/tests/index.test.js new file mode 100644 index 0000000000..39b8f0e113 --- /dev/null +++ b/packages/core/admin/admin/src/content-manager/pages/EditView/Information/tests/index.test.js @@ -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 ( + + + + + + + + ); +}; + +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(); + }); +}); diff --git a/packages/core/admin/admin/src/content-manager/pages/EditView/Informations/utils/getUnits.js b/packages/core/admin/admin/src/content-manager/pages/EditView/Information/utils/getUnits.js similarity index 100% rename from packages/core/admin/admin/src/content-manager/pages/EditView/Informations/utils/getUnits.js rename to packages/core/admin/admin/src/content-manager/pages/EditView/Information/utils/getUnits.js diff --git a/packages/core/admin/admin/src/content-manager/pages/EditView/Informations/tests/index.test.js b/packages/core/admin/admin/src/content-manager/pages/EditView/Informations/tests/index.test.js deleted file mode 100644 index ce3d0c4cbe..0000000000 --- a/packages/core/admin/admin/src/content-manager/pages/EditView/Informations/tests/index.test.js +++ /dev/null @@ -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 ( - - - - - - - - ); -}; - -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; - } - -
- - Information - -
-
-
-
-
- - Created - - - now - -
-
- - By - - - - - -
-
- - Last update - - - now - -
-
- - By - - - - - -
-
-
- `); - }); -}); diff --git a/packages/core/admin/admin/src/content-manager/pages/EditView/index.js b/packages/core/admin/admin/src/content-manager/pages/EditView/index.js index a692cf0f5c..ea4e4b654d 100644 --- a/packages/core/admin/admin/src/content-manager/pages/EditView/index.js +++ b/packages/core/admin/admin/src/content-manager/pages/EditView/index.js @@ -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 - + diff --git a/packages/core/admin/admin/src/translations/en.json b/packages/core/admin/admin/src/translations/en.json index 1f5b787d42..80e8ffdc46 100644 --- a/packages/core/admin/admin/src/translations/en.json +++ b/packages/core/admin/admin/src/translations/en.json @@ -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})", diff --git a/packages/core/content-manager/server/services/utils/configuration/metadatas.js b/packages/core/content-manager/server/services/utils/configuration/metadatas.js index 5033970694..406055e1b0 100644 --- a/packages/core/content-manager/server/services/utils/configuration/metadatas.js +++ b/packages/core/content-manager/server/services/utils/configuration/metadatas.js @@ -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); diff --git a/packages/core/database/lib/dialects/postgresql/index.js b/packages/core/database/lib/dialects/postgresql/index.js index b865534f4f..431f452eaf 100644 --- a/packages/core/database/lib/dialects/postgresql/index.js +++ b/packages/core/database/lib/dialects/postgresql/index.js @@ -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() { diff --git a/packages/core/database/package.json b/packages/core/database/package.json index df2ca89ca5..4014f0b8f0 100644 --- a/packages/core/database/package.json +++ b/packages/core/database/package.json @@ -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" diff --git a/packages/core/helper-plugin/lib/src/components/GenericInput/index.js b/packages/core/helper-plugin/lib/src/components/GenericInput/index.js index 1b584b0076..aa5e1910a1 100644 --- a/packages/core/helper-plugin/lib/src/components/GenericInput/index.js +++ b/packages/core/helper-plugin/lib/src/components/GenericInput/index.js @@ -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 ( `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, diff --git a/packages/core/helper-plugin/lib/src/hooks/useFieldHint/index.js b/packages/core/helper-plugin/lib/src/hooks/useFieldHint/index.js new file mode 100644 index 0000000000..a5db9e2b9d --- /dev/null +++ b/packages/core/helper-plugin/lib/src/hooks/useFieldHint/index.js @@ -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 ?
: null, + } + ); + }; + + return { hint: buildHint() }; +}; + +export default useFieldHint; diff --git a/packages/core/helper-plugin/lib/src/hooks/useFieldHint/tests/useFieldHint.test.js b/packages/core/helper-plugin/lib/src/hooks/useFieldHint/tests/useFieldHint.test.js new file mode 100644 index 0000000000..e27656d44e --- /dev/null +++ b/packages/core/helper-plugin/lib/src/hooks/useFieldHint/tests/useFieldHint.test.js @@ -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 }) => ( + + {children} + +); + +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'); + }); +}); diff --git a/packages/core/helper-plugin/lib/src/hooks/useFieldHint/utils/getFieldUnits.js b/packages/core/helper-plugin/lib/src/hooks/useFieldHint/utils/getFieldUnits.js new file mode 100644 index 0000000000..8a129af6b2 --- /dev/null +++ b/packages/core/helper-plugin/lib/src/hooks/useFieldHint/utils/getFieldUnits.js @@ -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; diff --git a/packages/core/helper-plugin/lib/src/hooks/useFieldHint/utils/getMinMax.js b/packages/core/helper-plugin/lib/src/hooks/useFieldHint/utils/getMinMax.js new file mode 100644 index 0000000000..453092c4ba --- /dev/null +++ b/packages/core/helper-plugin/lib/src/hooks/useFieldHint/utils/getMinMax.js @@ -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; diff --git a/packages/core/helper-plugin/lib/src/hooks/useFieldHint/utils/index.js b/packages/core/helper-plugin/lib/src/hooks/useFieldHint/utils/index.js new file mode 100644 index 0000000000..9cd686e859 --- /dev/null +++ b/packages/core/helper-plugin/lib/src/hooks/useFieldHint/utils/index.js @@ -0,0 +1,2 @@ +export { default as getFieldUnits } from './getFieldUnits'; +export { default as getMinMax } from './getMinMax'; diff --git a/packages/core/helper-plugin/lib/src/hooks/useFieldHint/utils/tests/getFieldUnits.test.js b/packages/core/helper-plugin/lib/src/hooks/useFieldHint/utils/tests/getFieldUnits.test.js new file mode 100644 index 0000000000..2921f28fd0 --- /dev/null +++ b/packages/core/helper-plugin/lib/src/hooks/useFieldHint/utils/tests/getFieldUnits.test.js @@ -0,0 +1,29 @@ +import { getFieldUnits } from '../index'; + +describe('Content Manager | Inputs | Utils', () => { + describe('getFieldUnits', () => { + it('returns for number types', () => { + expect(getFieldUnits({ type: 'number' })).toEqual({}); + }); + + it('returns for biginteger types', () => { + expect(getFieldUnits({ type: 'biginteger' })).toEqual({}); + }); + + it('returns 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, + }, + }); + }); + }); +}); diff --git a/packages/core/helper-plugin/lib/src/hooks/useFieldHint/utils/tests/getMinMax.test.js b/packages/core/helper-plugin/lib/src/hooks/useFieldHint/utils/tests/getMinMax.test.js new file mode 100644 index 0000000000..ea36389cc3 --- /dev/null +++ b/packages/core/helper-plugin/lib/src/hooks/useFieldHint/utils/tests/getMinMax.test.js @@ -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 }); + }); + }); +}); diff --git a/packages/core/strapi/lib/core/loaders/plugins/index.js b/packages/core/strapi/lib/core/loaders/plugins/index.js index d7b1b8e39b..bc6f5e8b3b 100644 --- a/packages/core/strapi/lib/core/loaders/plugins/index.js +++ b/packages/core/strapi/lib/core/loaders/plugins/index.js @@ -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))) { diff --git a/packages/core/strapi/lib/utils/startup-logger.js b/packages/core/strapi/lib/utils/startup-logger.js index 4aada5632c..0774c8fe11 100644 --- a/packages/core/strapi/lib/utils/startup-logger.js +++ b/packages/core/strapi/lib/utils/startup-logger.js @@ -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()); diff --git a/test/config/front/testUtils/commonTrads.json b/test/config/front/testUtils/commonTrads.json deleted file mode 100644 index 0569a5827e..0000000000 --- a/test/config/front/testUtils/commonTrads.json +++ /dev/null @@ -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" -} diff --git a/test/config/front/testUtils/formatMessages.js b/test/config/front/testUtils/formatMessages.js deleted file mode 100644 index 2e170b97c8..0000000000 --- a/test/config/front/testUtils/formatMessages.js +++ /dev/null @@ -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; diff --git a/yarn.lock b/yarn.lock index 1254bc6f36..3ec116a4f6 100644 --- a/yarn.lock +++ b/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"