mirror of
https://github.com/strapi/strapi.git
synced 2025-12-25 06:04:29 +00:00
Merge branch 'master' of github.com:strapi/strapi into features/marketplace-offline-layout
This commit is contained in:
commit
5838c0619e
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
@ -1 +1 @@
|
||||
open_collective: strapi
|
||||
open_collective: strapi
|
||||
|
||||
2
.github/actions/check-pr-status/package.json
vendored
2
.github/actions/check-pr-status/package.json
vendored
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "check-pr-status",
|
||||
"version": "4.1.5",
|
||||
"version": "4.1.6",
|
||||
"main": "dist/index.js",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
|
||||
4
.github/actions/run-e2e-tests/script.sh
vendored
4
.github/actions/run-e2e-tests/script.sh
vendored
@ -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 $@
|
||||
|
||||
32
.github/workflows/tests.yml
vendored
32
.github/workflows/tests.yml
vendored
@ -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
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "4.1.5",
|
||||
"version": "4.1.6",
|
||||
"packages": [
|
||||
"packages/*",
|
||||
"examples/*"
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -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"
|
||||
},
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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]);
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
@ -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;
|
||||
}
|
||||
@ -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';
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -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',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -106,7 +106,6 @@ const FieldComponent = ({
|
||||
componentValue={componentValue}
|
||||
componentValueLength={componentValueLength}
|
||||
componentUid={componentUid}
|
||||
isNested={isNested}
|
||||
isReadOnly={isReadOnly}
|
||||
max={max}
|
||||
min={min}
|
||||
|
||||
@ -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 && (
|
||||
|
||||
@ -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={{
|
||||
|
||||
@ -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 };
|
||||
|
||||
@ -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('.')
|
||||
);
|
||||
}
|
||||
@ -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',
|
||||
]);
|
||||
});
|
||||
});
|
||||
@ -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]
|
||||
|
||||
@ -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>
|
||||
)}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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'];
|
||||
|
||||
|
||||
@ -59,7 +59,6 @@ const EditSettingsView = ({ mainLayout, components, isContentTypeView, slug, upd
|
||||
'relation',
|
||||
'component',
|
||||
'boolean',
|
||||
'date',
|
||||
'media',
|
||||
'richtext',
|
||||
'timestamp',
|
||||
|
||||
@ -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'],
|
||||
},
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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 }],
|
||||
];
|
||||
|
||||
|
||||
@ -71,7 +71,4 @@ DeleteLink.propTypes = {
|
||||
|
||||
const Memoized = memo(DeleteLink, isEqual);
|
||||
|
||||
export default connect(
|
||||
Memoized,
|
||||
select
|
||||
);
|
||||
export default connect(Memoized, select);
|
||||
|
||||
@ -88,8 +88,5 @@ DraftAndPublishBadge.propTypes = {
|
||||
isPublished: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
export default connect(
|
||||
DraftAndPublishBadge,
|
||||
select
|
||||
);
|
||||
export default connect(DraftAndPublishBadge, select);
|
||||
export { DraftAndPublishBadge };
|
||||
|
||||
@ -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'];
|
||||
|
||||
@ -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: [
|
||||
{
|
||||
|
||||
@ -0,0 +1,3 @@
|
||||
export default function isFieldTypeNumber(type) {
|
||||
return ['integer', 'biginteger', 'decimal', 'float', 'number'].includes(type);
|
||||
}
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -40,7 +40,7 @@ const UnauthenticatedLayout = ({ children }) => {
|
||||
<LocaleToggle />
|
||||
</Box>
|
||||
</Flex>
|
||||
<Box paddingTop={11} paddingBottom={11}>
|
||||
<Box paddingTop={2} paddingBottom={11}>
|
||||
{children}
|
||||
</Box>
|
||||
</div>
|
||||
|
||||
@ -33,7 +33,7 @@ describe('ADMIN | PAGES | AUTH | Oops', () => {
|
||||
}
|
||||
|
||||
.c9 {
|
||||
padding-top: 64px;
|
||||
padding-top: 8px;
|
||||
padding-bottom: 64px;
|
||||
}
|
||||
|
||||
|
||||
@ -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: (
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -38,7 +38,7 @@ describe('ADMIN | PAGES | AUTH | ResetPassword', () => {
|
||||
}
|
||||
|
||||
.c9 {
|
||||
padding-top: 64px;
|
||||
padding-top: 8px;
|
||||
padding-bottom: 64px;
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
@ -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>
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -58,7 +58,7 @@ describe('Admin | UseCasePage', () => {
|
||||
}
|
||||
|
||||
.c9 {
|
||||
padding-top: 64px;
|
||||
padding-top: 8px;
|
||||
padding-bottom: 64px;
|
||||
}
|
||||
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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({
|
||||
|
||||
12
packages/core/admin/server/bootstrap.js
vendored
12
packages/core/admin/server/bootstrap.js
vendored
@ -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');
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -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)));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -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 => {
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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())
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
64
packages/core/database/lib/connection.js
Normal file
64
packages/core/database/lib/connection.js
Normal 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;
|
||||
@ -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' };
|
||||
|
||||
@ -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' };
|
||||
|
||||
@ -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 ?
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -105,7 +105,7 @@ const getColumnType = attribute => {
|
||||
case 'increments': {
|
||||
return {
|
||||
type: 'increments',
|
||||
args: [{ primary: true }],
|
||||
args: [{ primary: true, primaryKey: true }],
|
||||
notNullable: true,
|
||||
};
|
||||
}
|
||||
|
||||
@ -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"
|
||||
},
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -8,7 +8,10 @@ const getYupInnerErrors = error => {
|
||||
.join('.')
|
||||
.split(']')
|
||||
.join('')
|
||||
] = { id: curr.message };
|
||||
] = {
|
||||
id: curr.message,
|
||||
defaultMessage: curr.message,
|
||||
};
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -21,6 +21,7 @@ const defaultConfig = {
|
||||
proxy: false,
|
||||
cron: { enabled: false },
|
||||
admin: { autoOpen: false },
|
||||
dirs: { public: './public' },
|
||||
},
|
||||
admin: {},
|
||||
api: {
|
||||
|
||||
10
packages/core/strapi/lib/core/bootstrap.js
vendored
10
packages/core/strapi/lib/core/bootstrap.js
vendored
@ -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.`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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: {},
|
||||
|
||||
36
packages/core/upload/server/bootstrap.js
vendored
36
packages/core/upload/server/bootstrap.js
vendored
@ -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 () => {
|
||||
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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,
|
||||
};
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strapi/utils",
|
||||
"version": "4.1.5",
|
||||
"version": "4.1.6",
|
||||
"description": "Shared utilities for the Strapi packages",
|
||||
"keywords": [
|
||||
"strapi",
|
||||
|
||||
@ -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 = [
|
||||
{
|
||||
|
||||
@ -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 }),
|
||||
};
|
||||
|
||||
@ -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 {
|
||||
|
||||
13
packages/generators/app/lib/utils/db-client-name.js
Normal file
13
packages/generators/app/lib/utils/db-client-name.js
Normal file
@ -0,0 +1,13 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Client
|
||||
*/
|
||||
module.exports = ({ client }) => {
|
||||
switch (client) {
|
||||
case 'sqlite-legacy':
|
||||
return 'sqlite';
|
||||
default:
|
||||
return client;
|
||||
}
|
||||
};
|
||||
@ -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',
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strapi/generate-new",
|
||||
"version": "4.1.5",
|
||||
"version": "4.1.6",
|
||||
"description": "Generate a new Strapi application.",
|
||||
"keywords": [
|
||||
"generate",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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
Loading…
x
Reference in New Issue
Block a user