mirror of
https://github.com/strapi/strapi.git
synced 2025-11-03 11:25:17 +00:00
Merge branch 'features/typescript' into typescript/telemetry
This commit is contained in:
commit
77daaf7e72
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
@ -1 +1 @@
|
||||
open_collective: strapi
|
||||
open_collective: strapi
|
||||
|
||||
19
.github/ISSUE_TEMPLATE/BUG_REPORT.md
vendored
19
.github/ISSUE_TEMPLATE/BUG_REPORT.md
vendored
@ -18,6 +18,17 @@ https://github.com/strapi/strapi/blob/master/CONTRIBUTING.md#reporting-an-issue
|
||||
|
||||
## Bug report
|
||||
|
||||
### Required System information
|
||||
|
||||
<!-- Please ensure you are using the Node LTS version (v14 or v16) -->
|
||||
<!-- Strapi v3 is not supported unless it is a critical/high security issue -->
|
||||
|
||||
- Node.js version:
|
||||
- NPM version:
|
||||
- Strapi version:
|
||||
- Database:
|
||||
- Operating system:
|
||||
|
||||
### Describe the bug
|
||||
|
||||
A clear and concise description of what the bug is.
|
||||
@ -41,14 +52,6 @@ If applicable, add screenshots to help explain your problem.
|
||||
|
||||
If applicable, add code samples to help explain your problem.
|
||||
|
||||
### System
|
||||
|
||||
- Node.js version: <!-- Please ensure you are using the Node LTS version (v12, v14, v16) -->
|
||||
- NPM version:
|
||||
- Strapi version: <!-- v3 is not supported unless it is a critical/high security issue -->
|
||||
- Database:
|
||||
- Operating system:
|
||||
|
||||
### Additional context
|
||||
|
||||
Add any other context about the problem here.
|
||||
|
||||
35
.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md
vendored
35
.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md
vendored
@ -1,35 +0,0 @@
|
||||
---
|
||||
name: 🚀 Feature Request
|
||||
about: Suggest an idea to help make Strapi even better!
|
||||
---
|
||||
|
||||
<!--
|
||||
Hello 👋 Thank you for submitting a feature request.
|
||||
|
||||
We are using ProductBoard to manage our roadmap and feature requests.
|
||||
|
||||
Can you please submit your feature request here: https://portal.productboard.com/strapi
|
||||
-->
|
||||
|
||||
## Feature request
|
||||
|
||||
### Please describe your feature request
|
||||
|
||||
- [ ] **I have created my request on the Product Board before I submitted this issue**
|
||||
- [ ] **I have looked at all the other requests on the Product Board before I submitted this issue**
|
||||
|
||||
### Summary
|
||||
|
||||
Quick summary what's this feature request about.
|
||||
|
||||
### Why is it needed?
|
||||
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
### Suggested solution(s)
|
||||
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
### Related issue(s)/PR(s)
|
||||
|
||||
Let us know if this is related to any issue/pull request.
|
||||
10
.github/ISSUE_TEMPLATE/config.yml
vendored
10
.github/ISSUE_TEMPLATE/config.yml
vendored
@ -1,11 +1,17 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Product Feature Request
|
||||
url: https://feedback.strapi.io/feature-requests
|
||||
about: Provide feedback to the Strapi team and ask for new features or enhancements!
|
||||
- name: Documentation Bug Report
|
||||
url: https://github.com/strapi/documentation/issues/new?template=BUG_REPORT.md&title%5B%5D=BUG
|
||||
about: Create a report to help us improve the Strapi documentation.
|
||||
- name: Documentation Request
|
||||
url: https://github.com/strapi/documentation/issues/new?template=DOC_REQUEST.md&title%5B%5D=REQUEST
|
||||
url: https://feedback.strapi.io/documentation
|
||||
about: Suggest a new part of the documentation we are missing!
|
||||
- name: Strapi Questions and Discussions
|
||||
url: https://forum.strapi.io
|
||||
about: Please ask and answer questions on our forums.
|
||||
about: Please ask and answer questions on the community forums.
|
||||
- name: Join the Community Discord
|
||||
url: https://discord.strapi.io
|
||||
about: Come and chat with other community members!
|
||||
|
||||
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.2.0-beta.0",
|
||||
"version": "4.1.7",
|
||||
"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.4",
|
||||
"version": "4.1.7",
|
||||
"description": "A Strapi application.",
|
||||
"scripts": {
|
||||
"develop": "strapi develop",
|
||||
@ -12,17 +12,19 @@
|
||||
"strapi": "strapi"
|
||||
},
|
||||
"dependencies": {
|
||||
"@strapi/admin": "4.1.4",
|
||||
"@strapi/plugin-documentation": "4.1.4",
|
||||
"@strapi/plugin-graphql": "4.1.4",
|
||||
"@strapi/plugin-i18n": "4.1.4",
|
||||
"@strapi/plugin-sentry": "4.1.4",
|
||||
"@strapi/plugin-users-permissions": "4.1.4",
|
||||
"@strapi/provider-email-mailgun": "4.1.4",
|
||||
"@strapi/provider-upload-aws-s3": "4.1.4",
|
||||
"@strapi/provider-upload-cloudinary": "4.1.4",
|
||||
"@strapi/strapi": "4.1.4",
|
||||
"@strapi/utils": "4.1.4",
|
||||
"@strapi/admin": "4.1.7",
|
||||
"@strapi/plugin-documentation": "4.1.7",
|
||||
"@strapi/plugin-graphql": "4.1.7",
|
||||
"@strapi/plugin-i18n": "4.1.7",
|
||||
"@strapi/plugin-sentry": "4.1.7",
|
||||
"@strapi/plugin-users-permissions": "4.1.7",
|
||||
"@strapi/provider-email-mailgun": "4.1.7",
|
||||
"@strapi/provider-upload-aws-s3": "4.1.7",
|
||||
"@strapi/provider-upload-cloudinary": "4.1.7",
|
||||
"@strapi/strapi": "4.1.7",
|
||||
"@strapi/utils": "4.1.7",
|
||||
"@vscode/sqlite3": "5.0.8",
|
||||
"better-sqlite3": "7.4.6",
|
||||
"lodash": "4.17.21",
|
||||
"mysql": "2.18.1",
|
||||
"passport-google-oauth2": "0.2.0",
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "kitchensink-ts",
|
||||
"private": true,
|
||||
"version": "4.2.0-beta.0",
|
||||
"version": "4.1.7",
|
||||
"description": "A Strapi application.",
|
||||
"scripts": {
|
||||
"develop": "strapi develop",
|
||||
@ -12,12 +12,12 @@
|
||||
"strapi": "strapi"
|
||||
},
|
||||
"dependencies": {
|
||||
"@strapi/admin": "4.1.4",
|
||||
"@strapi/provider-email-mailgun": "4.1.4",
|
||||
"@strapi/provider-upload-aws-s3": "4.1.4",
|
||||
"@strapi/provider-upload-cloudinary": "4.1.4",
|
||||
"@strapi/strapi": "4.1.4",
|
||||
"@strapi/utils": "4.1.4",
|
||||
"@strapi/admin": "4.1.7",
|
||||
"@strapi/provider-email-mailgun": "4.1.7",
|
||||
"@strapi/provider-upload-aws-s3": "4.1.7",
|
||||
"@strapi/provider-upload-cloudinary": "4.1.7",
|
||||
"@strapi/strapi": "4.1.7",
|
||||
"@strapi/utils": "4.1.7",
|
||||
"lodash": "4.17.21",
|
||||
"mysql": "2.18.1",
|
||||
"passport-google-oauth2": "0.2.0",
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "kitchensink",
|
||||
"private": true,
|
||||
"version": "4.2.0-beta.0",
|
||||
"version": "4.1.7",
|
||||
"description": "A Strapi application.",
|
||||
"scripts": {
|
||||
"develop": "strapi develop",
|
||||
@ -12,12 +12,12 @@
|
||||
"strapi": "strapi"
|
||||
},
|
||||
"dependencies": {
|
||||
"@strapi/admin": "4.2.0-beta.0",
|
||||
"@strapi/provider-email-mailgun": "4.2.0-beta.0",
|
||||
"@strapi/provider-upload-aws-s3": "4.2.0-beta.0",
|
||||
"@strapi/provider-upload-cloudinary": "4.2.0-beta.0",
|
||||
"@strapi/strapi": "4.2.0-beta.0",
|
||||
"@strapi/utils": "4.2.0-beta.0",
|
||||
"@strapi/admin": "4.1.7",
|
||||
"@strapi/provider-email-mailgun": "4.1.7",
|
||||
"@strapi/provider-upload-aws-s3": "4.1.7",
|
||||
"@strapi/provider-upload-cloudinary": "4.1.7",
|
||||
"@strapi/strapi": "4.1.7",
|
||||
"@strapi/utils": "4.1.7",
|
||||
"lodash": "4.17.21",
|
||||
"mysql": "2.18.1",
|
||||
"passport-google-oauth2": "0.2.0",
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "4.2.0-beta.0",
|
||||
"version": "4.1.7",
|
||||
"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",
|
||||
|
||||
@ -15,6 +15,7 @@ const reducers = {
|
||||
'content-manager_listView': jest.fn(() => ({
|
||||
data: [],
|
||||
isLoading: true,
|
||||
components: [],
|
||||
contentType: {},
|
||||
initialDisplayedHeaders: [],
|
||||
displayedHeaders: [],
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strapi/admin-test-utils",
|
||||
"version": "4.2.0-beta.0",
|
||||
"version": "4.1.7",
|
||||
"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.4",
|
||||
"version": "4.1.7",
|
||||
"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.4",
|
||||
"@strapi/generate-new": "4.1.7",
|
||||
"commander": "6.1.0",
|
||||
"inquirer": "8.2.0"
|
||||
},
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "create-strapi-starter",
|
||||
"version": "4.1.4",
|
||||
"version": "4.1.7",
|
||||
"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.4",
|
||||
"@strapi/generate-new": "4.1.7",
|
||||
"chalk": "4.1.1",
|
||||
"ci-info": "3.1.1",
|
||||
"commander": "7.1.0",
|
||||
|
||||
@ -0,0 +1,5 @@
|
||||
<svg width="88" height="88" viewBox="0 0 88 88" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="0.5" y="0.5" width="87" height="87" rx="43.5" fill="#F0F0FF"/>
|
||||
<path d="M34.0469 39.2969H30C27.4479 39.2969 25.2604 40.2448 23.4375 42.1406C21.6146 43.9635 20.7031 46.151 20.7031 48.7031C20.7031 51.2552 21.6146 53.4427 23.4375 55.2656C25.2604 57.0885 27.4479 58 30 58H52.75L34.0469 39.2969ZM23 28.25L25.9531 25.2969L65 64.3438L62.0469 67.2969L57.3438 62.7031H30C26.1354 62.7031 22.8177 61.3542 20.0469 58.6562C17.349 55.8854 16 52.5677 16 48.7031C16 44.9115 17.3125 41.6667 19.9375 38.9688C22.5625 36.2708 25.7344 34.849 29.4531 34.7031L23 28.25ZM61.1719 39.4062C64.1615 39.625 66.7135 40.8646 68.8281 43.125C70.9427 45.3125 72 47.9375 72 51C72 55.0104 70.3594 58.1823 67.0781 60.5156L63.6875 57.125C66.0938 55.8125 67.2969 53.7708 67.2969 51C67.2969 49.1042 66.6042 47.4635 65.2188 46.0781C63.8333 44.6927 62.1927 44 60.2969 44H56.7969V42.7969C56.7969 39.224 55.5573 36.1979 53.0781 33.7188C50.599 31.2396 47.5729 30 44 30C41.9583 30 39.9896 30.474 38.0938 31.4219L34.5938 28.0312C37.4375 26.2083 40.5729 25.2969 44 25.2969C47.9375 25.2969 51.5833 26.6823 54.9375 29.4531C58.3646 32.224 60.4427 35.5417 61.1719 39.4062Z" fill="#4945FF"/>
|
||||
<rect x="0.5" y="0.5" width="87" height="87" rx="43.5" stroke="#D9D8FF"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@ -2,7 +2,7 @@ import React from 'react';
|
||||
import { render, waitFor } from '@testing-library/react';
|
||||
import { QueryClientProvider, QueryClient } from 'react-query';
|
||||
import { useGuidedTour } from '@strapi/helper-plugin';
|
||||
import { lightTheme } from '@strapi/design-system';
|
||||
import { lightTheme, darkTheme } from '@strapi/design-system';
|
||||
import { ConfigurationsContext } from '../../../contexts';
|
||||
import {
|
||||
fetchAppInfo,
|
||||
@ -48,7 +48,7 @@ const queryClient = new QueryClient({
|
||||
});
|
||||
|
||||
const app = (
|
||||
<ThemeToggleProvider themes={{ light: lightTheme }}>
|
||||
<ThemeToggleProvider themes={{ light: lightTheme, dark: darkTheme }}>
|
||||
<Theme>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ConfigurationsContext.Provider value={{ showReleaseNotification: false }}>
|
||||
|
||||
@ -21,7 +21,7 @@ const fetchStrapiLatestRelease = async toggleNotification => {
|
||||
link: {
|
||||
url: `https://github.com/strapi/strapi/releases/tag/${tag_name}`,
|
||||
label: {
|
||||
id: 'notification.version.update.link',
|
||||
id: 'global.see-more',
|
||||
},
|
||||
},
|
||||
blockTransition: true,
|
||||
|
||||
@ -78,7 +78,7 @@ const Blocker = ({ displayedIcon, description, title, isOpen }) => {
|
||||
rel="noopener noreferrer nofollow"
|
||||
>
|
||||
{formatMessage({
|
||||
id: 'app.components.BlockLink.documentation',
|
||||
id: 'global.documentation',
|
||||
defaultMessage: 'Read the documentation',
|
||||
})}
|
||||
</Link>
|
||||
|
||||
@ -2,7 +2,7 @@ import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
import { useGuidedTour } from '@strapi/helper-plugin';
|
||||
import { lightTheme } from '@strapi/design-system';
|
||||
import { lightTheme, darkTheme } from '@strapi/design-system';
|
||||
import Theme from '../../../Theme';
|
||||
import ThemeToggleProvider from '../../../ThemeToggleProvider';
|
||||
import GuidedTourModal from '../index';
|
||||
@ -30,7 +30,7 @@ jest.mock('@strapi/helper-plugin', () => ({
|
||||
}));
|
||||
|
||||
const App = (
|
||||
<ThemeToggleProvider themes={{ light: lightTheme }}>
|
||||
<ThemeToggleProvider themes={{ light: lightTheme, dark: darkTheme }}>
|
||||
<Theme>
|
||||
<IntlProvider locale="en" messages={{}} defaultLocale="en" textComponent="span">
|
||||
<GuidedTourModal />
|
||||
|
||||
@ -100,7 +100,7 @@ const LeftMenu = ({ generalSectionLinks, pluginsSectionLinks }) => {
|
||||
|
||||
<NavSections>
|
||||
<NavLink to="/content-manager" icon={<Write />}>
|
||||
{formatMessage({ id: 'content-manager.plugin.name', defaultMessage: 'Content manager' })}
|
||||
{formatMessage({ id: 'global.content-manager', defaultMessage: 'Content manager' })}
|
||||
</NavLink>
|
||||
|
||||
{pluginsSectionLinks.length > 0 ? (
|
||||
@ -160,7 +160,7 @@ const LeftMenu = ({ generalSectionLinks, pluginsSectionLinks }) => {
|
||||
<LinkUser tabIndex={0} onClick={handleToggleUserLinks} to="/me">
|
||||
<Typography>
|
||||
{formatMessage({
|
||||
id: 'app.components.LeftMenu.profile',
|
||||
id: 'global.profile',
|
||||
defaultMessage: 'Profile',
|
||||
})}
|
||||
</Typography>
|
||||
|
||||
@ -9,7 +9,7 @@ import { render, fireEvent, screen } from '@testing-library/react';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
import { useNotification } from '@strapi/helper-plugin';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { lightTheme } from '@strapi/design-system';
|
||||
import { lightTheme, darkTheme } from '@strapi/design-system';
|
||||
import Theme from '../../Theme';
|
||||
import ThemeToggleProvider from '../../ThemeToggleProvider';
|
||||
import Notifications from '../index';
|
||||
@ -21,7 +21,7 @@ describe('<Notifications />', () => {
|
||||
const {
|
||||
container: { firstChild },
|
||||
} = render(
|
||||
<ThemeToggleProvider themes={{ light: lightTheme }}>
|
||||
<ThemeToggleProvider themes={{ light: lightTheme, dark: darkTheme }}>
|
||||
<Theme>
|
||||
<IntlProvider locale="en" messages={messages} defaultLocale="en" textComponent="span">
|
||||
<Notifications>
|
||||
@ -85,7 +85,7 @@ describe('<Notifications />', () => {
|
||||
};
|
||||
|
||||
render(
|
||||
<ThemeToggleProvider themes={{ light: lightTheme }}>
|
||||
<ThemeToggleProvider themes={{ light: lightTheme, dark: darkTheme }}>
|
||||
<Theme>
|
||||
<IntlProvider locale="en" defaultLocale="en" messages={messages} textComponent="span">
|
||||
<Notifications>
|
||||
@ -128,7 +128,7 @@ describe('<Notifications />', () => {
|
||||
};
|
||||
|
||||
render(
|
||||
<ThemeToggleProvider themes={{ light: lightTheme }}>
|
||||
<ThemeToggleProvider themes={{ light: lightTheme, dark: darkTheme }}>
|
||||
<Theme>
|
||||
<IntlProvider locale="en" defaultLocale="en" messages={messages} textComponent="span">
|
||||
<Notifications>
|
||||
|
||||
@ -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]);
|
||||
|
||||
|
||||
@ -57,10 +57,13 @@ const ComponentInitializer = ({ error, isReadOnly, onClick }) => {
|
||||
</Box>
|
||||
{error?.id && (
|
||||
<Typography textColor="danger600" variant="pi">
|
||||
{formatMessage({
|
||||
id: error.id,
|
||||
defaultMessage: error.id,
|
||||
})}
|
||||
{formatMessage(
|
||||
{
|
||||
id: error.id,
|
||||
defaultMessage: error.id,
|
||||
},
|
||||
{ ...error.values }
|
||||
)}
|
||||
</Typography>
|
||||
)}
|
||||
</>
|
||||
@ -75,6 +78,8 @@ ComponentInitializer.defaultProps = {
|
||||
ComponentInitializer.propTypes = {
|
||||
error: PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
defaultMessage: PropTypes.string.isRequired,
|
||||
values: PropTypes.object,
|
||||
}),
|
||||
isReadOnly: PropTypes.bool,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
|
||||
@ -1,87 +0,0 @@
|
||||
import React from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { Box } from '@strapi/design-system/Box';
|
||||
import { Typography } from '@strapi/design-system/Typography';
|
||||
import { Loader } from '@strapi/design-system/Loader';
|
||||
import { useNotifyAT } from '@strapi/design-system/LiveRegions';
|
||||
import { axiosInstance } from '../../../../../core/utils';
|
||||
import { getRequestUrl, getTrad } from '../../../../utils';
|
||||
import CellValue from '../CellValue';
|
||||
|
||||
const fetchRelation = async (endPoint, notifyStatus) => {
|
||||
const {
|
||||
data: { results, pagination },
|
||||
} = await axiosInstance.get(endPoint);
|
||||
|
||||
notifyStatus();
|
||||
|
||||
return { results, pagination };
|
||||
};
|
||||
|
||||
const PopoverContent = ({ fieldSchema, name, rowId, targetModel, queryInfos }) => {
|
||||
const requestURL = getRequestUrl(`${queryInfos.endPoint}/${rowId}/${name.split('.')[0]}`);
|
||||
const { notifyStatus } = useNotifyAT();
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
const notify = () => {
|
||||
const message = formatMessage({
|
||||
id: getTrad('DynamicTable.relation-loaded'),
|
||||
defaultMessage: 'The relations have been loaded',
|
||||
});
|
||||
notifyStatus(message);
|
||||
};
|
||||
|
||||
const { data, status } = useQuery([targetModel, rowId], () => fetchRelation(requestURL, notify), {
|
||||
staleTime: 0,
|
||||
});
|
||||
|
||||
if (status !== 'success') {
|
||||
return (
|
||||
<Box>
|
||||
<Loader>Loading content</Loader>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ul>
|
||||
{data?.results.map(entry => {
|
||||
const value = entry[fieldSchema.name];
|
||||
|
||||
return (
|
||||
<Box as="li" key={entry.id} padding={3}>
|
||||
<Typography>
|
||||
{value ? (
|
||||
<CellValue type={fieldSchema.schema.type} value={entry[fieldSchema.name]} />
|
||||
) : (
|
||||
'-'
|
||||
)}
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
{data?.pagination.total > 10 && (
|
||||
<Box as="li" padding={3}>
|
||||
<Typography>[...]</Typography>
|
||||
</Box>
|
||||
)}
|
||||
</ul>
|
||||
);
|
||||
};
|
||||
|
||||
PopoverContent.propTypes = {
|
||||
fieldSchema: PropTypes.shape({
|
||||
name: PropTypes.string,
|
||||
schema: PropTypes.shape({ type: PropTypes.string }).isRequired,
|
||||
}).isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
rowId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||
targetModel: PropTypes.string.isRequired,
|
||||
queryInfos: PropTypes.shape({
|
||||
endPoint: PropTypes.string,
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
export default PopoverContent;
|
||||
@ -1,107 +0,0 @@
|
||||
import React, { useState, useRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { IconButton } from '@strapi/design-system/IconButton';
|
||||
import { Typography } from '@strapi/design-system/Typography';
|
||||
import { Box } from '@strapi/design-system/Box';
|
||||
import { Badge } from '@strapi/design-system/Badge';
|
||||
import { Flex } from '@strapi/design-system/Flex';
|
||||
import { Popover } from '@strapi/design-system/Popover';
|
||||
import { SortIcon, stopPropagation } from '@strapi/helper-plugin';
|
||||
import styled from 'styled-components';
|
||||
import PopoverContent from './PopoverContent';
|
||||
import CellValue from '../CellValue';
|
||||
|
||||
const SINGLE_RELATIONS = ['oneToOne', 'manyToOne'];
|
||||
|
||||
const ActionWrapper = styled.span`
|
||||
svg {
|
||||
height: ${4 / 16}rem;
|
||||
}
|
||||
`;
|
||||
|
||||
const RelationCountBadge = styled(Badge)`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: ${20 / 16}rem;
|
||||
width: ${16 / 16}rem;
|
||||
`;
|
||||
|
||||
const Relation = ({ fieldSchema, metadatas, queryInfos, name, rowId, value }) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const buttonRef = useRef();
|
||||
|
||||
if (SINGLE_RELATIONS.includes(fieldSchema.relation)) {
|
||||
return (
|
||||
<Typography textColor="neutral800">
|
||||
<CellValue type={metadatas.mainField.schema.type} value={value[metadatas.mainField.name]} />
|
||||
</Typography>
|
||||
);
|
||||
}
|
||||
|
||||
const handleTogglePopover = () => setVisible(prev => !prev);
|
||||
|
||||
return (
|
||||
<Flex {...stopPropagation}>
|
||||
<RelationCountBadge>{value.count}</RelationCountBadge>
|
||||
<Box paddingLeft={2}>
|
||||
<Typography textColor="neutral800">
|
||||
{formatMessage(
|
||||
{
|
||||
id: 'content-manager.containers.ListPage.items',
|
||||
defaultMessage: '{number, plural, =0 {items} one {item} other {items}}',
|
||||
},
|
||||
{ number: value.count }
|
||||
)}
|
||||
</Typography>
|
||||
</Box>
|
||||
{value.count > 0 && (
|
||||
<ActionWrapper>
|
||||
<IconButton
|
||||
onClick={handleTogglePopover}
|
||||
ref={buttonRef}
|
||||
noBorder
|
||||
label={formatMessage({
|
||||
id: 'content-manager.popover.display-relations.label',
|
||||
defaultMessage: 'Display relations',
|
||||
})}
|
||||
icon={<SortIcon isUp={visible} />}
|
||||
/>
|
||||
{visible && (
|
||||
<Popover source={buttonRef} spacing={16} centered>
|
||||
<PopoverContent
|
||||
queryInfos={queryInfos}
|
||||
name={name}
|
||||
fieldSchema={metadatas.mainField}
|
||||
targetModel={fieldSchema.targetModel}
|
||||
rowId={rowId}
|
||||
count={value.count}
|
||||
/>
|
||||
</Popover>
|
||||
)}
|
||||
</ActionWrapper>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
Relation.propTypes = {
|
||||
fieldSchema: PropTypes.shape({
|
||||
relation: PropTypes.string,
|
||||
targetModel: PropTypes.string,
|
||||
type: PropTypes.string.isRequired,
|
||||
}).isRequired,
|
||||
metadatas: PropTypes.shape({
|
||||
mainField: PropTypes.shape({
|
||||
name: PropTypes.string.isRequired,
|
||||
schema: PropTypes.shape({ type: PropTypes.string.isRequired }).isRequired,
|
||||
}),
|
||||
}).isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
rowId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||
queryInfos: PropTypes.shape({ endPoint: PropTypes.string.isRequired }).isRequired,
|
||||
value: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default Relation;
|
||||
@ -0,0 +1,135 @@
|
||||
import React, { useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useQuery } from 'react-query';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { Typography } from '@strapi/design-system/Typography';
|
||||
import { Box } from '@strapi/design-system/Box';
|
||||
import { Badge } from '@strapi/design-system/Badge';
|
||||
import { SimpleMenu, MenuItem } from '@strapi/design-system/SimpleMenu';
|
||||
import { Loader } from '@strapi/design-system/Loader';
|
||||
import styled from 'styled-components';
|
||||
import { useNotifyAT } from '@strapi/design-system/LiveRegions';
|
||||
import { stopPropagation } from '@strapi/helper-plugin';
|
||||
import CellValue from '../CellValue';
|
||||
import { axiosInstance } from '../../../../../core/utils';
|
||||
import { getRequestUrl, getTrad } from '../../../../utils';
|
||||
|
||||
const TypographyMaxWidth = styled(Typography)`
|
||||
max-width: 500px;
|
||||
`;
|
||||
|
||||
const fetchRelation = async (endPoint, notifyStatus) => {
|
||||
const {
|
||||
data: { results, pagination },
|
||||
} = await axiosInstance.get(endPoint);
|
||||
|
||||
notifyStatus();
|
||||
|
||||
return { results, pagination };
|
||||
};
|
||||
|
||||
const RelationMultiple = ({ fieldSchema, metadatas, queryInfos, name, rowId, value }) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const { notifyStatus } = useNotifyAT();
|
||||
const requestURL = getRequestUrl(`${queryInfos.endPoint}/${rowId}/${name.split('.')[0]}`);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const Label = (
|
||||
<>
|
||||
<Badge>{value.count}</Badge>{' '}
|
||||
{formatMessage(
|
||||
{
|
||||
id: 'content-manager.containers.ListPage.items',
|
||||
defaultMessage: '{number, plural, =0 {items} one {item} other {items}}',
|
||||
},
|
||||
{ number: value.count }
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
const notify = () => {
|
||||
const message = formatMessage({
|
||||
id: getTrad('DynamicTable.relation-loaded'),
|
||||
defaultMessage: 'Relations have been loaded',
|
||||
});
|
||||
notifyStatus(message);
|
||||
};
|
||||
|
||||
const { data, status } = useQuery(
|
||||
[fieldSchema.targetModel, rowId],
|
||||
() => fetchRelation(requestURL, notify),
|
||||
{
|
||||
enabled: isOpen,
|
||||
staleTime: 0,
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<Box {...stopPropagation}>
|
||||
<SimpleMenu
|
||||
label={Label}
|
||||
size="S"
|
||||
onOpen={() => setIsOpen(true)}
|
||||
onClose={() => setIsOpen(false)}
|
||||
>
|
||||
{status !== 'success' && (
|
||||
<MenuItem aria-disabled>
|
||||
<Loader small>
|
||||
{formatMessage({
|
||||
id: getTrad('DynamicTable.relation-loading'),
|
||||
defaultMessage: 'Relations are loading',
|
||||
})}
|
||||
</Loader>
|
||||
</MenuItem>
|
||||
)}
|
||||
|
||||
{status === 'success' && (
|
||||
<>
|
||||
{data?.results.map(entry => (
|
||||
<MenuItem key={entry.id} aria-disabled>
|
||||
<TypographyMaxWidth ellipsis>
|
||||
<CellValue
|
||||
type={metadatas.mainField.schema.type}
|
||||
value={entry[metadatas.mainField.name] || entry.id}
|
||||
/>
|
||||
</TypographyMaxWidth>
|
||||
</MenuItem>
|
||||
))}
|
||||
|
||||
{data?.pagination.total > 10 && (
|
||||
<MenuItem
|
||||
aria-disabled
|
||||
aria-label={formatMessage({
|
||||
id: getTrad('DynamicTable.relation-more'),
|
||||
defaultMessage: 'This relation contains more entities than displayed',
|
||||
})}
|
||||
>
|
||||
<Typography>...</Typography>
|
||||
</MenuItem>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</SimpleMenu>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
RelationMultiple.propTypes = {
|
||||
fieldSchema: PropTypes.shape({
|
||||
relation: PropTypes.string,
|
||||
targetModel: PropTypes.string,
|
||||
type: PropTypes.string.isRequired,
|
||||
}).isRequired,
|
||||
metadatas: PropTypes.shape({
|
||||
mainField: PropTypes.shape({
|
||||
name: PropTypes.string.isRequired,
|
||||
schema: PropTypes.shape({ type: PropTypes.string.isRequired }).isRequired,
|
||||
}),
|
||||
}).isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
rowId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||
queryInfos: PropTypes.shape({ endPoint: PropTypes.string.isRequired }).isRequired,
|
||||
value: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default RelationMultiple;
|
||||
@ -0,0 +1,291 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`DynamicTabe / Cellcontent / RelationMultiple renders and matches the snapshot 1`] = `
|
||||
.c11 {
|
||||
border: 0;
|
||||
-webkit-clip: rect(0 0 0 0);
|
||||
clip: rect(0 0 0 0);
|
||||
height: 1px;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
}
|
||||
|
||||
.c5 {
|
||||
background: #f6f6f9;
|
||||
padding: 4px;
|
||||
border-radius: 4px;
|
||||
min-width: 20px;
|
||||
}
|
||||
|
||||
.c6 {
|
||||
display: -webkit-inline-box;
|
||||
display: -webkit-inline-flex;
|
||||
display: -ms-inline-flexbox;
|
||||
display: inline-flex;
|
||||
-webkit-flex-direction: row;
|
||||
-ms-flex-direction: row;
|
||||
flex-direction: row;
|
||||
-webkit-box-pack: center;
|
||||
-webkit-justify-content: center;
|
||||
-ms-flex-pack: center;
|
||||
justify-content: center;
|
||||
-webkit-align-items: center;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.c7 {
|
||||
color: #666687;
|
||||
font-weight: 600;
|
||||
font-size: 0.6875rem;
|
||||
line-height: 1.45;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.c4 {
|
||||
font-weight: 600;
|
||||
color: #32324d;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.33;
|
||||
}
|
||||
|
||||
.c9 {
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
.c0 {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
background: #ffffff;
|
||||
border: 1px solid #dcdce4;
|
||||
position: relative;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.c0 svg {
|
||||
height: 12px;
|
||||
width: 12px;
|
||||
}
|
||||
|
||||
.c0 svg > g,
|
||||
.c0 svg path {
|
||||
fill: #ffffff;
|
||||
}
|
||||
|
||||
.c0[aria-disabled='true'] {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.c0:after {
|
||||
-webkit-transition-property: all;
|
||||
transition-property: all;
|
||||
-webkit-transition-duration: 0.2s;
|
||||
transition-duration: 0.2s;
|
||||
border-radius: 8px;
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -4px;
|
||||
bottom: -4px;
|
||||
left: -4px;
|
||||
right: -4px;
|
||||
border: 2px solid transparent;
|
||||
}
|
||||
|
||||
.c0:focus-visible {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.c0:focus-visible:after {
|
||||
border-radius: 8px;
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -5px;
|
||||
bottom: -5px;
|
||||
left: -5px;
|
||||
right: -5px;
|
||||
border: 2px solid #4945ff;
|
||||
}
|
||||
|
||||
.c1 {
|
||||
-webkit-align-items: center;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
padding: 8px 16px;
|
||||
background: #4945ff;
|
||||
border: none;
|
||||
border: 1px solid transparent;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.c1 .c8 {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-align-items: center;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.c1 .c3 {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.c1[aria-disabled='true'] {
|
||||
border: 1px solid #dcdce4;
|
||||
background: #eaeaef;
|
||||
}
|
||||
|
||||
.c1[aria-disabled='true'] .c3 {
|
||||
color: #666687;
|
||||
}
|
||||
|
||||
.c1[aria-disabled='true'] svg > g,
|
||||
.c1[aria-disabled='true'] svg path {
|
||||
fill: #666687;
|
||||
}
|
||||
|
||||
.c1[aria-disabled='true']:active {
|
||||
border: 1px solid #dcdce4;
|
||||
background: #eaeaef;
|
||||
}
|
||||
|
||||
.c1[aria-disabled='true']:active .c3 {
|
||||
color: #666687;
|
||||
}
|
||||
|
||||
.c1[aria-disabled='true']:active svg > g,
|
||||
.c1[aria-disabled='true']:active svg path {
|
||||
fill: #666687;
|
||||
}
|
||||
|
||||
.c1:hover {
|
||||
background-color: #f6f6f9;
|
||||
}
|
||||
|
||||
.c1:active {
|
||||
border: 1px solid undefined;
|
||||
background: undefined;
|
||||
}
|
||||
|
||||
.c1 .c3 {
|
||||
color: #32324d;
|
||||
}
|
||||
|
||||
.c1 svg > g,
|
||||
.c1 svg path {
|
||||
fill: #8e8ea9;
|
||||
}
|
||||
|
||||
.c10 {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-align-items: center;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.c10 svg {
|
||||
height: 4px;
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.c2 {
|
||||
padding: 4px 12px;
|
||||
}
|
||||
|
||||
<div>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class=""
|
||||
role="button"
|
||||
>
|
||||
<div>
|
||||
<button
|
||||
aria-controls="simplemenu-1"
|
||||
aria-disabled="false"
|
||||
aria-expanded="false"
|
||||
aria-haspopup="true"
|
||||
class="c0 c1 c2"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="c3 c4"
|
||||
>
|
||||
<div
|
||||
class="c5 c6"
|
||||
>
|
||||
<span
|
||||
class="c7"
|
||||
>
|
||||
1
|
||||
</span>
|
||||
</div>
|
||||
|
||||
item
|
||||
</span>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="c8 c9"
|
||||
>
|
||||
<span
|
||||
class="c10"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="none"
|
||||
height="1em"
|
||||
viewBox="0 0 14 8"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
clip-rule="evenodd"
|
||||
d="M14 .889a.86.86 0 01-.26.625L7.615 7.736A.834.834 0 017 8a.834.834 0 01-.615-.264L.26 1.514A.861.861 0 010 .889c0-.24.087-.45.26-.625A.834.834 0 01.875 0h12.25c.237 0 .442.088.615.264a.86.86 0 01.26.625z"
|
||||
fill="#32324D"
|
||||
fill-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c11"
|
||||
>
|
||||
<p
|
||||
aria-live="polite"
|
||||
aria-relevant="all"
|
||||
id="live-region-log"
|
||||
role="log"
|
||||
/>
|
||||
<p
|
||||
aria-live="polite"
|
||||
aria-relevant="all"
|
||||
id="live-region-status"
|
||||
role="status"
|
||||
/>
|
||||
<p
|
||||
aria-live="assertive"
|
||||
aria-relevant="all"
|
||||
id="live-region-alert"
|
||||
role="alert"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@ -0,0 +1,80 @@
|
||||
import React from 'react';
|
||||
import { render, fireEvent, screen, waitFor } from '@testing-library/react';
|
||||
import { ThemeProvider, lightTheme } from '@strapi/design-system';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
import { QueryClientProvider, QueryClient } from 'react-query';
|
||||
|
||||
import { axiosInstance } from '../../../../../../core/utils';
|
||||
import RelationMultiple from '../index';
|
||||
|
||||
jest.spyOn(axiosInstance, 'get').mockResolvedValue({
|
||||
data: {
|
||||
results: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Relation entity 1',
|
||||
},
|
||||
],
|
||||
|
||||
pagination: {
|
||||
total: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const DEFAULT_PROPS_FIXTURE = {
|
||||
fieldSchema: {
|
||||
type: 'relation',
|
||||
relation: 'manyToMany',
|
||||
target: 'api::category.category',
|
||||
},
|
||||
queryInfos: {
|
||||
endPoint: 'collection-types/api::address.address',
|
||||
},
|
||||
metadatas: {
|
||||
mainField: {
|
||||
name: 'name',
|
||||
schema: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
value: {
|
||||
count: 1,
|
||||
},
|
||||
name: 'categories.name',
|
||||
rowId: 1,
|
||||
};
|
||||
|
||||
const ComponentFixture = () => {
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ThemeProvider theme={lightTheme}>
|
||||
<IntlProvider locale="en" messages={{}} defaultLocale="en">
|
||||
<RelationMultiple {...DEFAULT_PROPS_FIXTURE} />
|
||||
</IntlProvider>
|
||||
</ThemeProvider>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
};
|
||||
|
||||
describe('DynamicTabe / Cellcontent / RelationMultiple', () => {
|
||||
it('renders and matches the snapshot', async () => {
|
||||
const { container } = render(<ComponentFixture />);
|
||||
expect(container).toMatchSnapshot();
|
||||
expect(axiosInstance.get).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('fetches relation entities once the menu is opened', async () => {
|
||||
const { container } = render(<ComponentFixture />);
|
||||
const button = container.querySelector('[type=button]');
|
||||
|
||||
fireEvent(button, new MouseEvent('mousedown', { bubbles: true }));
|
||||
|
||||
expect(screen.getByText('Relations are loading')).toBeInTheDocument();
|
||||
expect(axiosInstance.get).toHaveBeenCalledTimes(1);
|
||||
await waitFor(() => expect(screen.getByText('Relation entity 1')).toBeInTheDocument());
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,32 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Typography } from '@strapi/design-system/Typography';
|
||||
import styled from 'styled-components';
|
||||
import CellValue from '../CellValue';
|
||||
|
||||
const TypographyMaxWidth = styled(Typography)`
|
||||
max-width: 500px;
|
||||
`;
|
||||
|
||||
const RelationSingle = ({ metadatas, value }) => {
|
||||
return (
|
||||
<TypographyMaxWidth textColor="neutral800" ellipsis>
|
||||
<CellValue
|
||||
type={metadatas.mainField.schema.type}
|
||||
value={value[metadatas.mainField.name] || value.id}
|
||||
/>
|
||||
</TypographyMaxWidth>
|
||||
);
|
||||
};
|
||||
|
||||
RelationSingle.propTypes = {
|
||||
metadatas: PropTypes.shape({
|
||||
mainField: PropTypes.shape({
|
||||
name: PropTypes.string.isRequired,
|
||||
schema: PropTypes.shape({ type: PropTypes.string.isRequired }).isRequired,
|
||||
}),
|
||||
}).isRequired,
|
||||
value: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default RelationSingle;
|
||||
@ -0,0 +1,59 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`DynamicTabe / Cellcontent / RelationSingle renders and matches the snapshot 1`] = `
|
||||
.c2 {
|
||||
border: 0;
|
||||
-webkit-clip: rect(0 0 0 0);
|
||||
clip: rect(0 0 0 0);
|
||||
height: 1px;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
}
|
||||
|
||||
.c0 {
|
||||
color: #32324d;
|
||||
display: block;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.43;
|
||||
}
|
||||
|
||||
.c1 {
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
<div>
|
||||
<span
|
||||
class="c0 c1"
|
||||
>
|
||||
|
||||
</span>
|
||||
<div
|
||||
class="c2"
|
||||
>
|
||||
<p
|
||||
aria-live="polite"
|
||||
aria-relevant="all"
|
||||
id="live-region-log"
|
||||
role="log"
|
||||
/>
|
||||
<p
|
||||
aria-live="polite"
|
||||
aria-relevant="all"
|
||||
id="live-region-status"
|
||||
role="status"
|
||||
/>
|
||||
<p
|
||||
aria-live="assertive"
|
||||
aria-relevant="all"
|
||||
id="live-region-alert"
|
||||
role="alert"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@ -0,0 +1,37 @@
|
||||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import { ThemeProvider, lightTheme } from '@strapi/design-system';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
|
||||
import RelationSingle from '../index';
|
||||
|
||||
const DEFAULT_PROPS_FIXTURE = {
|
||||
metadatas: {
|
||||
mainField: {
|
||||
name: 'name',
|
||||
schema: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
value: {
|
||||
count: 1,
|
||||
},
|
||||
};
|
||||
|
||||
const ComponentFixture = () => {
|
||||
return (
|
||||
<ThemeProvider theme={lightTheme}>
|
||||
<IntlProvider locale="en" messages={{}} defaultLocale="en">
|
||||
<RelationSingle {...DEFAULT_PROPS_FIXTURE} />
|
||||
</IntlProvider>
|
||||
</ThemeProvider>
|
||||
);
|
||||
};
|
||||
|
||||
describe('DynamicTabe / Cellcontent / RelationSingle', () => {
|
||||
it('renders and matches the snapshot', async () => {
|
||||
const { container } = render(<ComponentFixture />);
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,62 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styled from 'styled-components';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { Badge } from '@strapi/design-system/Badge';
|
||||
import { Box } from '@strapi/design-system/Box';
|
||||
import { Typography } from '@strapi/design-system/Typography';
|
||||
import { SimpleMenu, MenuItem } from '@strapi/design-system/SimpleMenu';
|
||||
import { stopPropagation } from '@strapi/helper-plugin';
|
||||
|
||||
import CellValue from '../CellValue';
|
||||
|
||||
const TypographyMaxWidth = styled(Typography)`
|
||||
max-width: 500px;
|
||||
`;
|
||||
|
||||
const RepeatableComponentCell = ({ value, metadatas }) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const {
|
||||
mainField: { type: mainFieldType, name: mainFieldName },
|
||||
} = metadatas;
|
||||
|
||||
const Label = (
|
||||
<>
|
||||
<Badge>{value.length}</Badge>{' '}
|
||||
{formatMessage(
|
||||
{
|
||||
id: 'content-manager.containers.ListPage.items',
|
||||
defaultMessage: '{number, plural, =0 {items} one {item} other {items}}',
|
||||
},
|
||||
{ number: value.length }
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<Box {...stopPropagation}>
|
||||
<SimpleMenu label={Label} size="S">
|
||||
{value.map(item => (
|
||||
<MenuItem key={item.id} aria-disabled>
|
||||
<TypographyMaxWidth ellipsis>
|
||||
<CellValue type={mainFieldType} value={item[mainFieldName] || item.id} />
|
||||
</TypographyMaxWidth>
|
||||
</MenuItem>
|
||||
))}
|
||||
</SimpleMenu>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
RepeatableComponentCell.propTypes = {
|
||||
metadatas: PropTypes.shape({
|
||||
mainField: PropTypes.shape({
|
||||
name: PropTypes.string,
|
||||
type: PropTypes.string,
|
||||
value: PropTypes.string,
|
||||
}),
|
||||
}).isRequired,
|
||||
value: PropTypes.array.isRequired,
|
||||
};
|
||||
|
||||
export default RepeatableComponentCell;
|
||||
@ -0,0 +1,37 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styled from 'styled-components';
|
||||
import { Tooltip } from '@strapi/design-system/Tooltip';
|
||||
import { Typography } from '@strapi/design-system/Typography';
|
||||
|
||||
import CellValue from '../CellValue';
|
||||
|
||||
const TypographyMaxWidth = styled(Typography)`
|
||||
max-width: 250px;
|
||||
`;
|
||||
|
||||
const SingleComponentCell = ({ value, metadatas }) => {
|
||||
const { mainField } = metadatas;
|
||||
const content = value[mainField.name];
|
||||
|
||||
return (
|
||||
<Tooltip label={content}>
|
||||
<TypographyMaxWidth textColor="neutral800" ellipsis>
|
||||
<CellValue type={mainField.type} value={content} />
|
||||
</TypographyMaxWidth>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
SingleComponentCell.propTypes = {
|
||||
metadatas: PropTypes.shape({
|
||||
mainField: PropTypes.shape({
|
||||
name: PropTypes.string,
|
||||
type: PropTypes.string,
|
||||
value: PropTypes.string,
|
||||
}),
|
||||
}).isRequired,
|
||||
value: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default SingleComponentCell;
|
||||
@ -4,44 +4,64 @@ import styled from 'styled-components';
|
||||
import { Typography } from '@strapi/design-system/Typography';
|
||||
import Media from './Media';
|
||||
import MultipleMedias from './MultipleMedias';
|
||||
import Relation from './Relation';
|
||||
import RelationMultiple from './RelationMultiple';
|
||||
import RelationSingle from './RelationSingle';
|
||||
import RepeatableComponent from './RepeatableComponent';
|
||||
import SingleComponent from './SingleComponent';
|
||||
import CellValue from './CellValue';
|
||||
import hasContent from './utils/hasContent';
|
||||
import isSingleRelation from './utils/isSingleRelation';
|
||||
|
||||
const TypographyMaxWidth = styled(Typography)`
|
||||
max-width: 300px;
|
||||
`;
|
||||
|
||||
const CellContent = ({ content, fieldSchema, metadatas, name, queryInfos, rowId }) => {
|
||||
if (content === null || content === undefined) {
|
||||
const { type } = fieldSchema;
|
||||
|
||||
if (!hasContent(type, content, metadatas, fieldSchema)) {
|
||||
return <Typography textColor="neutral800">-</Typography>;
|
||||
}
|
||||
|
||||
if (fieldSchema.type === 'media' && !fieldSchema.multiple) {
|
||||
return <Media {...content} />;
|
||||
}
|
||||
switch (type) {
|
||||
case 'media':
|
||||
if (!fieldSchema.multiple) {
|
||||
return <Media {...content} />;
|
||||
}
|
||||
|
||||
if (fieldSchema.type === 'media' && fieldSchema.multiple) {
|
||||
return <MultipleMedias value={content} />;
|
||||
}
|
||||
return <MultipleMedias value={content} />;
|
||||
|
||||
if (fieldSchema.type === 'relation') {
|
||||
return (
|
||||
<Relation
|
||||
fieldSchema={fieldSchema}
|
||||
queryInfos={queryInfos}
|
||||
metadatas={metadatas}
|
||||
value={content}
|
||||
name={name}
|
||||
rowId={rowId}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case 'relation': {
|
||||
if (isSingleRelation(fieldSchema.relation)) {
|
||||
return <RelationSingle metadatas={metadatas} value={content} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<TypographyMaxWidth ellipsis textColor="neutral800">
|
||||
<CellValue type={fieldSchema.type} value={content} />
|
||||
</TypographyMaxWidth>
|
||||
);
|
||||
return (
|
||||
<RelationMultiple
|
||||
fieldSchema={fieldSchema}
|
||||
queryInfos={queryInfos}
|
||||
metadatas={metadatas}
|
||||
value={content}
|
||||
name={name}
|
||||
rowId={rowId}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
case 'component':
|
||||
if (fieldSchema.repeatable === true) {
|
||||
return <RepeatableComponent value={content} metadatas={metadatas} />;
|
||||
}
|
||||
|
||||
return <SingleComponent value={content} metadatas={metadatas} />;
|
||||
|
||||
default:
|
||||
return (
|
||||
<TypographyMaxWidth ellipsis textColor="neutral800">
|
||||
<CellValue type={type} value={content} />
|
||||
</TypographyMaxWidth>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
CellContent.defaultProps = {
|
||||
@ -51,8 +71,13 @@ CellContent.defaultProps = {
|
||||
|
||||
CellContent.propTypes = {
|
||||
content: PropTypes.any,
|
||||
fieldSchema: PropTypes.shape({ multiple: PropTypes.bool, type: PropTypes.string.isRequired })
|
||||
.isRequired,
|
||||
fieldSchema: PropTypes.shape({
|
||||
component: PropTypes.string,
|
||||
multiple: PropTypes.bool,
|
||||
type: PropTypes.string.isRequired,
|
||||
repeatable: PropTypes.bool,
|
||||
relation: PropTypes.string,
|
||||
}).isRequired,
|
||||
metadatas: PropTypes.object.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
rowId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||
|
||||
@ -0,0 +1,60 @@
|
||||
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, type: mainFieldType },
|
||||
} = metadatas;
|
||||
|
||||
// Repeatable fields show the ID as fallback, in case the mainField
|
||||
// doesn't have any content
|
||||
if (fieldSchema?.repeatable) {
|
||||
return content.length > 0;
|
||||
}
|
||||
|
||||
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') {
|
||||
if (isSingleRelation(fieldSchema.relation)) {
|
||||
return !isEmpty(content);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
export default function isSingleRelation(type) {
|
||||
return ['oneToOne', 'manyToOne', 'oneToOneMorph'].includes(type);
|
||||
}
|
||||
@ -0,0 +1,262 @@
|
||||
import hasContent from '../hasContent';
|
||||
|
||||
describe('hasContent', () => {
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ID', () => {
|
||||
it('returns true for id main fields', () => {
|
||||
const normalizedContent = hasContent('media', { id: 1 });
|
||||
expect(normalizedContent).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
describe('relations', () => {
|
||||
it('extracts content from multiple relations with content', () => {
|
||||
const normalizedContent = hasContent('relation', { count: 1 }, undefined, {
|
||||
relation: 'manyToMany',
|
||||
});
|
||||
expect(normalizedContent).toEqual(true);
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,13 @@
|
||||
import isSingleRelation from '../isSingleRelation';
|
||||
|
||||
describe('isSingleRelation', () => {
|
||||
['oneToOne', 'manyToOne', 'oneToOneMorph'].forEach(type => {
|
||||
test(`is single relation: ${type}`, () => {
|
||||
expect(isSingleRelation(type)).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
test('is not single relation', () => {
|
||||
expect(isSingleRelation('manyToMany')).toBeFalsy();
|
||||
});
|
||||
});
|
||||
@ -148,7 +148,7 @@ const TableRows = ({
|
||||
onClickDelete(data.id);
|
||||
}}
|
||||
label={formatMessage(
|
||||
{ id: 'app.component.table.delete', defaultMessage: 'Delete {target}' },
|
||||
{ id: 'global.delete-target', defaultMessage: 'Delete {target}' },
|
||||
{ target: itemLineText }
|
||||
)}
|
||||
noBorder
|
||||
|
||||
@ -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={{
|
||||
|
||||
@ -92,7 +92,7 @@ const AccordionGroupCustom = ({ children, footer, label, labelAction, error }) =
|
||||
{error && (
|
||||
<Box paddingTop={1}>
|
||||
<Typography variant="pi" textColor="danger600">
|
||||
{formatMessage({ id: error.id, defaultMessage: error.id })}
|
||||
{formatMessage({ id: error.id, defaultMessage: error.id }, { ...error.values })}
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
@ -111,6 +111,8 @@ AccordionGroupCustom.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
error: PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
defaultMessage: PropTypes.string.isRequired,
|
||||
values: PropTypes.object,
|
||||
}),
|
||||
footer: PropTypes.node,
|
||||
label: PropTypes.string,
|
||||
|
||||
@ -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',
|
||||
]);
|
||||
});
|
||||
});
|
||||
@ -70,7 +70,7 @@ function SelectMany({
|
||||
onMenuOpen={onMenuOpen}
|
||||
onMenuScrollToBottom={onMenuScrollToBottom}
|
||||
placeholder={formatMessage(
|
||||
placeholder || { id: 'components.Select.placeholder', defaultMessage: 'Select...' }
|
||||
placeholder || { id: 'global.select', defaultMessage: 'Select...' }
|
||||
)}
|
||||
styles={styles}
|
||||
value={[]}
|
||||
|
||||
@ -46,7 +46,7 @@ function SelectOne({
|
||||
onMenuOpen={onMenuOpen}
|
||||
onMenuScrollToBottom={onMenuScrollToBottom}
|
||||
placeholder={formatMessage(
|
||||
placeholder || { id: 'components.Select.placeholder', defaultMessage: 'Select...' }
|
||||
placeholder || { id: 'global.select', defaultMessage: 'Select...' }
|
||||
)}
|
||||
styles={styles}
|
||||
value={isNull(value) ? null : { label: get(value, [mainField.name], ''), value }}
|
||||
|
||||
@ -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>
|
||||
)}
|
||||
|
||||
@ -40,7 +40,7 @@ const formatLayouts = (initialData, models) => {
|
||||
const formattedCTEditLayout = formatLayoutWithMetas(data.contentType, null, models);
|
||||
const ctUid = data.contentType.uid;
|
||||
const formattedEditRelationsLayout = formatEditRelationsLayoutWithMetas(data.contentType, models);
|
||||
const formattedListLayout = formatListLayoutWithMetas(data.contentType, models);
|
||||
const formattedListLayout = formatListLayoutWithMetas(data.contentType, data.components);
|
||||
|
||||
set(data, ['contentType', 'layouts', 'edit'], formattedCTEditLayout);
|
||||
set(data, ['contentType', 'layouts', 'editRelations'], formattedEditRelationsLayout);
|
||||
@ -146,7 +146,7 @@ const formatLayoutWithMetas = (contentTypeConfiguration, ctUid, models) => {
|
||||
return formatted;
|
||||
};
|
||||
|
||||
const formatListLayoutWithMetas = contentTypeConfiguration => {
|
||||
const formatListLayoutWithMetas = (contentTypeConfiguration, components) => {
|
||||
const formatted = contentTypeConfiguration.layouts.list.reduce((acc, current) => {
|
||||
const fieldSchema = get(contentTypeConfiguration, ['attributes', current], {});
|
||||
const metadatas = get(contentTypeConfiguration, ['metadatas', current, 'list'], {});
|
||||
@ -164,6 +164,27 @@ const formatListLayoutWithMetas = contentTypeConfiguration => {
|
||||
return acc;
|
||||
}
|
||||
|
||||
if (type === 'component') {
|
||||
const component = components[fieldSchema.component];
|
||||
const mainFieldName = component.settings.mainField;
|
||||
const mainFieldAttribute = component.attributes[mainFieldName];
|
||||
|
||||
acc.push({
|
||||
key: `__${current}_key__`,
|
||||
name: current,
|
||||
fieldSchema,
|
||||
metadatas: {
|
||||
...metadatas,
|
||||
mainField: {
|
||||
...mainFieldAttribute,
|
||||
name: mainFieldName,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return acc;
|
||||
}
|
||||
|
||||
acc.push({ key: `__${current}_key__`, name: current, fieldSchema, metadatas });
|
||||
|
||||
return acc;
|
||||
|
||||
@ -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 }],
|
||||
],
|
||||
},
|
||||
@ -485,12 +494,22 @@ describe('Content Manager | hooks | useFetchContentTypeLayout | utils ', () => {
|
||||
const data = {
|
||||
uid: 'address',
|
||||
layouts: {
|
||||
list: ['test', 'categories'],
|
||||
list: ['test', 'categories', 'component'],
|
||||
},
|
||||
metadatas: {
|
||||
test: {
|
||||
list: { ok: true },
|
||||
},
|
||||
component: {
|
||||
list: {
|
||||
mainField: {
|
||||
name: 'name',
|
||||
schema: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
categories: {
|
||||
list: {
|
||||
ok: true,
|
||||
@ -509,6 +528,22 @@ describe('Content Manager | hooks | useFetchContentTypeLayout | utils ', () => {
|
||||
type: 'relation',
|
||||
targetModel: 'category',
|
||||
},
|
||||
component: {
|
||||
type: 'component',
|
||||
component: 'some.component',
|
||||
repeatable: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
const components = {
|
||||
'some.component': {
|
||||
settings: {
|
||||
mainField: 'name',
|
||||
},
|
||||
|
||||
attributes: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
};
|
||||
const expected = [
|
||||
@ -533,9 +568,23 @@ describe('Content Manager | hooks | useFetchContentTypeLayout | utils ', () => {
|
||||
fieldSchema: { type: 'relation', targetModel: 'category' },
|
||||
queryInfos: { defaultParams: {}, endPoint: 'collection-types/address' },
|
||||
},
|
||||
{
|
||||
name: 'component',
|
||||
key: '__component_key__',
|
||||
metadatas: {
|
||||
mainField: {
|
||||
name: 'name',
|
||||
},
|
||||
},
|
||||
fieldSchema: {
|
||||
type: 'component',
|
||||
component: 'some.component',
|
||||
repeatable: false,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
expect(formatListLayoutWithMetas(data)).toEqual(expected);
|
||||
expect(formatListLayoutWithMetas(data, components)).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
@ -572,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);
|
||||
|
||||
@ -8,7 +8,7 @@ import { DndProvider } from 'react-dnd';
|
||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
import { lightTheme } from '@strapi/design-system';
|
||||
import { lightTheme, darkTheme } from '@strapi/design-system';
|
||||
import Theme from '../../../../components/Theme';
|
||||
import ThemeToggleProvider from '../../../../components/ThemeToggleProvider';
|
||||
import { App as ContentManagerApp } from '..';
|
||||
@ -98,7 +98,7 @@ describe('Content manager | App | main', () => {
|
||||
|
||||
const { container } = render(
|
||||
<IntlProvider messages={{}} defaultLocale="en" locale="en">
|
||||
<ThemeToggleProvider themes={{ light: lightTheme }}>
|
||||
<ThemeToggleProvider themes={{ light: lightTheme, dark: darkTheme }}>
|
||||
<Theme>
|
||||
<DndProvider backend={HTML5Backend}>
|
||||
<Provider store={store}>
|
||||
@ -808,7 +808,7 @@ describe('Content manager | App | main', () => {
|
||||
|
||||
render(
|
||||
<IntlProvider messages={{}} defaultLocale="en" locale="en">
|
||||
<ThemeToggleProvider themes={{ light: lightTheme }}>
|
||||
<ThemeToggleProvider themes={{ light: lightTheme, dark: darkTheme }}>
|
||||
<Theme>
|
||||
<DndProvider backend={HTML5Backend}>
|
||||
<Provider store={store}>
|
||||
@ -854,7 +854,7 @@ describe('Content manager | App | main', () => {
|
||||
|
||||
render(
|
||||
<IntlProvider messages={{}} defaultLocale="en" locale="en">
|
||||
<ThemeToggleProvider themes={{ light: lightTheme }}>
|
||||
<ThemeToggleProvider themes={{ light: lightTheme, dark: darkTheme }}>
|
||||
<Theme>
|
||||
<DndProvider backend={HTML5Backend}>
|
||||
<Provider store={store}>
|
||||
@ -899,7 +899,7 @@ describe('Content manager | App | main', () => {
|
||||
|
||||
render(
|
||||
<IntlProvider messages={{}} defaultLocale="en" locale="en">
|
||||
<ThemeToggleProvider themes={{ light: lightTheme }}>
|
||||
<ThemeToggleProvider themes={{ light: lightTheme, dark: darkTheme }}>
|
||||
<Theme>
|
||||
<DndProvider backend={HTML5Backend}>
|
||||
<Provider store={store}>
|
||||
|
||||
@ -44,7 +44,7 @@ const FieldButtonContent = ({ attribute, onEditField, onDeleteField, children })
|
||||
<CustomIconButton
|
||||
label={formatMessage(
|
||||
{
|
||||
id: getTrad('app.component.table.delete'),
|
||||
id: 'global.delete-target',
|
||||
defaultMessage: `Delete {target}`,
|
||||
},
|
||||
{
|
||||
|
||||
@ -72,7 +72,7 @@ const FormModal = ({ onToggle, onMetaChange, onSizeChange, onSubmit, type }) =>
|
||||
}
|
||||
endActions={
|
||||
<Button type="submit">
|
||||
{formatMessage({ id: 'form.button.finish', defaultMessage: 'Finish' })}
|
||||
{formatMessage({ id: 'global.finish', defaultMessage: 'Finish' })}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
@ -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',
|
||||
@ -250,7 +249,7 @@ const EditSettingsView = ({ mainLayout, components, isContentTypeView, slug, upd
|
||||
to="/"
|
||||
>
|
||||
{formatMessage({
|
||||
id: 'app.components.go-back',
|
||||
id: 'global.back',
|
||||
defaultMessage: 'Back',
|
||||
})}
|
||||
</Link>
|
||||
@ -261,7 +260,7 @@ const EditSettingsView = ({ mainLayout, components, isContentTypeView, slug, upd
|
||||
startIcon={<Check />}
|
||||
type="submit"
|
||||
>
|
||||
{formatMessage({ id: 'form.button.save', defaultMessage: 'Save' })}
|
||||
{formatMessage({ id: 'global.save', defaultMessage: 'Save' })}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
@ -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 };
|
||||
|
||||
@ -172,7 +172,7 @@ const Header = ({
|
||||
to="/"
|
||||
>
|
||||
{formatMessage({
|
||||
id: 'app.components.HeaderLayout.link.go-back',
|
||||
id: 'global.back',
|
||||
defaultMessage: 'Back',
|
||||
})}
|
||||
</Link>
|
||||
|
||||
@ -8,7 +8,7 @@ import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { lightTheme } from '@strapi/design-system';
|
||||
import { lightTheme, darkTheme } from '@strapi/design-system';
|
||||
import Theme from '../../../../../components/Theme';
|
||||
import ThemeToggleProvider from '../../../../../components/ThemeToggleProvider';
|
||||
import { Header } from '../index';
|
||||
@ -33,7 +33,7 @@ const makeApp = (props = defaultProps) => {
|
||||
return (
|
||||
<MemoryRouter>
|
||||
<IntlProvider locale="en" defaultLocale="en" messages={{}}>
|
||||
<ThemeToggleProvider themes={{ light: lightTheme }}>
|
||||
<ThemeToggleProvider themes={{ light: lightTheme, dark: darkTheme }}>
|
||||
<Theme>
|
||||
<Header {...props} />
|
||||
</Theme>
|
||||
|
||||
@ -8,7 +8,7 @@ import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
import { useCMEditViewDataManager } from '@strapi/helper-plugin';
|
||||
import { lightTheme } from '@strapi/design-system';
|
||||
import { lightTheme, darkTheme } from '@strapi/design-system';
|
||||
import Theme from '../../../../../components/Theme';
|
||||
import ThemeToggleProvider from '../../../../../components/ThemeToggleProvider';
|
||||
import Informations from '../index';
|
||||
@ -24,7 +24,7 @@ const makeApp = () => {
|
||||
defaultLocale="en"
|
||||
messages={{ 'containers.Edit.information': 'Information' }}
|
||||
>
|
||||
<ThemeToggleProvider themes={{ light: lightTheme }}>
|
||||
<ThemeToggleProvider themes={{ light: lightTheme, dark: darkTheme }}>
|
||||
<Theme>
|
||||
<Informations />
|
||||
</Theme>
|
||||
|
||||
@ -113,7 +113,7 @@ const EditFieldForm = ({
|
||||
}
|
||||
endActions={
|
||||
<Button type="submit">
|
||||
{formatMessage({ id: 'form.button.finish', defaultMessage: 'Finish' })}
|
||||
{formatMessage({ id: 'global.finish', defaultMessage: 'Finish' })}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
@ -191,7 +191,7 @@ const ListSettingsView = ({ layout, slug }) => {
|
||||
<HeaderLayout
|
||||
navigationAction={
|
||||
<Link startIcon={<ArrowLeft />} to={goBackUrl} id="go-back">
|
||||
{formatMessage({ id: 'app.components.go-back', defaultMessage: 'Back' })}
|
||||
{formatMessage({ id: 'global.back', defaultMessage: 'Back' })}
|
||||
</Link>
|
||||
}
|
||||
primaryAction={
|
||||
@ -201,7 +201,7 @@ const ListSettingsView = ({ layout, slug }) => {
|
||||
disabled={isEqual(modifiedData, initialData)}
|
||||
type="submit"
|
||||
>
|
||||
{formatMessage({ id: 'form.button.save', defaultMessage: 'Save' })}
|
||||
{formatMessage({ id: 'global.save', defaultMessage: 'Save' })}
|
||||
</Button>
|
||||
}
|
||||
subtitle={formatMessage({
|
||||
|
||||
@ -21,11 +21,12 @@ export function resetProps() {
|
||||
return { type: RESET_PROPS };
|
||||
}
|
||||
|
||||
export const setLayout = contentType => {
|
||||
export const setLayout = ({ components, contentType }) => {
|
||||
const { layouts } = contentType;
|
||||
|
||||
return {
|
||||
contentType,
|
||||
components,
|
||||
displayedHeaders: layouts.list,
|
||||
type: SET_LIST_LAYOUT,
|
||||
};
|
||||
|
||||
@ -267,7 +267,7 @@ function ListView({
|
||||
navigationAction={
|
||||
<Link startIcon={<ArrowLeft />} to="/content-manager/">
|
||||
{formatMessage({
|
||||
id: 'app.components.HeaderLayout.link.go-back',
|
||||
id: 'global.back',
|
||||
defaultMessage: 'Back',
|
||||
})}
|
||||
</Link>
|
||||
@ -388,9 +388,6 @@ export function mapDispatchToProps(dispatch) {
|
||||
dispatch
|
||||
);
|
||||
}
|
||||
const withConnect = connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
);
|
||||
const withConnect = connect(mapStateToProps, mapDispatchToProps);
|
||||
|
||||
export default compose(withConnect)(memo(ListView, isEqual));
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
*/
|
||||
|
||||
import produce from 'immer';
|
||||
import get from 'lodash/get';
|
||||
import {
|
||||
GET_DATA,
|
||||
GET_DATA_SUCCEEDED,
|
||||
@ -17,6 +18,7 @@ export const initialState = {
|
||||
data: [],
|
||||
isLoading: true,
|
||||
contentType: {},
|
||||
components: [],
|
||||
initialDisplayedHeaders: [],
|
||||
displayedHeaders: [],
|
||||
pagination: {
|
||||
@ -26,21 +28,22 @@ export const initialState = {
|
||||
|
||||
const listViewReducer = (state = initialState, action) =>
|
||||
// eslint-disable-next-line consistent-return
|
||||
produce(state, drafState => {
|
||||
produce(state, draftState => {
|
||||
switch (action.type) {
|
||||
case GET_DATA: {
|
||||
return {
|
||||
...initialState,
|
||||
contentType: state.contentType,
|
||||
components: state.components,
|
||||
initialDisplayedHeaders: state.initialDisplayedHeaders,
|
||||
displayedHeaders: state.displayedHeaders,
|
||||
};
|
||||
}
|
||||
|
||||
case GET_DATA_SUCCEEDED: {
|
||||
drafState.pagination = action.pagination;
|
||||
drafState.data = action.data;
|
||||
drafState.isLoading = false;
|
||||
draftState.pagination = action.pagination;
|
||||
draftState.data = action.data;
|
||||
draftState.isLoading = false;
|
||||
break;
|
||||
}
|
||||
|
||||
@ -59,19 +62,49 @@ const listViewReducer = (state = initialState, action) =>
|
||||
key: `__${name}_key__`,
|
||||
};
|
||||
|
||||
if (attributes[name].type === 'relation') {
|
||||
drafState.displayedHeaders.push({
|
||||
...header,
|
||||
queryInfos: {
|
||||
defaultParams: {},
|
||||
endPoint: `collection-types/${uid}`,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
drafState.displayedHeaders.push(header);
|
||||
switch (attributes[name].type) {
|
||||
case 'component': {
|
||||
const componentName = attributes[name].component;
|
||||
const mainFieldName = get(
|
||||
state,
|
||||
['components', componentName, 'settings', 'mainField'],
|
||||
null
|
||||
);
|
||||
const mainFieldAttribute = get(state, [
|
||||
'components',
|
||||
componentName,
|
||||
'attributes',
|
||||
mainFieldName,
|
||||
]);
|
||||
|
||||
draftState.displayedHeaders.push({
|
||||
...header,
|
||||
metadatas: {
|
||||
...metas,
|
||||
mainField: {
|
||||
...mainFieldAttribute,
|
||||
name: mainFieldName,
|
||||
},
|
||||
},
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case 'relation':
|
||||
draftState.displayedHeaders.push({
|
||||
...header,
|
||||
queryInfos: {
|
||||
defaultParams: {},
|
||||
endPoint: `collection-types/${uid}`,
|
||||
},
|
||||
});
|
||||
break;
|
||||
|
||||
default:
|
||||
draftState.displayedHeaders.push(header);
|
||||
}
|
||||
} else {
|
||||
drafState.displayedHeaders = state.displayedHeaders.filter(
|
||||
draftState.displayedHeaders = state.displayedHeaders.filter(
|
||||
header => header.name !== name
|
||||
);
|
||||
}
|
||||
@ -79,23 +112,24 @@ const listViewReducer = (state = initialState, action) =>
|
||||
break;
|
||||
}
|
||||
case ON_RESET_LIST_HEADERS: {
|
||||
drafState.displayedHeaders = state.initialDisplayedHeaders;
|
||||
draftState.displayedHeaders = state.initialDisplayedHeaders;
|
||||
break;
|
||||
}
|
||||
case RESET_PROPS: {
|
||||
return initialState;
|
||||
}
|
||||
case SET_LIST_LAYOUT: {
|
||||
const { contentType, displayedHeaders } = action;
|
||||
const { contentType, components, displayedHeaders } = action;
|
||||
|
||||
drafState.contentType = contentType;
|
||||
drafState.displayedHeaders = displayedHeaders;
|
||||
drafState.initialDisplayedHeaders = displayedHeaders;
|
||||
draftState.contentType = contentType;
|
||||
draftState.components = components;
|
||||
draftState.displayedHeaders = displayedHeaders;
|
||||
draftState.initialDisplayedHeaders = displayedHeaders;
|
||||
|
||||
break;
|
||||
}
|
||||
default:
|
||||
return drafState;
|
||||
return draftState;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@ -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'];
|
||||
|
||||
@ -10,6 +10,7 @@ describe('CONTENT MANAGER | CONTAINERS | ListView | reducer', () => {
|
||||
state = {
|
||||
data: [],
|
||||
isLoading: true,
|
||||
components: [],
|
||||
contentType: {},
|
||||
initialDisplayedHeaders: [],
|
||||
displayedHeaders: [],
|
||||
|
||||
@ -21,7 +21,7 @@ const ListViewLayout = ({ layout, ...props }) => {
|
||||
}, [rawQuery, replace, redirectionLink]);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(setLayout(layout.contentType));
|
||||
dispatch(setLayout(layout));
|
||||
}, [dispatch, layout]);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@ -3,7 +3,7 @@ import { render } from '@testing-library/react';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
import { Router } from 'react-router-dom';
|
||||
import { createMemoryHistory } from 'history';
|
||||
import { lightTheme } from '@strapi/design-system';
|
||||
import { lightTheme, darkTheme } from '@strapi/design-system';
|
||||
import Theme from '../../../../components/Theme';
|
||||
import ThemeToggleProvider from '../../../../components/ThemeToggleProvider';
|
||||
import NoContentType from '../index';
|
||||
@ -19,7 +19,7 @@ describe('CONTENT MANAGER | pages | NoContentType', () => {
|
||||
} = render(
|
||||
<Router history={createMemoryHistory()}>
|
||||
<IntlProvider messages={{}} defaultLocale="en" textComponent="span" locale="en">
|
||||
<ThemeToggleProvider themes={{ light: lightTheme }}>
|
||||
<ThemeToggleProvider themes={{ light: lightTheme, dark: darkTheme }}>
|
||||
<Theme>
|
||||
<NoContentType />
|
||||
</Theme>
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
import { lightTheme } from '@strapi/design-system';
|
||||
import { lightTheme, darkTheme } from '@strapi/design-system';
|
||||
import Theme from '../../../../components/Theme';
|
||||
import ThemeToggleProvider from '../../../../components/ThemeToggleProvider';
|
||||
import NoPermissions from '../index';
|
||||
@ -23,7 +23,7 @@ describe('<NoPermissions />', () => {
|
||||
container: { firstChild },
|
||||
} = render(
|
||||
<IntlProvider locale="en" messages={{}} defaultLocale="en" textComponent="span">
|
||||
<ThemeToggleProvider themes={{ light: lightTheme }}>
|
||||
<ThemeToggleProvider themes={{ light: lightTheme, dark: darkTheme }}>
|
||||
<Theme>
|
||||
<NoPermissions />
|
||||
</Theme>
|
||||
|
||||
@ -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: [
|
||||
{
|
||||
|
||||
@ -7,7 +7,7 @@ const checkIfAttributeIsDisplayable = attribute => {
|
||||
return !toLower(attribute.relationType).includes('morph');
|
||||
}
|
||||
|
||||
return !['json', 'component', 'dynamiczone', 'richtext', 'password'].includes(type) && !!type;
|
||||
return !['json', 'dynamiczone', 'richtext', 'password'].includes(type) && !!type;
|
||||
};
|
||||
|
||||
export default checkIfAttributeIsDisplayable;
|
||||
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -10,7 +10,7 @@ const initialState = {
|
||||
{
|
||||
icon: Puzzle,
|
||||
intlLabel: {
|
||||
id: 'app.components.LeftMenuLinkContainer.listPlugins',
|
||||
id: 'global.plugins',
|
||||
defaultMessage: 'Plugins',
|
||||
},
|
||||
to: '/list-plugins',
|
||||
@ -19,7 +19,7 @@ const initialState = {
|
||||
{
|
||||
icon: ShoppingCart,
|
||||
intlLabel: {
|
||||
id: 'app.components.LeftMenuLinkContainer.installNewPlugin',
|
||||
id: 'global.marketplace',
|
||||
defaultMessage: 'Marketplace',
|
||||
},
|
||||
to: '/marketplace',
|
||||
@ -28,7 +28,7 @@ const initialState = {
|
||||
{
|
||||
icon: Cog,
|
||||
intlLabel: {
|
||||
id: 'app.components.LeftMenuLinkContainer.settings',
|
||||
id: 'global.settings',
|
||||
defaultMessage: 'Settings',
|
||||
},
|
||||
to: '/settings',
|
||||
|
||||
@ -0,0 +1,31 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
/**
|
||||
* For more details about this hook see:
|
||||
* https://www.30secondsofcode.org/react/s/use-navigator-on-line
|
||||
*/
|
||||
const useNavigatorOnLine = () => {
|
||||
const onlineStatus =
|
||||
typeof navigator !== 'undefined' && typeof navigator.onLine === 'boolean'
|
||||
? navigator.onLine
|
||||
: true;
|
||||
|
||||
const [isOnline, setIsOnline] = useState(onlineStatus);
|
||||
|
||||
const setOnline = () => setIsOnline(true);
|
||||
const setOffline = () => setIsOnline(false);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('online', setOnline);
|
||||
window.addEventListener('offline', setOffline);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('online', setOnline);
|
||||
window.removeEventListener('offline', setOffline);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return isOnline;
|
||||
};
|
||||
|
||||
export default useNavigatorOnLine;
|
||||
@ -0,0 +1,48 @@
|
||||
import { renderHook, act } from '@testing-library/react-hooks';
|
||||
import useNavigatorOnLine from '../index';
|
||||
|
||||
describe('useNavigatorOnLine', () => {
|
||||
it('returns the online state', () => {
|
||||
jest.spyOn(window.navigator, 'onLine', 'get').mockReturnValue(true);
|
||||
const { result } = renderHook(() => useNavigatorOnLine());
|
||||
|
||||
expect(result.current).toEqual(true);
|
||||
});
|
||||
|
||||
it('returns the offline state', () => {
|
||||
jest.spyOn(window.navigator, 'onLine', 'get').mockReturnValue(false);
|
||||
const { result } = renderHook(() => useNavigatorOnLine());
|
||||
|
||||
expect(result.current).toEqual(false);
|
||||
});
|
||||
|
||||
it('listens for network change online', async () => {
|
||||
// Initialize an offline state
|
||||
jest.spyOn(window.navigator, 'onLine', 'get').mockReturnValue(false);
|
||||
const { result, waitForNextUpdate } = renderHook(() => useNavigatorOnLine());
|
||||
|
||||
await act(async () => {
|
||||
// Simulate a change from offline to online
|
||||
window.dispatchEvent(new window.Event('online'));
|
||||
|
||||
await waitForNextUpdate();
|
||||
});
|
||||
|
||||
expect(result.current).toEqual(true);
|
||||
});
|
||||
|
||||
it('listens for network change offline', async () => {
|
||||
// Initialize an online state
|
||||
jest.spyOn(window.navigator, 'onLine', 'get').mockReturnValue(true);
|
||||
const { result, waitForNextUpdate } = renderHook(() => useNavigatorOnLine());
|
||||
|
||||
await act(async () => {
|
||||
// Simulate a change from online to offline
|
||||
window.dispatchEvent(new window.Event('offline'));
|
||||
|
||||
await waitForNextUpdate();
|
||||
});
|
||||
|
||||
expect(result.current).toEqual(false);
|
||||
});
|
||||
});
|
||||
@ -15,7 +15,7 @@ const useReleaseNotification = () => {
|
||||
link: {
|
||||
url: `https://github.com/strapi/strapi/releases/tag/${latestStrapiReleaseTag}`,
|
||||
label: {
|
||||
id: 'notification.version.update.link',
|
||||
id: 'global.see-more',
|
||||
},
|
||||
},
|
||||
blockTransition: true,
|
||||
|
||||
@ -25,14 +25,14 @@ const init = (initialState, { settings, shouldUpdateStrapi }) => {
|
||||
intlLabel: { id: 'Settings.permissions', defaultMessage: 'Administration Panel' },
|
||||
links: [
|
||||
{
|
||||
intlLabel: { id: 'Settings.permissions.menu.link.roles.label', defaultMessage: 'Roles' },
|
||||
intlLabel: { id: 'global.roles', defaultMessage: 'Roles' },
|
||||
to: '/settings/roles',
|
||||
id: 'roles',
|
||||
isDisplayed: false,
|
||||
permissions: adminPermissions.settings.roles.main,
|
||||
},
|
||||
{
|
||||
intlLabel: { id: 'Settings.permissions.menu.link.users.label' },
|
||||
intlLabel: { id: 'global.users' },
|
||||
// Init the search params directly
|
||||
to: '/settings/users?pageSize=10&page=1&sort=firstname',
|
||||
id: 'users',
|
||||
|
||||
@ -40,7 +40,7 @@ const UnauthenticatedLayout = ({ children }) => {
|
||||
<LocaleToggle />
|
||||
</Box>
|
||||
</Flex>
|
||||
<Box paddingTop={11} paddingBottom={11}>
|
||||
<Box paddingTop={2} paddingBottom={11}>
|
||||
{children}
|
||||
</Box>
|
||||
</div>
|
||||
|
||||
@ -75,7 +75,7 @@ const Onboarding = () => {
|
||||
{
|
||||
icon: 'book',
|
||||
label: formatMessage({
|
||||
id: 'app.components.LeftMenuFooter.documentation',
|
||||
id: 'global.documentation',
|
||||
defaultMessage: 'Documentation',
|
||||
}),
|
||||
destination: 'https://docs.strapi.io',
|
||||
|
||||
@ -107,7 +107,7 @@ const Login = ({ onSubmit, schema, children }) => {
|
||||
onChange={handleChange}
|
||||
value={values.password}
|
||||
label={formatMessage({
|
||||
id: 'Auth.form.password.label',
|
||||
id: 'global.password',
|
||||
defaultMessage: 'Password',
|
||||
})}
|
||||
name="password"
|
||||
|
||||
@ -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,11 +227,11 @@ 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({
|
||||
id: 'Auth.form.password.label',
|
||||
id: 'global.password',
|
||||
defaultMessage: 'Password',
|
||||
})}
|
||||
type={passwordShown ? 'text' : 'password'}
|
||||
@ -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: (
|
||||
|
||||
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