Merge branch 'master' of github.com:strapi/strapi into features/marketplace-offline-layout

This commit is contained in:
Mark Kaylor 2022-04-01 09:45:35 +02:00
commit 5838c0619e
124 changed files with 1790 additions and 897 deletions

2
.github/FUNDING.yml vendored
View File

@ -1 +1 @@
open_collective: strapi
open_collective: strapi

View File

@ -1,6 +1,6 @@
{
"name": "check-pr-status",
"version": "4.1.5",
"version": "4.1.6",
"main": "dist/index.js",
"license": "MIT",
"private": true,

View File

@ -7,5 +7,5 @@ export ENV_PATH="$(pwd)/testApp/.env"
opts=($DB_OPTIONS)
yarn run -s test:generate-app "${opts[@]}" $@
yarn run -s test:e2e
yarn run -s test:generate-app "${opts[@]}"
yarn run -s test:e2e $@

View File

@ -15,9 +15,10 @@ jobs:
node: [12, 14, 16]
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2-beta
- uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node }}
cache: yarn
- uses: ./.github/actions/install-modules
- name: Run lint
run: yarn run -s lint
@ -33,9 +34,10 @@ jobs:
node: [12, 14, 16]
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2-beta
- uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node }}
cache: yarn
- uses: ./.github/actions/install-modules
with:
globalPackages: codecov
@ -53,9 +55,10 @@ jobs:
node: [12, 14, 16]
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2-beta
- uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node }}
cache: yarn
- uses: ./.github/actions/install-modules
with:
globalPackages: codecov
@ -71,7 +74,6 @@ jobs:
strategy:
matrix:
node: [12, 14, 16]
max-parallel: 3
services:
postgres:
# Docker Hub image
@ -92,9 +94,10 @@ jobs:
- 5432:5432
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2-beta
- uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node }}
cache: yarn
- uses: ./.github/actions/install-modules
- uses: ./.github/actions/run-e2e-tests
with:
@ -107,7 +110,6 @@ jobs:
strategy:
matrix:
node: [12, 14, 16]
max-parallel: 3
services:
mysql:
image: mysql
@ -143,7 +145,6 @@ jobs:
strategy:
matrix:
node: [12, 14, 16]
max-parallel: 3
services:
mysql:
image: mysql:5
@ -175,11 +176,11 @@ jobs:
e2e_ce_sqlite:
runs-on: ubuntu-latest
needs: [lint, unit_back, unit_front]
name: '[CE] E2E (sqlite, node: ${{ matrix.node }})'
name: '[CE] E2E (sqlite: ${{ matrix.sqlite_pkg }}, node: ${{ matrix.node }})'
strategy:
matrix:
node: [12, 14, 16]
max-parallel: 3
sqlite_pkg: ['better-sqlite3', 'sqlite3', '@vscode/sqlite3']
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
@ -188,9 +189,10 @@ jobs:
cache: yarn
- uses: ./.github/actions/install-modules
- uses: ./.github/actions/run-e2e-tests
env:
SQLITE_PKG: ${{ matrix.sqlite_pkg }}
with:
dbOptions: '--dbclient=sqlite --dbfile=./tmp/data.db'
dbOptions: '--dbclient=sqlite-legacy --dbfile=./tmp/data.db'
# EE
e2e_ee_pg:
runs-on: ubuntu-latest
@ -202,7 +204,6 @@ jobs:
strategy:
matrix:
node: [12, 14, 16]
max-parallel: 3
services:
postgres:
# Docker Hub image
@ -243,7 +244,6 @@ jobs:
strategy:
matrix:
node: [12, 14, 16]
max-parallel: 3
services:
mysql:
image: mysql
@ -276,14 +276,14 @@ jobs:
e2e_ee_sqlite:
runs-on: ubuntu-latest
needs: [lint, unit_back, unit_front]
name: '[EE] E2E (sqlite, node: ${{ matrix.node }})'
name: '[EE] E2E (sqlite: ${{ matrix.sqlite_pkg }}, node: ${{ matrix.node }})'
if: github.event.pull_request.head.repo.full_name == github.repository && !(github.actor == 'dependabot[bot]' || github.actor == 'dependabot-preview[bot]')
env:
STRAPI_LICENSE: ${{ secrets.strapiLicense }}
strategy:
matrix:
node: [12, 14, 16]
max-parallel: 3
sqlite_pkg: ['better-sqlite3', 'sqlite3', '@vscode/sqlite3']
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
@ -292,6 +292,8 @@ jobs:
cache: yarn
- uses: ./.github/actions/install-modules
- uses: ./.github/actions/run-e2e-tests
env:
SQLITE_PKG: ${{ matrix.sqlite_pkg }}
with:
dbOptions: '--dbclient=sqlite --dbfile=./tmp/data.db'
runEE: true

View File

@ -1,7 +1,7 @@
{
"name": "getstarted",
"private": true,
"version": "4.1.5",
"version": "4.1.6",
"description": "A Strapi application.",
"scripts": {
"develop": "strapi develop",
@ -12,17 +12,19 @@
"strapi": "strapi"
},
"dependencies": {
"@strapi/admin": "4.1.5",
"@strapi/plugin-documentation": "4.1.5",
"@strapi/plugin-graphql": "4.1.5",
"@strapi/plugin-i18n": "4.1.5",
"@strapi/plugin-sentry": "4.1.5",
"@strapi/plugin-users-permissions": "4.1.5",
"@strapi/provider-email-mailgun": "4.1.5",
"@strapi/provider-upload-aws-s3": "4.1.5",
"@strapi/provider-upload-cloudinary": "4.1.5",
"@strapi/strapi": "4.1.5",
"@strapi/utils": "4.1.5",
"@strapi/admin": "4.1.6",
"@strapi/plugin-documentation": "4.1.6",
"@strapi/plugin-graphql": "4.1.6",
"@strapi/plugin-i18n": "4.1.6",
"@strapi/plugin-sentry": "4.1.6",
"@strapi/plugin-users-permissions": "4.1.6",
"@strapi/provider-email-mailgun": "4.1.6",
"@strapi/provider-upload-aws-s3": "4.1.6",
"@strapi/provider-upload-cloudinary": "4.1.6",
"@strapi/strapi": "4.1.6",
"@strapi/utils": "4.1.6",
"@vscode/sqlite3": "5.0.8",
"better-sqlite3": "7.5.0",
"lodash": "4.17.21",
"mysql": "2.18.1",
"passport-google-oauth2": "0.2.0",

View File

@ -1,7 +1,7 @@
{
"name": "kitchensink",
"private": true,
"version": "4.1.5",
"version": "4.1.6",
"description": "A Strapi application.",
"scripts": {
"develop": "strapi develop",
@ -12,12 +12,12 @@
"strapi": "strapi"
},
"dependencies": {
"@strapi/admin": "4.1.5",
"@strapi/provider-email-mailgun": "4.1.5",
"@strapi/provider-upload-aws-s3": "4.1.5",
"@strapi/provider-upload-cloudinary": "4.1.5",
"@strapi/strapi": "4.1.5",
"@strapi/utils": "4.1.5",
"@strapi/admin": "4.1.6",
"@strapi/provider-email-mailgun": "4.1.6",
"@strapi/provider-upload-aws-s3": "4.1.6",
"@strapi/provider-upload-cloudinary": "4.1.6",
"@strapi/strapi": "4.1.6",
"@strapi/utils": "4.1.6",
"lodash": "4.17.21",
"mysql": "2.18.1",
"passport-google-oauth2": "0.2.0",

View File

@ -1,5 +1,5 @@
{
"version": "4.1.5",
"version": "4.1.6",
"packages": [
"packages/*",
"examples/*"

View File

@ -90,7 +90,7 @@
"eslint-plugin-jsx-a11y": "6.5.1",
"eslint-plugin-node": "11.1.0",
"eslint-plugin-react": "7.29.4",
"eslint-plugin-react-hooks": "4.3.0",
"eslint-plugin-react-hooks": "4.4.0",
"eslint-plugin-redux-saga": "1.3.2",
"execa": "1.0.0",
"fs-extra": "10.0.1",

View File

@ -1,6 +1,6 @@
{
"name": "@strapi/admin-test-utils",
"version": "4.1.5",
"version": "4.1.6",
"private": true,
"description": "Test utilities for the Strapi administration panel",
"license": "MIT",
@ -21,7 +21,7 @@
"@babel/polyfill": "7.12.1"
},
"devDependencies": {
"@testing-library/jest-dom": "5.16.2",
"@testing-library/jest-dom": "5.16.3",
"jest-styled-components": "7.0.2"
},
"scripts": {

View File

@ -1,6 +1,6 @@
{
"name": "create-strapi-app",
"version": "4.1.5",
"version": "4.1.6",
"description": "Generate a new Strapi application.",
"keywords": [
"create-strapi-app",
@ -38,7 +38,7 @@
"test": "echo \"no tests yet\""
},
"dependencies": {
"@strapi/generate-new": "4.1.5",
"@strapi/generate-new": "4.1.6",
"commander": "6.1.0",
"inquirer": "8.2.0"
},

View File

@ -1,6 +1,6 @@
{
"name": "create-strapi-starter",
"version": "4.1.5",
"version": "4.1.6",
"description": "Generate a new Strapi application.",
"keywords": [
"create-strapi-starter",
@ -38,7 +38,7 @@
"test": "echo \"no tests yet\""
},
"dependencies": {
"@strapi/generate-new": "4.1.5",
"@strapi/generate-new": "4.1.6",
"chalk": "4.1.1",
"ci-info": "3.1.1",
"commander": "7.1.0",

View File

@ -203,8 +203,6 @@ const CollectionTypeFormWrapper = ({ allLayoutData, children, slug, id, origin }
const displayErrors = useCallback(
err => {
const errorPayload = err.response.data;
console.error(errorPayload);
let errorMessage = get(errorPayload, ['error', 'message'], 'Bad Request');
// TODO handle errors correctly when back-end ready
@ -272,10 +270,14 @@ const CollectionTypeFormWrapper = ({ allLayoutData, children, slug, id, origin }
dispatch(setStatus('resolved'));
replace(`/content-manager/collectionType/${slug}/${data.id}${rawQuery}`);
return Promise.resolve(data);
} catch (err) {
trackUsageRef.current('didNotCreateEntry', { error: err, trackerProperty });
displayErrors(err);
trackUsageRef.current('didNotCreateEntry', { error: err, trackerProperty });
dispatch(setStatus('resolved'));
return Promise.reject(err);
}
},
[
@ -308,9 +310,13 @@ const CollectionTypeFormWrapper = ({ allLayoutData, children, slug, id, origin }
type: 'success',
message: { id: getTrad('success.record.publish') },
});
return Promise.resolve(data);
} catch (err) {
displayErrors(err);
dispatch(setStatus('resolved'));
return Promise.reject(err);
}
}, [cleanReceivedData, displayErrors, id, slug, dispatch, toggleNotification]);
@ -334,11 +340,15 @@ const CollectionTypeFormWrapper = ({ allLayoutData, children, slug, id, origin }
dispatch(submitSucceeded(cleanReceivedData(data)));
dispatch(setStatus('resolved'));
return Promise.resolve(data);
} catch (err) {
trackUsageRef.current('didNotEditEntry', { error: err, trackerProperty });
displayErrors(err);
dispatch(setStatus('resolved'));
return Promise.reject(err);
}
},
[cleanReceivedData, displayErrors, slug, id, dispatch, toggleNotification]
@ -362,9 +372,13 @@ const CollectionTypeFormWrapper = ({ allLayoutData, children, slug, id, origin }
dispatch(submitSucceeded(cleanReceivedData(data)));
dispatch(setStatus('resolved'));
return Promise.resolve(data);
} catch (err) {
dispatch(setStatus('resolved'));
displayErrors(err);
return Promise.reject(err);
}
}, [cleanReceivedData, displayErrors, id, slug, dispatch, toggleNotification]);

View File

@ -1,11 +1,13 @@
import isEmpty from 'lodash/isEmpty';
import isNumber from 'lodash/isNumber';
import isSingleRelation from './isSingleRelation';
import isFieldTypeNumber from '../../../../utils/isFieldTypeNumber';
export default function hasContent(type, content, metadatas, fieldSchema) {
if (type === 'component') {
const {
mainField: { name: mainFieldName },
mainField: { name: mainFieldName, type: mainFieldType },
} = metadatas;
// Repeatable fields show the ID as fallback, in case the mainField
@ -14,7 +16,28 @@ export default function hasContent(type, content, metadatas, fieldSchema) {
return content.length > 0;
}
return !isEmpty(content[mainFieldName]);
const value = content?.[mainFieldName];
// relations, media ... show the id as fallback
if (mainFieldName === 'id' && ![undefined, null].includes(value)) {
return true;
}
/* The ID field reports itself as type `integer`, which makes it
impossible to distinguish it from other number fields.
Biginteger fields need to be treated as strings, as `isNumber`
doesn't deal with them.
*/
if (
isFieldTypeNumber(mainFieldType) &&
mainFieldType !== 'biginteger' &&
mainFieldName !== 'id'
) {
return isNumber(value);
}
return !isEmpty(value);
}
if (type === 'relation') {
@ -25,5 +48,13 @@ export default function hasContent(type, content, metadatas, fieldSchema) {
return content.count > 0;
}
/*
Biginteger fields need to be treated as strings, as `isNumber`
doesn't deal with them.
*/
if (isFieldTypeNumber(type) && type !== 'biginteger') {
return isNumber(content);
}
return !isEmpty(content);
}

View File

@ -1,125 +1,262 @@
import hasContent from '../hasContent';
describe('hasContent', () => {
it('returns true for text content', () => {
const normalizedContent = hasContent('text', 'content');
expect(normalizedContent).toEqual(true);
});
it('returns false for empty text content', () => {
const normalizedContent = hasContent('text', '');
expect(normalizedContent).toEqual(false);
});
it('returns false for undefined text content', () => {
const normalizedContent = hasContent('text', undefined);
expect(normalizedContent).toEqual(false);
});
it('extracts content from single components with content', () => {
const normalizedContent = hasContent(
'component',
{ name: 'content', id: 1 },
{ mainField: { name: 'name' } }
);
expect(normalizedContent).toEqual(true);
});
it('extracts content from single components without content', () => {
const normalizedContent = hasContent(
'component',
{ name: '', id: 1 },
{ mainField: { name: 'name' } }
);
expect(normalizedContent).toEqual(false);
});
it('extracts content from repeatable components with content', () => {
const normalizedContent = hasContent(
'component',
[{ name: 'content_2', value: 'truthy', id: 1 }],
{ mainField: { name: 'content_2' } },
{ repeatable: true }
);
expect(normalizedContent).toEqual(true);
});
it('extracts content from repeatable components without content', () => {
const normalizedContent = hasContent(
'component',
[{ name: 'content_2', value: '', id: 1 }],
{ mainField: { name: 'content_2' } },
{ repeatable: true }
);
expect(normalizedContent).toEqual(true);
});
it('extracts content from repeatable components without content', () => {
const normalizedContent = hasContent(
'component',
[{ id: 1 }, { id: 2 }],
{ mainField: { name: 'content_2' } },
{ repeatable: true }
);
expect(normalizedContent).toEqual(true);
});
it('extracts content from repeatable components without content', () => {
const normalizedContent = hasContent(
'component',
[],
{ mainField: { name: 'content_2' } },
{ repeatable: true }
);
expect(normalizedContent).toEqual(false);
});
it('extracts content from multiple relations with content', () => {
const normalizedContent = hasContent('relation', { count: 1 }, undefined, {
relation: 'manyToMany',
describe('number fields', () => {
it('returns true for integer', () => {
const normalizedContent = hasContent('integer', 1);
expect(normalizedContent).toEqual(true);
});
it('returns false for string integer', () => {
const normalizedContent = hasContent('integer', '1');
expect(normalizedContent).toEqual(false);
});
it('returns false for undefined text', () => {
const normalizedContent = hasContent('integer', undefined);
expect(normalizedContent).toEqual(false);
});
it('returns true for float', () => {
const normalizedContent = hasContent('float', 1.111);
expect(normalizedContent).toEqual(true);
});
it('returns true for decimal', () => {
const normalizedContent = hasContent('decimal', 1.111);
expect(normalizedContent).toEqual(true);
});
it('returns true for biginteger', () => {
const normalizedContent = hasContent('biginteger', '12345678901234567890');
expect(normalizedContent).toEqual(true);
});
expect(normalizedContent).toEqual(true);
});
it('extracts content from multiple relations without content', () => {
const normalizedContent = hasContent('relation', { count: 0 }, undefined, {
relation: 'manyToMany',
describe('text', () => {
it('returns true for text content', () => {
const normalizedContent = hasContent('text', 'content');
expect(normalizedContent).toEqual(true);
});
it('returns false for empty text content', () => {
const normalizedContent = hasContent('text', '');
expect(normalizedContent).toEqual(false);
});
it('returns false for undefined text content', () => {
const normalizedContent = hasContent('text', undefined);
expect(normalizedContent).toEqual(false);
});
expect(normalizedContent).toEqual(false);
});
it('extracts content from single relations with content', () => {
const normalizedContent = hasContent('relation', { id: 1 }, undefined, {
relation: 'oneToOne',
describe('ID', () => {
it('returns true for id main fields', () => {
const normalizedContent = hasContent('media', { id: 1 });
expect(normalizedContent).toEqual(true);
});
expect(normalizedContent).toEqual(true);
});
it('extracts content from single relations without content', () => {
const normalizedContent = hasContent('relation', null, undefined, {
relation: 'oneToOne',
describe('single component', () => {
it('extracts content with content', () => {
const normalizedContent = hasContent(
'component',
{ name: 'content', id: 1 },
{ mainField: { name: 'name' } }
);
expect(normalizedContent).toEqual(true);
});
it('extracts content without content', () => {
const normalizedContent = hasContent(
'component',
{ name: '', id: 1 },
{ mainField: { name: 'name' } }
);
expect(normalizedContent).toEqual(false);
});
it('extracts integers with content', () => {
const normalizedContent = hasContent(
'component',
{ number: 1, id: 1 },
{ mainField: { name: 'number', type: 'integer' } }
);
expect(normalizedContent).toEqual(true);
});
it('extracts integers without content', () => {
const normalizedContent = hasContent(
'component',
{ number: null, id: 1 },
{ mainField: { name: 'number', type: 'integer' } }
);
expect(normalizedContent).toEqual(false);
});
it('extracts float with content', () => {
const normalizedContent = hasContent(
'component',
{ number: 1.11, id: 1 },
{ mainField: { name: 'number', type: 'float' } }
);
expect(normalizedContent).toEqual(true);
});
it('extracts float without content', () => {
const normalizedContent = hasContent(
'component',
{ number: null, id: 1 },
{ mainField: { name: 'number', type: 'float' } }
);
expect(normalizedContent).toEqual(false);
});
it('extracts decimal with content', () => {
const normalizedContent = hasContent(
'component',
{ number: 1.11, id: 1 },
{ mainField: { name: 'number', type: 'decimal' } }
);
expect(normalizedContent).toEqual(true);
});
it('extracts decimal without content', () => {
const normalizedContent = hasContent(
'component',
{ number: null, id: 1 },
{ mainField: { name: 'number', type: 'decimal' } }
);
expect(normalizedContent).toEqual(false);
});
it('extracts biginteger with content', () => {
const normalizedContent = hasContent(
'component',
{ number: '12345678901234567890', id: 1 },
{ mainField: { name: 'number', type: 'biginteger' } }
);
expect(normalizedContent).toEqual(true);
});
it('extracts biginteger without content', () => {
const normalizedContent = hasContent(
'component',
{ number: null, id: 1 },
{ mainField: { name: 'number', type: 'biginteger' } }
);
expect(normalizedContent).toEqual(false);
});
it('does not fail if the attribute is not set', () => {
const normalizedContent = hasContent(
'component',
{ id: 1 },
{ mainField: { name: 'number', type: 'biginteger' } }
);
expect(normalizedContent).toEqual(false);
});
it('returns true id the main field is an id', () => {
const normalizedContent = hasContent(
'component',
{ id: 1 },
{ mainField: { name: 'id', type: 'integer' } }
);
expect(normalizedContent).toEqual(true);
});
expect(normalizedContent).toEqual(false);
});
it('returns oneToManyMorph relations as false with content', () => {
const normalizedContent = hasContent('relation', { id: 1 }, undefined, {
relation: 'oneToManyMorph',
describe('repeatable components', () => {
it('extracts content with content', () => {
const normalizedContent = hasContent(
'component',
[{ name: 'content_2', value: 'truthy', id: 1 }],
{ mainField: { name: 'content_2' } },
{ repeatable: true }
);
expect(normalizedContent).toEqual(true);
});
it('extracts content without content', () => {
const normalizedContent = hasContent(
'component',
[{ name: 'content_2', value: '', id: 1 }],
{ mainField: { name: 'content_2' } },
{ repeatable: true }
);
expect(normalizedContent).toEqual(true);
});
it('extracts content without content', () => {
const normalizedContent = hasContent(
'component',
[{ id: 1 }, { id: 2 }],
{ mainField: { name: 'content_2' } },
{ repeatable: true }
);
expect(normalizedContent).toEqual(true);
});
it('extracts content without content', () => {
const normalizedContent = hasContent(
'component',
[],
{ mainField: { name: 'content_2' } },
{ repeatable: true }
);
expect(normalizedContent).toEqual(false);
});
expect(normalizedContent).toEqual(false);
});
it('extracts content from oneToManyMorph relations with content', () => {
const normalizedContent = hasContent('relation', { id: 1 }, undefined, {
relation: 'oneToOneMorph',
describe('relations', () => {
it('extracts content from multiple relations with content', () => {
const normalizedContent = hasContent('relation', { count: 1 }, undefined, {
relation: 'manyToMany',
});
expect(normalizedContent).toEqual(true);
});
expect(normalizedContent).toEqual(true);
});
it('extracts content from oneToManyMorph relations with content', () => {
const normalizedContent = hasContent('relation', null, undefined, {
relation: 'oneToOneMorph',
it('extracts content from multiple relations without content', () => {
const normalizedContent = hasContent('relation', { count: 0 }, undefined, {
relation: 'manyToMany',
});
expect(normalizedContent).toEqual(false);
});
it('extracts content from single relations with content', () => {
const normalizedContent = hasContent('relation', { id: 1 }, undefined, {
relation: 'oneToOne',
});
expect(normalizedContent).toEqual(true);
});
it('extracts content from single relations without content', () => {
const normalizedContent = hasContent('relation', null, undefined, {
relation: 'oneToOne',
});
expect(normalizedContent).toEqual(false);
});
it('returns oneToManyMorph relations as false with content', () => {
const normalizedContent = hasContent('relation', { id: 1 }, undefined, {
relation: 'oneToManyMorph',
});
expect(normalizedContent).toEqual(false);
});
it('extracts content from oneToManyMorph relations with content', () => {
const normalizedContent = hasContent('relation', { id: 1 }, undefined, {
relation: 'oneToOneMorph',
});
expect(normalizedContent).toEqual(true);
});
it('extracts content from oneToManyMorph relations with content', () => {
const normalizedContent = hasContent('relation', null, undefined, {
relation: 'oneToOneMorph',
});
expect(normalizedContent).toEqual(false);
});
expect(normalizedContent).toEqual(false);
});
});

View File

@ -1,5 +1,9 @@
import React, { useCallback, useEffect, useMemo, useRef, useReducer } from 'react';
import { cloneDeep, get, isEmpty, isEqual, set } from 'lodash';
import isEmpty from 'lodash/isEmpty';
import cloneDeep from 'lodash/cloneDeep';
import get from 'lodash/get';
import isEqual from 'lodash/isEqual';
import set from 'lodash/set';
import PropTypes from 'prop-types';
import { useIntl } from 'react-intl';
import { Prompt, Redirect } from 'react-router-dom';
@ -10,10 +14,13 @@ import {
useNotification,
useOverlayBlocker,
useTracking,
getYupInnerErrors,
} from '@strapi/helper-plugin';
import { getTrad, removeKeyInObject } from '../../utils';
import reducer, { initialState } from './reducer';
import { cleanData, createYupSchema, getYupInnerErrors } from './utils';
import { cleanData, createYupSchema } from './utils';
import { getAPIInnerError } from './utils/getAPIInnerError';
const EditViewDataManagerProvider = ({
allLayoutData,
@ -290,30 +297,27 @@ const EditViewDataManagerProvider = ({
e.preventDefault();
let errors = {};
// First validate the form
try {
await yupSchema.validate(modifiedData, { abortEarly: false });
} catch (err) {
errors = getYupInnerErrors(err);
}
const formData = createFormData(modifiedData);
try {
if (isEmpty(errors)) {
const formData = createFormData(modifiedData);
if (isCreatingEntry) {
onPost(formData, trackerProperty);
} else {
onPut(formData, trackerProperty);
if (isCreatingEntry) {
await onPost(formData, trackerProperty);
} else {
await onPut(formData, trackerProperty);
}
}
} catch (err) {
console.log('ValidationError');
console.log(err);
errors = getYupInnerErrors(err);
toggleNotification({
type: 'warning',
message: {
id: getTrad('containers.EditView.notification.errors'),
defaultMessage: 'The form contains some errors',
},
});
errors = {
...errors,
...getAPIInnerError(err),
};
}
dispatch({
@ -321,16 +325,7 @@ const EditViewDataManagerProvider = ({
errors,
});
},
[
createFormData,
isCreatingEntry,
modifiedData,
onPost,
onPut,
toggleNotification,
trackerProperty,
yupSchema,
]
[createFormData, isCreatingEntry, modifiedData, onPost, onPut, trackerProperty, yupSchema]
);
const handlePublish = useCallback(async () => {
@ -345,17 +340,22 @@ const EditViewDataManagerProvider = ({
let errors = {};
try {
// Validate the form using yup
await schema.validate(modifiedData, { abortEarly: false });
onPublish();
} catch (err) {
console.error('ValidationError');
console.error(err);
errors = getYupInnerErrors(err);
}
try {
if (isEmpty(errors)) {
await onPublish();
}
} catch (err) {
errors = {
...errors,
...getAPIInnerError(err),
};
}
dispatch({
type: 'SET_FORM_ERRORS',
errors,

View File

@ -0,0 +1,18 @@
import { getTrad } from '../../../utils';
export function getAPIInnerError(error) {
const errorPayload = error.response.data.error.details.errors;
const validationErrors = errorPayload.reduce((acc, err) => {
acc[err.path.join('.')] = {
id: getTrad(`apiError.${err.message}`),
defaultMessage: err.message,
values: {
field: err.path[err.path.length - 1],
},
};
return acc;
}, {});
return validationErrors;
}

View File

@ -1,17 +0,0 @@
import { get } from 'lodash';
const getYupInnerErrors = error => {
return get(error, 'inner', []).reduce((acc, curr) => {
acc[
curr.path
.split('[')
.join('.')
.split(']')
.join('')
] = { id: curr.message };
return acc;
}, {});
};
export default getYupInnerErrors;

View File

@ -0,0 +1,15 @@
import { getTrad } from '../../../utils';
export function handleAPIError(error) {
const errorPayload = error.response.data.error.details.errors;
const validationErrors = errorPayload.reduce((acc, err) => {
acc[err.path.join('.')] = {
id: getTrad(`apiError.${err.message}`),
defaultMessage: err.message,
};
return acc;
}, {});
return validationErrors;
}

View File

@ -1,4 +1,3 @@
export { default as moveFields } from './moveFields';
export { default as cleanData } from './cleanData';
export { default as getYupInnerErrors } from './getYupInnerErrors';
export { default as createYupSchema } from './schema';

View File

@ -7,6 +7,8 @@ import toNumber from 'lodash/toNumber';
import * as yup from 'yup';
import { translatedErrors as errorsTrads } from '@strapi/helper-plugin';
import isFieldTypeNumber from '../../../utils/isFieldTypeNumber';
yup.addMethod(yup.mixed, 'defined', function() {
return this.test('defined', errorsTrads.required, value => value !== undefined);
});
@ -240,14 +242,14 @@ const createYupSchemaAttribute = (type, validations, options) => {
.typeError();
}
if (['date', 'datetime'].includes(type)) {
schema = yup.date();
}
if (type === 'biginteger') {
schema = yup.string().matches(/^-?\d*$/);
}
if (['date', 'datetime'].includes(type)) {
schema = yup.date();
}
Object.keys(validations).forEach(validation => {
const validationValue = validations[validation];
@ -273,7 +275,7 @@ const createYupSchemaAttribute = (type, validations, options) => {
return true;
}
if (['number', 'integer', 'biginteger', 'float', 'decimal'].includes(type)) {
if (isFieldTypeNumber(type)) {
if (value === 0) {
return true;
}
@ -344,12 +346,12 @@ const createYupSchemaAttribute = (type, validations, options) => {
}
break;
case 'positive':
if (['number', 'integer', 'bigint', 'float', 'decimal'].includes(type)) {
if (isFieldTypeNumber(type)) {
schema = schema.positive();
}
break;
case 'negative':
if (['number', 'integer', 'bigint', 'float', 'decimal'].includes(type)) {
if (isFieldTypeNumber(type)) {
schema = schema.negative();
}
break;

View File

@ -0,0 +1,36 @@
import { getAPIInnerError } from '../getAPIInnerError';
const API_ERROR_FIXTURE = {
response: {
data: {
error: {
details: {
errors: [
{
path: ['field', '0', 'name'],
message: 'Field contains errors',
},
{
path: ['field'],
message: 'Field must be unique',
},
],
},
},
},
},
};
describe('getAPIInnerError', () => {
test('transforms API errors into errors, which can be rendered by the CM', () => {
expect(getAPIInnerError(API_ERROR_FIXTURE)).toMatchObject({
'field.0.name': {
id: 'content-manager.apiError.Field contains errors',
},
field: {
id: 'content-manager.apiError.Field must be unique',
},
});
});
});

View File

@ -0,0 +1,36 @@
import { handleAPIError } from '../handleAPIError';
const API_ERROR_FIXTURE = {
response: {
data: {
error: {
details: {
errors: [
{
path: ['field', '0', 'name'],
message: 'Field contains errors',
},
{
path: ['field'],
message: 'Field must be unique',
},
],
},
},
},
},
};
describe('handleAPIError', () => {
test('transforms API errors into errors, which can be rendered by the CM', () => {
expect(handleAPIError(API_ERROR_FIXTURE)).toMatchObject({
'field.0.name': {
id: 'content-manager.apiError.Field contains errors',
},
field: {
id: 'content-manager.apiError.Field must be unique',
},
});
});
});

View File

@ -106,7 +106,6 @@ const FieldComponent = ({
componentValue={componentValue}
componentValueLength={componentValueLength}
componentUid={componentUid}
isNested={isNested}
isReadOnly={isReadOnly}
max={max}
min={min}

View File

@ -82,7 +82,6 @@ const InputUID = ({
onChange({ target: { name, value: data, type: 'text' } }, shouldSetInitialValue);
setIsLoading(false);
} catch (err) {
console.error({ err });
setIsLoading(false);
}
};
@ -107,7 +106,6 @@ const InputUID = ({
setIsLoading(false);
} catch (err) {
console.error({ err });
setIsLoading(false);
}
};
@ -184,12 +182,10 @@ const InputUID = ({
onChange(e);
};
const formattedError = error ? formatMessage({ id: error, defaultMessage: error }) : undefined;
return (
<TextInput
disabled={disabled}
error={formattedError}
error={error}
endAction={
<EndActionWrapper>
{availability && availability.isAvailable && !regenerateLabel && (

View File

@ -42,10 +42,7 @@ function Inputs({
const disabled = useMemo(() => !get(metadatas, 'editable', true), [metadatas]);
const type = fieldSchema.type;
const errorId = useMemo(() => {
return get(formErrors, [keys, 'id'], null);
}, [formErrors, keys]);
const error = get(formErrors, [keys], null);
const fieldName = useMemo(() => {
return getFieldName(keys);
@ -177,7 +174,7 @@ function Inputs({
description={description ? { id: description, defaultMessage: description } : null}
intlLabel={{ id: label, defaultMessage: label }}
labelAction={labelAction}
error={errorId}
error={error && formatMessage(error)}
name={keys}
required={isRequired}
/>
@ -215,6 +212,7 @@ function Inputs({
}
queryInfos={queryInfos}
value={value}
error={error && formatMessage(error)}
/>
);
}
@ -228,7 +226,7 @@ function Inputs({
isNullable={inputType === 'bool' && [null, undefined].includes(fieldSchema.default)}
description={description ? { id: description, defaultMessage: description } : null}
disabled={shouldDisableField}
error={errorId}
error={error}
labelAction={labelAction}
contentTypeUID={currentContentTypeLayout.uid}
customInputs={{

View File

@ -5,7 +5,6 @@ import { useIntl } from 'react-intl';
import styled from 'styled-components';
import PropTypes from 'prop-types';
import get from 'lodash/get';
import take from 'lodash/take';
import { useNotification } from '@strapi/helper-plugin';
import { Box } from '@strapi/design-system/Box';
import { Flex } from '@strapi/design-system/Flex';
@ -17,6 +16,7 @@ import ItemTypes from '../../utils/ItemTypes';
import ComponentInitializer from '../ComponentInitializer';
import connect from './utils/connect';
import select from './utils/select';
import getComponentErrorKeys from './utils/getComponentErrorKeys';
import DraggedItem from './DraggedItem';
import AccordionGroupCustom from './AccordionGroupCustom';
@ -38,7 +38,6 @@ const RepeatableComponent = ({
componentUid,
componentValue,
componentValueLength,
isNested,
isReadOnly,
max,
min,
@ -59,16 +58,7 @@ const RepeatableComponent = ({
return getMaxTempKey(componentValue || []) + 1;
}, [componentValue]);
const componentErrorKeys = Object.keys(formErrors)
.filter(errorKey => {
return take(errorKey.split('.'), isNested ? 3 : 1).join('.') === name;
})
.map(errorKey => {
return errorKey
.split('.')
.slice(0, name.split('.').length + 1)
.join('.');
});
const componentErrorKeys = getComponentErrorKeys(name, formErrors);
const toggleCollapses = () => {
setCollapseToOpen('');
@ -187,7 +177,6 @@ RepeatableComponent.defaultProps = {
componentValue: null,
componentValueLength: 0,
formErrors: {},
isNested: false,
max: Infinity,
min: 0,
};
@ -198,7 +187,6 @@ RepeatableComponent.propTypes = {
componentValue: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
componentValueLength: PropTypes.number,
formErrors: PropTypes.object,
isNested: PropTypes.bool,
isReadOnly: PropTypes.bool.isRequired,
max: PropTypes.number,
min: PropTypes.number,
@ -207,9 +195,6 @@ RepeatableComponent.propTypes = {
const Memoized = memo(RepeatableComponent);
export default connect(
Memoized,
select
);
export default connect(Memoized, select);
export { RepeatableComponent };

View File

@ -0,0 +1,10 @@
export default function getComponentErrorKeys(name, formErrors) {
return Object.keys(formErrors)
.filter(errorKey => errorKey.startsWith(name))
.map(errorKey =>
errorKey
.split('.')
.slice(0, name.split('.').length + 1)
.join('.')
);
}

View File

@ -0,0 +1,27 @@
import getComponentErrorKeys from '../getComponentErrorKeys';
describe('getComponentErrorKeys', () => {
test('retrieves error keys for non nested components', () => {
const FIXTURE = {
'component.0.name': 'unique-error',
'component.1.field': 'validation-error',
};
expect(getComponentErrorKeys('component', FIXTURE)).toStrictEqual([
'component.0',
'component.1',
]);
});
test('retrieves error keys for nested components', () => {
const FIXTURE = {
'parent.child.0.name': 'unique-error',
'parent.child.1.field': 'validation-error',
};
expect(getComponentErrorKeys('parent.child', FIXTURE)).toStrictEqual([
'parent.child.0',
'parent.child.1',
]);
});
});

View File

@ -144,8 +144,6 @@ const SingleTypeFormWrapper = ({ allLayoutData, children, slug }) => {
const displayErrors = useCallback(
err => {
const errorPayload = err.response.payload;
console.error(errorPayload);
let errorMessage = get(errorPayload, ['message'], 'Bad Request');
// TODO handle errors correctly when back-end ready
@ -178,10 +176,12 @@ const SingleTypeFormWrapper = ({ allLayoutData, children, slug }) => {
} catch (err) {
trackUsageRef.current('didNotDeleteEntry', { error: err, ...trackerProperty });
displayErrors(err);
return Promise.reject(err);
}
},
[slug, toggleNotification, searchToSend]
[slug, displayErrors, toggleNotification, searchToSend]
);
const onDeleteSucceeded = useCallback(() => {
@ -211,12 +211,16 @@ const SingleTypeFormWrapper = ({ allLayoutData, children, slug }) => {
setIsCreatingEntry(false);
dispatch(setStatus('resolved'));
return Promise.resolve(data);
} catch (err) {
trackUsageRef.current('didNotCreateEntry', { error: err, trackerProperty });
displayErrors(err);
dispatch(setStatus('resolved'));
return Promise.reject(err);
}
},
[cleanReceivedData, displayErrors, slug, dispatch, rawQuery, toggleNotification, setCurrentStep]
@ -239,10 +243,14 @@ const SingleTypeFormWrapper = ({ allLayoutData, children, slug }) => {
dispatch(submitSucceeded(cleanReceivedData(data)));
dispatch(setStatus('resolved'));
return Promise.resolve(data);
} catch (err) {
displayErrors(err);
dispatch(setStatus('resolved'));
return Promise.reject(err);
}
}, [cleanReceivedData, displayErrors, slug, searchToSend, dispatch, toggleNotification]);
@ -267,12 +275,16 @@ const SingleTypeFormWrapper = ({ allLayoutData, children, slug }) => {
dispatch(submitSucceeded(cleanReceivedData(data)));
dispatch(setStatus('resolved'));
return Promise.resolve(data);
} catch (err) {
displayErrors(err);
trackUsageRef.current('didNotEditEntry', { error: err, trackerProperty });
dispatch(setStatus('resolved'));
return Promise.reject(err);
}
},
[cleanReceivedData, displayErrors, slug, dispatch, rawQuery, toggleNotification]

View File

@ -118,7 +118,6 @@ const Wysiwyg = ({
)
: '';
const errorMessage = error ? formatMessage({ id: error, defaultMessage: error }) : '';
const label = intlLabel.id
? formatMessage(
{ id: intlLabel.id, defaultMessage: intlLabel.defaultMessage },
@ -157,7 +156,7 @@ const Wysiwyg = ({
disabled={disabled}
isExpandMode={isExpandMode}
editorRef={editorRef}
error={errorMessage}
error={error}
isPreviewMode={isPreviewMode}
name={name}
onChange={onChange}
@ -171,10 +170,10 @@ const Wysiwyg = ({
<Hint description={description} name={name} error={error} />
</Stack>
{errorMessage && (
{error && (
<Box paddingTop={1}>
<Typography variant="pi" textColor="danger600" data-strapi-field-error>
{errorMessage}
{error}
</Typography>
</Box>
)}

View File

@ -124,7 +124,10 @@ describe('Content Manager | hooks | useFetchContentTypeLayout | utils ', () => {
uid: 'compo',
layouts: {
edit: [
[{ name: 'full_name', size: 6 }, { name: 'city', size: 6 }],
[
{ name: 'full_name', size: 6 },
{ name: 'city', size: 6 },
],
[{ name: 'compo', size: 12 }],
],
},
@ -166,7 +169,10 @@ describe('Content Manager | hooks | useFetchContentTypeLayout | utils ', () => {
editRelations: [],
edit: [
[{ name: 'dz', size: 12 }],
[{ name: 'full_name', size: 6 }, { name: 'city', size: 6 }],
[
{ name: 'full_name', size: 6 },
{ name: 'city', size: 6 },
],
[{ name: 'compo', size: 12 }],
],
},
@ -364,7 +370,10 @@ describe('Content Manager | hooks | useFetchContentTypeLayout | utils ', () => {
layouts: {
edit: [
[{ name: 'dz', size: 12 }],
[{ name: 'full_name', size: 6 }, { name: 'city', size: 6 }],
[
{ name: 'full_name', size: 6 },
{ name: 'city', size: 6 },
],
[{ name: 'compo', size: 12 }],
],
},
@ -612,7 +621,10 @@ describe('Content Manager | hooks | useFetchContentTypeLayout | utils ', () => {
describe('getDisplayedModels', () => {
it('should return an array containing only the displayable models', () => {
const models = [{ uid: 'test', isDisplayed: false }, { uid: 'testtest', isDisplayed: true }];
const models = [
{ uid: 'test', isDisplayed: false },
{ uid: 'testtest', isDisplayed: true },
];
expect(getDisplayedModels([])).toHaveLength(0);
expect(getDisplayedModels(models)).toHaveLength(1);

View File

@ -11,7 +11,12 @@ import { makeSelectModelAndComponentSchemas } from '../../App/selectors';
import getTrad from '../../../utils/getTrad';
import GenericInput from './GenericInput';
const FIELD_SIZES = [[4, '33%'], [6, '50%'], [8, '66%'], [12, '100%']];
const FIELD_SIZES = [
[4, '33%'],
[6, '50%'],
[8, '66%'],
[12, '100%'],
];
const NON_RESIZABLE_FIELD_TYPES = ['dynamiczone', 'component', 'json', 'richtext'];

View File

@ -59,7 +59,6 @@ const EditSettingsView = ({ mainLayout, components, isContentTypeView, slug, upd
'relation',
'component',
'boolean',
'date',
'media',
'richtext',
'timestamp',

View File

@ -34,7 +34,12 @@ const makeApp = (history, layout) => {
},
kind: 'collectionType',
layouts: {
edit: [[{ name: 'postal_code', size: 6 }, { name: 'city', size: 6 }]],
edit: [
[
{ name: 'postal_code', size: 6 },
{ name: 'city', size: 6 },
],
],
list: ['postal_code', 'categories'],
editRelations: ['categories'],
},

View File

@ -150,7 +150,10 @@ describe('CONTENT MANAGER | CONTAINERS | EditSettingsView | reducer', () => {
edit: [
{
rowId: 0,
rowContent: [{ name: 'title', size: 6 }, { name: '_TEMP_', size: 6 }],
rowContent: [
{ name: 'title', size: 6 },
{ name: '_TEMP_', size: 6 },
],
},
],
},
@ -170,7 +173,10 @@ describe('CONTENT MANAGER | CONTAINERS | EditSettingsView | reducer', () => {
edit: [
{
rowId: 0,
rowContent: [{ name: 'title', size: 8 }, { name: '_TEMP_', size: 4 }],
rowContent: [
{ name: 'title', size: 8 },
{ name: '_TEMP_', size: 4 },
],
},
],
};
@ -186,7 +192,10 @@ describe('CONTENT MANAGER | CONTAINERS | EditSettingsView | reducer', () => {
edit: [
{
rowId: 0,
rowContent: [{ name: 'title', size: 8 }, { name: 'isActive', size: 4 }],
rowContent: [
{ name: 'title', size: 8 },
{ name: 'isActive', size: 4 },
],
},
],
},
@ -234,7 +243,10 @@ describe('CONTENT MANAGER | CONTAINERS | EditSettingsView | reducer', () => {
},
{
rowId: 1,
rowContent: [{ name: 'title', size: 6 }, { name: '_TEMP_', size: 6 }],
rowContent: [
{ name: 'title', size: 6 },
{ name: '_TEMP_', size: 6 },
],
},
],
},
@ -358,7 +370,10 @@ describe('CONTENT MANAGER | CONTAINERS | EditSettingsView | reducer', () => {
edit: [
{
rowId: 0,
rowContent: [{ name: 'isActive', size: 4 }, { name: '_TEMP_', size: 8 }],
rowContent: [
{ name: 'isActive', size: 4 },
{ name: '_TEMP_', size: 8 },
],
},
],
},
@ -404,7 +419,10 @@ describe('CONTENT MANAGER | CONTAINERS | EditSettingsView | reducer', () => {
},
{
rowId: 1,
rowContent: [{ name: 'slug', size: 6 }, { name: '_TEMP_', size: 6 }],
rowContent: [
{ name: 'slug', size: 6 },
{ name: '_TEMP_', size: 6 },
],
},
],
},
@ -424,7 +442,10 @@ describe('CONTENT MANAGER | CONTAINERS | EditSettingsView | reducer', () => {
},
{
rowId: 1,
rowContent: [{ name: 'second', size: 4 }, { name: '_TEMP_', size: 8 }],
rowContent: [
{ name: 'second', size: 4 },
{ name: '_TEMP_', size: 8 },
],
},
],
},
@ -455,7 +476,10 @@ describe('CONTENT MANAGER | CONTAINERS | EditSettingsView | reducer', () => {
},
{
rowId: 1,
rowContent: [{ name: 'slug', size: 6 }, { name: '_TEMP_', size: 6 }],
rowContent: [
{ name: 'slug', size: 6 },
{ name: '_TEMP_', size: 6 },
],
},
],
},
@ -475,7 +499,10 @@ describe('CONTENT MANAGER | CONTAINERS | EditSettingsView | reducer', () => {
},
{
rowId: 1,
rowContent: [{ name: 'slug', size: 6 }, { name: '_TEMP_', size: 6 }],
rowContent: [
{ name: 'slug', size: 6 },
{ name: '_TEMP_', size: 6 },
],
},
],
},
@ -499,7 +526,10 @@ describe('CONTENT MANAGER | CONTAINERS | EditSettingsView | reducer', () => {
edit: [
{
rowId: 0,
rowContent: [{ name: 'city', size: 6 }, { name: 'slug', size: 6 }],
rowContent: [
{ name: 'city', size: 6 },
{ name: 'slug', size: 6 },
],
},
{
rowId: 1,
@ -518,7 +548,10 @@ describe('CONTENT MANAGER | CONTAINERS | EditSettingsView | reducer', () => {
edit: [
{
rowId: 0,
rowContent: [{ name: 'city', size: 6 }, { name: 'slug', size: 6 }],
rowContent: [
{ name: 'city', size: 6 },
{ name: 'slug', size: 6 },
],
},
{
rowId: 1,

View File

@ -28,7 +28,13 @@ const formatLayout = arr => {
return acc2;
}, []);
const rowId = acc.length === 0 ? 0 : Math.max.apply(Math, acc.map(o => o.rowId)) + 1;
const rowId =
acc.length === 0
? 0
: Math.max.apply(
Math,
acc.map(o => o.rowId)
) + 1;
const currentRowSize = getRowSize(currentRow);

View File

@ -12,19 +12,31 @@ describe('Content Manager | containers | EditSettingsView | utils | layout', ()
describe('createLayout', () => {
it('should return an array of object with keys rowId and rowContent', () => {
const data = [
[{ name: 'test', size: 4 }, { name: 'test1', size: 4 }],
[
{ name: 'test', size: 4 },
{ name: 'test1', size: 4 },
],
[{ name: 'test2', size: 12 }],
[{ name: 'test3', size: 6 }, { name: 'test4', size: 1 }],
[
{ name: 'test3', size: 6 },
{ name: 'test4', size: 1 },
],
];
const expected = [
{
rowId: 0,
rowContent: [{ name: 'test', size: 4 }, { name: 'test1', size: 4 }],
rowContent: [
{ name: 'test', size: 4 },
{ name: 'test1', size: 4 },
],
},
{ rowId: 1, rowContent: [{ name: 'test2', size: 12 }] },
{
rowId: 2,
rowContent: [{ name: 'test3', size: 6 }, { name: 'test4', size: 1 }],
rowContent: [
{ name: 'test3', size: 6 },
{ name: 'test4', size: 1 },
],
},
];
@ -37,12 +49,18 @@ describe('Content Manager | containers | EditSettingsView | utils | layout', ()
const data = [
{
rowId: 0,
rowContent: [{ name: 'test', size: 4 }, { name: 'test1', size: 4 }],
rowContent: [
{ name: 'test', size: 4 },
{ name: 'test1', size: 4 },
],
},
{ rowId: 1, rowContent: [{ name: 'test2', size: 12 }] },
{
rowId: 2,
rowContent: [{ name: 'test3', size: 6 }, { name: 'test4', size: 1 }],
rowContent: [
{ name: 'test3', size: 6 },
{ name: 'test4', size: 1 },
],
},
];
const expected = [
@ -89,7 +107,10 @@ describe('Content Manager | containers | EditSettingsView | utils | layout', ()
},
{
rowId: 3,
rowContent: [{ name: 'test5', size: 6 }, { name: 'test6', size: 6 }],
rowContent: [
{ name: 'test5', size: 6 },
{ name: 'test6', size: 6 },
],
},
];
@ -238,7 +259,10 @@ describe('Content Manager | containers | EditSettingsView | utils | layout', ()
},
];
const expected = [
[{ name: 'name', size: 6 }, { name: 'test', size: 4 }],
[
{ name: 'name', size: 6 },
{ name: 'test', size: 4 },
],
[{ name: 'name1', size: 4 }],
];

View File

@ -71,7 +71,4 @@ DeleteLink.propTypes = {
const Memoized = memo(DeleteLink, isEqual);
export default connect(
Memoized,
select
);
export default connect(Memoized, select);

View File

@ -88,8 +88,5 @@ DraftAndPublishBadge.propTypes = {
isPublished: PropTypes.bool.isRequired,
};
export default connect(
DraftAndPublishBadge,
select
);
export default connect(DraftAndPublishBadge, select);
export { DraftAndPublishBadge };

View File

@ -15,12 +15,9 @@ const listViewDomain = () => state => state['content-manager_listView'] || initi
*/
const makeSelectListView = () =>
createSelector(
listViewDomain(),
substate => {
return substate;
}
);
createSelector(listViewDomain(), substate => {
return substate;
});
const selectDisplayedHeaders = state => {
const { displayedHeaders } = state['content-manager_listView'];

View File

@ -140,7 +140,11 @@ const testData = {
id: 1,
name: 'name',
subcomponotrepeatable: { id: 4, name: 'name' },
subrepeatable: [{ id: 1, name: 'name' }, { id: 2, name: 'name' }, { id: 3, name: 'name' }],
subrepeatable: [
{ id: 1, name: 'name' },
{ id: 2, name: 'name' },
{ id: 3, name: 'name' },
],
},
repeatable: [
{

View File

@ -0,0 +1,3 @@
export default function isFieldTypeNumber(type) {
return ['integer', 'biginteger', 'decimal', 'float', 'number'].includes(type);
}

View File

@ -0,0 +1,18 @@
import isFieldTypeNumber from '../isFieldTypeNumber';
const FIXTURE = [
['integer', true],
['float', true],
['decimal', true],
['biginteger', true],
['number', true],
['text', false],
];
describe('isFieldTypeNumber', () => {
FIXTURE.forEach(([type, expectation]) => {
test(`${type} is ${expectation}`, () => {
expect(isFieldTypeNumber(type)).toBe(expectation);
});
});
});

View File

@ -40,7 +40,7 @@ const UnauthenticatedLayout = ({ children }) => {
<LocaleToggle />
</Box>
</Flex>
<Box paddingTop={11} paddingBottom={11}>
<Box paddingTop={2} paddingBottom={11}>
{children}
</Box>
</div>

View File

@ -33,7 +33,7 @@ describe('ADMIN | PAGES | AUTH | Oops', () => {
}
.c9 {
padding-top: 64px;
padding-top: 8px;
padding-bottom: 64px;
}

View File

@ -143,7 +143,7 @@ const Register = ({ authType, fieldsToDisable, noSignin, onSubmit, schema }) =>
<Typography as="h1" variant="alpha">
{formatMessage({
id: 'Auth.form.welcome.title',
defaultMessage: 'Welcome!',
defaultMessage: 'Welcome to Strapi!',
})}
</Typography>
</Box>
@ -152,12 +152,12 @@ const Register = ({ authType, fieldsToDisable, noSignin, onSubmit, schema }) =>
{formatMessage({
id: 'Auth.form.register.subtitle',
defaultMessage:
'Your credentials are only used to authenticate yourself on the admin panel. All saved data will be stored in your own database.',
'Credentials are only used to authenticate in Strapi. All saved data will be stored in your database.',
})}
</Typography>
</CenteredBox>
</Column>
<Stack spacing={7}>
<Stack spacing={6}>
<Grid gap={4}>
<GridItem col={6}>
<TextInput
@ -227,7 +227,7 @@ const Register = ({ authType, fieldsToDisable, noSignin, onSubmit, schema }) =>
hint={formatMessage({
id: 'Auth.form.password.hint',
defaultMessage:
'Password must contain at least 8 characters, 1 uppercase, 1 lowercase and 1 number',
'Must be at least 8 characters, 1 uppercase, 1 lowercase & 1 number',
})}
required
label={formatMessage({
@ -284,7 +284,7 @@ const Register = ({ authType, fieldsToDisable, noSignin, onSubmit, schema }) =>
{
id: 'Auth.form.register.news.label',
defaultMessage:
'Keep me updated about the new features and upcoming improvements (by doing this you accept the {terms} and the {policy}).',
'Keep me updated about new features & upcoming improvements (by doing this you accept the {terms} and the {policy}).',
},
{
terms: (

View File

@ -38,7 +38,7 @@ describe('ADMIN | PAGES | AUTH | Register', () => {
}
.c9 {
padding-top: 64px;
padding-top: 8px;
padding-bottom: 64px;
}
@ -77,7 +77,7 @@ describe('ADMIN | PAGES | AUTH | Register', () => {
}
.c21 > * + * {
margin-top: 32px;
margin-top: 24px;
}
.c12:focus-visible {
@ -876,7 +876,7 @@ describe('ADMIN | PAGES | AUTH | Register', () => {
<h1
class="c17"
>
Welcome!
Welcome to Strapi!
</h1>
</div>
<div
@ -885,13 +885,13 @@ describe('ADMIN | PAGES | AUTH | Register', () => {
<span
class="c20"
>
Your credentials are only used to authenticate yourself on the admin panel. All saved data will be stored in your own database.
Credentials are only used to authenticate in Strapi. All saved data will be stored in your database.
</span>
</div>
</div>
<div
class="c21"
spacing="7"
spacing="6"
>
<div
class="c22"
@ -1081,7 +1081,7 @@ describe('ADMIN | PAGES | AUTH | Register', () => {
class="c37"
id="textinput-4-hint"
>
Password must contain at least 8 characters, 1 uppercase, 1 lowercase and 1 number
Must be at least 8 characters, 1 uppercase, 1 lowercase & 1 number
</p>
</div>
</div>
@ -1164,7 +1164,7 @@ describe('ADMIN | PAGES | AUTH | Register', () => {
<div
class="c42"
>
Keep me updated about the new features and upcoming improvements (by doing this you accept the
Keep me updated about new features & upcoming improvements (by doing this you accept the
<a
class="c43"
href="https://strapi.io/terms"

View File

@ -38,7 +38,7 @@ describe('ADMIN | PAGES | AUTH | ResetPassword', () => {
}
.c9 {
padding-top: 64px;
padding-top: 8px;
padding-bottom: 64px;
}

View File

@ -0,0 +1,40 @@
import React from 'react';
import { useIntl } from 'react-intl';
import { ContentBox, useTracking } from '@strapi/helper-plugin';
import GlassesSquare from '@strapi/icons/GlassesSquare';
import ExternalLink from '@strapi/icons/ExternalLink';
import { Icon } from '@strapi/design-system/Icon';
const MissingPluginBanner = () => {
const { formatMessage } = useIntl();
const { trackUsage } = useTracking();
return (
<a
href="https://strapi.canny.io/plugin-requests"
target="_blank"
rel="noopener noreferrer nofollow"
style={{ textDecoration: 'none' }}
onClick={() => trackUsage('didMissMarketplacePlugin')}
>
<ContentBox
title={formatMessage({
id: 'admin.pages.MarketPlacePage.missingPlugin.title',
defaultMessage: 'Documentation',
})}
subtitle={formatMessage({
id: 'admin.pages.MarketPlacePage.missingPlugin.description',
defaultMessage:
"Tell us what plugin you are looking for and we'll let our community plugin developers know in case they are in search for inspiration!",
})}
icon={<GlassesSquare />}
iconBackground="alternative100"
endAction={
<Icon as={ExternalLink} color="neutral600" width={3} height={3} marginLeft={2} />
}
/>
</a>
);
};
export default MissingPluginBanner;

View File

@ -30,6 +30,7 @@ import useFetchMarketplacePlugins from '../../hooks/useFetchMarketplacePlugins';
import adminPermissions from '../../permissions';
import offlineCloud from '../../assets/images/icon_offline-cloud.svg';
import useNavigatorOnLine from '../../hooks/useNavigatorOnLine';
import MissingPluginBanner from './components/MissingPluginBanner';
const matchSearch = (plugins, search) => {
return matchSorter(plugins, search, {
@ -237,6 +238,9 @@ const MarketPlacePage = () => {
))}
</Grid>
)}
<Box paddingTop={7}>
<MissingPluginBanner />
</Box>
</ContentLayout>
</Main>
</Layout>

View File

@ -90,6 +90,10 @@ describe('Marketplace page', () => {
padding-left: 16px;
}
.c56 {
padding-top: 32px;
}
.c49 {
font-weight: 600;
color: #32324d;
@ -256,6 +260,19 @@ describe('Marketplace page', () => {
height: 100%;
}
.c57 {
background: #ffffff;
padding: 24px;
border-radius: 4px;
box-shadow: 0px 1px 4px rgba(33,33,52,0.1);
}
.c58 {
background: #f6ecfc;
padding: 12px;
border-radius: 4px;
}
.c31 {
display: -webkit-box;
display: -webkit-flex;
@ -300,10 +317,21 @@ describe('Marketplace page', () => {
height: 12;
}
.c63 {
color: #666687;
margin-left: 8px;
width: 12px;
height: 12px;
}
.c51 path {
fill: #328048;
}
.c64 path {
fill: #666687;
}
.c24 {
padding-right: 8px;
padding-left: 12px;
@ -470,6 +498,21 @@ describe('Marketplace page', () => {
align-items: center;
}
.c60 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-flex-direction: column;
-ms-flex-direction: column;
flex-direction: column;
}
.c60 > * {
margin-top: 0;
margin-bottom: 0;
}
.c41 > * {
margin-left: 0;
margin-right: 0;
@ -499,6 +542,26 @@ describe('Marketplace page', () => {
line-height: 1.43;
}
.c61 {
font-weight: 500;
color: #32324d;
font-size: 0.75rem;
line-height: 1.33;
}
.c59 {
margin-right: 24px;
}
.c59 svg {
width: 2rem;
height: 2rem;
}
.c62 {
word-break: break-all;
}
.c28 {
display: grid;
grid-template-columns: repeat(12,1fr);
@ -1499,6 +1562,74 @@ describe('Marketplace page', () => {
</div>
</div>
</div>
<div
class="c56"
>
<a
href="https://strapi.canny.io/plugin-requests"
rel="noopener noreferrer nofollow"
style="text-decoration: none;"
target="_blank"
>
<div
class="c57 c35"
>
<div
class="c58 c35 c59"
>
<svg
fill="none"
height="1em"
viewBox="0 0 32 32"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M0 4a4 4 0 014-4h24a4 4 0 014 4v24a4 4 0 01-4 4H4a4 4 0 01-4-4V4z"
fill="#AC73E6"
/>
<path
clip-rule="evenodd"
d="M15.027 13.839c-3.19-.836-6.305-1.064-10.18-.608-1.215.152-1.063 1.975.076 2.203.304.836.456 2.355.912 3.267.987 2.279 5.622 1.975 7.369.835 1.14-.683 1.443-2.279 1.9-3.494.227-.684 1.595-.684 1.822 0 .38 1.215.76 2.81 1.9 3.494 1.747 1.14 6.381 1.444 7.369-.835.456-.912.607-2.431.911-3.267 1.14-.228 1.216-2.051.076-2.203-3.874-.456-6.989-.228-10.18.608-.455.075-1.519.075-1.975 0z"
fill="#fff"
fill-rule="evenodd"
/>
</svg>
</div>
<div
class="c60"
>
<div
class="c35"
>
<span
class="c61 c62"
>
Documentation
</span>
<svg
class="c63 c64"
fill="none"
height="3"
viewBox="0 0 24 24"
width="3"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M16.235 2.824a1.412 1.412 0 010-2.824h6.353C23.368 0 24 .633 24 1.412v6.353a1.412 1.412 0 01-2.823 0V4.82l-8.179 8.178a1.412 1.412 0 01-1.996-1.996l8.178-8.178h-2.945zm4.942 10.588a1.412 1.412 0 012.823 0v9.176c0 .78-.632 1.412-1.412 1.412H1.412C.632 24 0 23.368 0 22.588V1.412C0 .632.632 0 1.412 0h9.176a1.412 1.412 0 010 2.824H2.824v18.353h18.353v-7.765z"
fill="#32324D"
/>
</svg>
</div>
<span
class="c37"
>
Tell us what plugin you are looking for and we'll let our community plugin developers know in case they are in search for inspiration!
</span>
</div>
</div>
</a>
</div>
</div>
</main>
</div>
@ -1518,8 +1649,8 @@ describe('Marketplace page', () => {
it('should return search results matching the query', async () => {
const { container } = render(App);
const input = await getByPlaceholderText(container, 'Search for a plugin');
fireEvent.change(input, { target: { value: 'documentation' } });
const match = screen.getByText('Documentation');
fireEvent.change(input, { target: { value: 'comment' } });
const match = screen.getByText('Comments');
const notMatch = screen.queryByText('Sentry');
expect(match).toBeVisible();

View File

@ -10,7 +10,10 @@ describe('ADMIN | COMPONENTS | PERMISSIONS | GlobalActions | utils', () => {
});
it('should return an array of actionId string', () => {
const data = [{ test: true, actionId: 'create' }, { test: 'false', actionId: 'read' }];
const data = [
{ test: true, actionId: 'create' },
{ test: 'false', actionId: 'read' },
];
const expected = ['create', 'read'];
expect(getActionsIds(data)).toEqual(expected);

View File

@ -58,7 +58,7 @@ describe('Admin | UseCasePage', () => {
}
.c9 {
padding-top: 64px;
padding-top: 8px;
padding-bottom: 64px;
}

View File

@ -36,15 +36,15 @@
"Auth.form.lastname.label": "Last name",
"Auth.form.lastname.placeholder": "e.g. Doe",
"Auth.form.password.hide-password": "Hide password",
"Auth.form.password.hint": "Password must contain at least 8 characters, 1 uppercase, 1 lowercase, and 1 number",
"Auth.form.password.hint": "Must be at least 8 characters, 1 uppercase, 1 lowercase & 1 number",
"Auth.form.password.show-password": "Show password",
"Auth.form.register.news.label": "Keep me updated about the new features and upcoming improvements (by doing this you accept the {terms} and the {policy}).",
"Auth.form.register.subtitle": "Your credentials are only used to authenticate yourself on the admin panel. All saved data will be stored in your own database.",
"Auth.form.register.news.label": "Keep me updated about new features & upcoming improvements (by doing this you accept the {terms} and the {policy}).",
"Auth.form.register.subtitle": "Credentials are only used to authenticate in Strapi. All saved data will be stored in your database.",
"Auth.form.rememberMe.label": "Remember me",
"Auth.form.username.label": "Username",
"Auth.form.username.placeholder": "e.g. Kai_Doe",
"Auth.form.welcome.subtitle": "Log in to your Strapi account",
"Auth.form.welcome.title": "Welcome!",
"Auth.form.welcome.title": "Welcome to Strapi!",
"Auth.link.forgot-password": "Forgot your password?",
"Auth.link.ready": "Ready to sign in?",
"Auth.link.signin": "Sign in",
@ -220,6 +220,8 @@
"admin.pages.MarketPlacePage.search.placeholder": "Search for a plugin",
"admin.pages.MarketPlacePage.submit.plugin.link": "Submit your plugin",
"admin.pages.MarketPlacePage.subtitle": "Get more out of Strapi",
"admin.pages.MarketPlacePage.missingPlugin.title": "Missing a plugin?",
"admin.pages.MarketPlacePage.missingPlugin.description": "Tell us what plugin you are looking for and we'll let our community plugin developers know in case they are in search for inspiration!",
"anErrorOccurred": "Woops! Something went wrong. Please, try again.",
"app.component.CopyToClipboard.label": "Copy to clipboard",
"app.component.search.label": "Search for {target}",
@ -644,6 +646,8 @@
"content-manager.success.record.save": "Saved",
"content-manager.success.record.unpublish": "Unpublished",
"content-manager.utils.data-loaded": "The {number, plural, =1 {entry has} other {entries have}} successfully been loaded",
"content-manager.apiError.This attribute must be unique": "{field} must be unique",
"form.button.continue": "Continue",
"form.button.done": "Done",
"global.actions": "Actions",
"global.back": "Back",

View File

@ -1,6 +1,6 @@
{
"name": "@strapi/admin",
"version": "4.1.5",
"version": "4.1.6",
"description": "Strapi Admin",
"repository": {
"type": "git",
@ -52,11 +52,11 @@
"@fortawesome/free-brands-svg-icons": "^5.15.3",
"@fortawesome/free-solid-svg-icons": "^5.15.3",
"@fortawesome/react-fontawesome": "^0.1.14",
"@strapi/babel-plugin-switch-ee-ce": "4.1.5",
"@strapi/babel-plugin-switch-ee-ce": "4.1.6",
"@strapi/design-system": "0.0.1-alpha.79",
"@strapi/helper-plugin": "4.1.5",
"@strapi/helper-plugin": "4.1.6",
"@strapi/icons": "0.0.1-alpha.79",
"@strapi/utils": "4.1.5",
"@strapi/utils": "4.1.6",
"axios": "0.24.0",
"babel-loader": "8.2.3",
"babel-plugin-styled-components": "2.0.2",

View File

@ -2,6 +2,7 @@
const path = require('path');
const webpack = require('webpack');
const { isObject } = require('lodash');
const webpackConfig = require('../webpack.config');
const {
getCorePluginsPath,
@ -58,7 +59,20 @@ const buildAdmin = async () => {
if (messages.errors.length > 1) {
messages.errors.length = 1;
}
return reject(new Error(messages.errors.join('\n\n')));
return reject(
new Error(
messages.errors.reduce((acc, error) => {
if (isObject(error)) {
acc += error.message;
} else {
acc += error.join('\n\n');
}
return acc;
}, '')
)
);
}
return resolve({

View File

@ -12,12 +12,12 @@ const defaultAdminAuthSettings = {
},
};
const registerPermissionActions = () => {
getService('permission').actionProvider.registerMany(adminActions.actions);
const registerPermissionActions = async () => {
await getService('permission').actionProvider.registerMany(adminActions.actions);
};
const registerAdminConditions = () => {
getService('permission').conditionProvider.registerMany(adminConditions.conditions);
const registerAdminConditions = async () => {
await getService('permission').conditionProvider.registerMany(adminConditions.conditions);
};
const registerModelHooks = () => {
@ -53,8 +53,8 @@ const syncAuthSettings = async () => {
};
module.exports = async () => {
registerAdminConditions();
registerPermissionActions();
await registerAdminConditions();
await registerPermissionActions();
registerModelHooks();
const permissionService = getService('permission');

View File

@ -32,7 +32,11 @@ const checkPermissionsAreBound = role =>
for (const [subject, perms] of Object.entries(permsBySubject)) {
const boundActions = getBoundActionsBySubject(role, subject);
const missingActions = _.xor(perms.map(p => p.action), boundActions).length !== 0;
const missingActions =
_.xor(
perms.map(p => p.action),
boundActions
).length !== 0;
if (missingActions) return false;
const permsBoundByFields = perms.filter(p => BOUND_ACTIONS_FOR_FIELDS.includes(p.action));

View File

@ -1,6 +1,6 @@
{
"name": "@strapi/plugin-content-manager",
"version": "4.1.5",
"version": "4.1.6",
"description": "A powerful UI to easily manage your data.",
"repository": {
"type": "git",
@ -24,7 +24,7 @@
},
"dependencies": {
"@sindresorhus/slugify": "1.1.0",
"@strapi/utils": "4.1.5",
"@strapi/utils": "4.1.6",
"lodash": "4.17.21"
},
"engines": {

View File

@ -38,12 +38,27 @@ describe('metrics', () => {
const testData = [
[['fieldA'], [false]],
[['fieldA', 'fieldB'], [false]],
[['fieldA', 'field1'], [true, 2, 1]],
[['field1', 'field2'], [true, 2, 2]],
[
['fieldA', 'field1'],
[true, 2, 1],
],
[
['field1', 'field2'],
[true, 2, 2],
],
[['field1'], [true, 1, 1]],
[['fieldA', 'fieldB', 'field1', 'field2'], [true, 4, 2]],
[['fieldA', 'fieldB', 'field3', 'field4'], [true, 4, 2]],
[['fieldA', 'fieldB', 'field5', 'field6'], [true, 4, 2]],
[
['fieldA', 'fieldB', 'field1', 'field2'],
[true, 4, 2],
],
[
['fieldA', 'fieldB', 'field3', 'field4'],
[true, 4, 2],
],
[
['fieldA', 'fieldB', 'field5', 'field6'],
[true, 4, 2],
],
];
test.each(testData)('%s', async (list, expectedResult) => {

View File

@ -208,25 +208,26 @@ describe('x-to-many RF Preview', () => {
});
describe('Pagination', () => {
test.each([[1, 10], [2, 10], [5, 1], [4, 2], [1, 100]])(
'Custom pagination (%s, %s)',
async (page, pageSize) => {
const product = data.product[0];
test.each([
[1, 10],
[2, 10],
[5, 1],
[4, 2],
[1, 100],
])('Custom pagination (%s, %s)', async (page, pageSize) => {
const product = data.product[0];
const { body, statusCode } = await rq.get(
`${cmProductUrl}/${product.id}/shops?page=${page}&pageSize=${pageSize}`
);
const { body, statusCode } = await rq.get(
`${cmProductUrl}/${product.id}/shops?page=${page}&pageSize=${pageSize}`
);
expect(statusCode).toBe(200);
expect(statusCode).toBe(200);
const { pagination, results } = body;
const { pagination, results } = body;
expect(pagination.page).toBe(page);
expect(pagination.pageSize).toBe(pageSize);
expect(results).toHaveLength(
Math.min(pageSize, PRODUCT_SHOP_COUNT - pageSize * (page - 1))
);
}
);
expect(pagination.page).toBe(page);
expect(pagination.pageSize).toBe(pageSize);
expect(results).toHaveLength(Math.min(pageSize, PRODUCT_SHOP_COUNT - pageSize * (page - 1)));
});
});
});

View File

@ -51,13 +51,7 @@ const formsAPI = {
contentType.form.advanced.push(advanced);
contentType.form.base.push(base);
},
extendFields(
fields,
{
validator,
form: { advanced, base },
}
) {
extendFields(fields, { validator, form: { advanced, base } }) {
const formType = this.types.attribute;
fields.forEach(field => {

View File

@ -1,6 +1,6 @@
{
"name": "@strapi/plugin-content-type-builder",
"version": "4.1.5",
"version": "4.1.6",
"description": "Strapi plugin to create content type",
"repository": {
"type": "git",
@ -28,9 +28,9 @@
},
"dependencies": {
"@sindresorhus/slugify": "1.1.0",
"@strapi/generators": "4.1.5",
"@strapi/helper-plugin": "4.1.5",
"@strapi/utils": "4.1.5",
"@strapi/generators": "4.1.6",
"@strapi/helper-plugin": "4.1.6",
"@strapi/utils": "4.1.6",
"fs-extra": "10.0.0",
"lodash": "4.17.21",
"pluralize": "^8.0.0",

View File

@ -182,9 +182,10 @@ function createSchemaBuilder({ components, contentTypes }) {
*/
rollback() {
return Promise.all(
[...Array.from(tmpComponents.values()), ...Array.from(tmpContentTypes.values())].map(
schema => schema.rollback()
)
[
...Array.from(tmpComponents.values()),
...Array.from(tmpContentTypes.values()),
].map(schema => schema.rollback())
);
},
};

View File

@ -0,0 +1,64 @@
/* eslint-disable node/no-missing-require */
/* eslint-disable node/no-extraneous-require */
'use strict';
const knex = require('knex');
const SqliteClient = require('knex/lib/dialects/sqlite3/index');
const trySqlitePackage = packageName => {
try {
require.resolve(packageName);
return packageName;
} catch (error) {
if (error.code === 'MODULE_NOT_FOUND') {
return false;
}
throw error;
}
};
class LegacySqliteClient extends SqliteClient {
_driver() {
return require('sqlite3');
}
}
const clientMap = {
'better-sqlite3': 'better-sqlite3',
'@vscode/sqlite3': 'sqlite',
sqlite3: LegacySqliteClient,
};
const getSqlitePackageName = () => {
// NOTE: allow forcing the package to use (mostly used for testing purposes)
if (typeof process.env.SQLITE_PKG !== 'undefined') {
return process.env.SQLITE_PKG;
}
// NOTE: this tries to find the best sqlite module possible to use
// while keeping retro compatibility
return (
trySqlitePackage('better-sqlite3') ||
trySqlitePackage('@vscode/sqlite3') ||
trySqlitePackage('sqlite3')
);
};
const createConnection = config => {
const knexConfig = { ...config };
if (knexConfig.client === 'sqlite') {
const sqlitePackageName = getSqlitePackageName();
knexConfig.client = clientMap[sqlitePackageName];
}
const knexInstance = knex(knexConfig);
return Object.assign(knexInstance, {
getSchemaName() {
return this.client.connectionSettings.schema;
},
});
};
module.exports = createConnection;

View File

@ -48,7 +48,7 @@ const toStrapiType = column => {
switch (rootType) {
case 'int': {
if (column.column_key === 'PRI') {
return { type: 'increments', args: [{ primary: true }], unsigned: false };
return { type: 'increments', args: [{ primary: true, primaryKey: true }], unsigned: false };
}
return { type: 'integer' };

View File

@ -16,7 +16,7 @@ const toStrapiType = column => {
switch (rootType) {
case 'integer': {
if (column.pk) {
return { type: 'increments', args: [{ primary: true }] };
return { type: 'increments', args: [{ primary: true, primaryKey: true }] };
}
return { type: 'integer' };

View File

@ -166,10 +166,12 @@ const createEntityManager = db => {
const dataToInsert = processData(metadata, data, { withDefaults: true });
const [id] = await this.createQueryBuilder(uid)
const res = await this.createQueryBuilder(uid)
.insert(dataToInsert)
.execute();
const id = res[0].id || res[0];
await this.attachRelations(uid, id, data);
// TODO: in case there is no select or populate specified return the inserted data ?

View File

@ -1,28 +1,17 @@
'use strict';
const knex = require('knex');
const { getDialect } = require('./dialects');
const createSchemaProvider = require('./schema');
const createMetadata = require('./metadata');
const { createEntityManager } = require('./entity-manager');
const { createMigrationsProvider } = require('./migrations');
const { createLifecyclesProvider } = require('./lifecycles');
const createConnection = require('./connection');
const errors = require('./errors');
// TODO: move back into strapi
const { transformContentTypes } = require('./utils/content-types');
const createConnection = config => {
const knexInstance = knex(config);
return Object.assign(knexInstance, {
getSchemaName() {
return this.client.connectionSettings.schema;
},
});
};
class Database {
constructor(config) {
this.metadata = createMetadata(config.models);

View File

@ -105,7 +105,7 @@ const getColumnType = attribute => {
case 'increments': {
return {
type: 'increments',
args: [{ primary: true }],
args: [{ primary: true, primaryKey: true }],
notNullable: true,
};
}

View File

@ -1,6 +1,6 @@
{
"name": "@strapi/database",
"version": "4.1.5",
"version": "4.1.6",
"description": "Strapi's database layer",
"homepage": "https://strapi.io",
"bugs": {
@ -34,7 +34,7 @@
"date-fns": "2.22.1",
"debug": "4.3.1",
"fs-extra": "10.0.0",
"knex": "0.95.6",
"knex": "1.0.4",
"lodash": "4.17.21",
"umzug": "2.3.0"
},

View File

@ -1,6 +1,6 @@
{
"name": "@strapi/plugin-email",
"version": "4.1.5",
"version": "4.1.6",
"description": "Easily configure your Strapi application to send emails.",
"repository": {
"type": "git",
@ -26,12 +26,12 @@
"test:front:watch:ce": "cross-env IS_EE=false jest --config ./jest.config.front.js --watchAll"
},
"dependencies": {
"@strapi/provider-email-sendmail": "4.1.5",
"@strapi/utils": "4.1.5",
"@strapi/provider-email-sendmail": "4.1.6",
"@strapi/utils": "4.1.6",
"lodash": "4.17.21"
},
"devDependencies": {
"@strapi/helper-plugin": "4.1.5"
"@strapi/helper-plugin": "4.1.6"
},
"engines": {
"node": ">=12.22.0 <=16.x.x",

View File

@ -51,6 +51,39 @@ const GenericInput = ({
// therefore we cast this case to undefined
const value = defaultValue ?? undefined;
/*
TODO: ideally we should pass in `defaultValue` and `value` for
inputs, in order to make them controlled components. This variable
acts as a fallback for now, to prevent React errors in devopment mode
See: https://github.com/strapi/strapi/pull/12861
*/
const valueWithEmptyStringFallback = value ?? '';
function getErrorMessage(error) {
if (!error) {
return null;
}
const values = {
...error.values,
};
if (typeof error === 'string') {
return formatMessage({ id: error, defaultMessage: error }, values);
}
return formatMessage(
{
id: error.id,
defaultMessage: error?.defaultMessage ?? error.id,
},
values
);
}
const errorMessage = getErrorMessage(error);
if (CustomInput) {
return (
<CustomInput
@ -59,7 +92,7 @@ const GenericInput = ({
disabled={disabled}
intlLabel={intlLabel}
labelAction={labelAction}
error={error}
error={errorMessage}
name={name}
onChange={onChange}
options={options}
@ -92,8 +125,6 @@ const GenericInput = ({
)
: '';
const errorMessage = error ? formatMessage({ id: error, defaultMessage: error }) : '';
switch (type) {
case 'bool': {
const clearProps = {
@ -244,7 +275,7 @@ const GenericInput = ({
placeholder={formattedPlaceholder}
required={required}
type="email"
value={value}
value={valueWithEmptyStringFallback}
/>
);
}
@ -265,7 +296,7 @@ const GenericInput = ({
placeholder={formattedPlaceholder}
required={required}
type="text"
value={value}
value={valueWithEmptyStringFallback}
/>
);
}
@ -307,7 +338,7 @@ const GenericInput = ({
placeholder={formattedPlaceholder}
required={required}
type={showPassword ? 'text' : 'password'}
value={value}
value={valueWithEmptyStringFallback}
/>
);
}
@ -352,7 +383,7 @@ const GenericInput = ({
required={required}
placeholder={formattedPlaceholder}
type={type}
value={value}
value={valueWithEmptyStringFallback}
>
{value}
</Textarea>
@ -431,7 +462,13 @@ GenericInput.propTypes = {
values: PropTypes.object,
}),
disabled: PropTypes.bool,
error: PropTypes.string,
error: PropTypes.oneOfType([
PropTypes.string,
PropTypes.shape({
id: PropTypes.string.isRequired,
defaultMessage: PropTypes.string,
}),
]),
intlLabel: PropTypes.shape({
id: PropTypes.string.isRequired,
defaultMessage: PropTypes.string.isRequired,

View File

@ -8,7 +8,10 @@ const getYupInnerErrors = error => {
.join('.')
.split(']')
.join('')
] = { id: curr.message };
] = {
id: curr.message,
defaultMessage: curr.message,
};
return acc;
}, {});

View File

@ -1,6 +1,6 @@
{
"name": "@strapi/helper-plugin",
"version": "4.1.5",
"version": "4.1.6",
"description": "Helper for Strapi plugins development",
"repository": {
"type": "git",

View File

@ -50,8 +50,8 @@ const LIFECYCLES = {
class Strapi {
constructor(opts = {}) {
destroyOnSignal(this);
this.dirs = utils.getDirs(opts.dir || process.cwd());
const appConfig = loadConfiguration(this.dirs.root, opts);
const rootDir = opts.dir || process.cwd();
const appConfig = loadConfiguration(rootDir, opts);
this.container = createContainer(this);
this.container.register('config', createConfigProvider(appConfig));
this.container.register('content-types', contentTypesRegistry(this));
@ -65,6 +65,8 @@ class Strapi {
this.container.register('apis', apisRegistry(this));
this.container.register('auth', createAuth(this));
this.dirs = utils.getDirs(rootDir, { strapi: this });
this.isLoaded = false;
this.reload = this.reload();
this.server = createServer(this);

View File

@ -8,6 +8,7 @@ const execa = require('execa');
const { getOr } = require('lodash/fp');
const { createLogger } = require('@strapi/logger');
const { joinBy } = require('@strapi/utils');
const loadConfiguration = require('../core/app-configuration');
const strapi = require('../index');
const buildAdmin = require('./build');
@ -131,6 +132,8 @@ function watchFileChanges({ dir, strapiInstance, watchIgnoreFiles, polling }) {
'**/index.html',
'**/public',
'**/public/**',
strapiInstance.dirs.public,
joinBy('/', strapiInstance.dirs.public, '**'),
'**/*.db*',
'**/exports/**',
...watchIgnoreFiles,

View File

@ -21,6 +21,7 @@ const defaultConfig = {
proxy: false,
cron: { enabled: false },
admin: { autoOpen: false },
dirs: { public: './public' },
},
admin: {},
api: {

View File

@ -1,8 +1,9 @@
'use strict';
const { getConfigUrls } = require('@strapi/utils');
const fse = require('fs-extra');
module.exports = function({ strapi }) {
module.exports = async function({ strapi }) {
strapi.config.port = strapi.config.get('server.port') || strapi.config.port;
strapi.config.host = strapi.config.get('server.host') || strapi.config.host;
@ -22,4 +23,11 @@ module.exports = function({ strapi }) {
if (!shouldServeAdmin) {
strapi.config.serveAdminPanel = false;
}
// ensure public repository exists
if (!(await fse.pathExists(strapi.dirs.public))) {
throw new Error(
`The public folder (${strapi.dirs.public}) doesn't exist or is not accessible. Please make sure it exists.`
);
}
};

View File

@ -19,7 +19,9 @@ const defaultConfig = {
module.exports = (userConfig, { strapi }) => {
const keys = strapi.server.app.keys;
if (!isArray(keys) || isEmpty(keys) || keys.some(isEmpty)) {
throw new Error(`App keys are required. Please set app.keys in config/server.js (ex: keys: ['myKeyA', 'myKeyB'])`);
throw new Error(
`App keys are required. Please set app.keys in config/server.js (ex: keys: ['myKeyA', 'myKeyB'])`
);
}
const config = defaultsDeep(defaultConfig, userConfig);

View File

@ -30,7 +30,7 @@ const healthCheck = async ctx => {
const createServer = strapi => {
const app = createKoaApp({
proxy: strapi.config.get('server.proxy'),
keys: strapi.config.get('server.app.keys'),
keys: strapi.config.get('server.app.keys'),
});
const router = new Router();

View File

@ -1,8 +1,8 @@
'use strict';
const { join } = require('path');
const { join, resolve } = require('path');
const getDirs = root => ({
const getDirs = (root, { strapi }) => ({
root,
src: join(root, 'src'),
api: join(root, 'src', 'api'),
@ -11,7 +11,7 @@ const getDirs = root => ({
policies: join(root, 'src', 'policies'),
middlewares: join(root, 'src', 'middlewares'),
config: join(root, 'config'),
public: join(root, 'public'),
public: resolve(root, strapi.config.get('server.dirs.public')),
});
module.exports = getDirs;

View File

@ -1,6 +1,6 @@
{
"name": "@strapi/strapi",
"version": "4.1.5",
"version": "4.1.6",
"description": "An open source headless CMS solution to create and manage your own API. It provides a powerful dashboard and features to make your life easier. Databases supported: MySQL, MariaDB, PostgreSQL, SQLite",
"keywords": [
"strapi",
@ -80,16 +80,16 @@
"dependencies": {
"@koa/cors": "3.1.0",
"@koa/router": "10.1.1",
"@strapi/admin": "4.1.5",
"@strapi/database": "4.1.5",
"@strapi/generate-new": "4.1.5",
"@strapi/generators": "4.1.5",
"@strapi/logger": "4.1.5",
"@strapi/plugin-content-manager": "4.1.5",
"@strapi/plugin-content-type-builder": "4.1.5",
"@strapi/plugin-email": "4.1.5",
"@strapi/plugin-upload": "4.1.5",
"@strapi/utils": "4.1.5",
"@strapi/admin": "4.1.6",
"@strapi/database": "4.1.6",
"@strapi/generate-new": "4.1.6",
"@strapi/generators": "4.1.6",
"@strapi/logger": "4.1.6",
"@strapi/plugin-content-manager": "4.1.6",
"@strapi/plugin-content-type-builder": "4.1.6",
"@strapi/plugin-email": "4.1.6",
"@strapi/plugin-upload": "4.1.6",
"@strapi/utils": "4.1.6",
"bcryptjs": "2.4.3",
"boxen": "5.1.2",
"chalk": "4.1.2",

View File

@ -117,7 +117,6 @@ export const MediaLibraryInput = ({
setUploadedFiles(prev => [...prev, ...uploadedFiles]);
};
const errorMessage = error ? formatMessage({ id: error, defaultMessage: error }) : '';
const hint = description
? formatMessage(
{ id: description.id, defaultMessage: description.defaultMessage },
@ -148,7 +147,7 @@ export const MediaLibraryInput = ({
onEditAsset={handleAssetEdit}
onNext={handleNext}
onPrevious={handlePrevious}
error={errorMessage}
error={error}
hint={hint}
required={required}
selectedAssetIndex={selectedIndex}
@ -198,7 +197,7 @@ MediaLibraryInput.propTypes = {
defaultMessage: PropTypes.string,
values: PropTypes.shape({}),
}),
error: PropTypes.shape({ id: PropTypes.string, defaultMessage: PropTypes.string }),
error: PropTypes.string,
intlLabel: PropTypes.shape({ id: PropTypes.string, defaultMessage: PropTypes.string }),
multiple: PropTypes.bool,
onChange: PropTypes.func.isRequired,

View File

@ -1,6 +1,6 @@
{
"name": "@strapi/plugin-upload",
"version": "4.1.5",
"version": "4.1.6",
"description": "Makes it easy to upload images and files to your Strapi Application.",
"license": "SEE LICENSE IN LICENSE",
"author": {
@ -23,9 +23,9 @@
"test:front:watch:ce": "cross-env IS_EE=false jest --config ./jest.config.front.js --watchAll"
},
"dependencies": {
"@strapi/helper-plugin": "4.1.5",
"@strapi/provider-upload-local": "4.1.5",
"@strapi/utils": "4.1.5",
"@strapi/helper-plugin": "4.1.6",
"@strapi/provider-upload-local": "4.1.6",
"@strapi/utils": "4.1.6",
"byte-size": "7.0.1",
"cropperjs": "1.5.11",
"fs-extra": "10.0.0",

View File

@ -1,14 +1,26 @@
'use strict';
const { join } = require('path');
const bootstrap = require('../bootstrap');
jest.mock('@strapi/provider-upload-local', () => ({
init() {
return {
uploadStream: jest.fn(),
upload: jest.fn(),
delete: jest.fn(),
};
},
}));
describe('Upload plugin bootstrap function', () => {
test('Sets default config if id does not exist', async () => {
test('Sets default config if it does not exist', async () => {
const setStore = jest.fn(() => {});
const registerMany = jest.fn(() => {});
global.strapi = {
dirs: { root: process.cwd() },
dirs: { root: process.cwd(), public: join(process.cwd(), 'public') },
admin: {
services: { permission: { actionProvider: { registerMany } } },
},
@ -16,10 +28,7 @@ describe('Upload plugin bootstrap function', () => {
error() {},
},
config: {
get: jest
.fn()
.mockReturnValueOnce({ provider: 'local' })
.mockReturnValueOnce('public'),
get: jest.fn().mockReturnValueOnce({ provider: 'local' }),
paths: {},
info: {
dependencies: {},

View File

@ -49,27 +49,35 @@ const createProvider = config => {
const providerInstance = provider.init(providerOptions);
return Object.assign(Object.create(baseProvider), {
...providerInstance,
upload(file, options = actionOptions.upload) {
return providerInstance.upload(file, options);
},
delete(file, options = actionOptions.delete) {
return providerInstance.delete(file, options);
},
if (!providerInstance.delete) {
throw new Error(`The upload provider "${providerName}" doesn't implement the delete method.`);
}
if (!providerInstance.upload && !providerInstance.uploadStream) {
throw new Error(
`The upload provider "${providerName}" doesn't implement the uploadStream nor the upload method.`
);
}
if (!providerInstance.uploadStream) {
process.emitWarning(
`The upload provider "${providerName}" doesn't implement the uploadStream function. Strapi will fallback on the upload method. Some performance issues may occur.`
);
}
const wrappedProvider = _.mapValues(providerInstance, (method, methodName) => {
return async function(file, options = actionOptions[methodName]) {
return providerInstance[methodName](file, options);
};
});
return Object.assign(Object.create(baseProvider), wrappedProvider);
};
const baseProvider = {
extend(obj) {
Object.assign(this, obj);
},
upload() {
throw new Error('Provider upload method is not implemented');
},
delete() {
throw new Error('Provider delete method is not implemented');
},
};
const registerPermissionActions = async () => {

View File

@ -7,6 +7,7 @@ const {
getCommonBeginning,
getCommonPath,
toRegressedEnumValue,
joinBy,
} = require('../string-formatting');
describe('string-formatting', () => {
@ -121,4 +122,25 @@ describe('string-formatting', () => {
expect(toRegressedEnumValue(string)).toBe(expectedResult);
});
});
describe('joinBy', () => {
test.each([
[['/', ''], ''],
[['/', '/a/'], '/a/'],
[['/', 'a', 'b'], 'a/b'],
[['/', 'a', '/b'], 'a/b'],
[['/', 'a/', '/b'], 'a/b'],
[['/', 'a/', 'b'], 'a/b'],
[['/', 'a//', 'b'], 'a/b'],
[['/', 'a//', '//b'], 'a/b'],
[['/', 'a', '//b'], 'a/b'],
[['/', '/a//', '//b/'], '/a/b/'],
[['/', 'a', 'b', 'c'], 'a/b/c'],
[['/', 'a/', '/b/', '/c'], 'a/b/c'],
[['/', 'a//', '//b//', '//c'], 'a/b/c'],
[['/', '///a///', '///b///', '///c///'], '///a/b/c///'],
])('%s => %s', (args, expectedResult) => {
expect(joinBy(...args)).toBe(expectedResult);
});
});
});

View File

@ -21,6 +21,7 @@ const {
isCamelCase,
toRegressedEnumValue,
startsWithANumber,
joinBy,
} = require('./string-formatting');
const { removeUndefined } = require('./object-formatting');
const { getConfigUrls, getAbsoluteAdminUrl, getAbsoluteServerUrl } = require('./config');
@ -51,6 +52,7 @@ module.exports = {
nameToSlug,
toRegressedEnumValue,
startsWithANumber,
joinBy,
nameToCollectionName,
getCommonBeginning,
getConfigUrls,

View File

@ -47,10 +47,7 @@ const withDefaultPagination = (args, { defaults = {}, maxLimit = -1 } = {}) => {
const usePagePagination = !isNil(args.page) || !isNil(args.pageSize);
const useOffsetPagination = !isNil(args.start) || !isNil(args.limit);
const ensureValidValues = pipe(
ensureMinValues,
ensureMaxValues(maxLimit)
);
const ensureValidValues = pipe(ensureMinValues, ensureMaxValues(maxLimit));
// If there is no pagination attribute, don't modify the payload
if (!usePagePagination && !useOffsetPagination) {

View File

@ -1,5 +1,6 @@
'use strict';
const _ = require('lodash');
const { trimChars, trimCharsEnd, trimCharsStart } = require('lodash/fp');
const slugify = require('@sindresorhus/slugify');
const nameToSlug = (name, options = { separator: '-' }) => slugify(name, options);
@ -44,6 +45,19 @@ const isCamelCase = value => /^[a-z][a-zA-Z0-9]+$/.test(value);
const isKebabCase = value => /^([a-z][a-z0-9]*)(-[a-z0-9]+)*$/.test(value);
const startsWithANumber = value => /^[0-9]/.test(value);
const joinBy = (joint, ...args) => {
const trim = trimChars(joint);
const trimEnd = trimCharsEnd(joint);
const trimStart = trimCharsStart(joint);
return args.reduce((url, path, index) => {
if (args.length === 1) return path;
if (index === 0) return trimEnd(path);
if (index === args.length - 1) return url + joint + trimStart(path);
return url + joint + trim(path);
}, '');
};
module.exports = {
nameToSlug,
nameToCollectionName,
@ -56,4 +70,5 @@ module.exports = {
isKebabCase,
toRegressedEnumValue,
startsWithANumber,
joinBy,
};

View File

@ -1,6 +1,6 @@
{
"name": "@strapi/utils",
"version": "4.1.5",
"version": "4.1.6",
"description": "Shared utilities for the Strapi packages",
"keywords": [
"strapi",

View File

@ -6,10 +6,7 @@ const fileExistsInPackages = require('../utils/fileExistsInPackages');
const getPluginList = require('../utils/getPluginList');
const packagesFolder = require('../utils/packagesFolder');
const pascalCase = flow(
camelCase,
upperFirst
);
const pascalCase = flow(camelCase, upperFirst);
const prompts = [
{

View File

@ -5,6 +5,7 @@ const { merge } = require('lodash');
const { trackUsage } = require('./utils/usage');
const defaultConfigs = require('./utils/db-configs');
const clientDependencies = require('./utils/db-client-dependencies');
const getClientName = require('./utils/db-client-name');
const createProject = require('./create-project');
module.exports = async scope => {
@ -13,7 +14,7 @@ module.exports = async scope => {
const client = scope.database.client;
const configuration = {
client,
client: getClientName({ client }),
connection: merge({}, defaultConfigs[client] || {}, scope.database),
dependencies: clientDependencies({ scope, client }),
};

View File

@ -3,7 +3,8 @@
const sqlClientModule = {
mysql: { mysql: '2.18.1' },
postgres: { pg: '8.6.0' },
sqlite: { sqlite3: '5.0.2' },
sqlite: { 'better-sqlite3': '^7.5.0' },
'sqlite-legacy': { sqlite3: '^5.0.2' },
};
/**
@ -12,6 +13,7 @@ const sqlClientModule = {
module.exports = ({ client }) => {
switch (client) {
case 'sqlite':
case 'sqlite-legacy':
case 'postgres':
case 'mysql':
return {

View File

@ -0,0 +1,13 @@
'use strict';
/**
* Client
*/
module.exports = ({ client }) => {
switch (client) {
case 'sqlite-legacy':
return 'sqlite';
default:
return client;
}
};

View File

@ -54,6 +54,10 @@ function captureStderr(name, error) {
}
function trackEvent(event, body) {
if (process.env.NODE_ENV === 'test') {
return;
}
try {
return fetch('https://analytics.strapi.io/track', {
method: 'POST',

View File

@ -1,6 +1,6 @@
{
"name": "@strapi/generate-new",
"version": "4.1.5",
"version": "4.1.6",
"description": "Generate a new Strapi application.",
"keywords": [
"generate",

View File

@ -1,6 +1,6 @@
{
"name": "@strapi/generators",
"version": "4.1.5",
"version": "4.1.6",
"description": "Interactive API generator.",
"keywords": [
"strapi",
@ -30,7 +30,7 @@
"main": "lib/index.js",
"dependencies": {
"@sindresorhus/slugify": "1.1.0",
"@strapi/utils": "4.1.5",
"@strapi/utils": "4.1.6",
"chalk": "4.1.2",
"fs-extra": "10.0.0",
"node-plop": "0.26.3",

View File

@ -1,6 +1,6 @@
{
"name": "@strapi/plugin-documentation",
"version": "4.1.5",
"version": "4.1.6",
"description": "Create an OpenAPI Document and visualize your API with SWAGGER UI.",
"repository": {
"type": "git",
@ -24,8 +24,8 @@
"test": "echo \"no tests yet\""
},
"dependencies": {
"@strapi/helper-plugin": "4.1.5",
"@strapi/utils": "4.1.5",
"@strapi/helper-plugin": "4.1.6",
"@strapi/utils": "4.1.6",
"bcryptjs": "2.4.3",
"cheerio": "^1.0.0-rc.5",
"fs-extra": "10.0.0",

Some files were not shown because too many files have changed in this diff Show More