diff --git a/.eslintrc.js b/.eslintrc.js index dfca4d93..0cbd65cc 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -48,8 +48,13 @@ const defaultConfig = { }, }, rules: { - 'react/jsx-no-target-blank': 'off', - }, + 'react/jsx-no-target-blank': [ + 'error', + { + allowReferrer: true, + }, + ], + } }; const config = addonExtenders.reduce( diff --git a/CHANGELOG.md b/CHANGELOG.md index 15e09506..67a73d2f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,50 @@ All notable changes to this project will be documented in this file. Dates are d Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). -### [1.33.2](https://github.com/eea/volto-eea-website-theme/compare/1.33.1...1.33.2) - 13 April 2024 +### [2.0.0](https://github.com/eea/volto-eea-website-theme/compare/1.34.0...2.0.0) - 10 May 2024 + +#### :rocket: New Features + +- feat: Volto 17 support - refs #259049 [Teodor Voicu - [`79ce620`](https://github.com/eea/volto-eea-website-theme/commit/79ce6202469523c21462a86cb72b93016b10a2b6)] + +#### :bug: Bug Fixes + +- fix(draft_image): added fixes from 1.x.x branch [David Ichim - [`b62de0a`](https://github.com/eea/volto-eea-website-theme/commit/b62de0a8fd9c953210f4b371b5117d70f9e04040)] + +#### :nail_care: Enhancements + +- change(draft_image): added comments to DraftBackground code [David Ichim - [`0cb4304`](https://github.com/eea/volto-eea-website-theme/commit/0cb430424281f0d0a78e2f9b1326b6bc4128a6f3)] +- refactor: Disable data figure and plotly chart - refs #269278 [dobri1408 - [`d8fd0da`](https://github.com/eea/volto-eea-website-theme/commit/d8fd0dab10aceebf57cff5c1e777b795117b5a04)] +- change(draft-image): to remove image when published date is set to the future [David Ichim - [`9b9e023`](https://github.com/eea/volto-eea-website-theme/commit/9b9e0232fe6cd624c19dc87bc8152477f1ee83fd)] +- refactor: Move all customizations from volto-eea-website-policy [alin - [`07650fe`](https://github.com/eea/volto-eea-website-theme/commit/07650fe1c55571ec628dbe2cf8394709f0c7ae2d)] + +#### :house: Internal changes + +- style: Automated code fix [eea-jenkins - [`e671c83`](https://github.com/eea/volto-eea-website-theme/commit/e671c834773f9091e1dafb8ec7a1cbea88e53ee2)] +- style: Automated code fix [eea-jenkins - [`5156bb5`](https://github.com/eea/volto-eea-website-theme/commit/5156bb54b48f9731278ea860847a019fff10a84f)] + +#### :hammer_and_wrench: Others + +- Bump package version to 2.0.0 to signal major release due to Volto 17 jump [David Ichim - [`ffe3049`](https://github.com/eea/volto-eea-website-theme/commit/ffe3049b3b656093a44f05044dbe7cd63bac495f)] +### [1.34.0](https://github.com/eea/volto-eea-website-theme/compare/1.33.2...1.34.0) - 9 May 2024 + +#### :bug: Bug Fixes + +- fix: Make sure effectiveDate is not null/undefined [alin - [`43400bc`](https://github.com/eea/volto-eea-website-theme/commit/43400bcce422049d9d38f17c7cc29e88062da902)] +- fix: DraftBackground for effectiveDate in the future [alin - [`da7fa80`](https://github.com/eea/volto-eea-website-theme/commit/da7fa806e5d6edbb7b016f0356d5a886b75ba892)] + +#### :nail_care: Enhancements + +- refactor: Disable data figure and plotly chart - refs #269278 [dobri1408 - [`002ef00`](https://github.com/eea/volto-eea-website-theme/commit/002ef003ad872ea8dd5c74acf74a85ca1fd1992b)] +- change(draft-image): show draft image for items with publishing date in the future [David Ichim - [`59a3873`](https://github.com/eea/volto-eea-website-theme/commit/59a387364f40d8d66a747921ccff946e7f8814e1)] + +#### :hammer_and_wrench: Others + +- Release 1.34.0 [alin - [`92cc065`](https://github.com/eea/volto-eea-website-theme/commit/92cc065730f44412a04b2df7159c540d858f4607)] +- Revert "Release 1.40.0" [alin - [`c1a4f30`](https://github.com/eea/volto-eea-website-theme/commit/c1a4f3042a91ebb4a1d674914d3bccf68954c94f)] +- Revert "fix: DraftBackground for effectiveDate in the future" [alin - [`ed2ca9b`](https://github.com/eea/volto-eea-website-theme/commit/ed2ca9b5881c6991d82bb2a8d3f0fe8e29f1a6d7)] +- Release 1.40.0 [alin - [`210f833`](https://github.com/eea/volto-eea-website-theme/commit/210f83384b6401f7c9a0e08070d69dd1fed690b1)] +### [1.33.2](https://github.com/eea/volto-eea-website-theme/compare/1.33.1...1.33.2) - 16 April 2024 #### :bug: Bug Fixes @@ -12,7 +55,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). #### :hammer_and_wrench: Others -- Add Sonarqube tag using insitu-frontend addons list [EEA Jenkins - [`adc6730`](https://github.com/eea/volto-eea-website-theme/commit/adc6730e21a37afb865b842182624401de6a29f5)] ### [1.33.1](https://github.com/eea/volto-eea-website-theme/compare/1.33.0...1.33.1) - 4 April 2024 #### :bug: Bug Fixes @@ -146,8 +188,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - bump version [Razvan - [`721e939`](https://github.com/eea/volto-eea-website-theme/commit/721e939d12e324b459ebfa78a2e656ee7142a3d6)] - merge master into this branch [Razvan - [`586c8f9`](https://github.com/eea/volto-eea-website-theme/commit/586c8f910bac55a043bd8dda60e9444bd2ae1663)] -- Add Sonarqube tag using freshwater-frontend addons list [EEA Jenkins - [`fd90044`](https://github.com/eea/volto-eea-website-theme/commit/fd9004442a9d1d465f7601ecdefe3e23c61e6a9c)] -- Add Sonarqube tag using insitu-frontend addons list [EEA Jenkins - [`4bc3dd3`](https://github.com/eea/volto-eea-website-theme/commit/4bc3dd3ae412a66befd04b5b80fab3716c929240)] - test: Update jest,Jenkinsfile,lint to volto-addons-template PR30 [valentinab25 - [`c4dbd28`](https://github.com/eea/volto-eea-website-theme/commit/c4dbd289358205bc2d849aab7edb11ccf3b89cee)] - fix tests [Razvan - [`042330b`](https://github.com/eea/volto-eea-website-theme/commit/042330bc97d32ffe7ba769b4f2453f71cffed706)] - remove RemoveSchema logic [Razvan - [`08d10f8`](https://github.com/eea/volto-eea-website-theme/commit/08d10f8bf6f75478260e4e4c66d7316ba87b907a)] @@ -242,11 +282,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - test: Add real image to cypress test [Alin Voinea - [`4ff591a`](https://github.com/eea/volto-eea-website-theme/commit/4ff591ae3318c9588b4e2114582c0fa6cfdf31ae)] - test: Add cypress tests for Image block styling position and align [Alin Voinea - [`7341ef7`](https://github.com/eea/volto-eea-website-theme/commit/7341ef7b92714fc0cc3ab0c31c39033e7b3e19e7)] - Revert "change(tests): commented out rss test since title block config is missing" [Alin Voinea - [`fb61191`](https://github.com/eea/volto-eea-website-theme/commit/fb611918d6ca380b89b594f283dcf9f685a4b294)] -- test: [JENKINS] Use java17 for sonarqube scanner [valentinab25 - [`6a3be30`](https://github.com/eea/volto-eea-website-theme/commit/6a3be3092589411af7808a235f76de5222fd3868)] -- test: [JENKINS] Run cypress in started frontend container [valentinab25 - [`c3978f2`](https://github.com/eea/volto-eea-website-theme/commit/c3978f23375ef066e9fd6f6c2e34ba6c1c058f69)] -- test: [JENKINS] Add cpu limit on cypress docker [valentinab25 - [`f672779`](https://github.com/eea/volto-eea-website-theme/commit/f672779e845bec9240ccc901e9f53ec80c5a1819)] -- test: [JENKINS] Increase shm-size to cypress docker [valentinab25 - [`ae5d8e3`](https://github.com/eea/volto-eea-website-theme/commit/ae5d8e3f4e04dc2808d47ce2ee886e1b23b528da)] -- test: [JENKINS] Improve cypress time [valentinab25 - [`170ff0c`](https://github.com/eea/volto-eea-website-theme/commit/170ff0c8e3b30e69479bdf1117e811fea94f1027)] ### [1.23.0](https://github.com/eea/volto-eea-website-theme/compare/1.22.1...1.23.0) - 2 November 2023 #### :rocket: New Features @@ -259,7 +294,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). #### :house: Internal changes -- chore: [JENKINS] Refactor automated testing [valentinab25 - [`f28fce3`](https://github.com/eea/volto-eea-website-theme/commit/f28fce3d1eb815f95fb9aa40de42b10b7e8e30c5)] - chore: husky, lint-staged use fixed versions [valentinab25 - [`6d15088`](https://github.com/eea/volto-eea-website-theme/commit/6d150886c5aeb2ca0b569270486e60f7cc274e2c)] - chore:volto 16 in tests, update docs, fix stylelint overrides [valentinab25 - [`20c0323`](https://github.com/eea/volto-eea-website-theme/commit/20c032380b33c0077c869a05136f93e2fb68e5d4)] @@ -445,7 +479,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). #### :house: Internal changes -- chore: [JENKINS] Deprecate circularity website [valentinab25 - [`370dcbf`](https://github.com/eea/volto-eea-website-theme/commit/370dcbfbf1a8135ce7b1b3b271b004552a631837)] #### :hammer_and_wrench: Others @@ -601,7 +634,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). #### :hammer_and_wrench: Others -- Add Sonarqube tag using eea-website-frontend addons list [EEA Jenkins - [`6c5e2f8`](https://github.com/eea/volto-eea-website-theme/commit/6c5e2f80456e2061d9e9c15fd0a0b91b9ac70568)] ### [1.9.1](https://github.com/eea/volto-eea-website-theme/compare/1.9.0...1.9.1) - 28 February 2023 #### :bug: Bug Fixes @@ -748,7 +780,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - For some reasons types is a string [Alin Voinea - [`3769a09`](https://github.com/eea/volto-eea-website-theme/commit/3769a0981181d5b633f3498daebbe96be8b4b833)] - Fix(redirect): o.filter - refs #157627 [Alin Voinea - [`deb23da`](https://github.com/eea/volto-eea-website-theme/commit/deb23da846444cc96539697fd798429ae0abe89e)] -- Add Sonarqube tag using advisory-board-frontend addons list [EEA Jenkins - [`f1fffc5`](https://github.com/eea/volto-eea-website-theme/commit/f1fffc5db96725440863d545580b4e76cce4b796)] ### [1.5.0](https://github.com/eea/volto-eea-website-theme/compare/1.4.2...1.5.0) - 9 January 2023 #### :hammer_and_wrench: Others @@ -782,7 +813,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - Release 1.4.0 [Alin Voinea - [`bd42a0d`](https://github.com/eea/volto-eea-website-theme/commit/bd42a0d26e928cac5d99933194755da3db06b341)] - bump version to use as volto-eea-design-system [David Ichim - [`f4be047`](https://github.com/eea/volto-eea-website-theme/commit/f4be047328b46399b03b612d378b18aaf82e7dc1)] -- Add Sonarqube tag using advisory-board-frontend addons list [EEA Jenkins - [`9b7cfef`](https://github.com/eea/volto-eea-website-theme/commit/9b7cfefb4d34fc1c948015e491feb370f9795bd8)] - test(Jenkins): Run tests and cypress with latest canary @plone/volto [Alin Voinea - [`df252a9`](https://github.com/eea/volto-eea-website-theme/commit/df252a9bfed0bb86cadf53c59dd1603b1e2cd822)] ### [1.3.2](https://github.com/eea/volto-eea-website-theme/compare/1.3.1...1.3.2) - 16 December 2022 @@ -792,7 +822,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). #### :hammer_and_wrench: Others -- Add Sonarqube tag using cca-frontend addons list [EEA Jenkins - [`a43c658`](https://github.com/eea/volto-eea-website-theme/commit/a43c658a7920c8df95e763b9a637f38ce77eba2c)] - Better razzle.config [Tiberiu Ichim - [`81dbf48`](https://github.com/eea/volto-eea-website-theme/commit/81dbf48815fb27facb4f82c9b764540fdf188b2e)] - Better razzle.config [Tiberiu Ichim - [`7bc9da2`](https://github.com/eea/volto-eea-website-theme/commit/7bc9da2cd837ab62a95cd29979cdd9b0055b7d67)] ### [1.3.1](https://github.com/eea/volto-eea-website-theme/compare/1.3.0...1.3.1) - 28 November 2022 @@ -803,7 +832,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). #### :hammer_and_wrench: Others -- yarn 3 [Alin Voinea - [`ea7a709`](https://github.com/eea/volto-eea-website-theme/commit/ea7a7094945312776e9b6f44e371178603e92139)] ### [1.3.0](https://github.com/eea/volto-eea-website-theme/compare/1.2.0...1.3.0) - 22 November 2022 #### :rocket: New Features @@ -844,7 +872,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - Add subsite class to body [Tiberiu Ichim - [`74d700f`](https://github.com/eea/volto-eea-website-theme/commit/74d700fbfd6249a8604762a7e4e49cce857db0f3)] - Add subsite info to header [Tiberiu Ichim - [`47daf8b`](https://github.com/eea/volto-eea-website-theme/commit/47daf8bb6374a1222040626b19d4154df7ba1b83)] - fix eslint [Miu Razvan - [`eb8d0a7`](https://github.com/eea/volto-eea-website-theme/commit/eb8d0a790bc70c0aae256c6ff35f63c4885f338e)] -- Add Sonarqube tag using circularity-frontend addons list [EEA Jenkins - [`cc578a4`](https://github.com/eea/volto-eea-website-theme/commit/cc578a413b205a8e61e091fab3a88f94cedefc89)] ### [1.1.0](https://github.com/eea/volto-eea-website-theme/compare/1.0.0...1.1.0) - 28 October 2022 #### :nail_care: Enhancements @@ -892,7 +919,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). #### :hammer_and_wrench: Others -- Add Sonarqube tag using eea-website-frontend addons list [EEA Jenkins - [`33b56ac`](https://github.com/eea/volto-eea-website-theme/commit/33b56acb13fbaf0c5b79e8fc6e13c4b699c79c90)] ### [0.7.3](https://github.com/eea/volto-eea-website-theme/compare/0.7.2...0.7.3) - 22 September 2022 #### :hammer_and_wrench: Others @@ -1160,7 +1186,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - Header refactor, add custom logo #5 [ichim-david - [`4950235`](https://github.com/eea/volto-eea-website-theme/commit/49502358105437cfeac3b144e6d301cb59aa2346)] - Update footer.config with new publication card component [ichim-david - [`2e38e9a`](https://github.com/eea/volto-eea-website-theme/commit/2e38e9a417f835009d60c80d4eb4b30229f55e45)] - feature(breadcrumbs): implement eea-design-system breadcrumb as Volto component #32 #7 [ichim-david - [`181af41`](https://github.com/eea/volto-eea-website-theme/commit/181af4125ce2b9ddac56dab4723cb11c26633221)] -- Add Sonarqube tag using eea-website-frontend addons list [EEA Jenkins - [`da8ceb6`](https://github.com/eea/volto-eea-website-theme/commit/da8ceb68ea68bfbc9504e48ccd4d68277f11ab9a)] - use breadcrumbs from eea-design-system [nileshgulia1 - [`db2f9e9`](https://github.com/eea/volto-eea-website-theme/commit/db2f9e9a4327420a3cce9a9903cd88549b129eab)] - Update theme.config [ichim-david - [`8eca4f4`](https://github.com/eea/volto-eea-website-theme/commit/8eca4f40397a4aeca6d39029c92db78968d37064)] - Added keyContent component to theme.config [ichim-david - [`d86f202`](https://github.com/eea/volto-eea-website-theme/commit/d86f202d0274d839487a88b51cae9a0e899beb23)] @@ -1202,5 +1227,4 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). #### :hammer_and_wrench: Others -- yarn bootstrap [Alin Voinea - [`6995e9e`](https://github.com/eea/volto-eea-website-theme/commit/6995e9e091f21fdbbdffa8a44fc0e2c626f6d46a)] - Initial commit [Alin Voinea - [`6a9c03a`](https://github.com/eea/volto-eea-website-theme/commit/6a9c03a7cebe71ca87e82cf58c42904063e9d8d3)] diff --git a/Jenkinsfile b/Jenkinsfile index 20589a44..4aa6c428 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -13,7 +13,8 @@ pipeline { DEPENDENCIES = "" BACKEND_PROFILES = "eea.kitkat:testing" BACKEND_ADDONS = "" - VOLTO = "16" + VOLTO = "17" + VOLTO16_BREAKING_CHANGES = "yes" IMAGE_NAME = BUILD_TAG.toLowerCase() } @@ -44,6 +45,7 @@ pipeline { } steps { script { + checkout scm withCredentials([string(credentialsId: 'eea-jenkins-token', variable: 'GITHUB_TOKEN')]) { check_result = sh script: '''docker run --pull always -i --rm --name="$IMAGE_NAME-gitflow-check" -e GIT_TOKEN="$GITHUB_TOKEN" -e GIT_BRANCH="$BRANCH_NAME" -e GIT_ORG="$GIT_ORG" -e GIT_NAME="$GIT_NAME" eeacms/gitflow /check_if_testing_needed.sh''', returnStatus: true @@ -61,7 +63,6 @@ pipeline { allOf { not { environment name: 'CHANGE_ID', value: '' } environment name: 'CHANGE_TARGET', value: 'develop' - environment name: 'SKIP_TESTS', value: '' } allOf { environment name: 'CHANGE_ID', value: '' @@ -69,25 +70,27 @@ pipeline { not { changelog '.*^Automated release [0-9\\.]+$' } branch 'master' } - environment name: 'SKIP_TESTS', value: '' } } } - stages { - stage('Build test image') { - steps { - checkout scm - sh '''docker build --pull --build-arg="VOLTO_VERSION=$VOLTO" --build-arg="ADDON_NAME=$NAMESPACE/$GIT_NAME" --build-arg="ADDON_PATH=$GIT_NAME" . -t $IMAGE_NAME-frontend''' + parallel { + + stage('Volto 17') { + agent { node { label 'docker-1.13'} } + stages { + stage('Build test image') { + steps { + sh '''docker build --pull --build-arg="VOLTO_VERSION=$VOLTO" --build-arg="ADDON_NAME=$NAMESPACE/$GIT_NAME" --build-arg="ADDON_PATH=$GIT_NAME" . -t $IMAGE_NAME-frontend''' + } } - } - - stage('Fix code') { - when { + + stage('Fix code') { + when { environment name: 'CHANGE_ID', value: '' not { branch 'master' } - } - steps { - script { + } + steps { + script { fix_result = sh(script: '''docker run --name="$IMAGE_NAME-fix" --entrypoint=make --workdir=/app/src/addons/$GIT_NAME $IMAGE_NAME-frontend ci-fix''', returnStatus: true) sh '''docker cp $IMAGE_NAME-fix:/app/src/addons/$GIT_NAME/src .''' sh '''docker rm -v $IMAGE_NAME-fix''' @@ -105,31 +108,31 @@ pipeline { sh '''exit 1''' } } + } } - } - stage('ES lint') { - steps { - sh '''docker run --rm --name="$IMAGE_NAME-eslint" --entrypoint=make --workdir=/app/src/addons/$GIT_NAME $IMAGE_NAME-frontend lint''' + stage('ES lint') { + when { environment name: 'SKIP_TESTS', value: '' } + steps { + sh '''docker run --rm --name="$IMAGE_NAME-eslint" --entrypoint=make --workdir=/app/src/addons/$GIT_NAME $IMAGE_NAME-frontend lint''' + } } - } - stage('Style lint') { - steps { - sh '''docker run --rm --name="$IMAGE_NAME-stylelint" --entrypoint=make --workdir=/app/src/addons/$GIT_NAME $IMAGE_NAME-frontend stylelint''' + stage('Style lint') { + when { environment name: 'SKIP_TESTS', value: '' } + steps { + sh '''docker run --rm --name="$IMAGE_NAME-stylelint" --entrypoint=make --workdir=/app/src/addons/$GIT_NAME $IMAGE_NAME-frontend stylelint''' + } } - } - stage('Prettier') { - steps { - sh '''docker run --rm --name="$IMAGE_NAME-prettier" --entrypoint=make --workdir=/app/src/addons/$GIT_NAME $IMAGE_NAME-frontend prettier''' + stage('Prettier') { + when { environment name: 'SKIP_TESTS', value: '' } + steps { + sh '''docker run --rm --name="$IMAGE_NAME-prettier" --entrypoint=make --workdir=/app/src/addons/$GIT_NAME $IMAGE_NAME-frontend prettier''' + } } - } - - stage('Coverage Tests') { - parallel { - - stage('Unit tests') { + stage('Unit tests') { + when { environment name: 'SKIP_TESTS', value: '' } steps { script { try { @@ -155,9 +158,10 @@ pipeline { } } } - } + } - stage('Integration tests') { + stage('Integration tests') { + when { environment name: 'SKIP_TESTS', value: '' } steps { script { try { @@ -211,16 +215,7 @@ pipeline { } } } - } } - } - } - post { - always { - sh script: "docker rmi $IMAGE_NAME-frontend", returnStatus: true - } - } - } stage('Report to SonarQube') { when { @@ -228,9 +223,11 @@ pipeline { allOf { not { environment name: 'CHANGE_ID', value: '' } environment name: 'CHANGE_TARGET', value: 'develop' + environment name: 'SKIP_TESTS', value: '' } allOf { environment name: 'CHANGE_ID', value: '' + environment name: 'SKIP_TESTS', value: '' anyOf { allOf { branch 'develop' @@ -255,14 +252,107 @@ pipeline { } } + + } + } + + stage('Volto 16') { + agent { node { label 'integration'} } + when { + environment name: 'SKIP_TESTS', value: '' + not { environment name: 'VOLTO16_BREAKING_CHANGES', value: 'yes' } + } + stages { + stage('Build test image') { + steps { + sh '''docker build --pull --build-arg="VOLTO_VERSION=16" --build-arg="ADDON_NAME=$NAMESPACE/$GIT_NAME" --build-arg="ADDON_PATH=$GIT_NAME" . -t $IMAGE_NAME-frontend16''' + } + } + + stage('Unit tests Volto 16') { + steps { + script { + try { + sh '''docker run --name="$IMAGE_NAME-volto16" --entrypoint=make --workdir=/app/src/addons/$GIT_NAME $IMAGE_NAME-frontend16 test-ci''' + sh '''rm -rf xunit-reports16''' + sh '''mkdir -p xunit-reports16''' + sh '''docker cp $IMAGE_NAME-volto16:/app/junit.xml xunit-reports16/''' + } finally { + catchError(buildResult: 'SUCCESS', stageResult: 'SUCCESS') { + junit testResults: 'xunit-reports16/junit.xml', allowEmptyResults: true + } + sh script: '''docker rm -v $IMAGE_NAME-volto16''', returnStatus: true + } + } + } + } + + stage('Integration tests Volto 16') { + steps { + script { + try { + sh '''docker run --pull always --rm -d --name="$IMAGE_NAME-plone16" -e SITE="Plone" -e PROFILES="$BACKEND_PROFILES" -e ADDONS="$BACKEND_ADDONS" eeacms/plone-backend''' + sh '''docker run -d --shm-size=4g --link $IMAGE_NAME-plone16:plone --name="$IMAGE_NAME-cypress16" -e "RAZZLE_INTERNAL_API_PATH=http://plone:8080/Plone" --entrypoint=make --workdir=/app/src/addons/$GIT_NAME $IMAGE_NAME-frontend16 start-ci''' + frontend = sh script:'''docker exec --workdir=/app/src/addons/${GIT_NAME} $IMAGE_NAME-cypress16 make check-ci''', returnStatus: true + if ( frontend != 0 ) { + sh '''docker logs $IMAGE_NAME-cypress16; exit 1''' + } + sh '''timeout -s 9 1800 docker exec --workdir=/app/src/addons/${GIT_NAME} $IMAGE_NAME-cypress16 make cypress-ci''' + } finally { + try { + if ( frontend == 0 ) { + sh '''rm -rf cypress-videos16 cypress-results16 cypress-coverage16 cypress-screenshots16''' + sh '''mkdir -p cypress-videos16 cypress-results16 cypress-coverage16 cypress-screenshots16''' + videos = sh script: '''docker cp $IMAGE_NAME-cypress16:/app/src/addons/$GIT_NAME/cypress/videos cypress-videos16/''', returnStatus: true + sh '''docker cp $IMAGE_NAME-cypress16:/app/src/addons/$GIT_NAME/cypress/reports cypress-results16/''' + screenshots = sh script: '''docker cp $IMAGE_NAME-cypress16:/app/src/addons/$GIT_NAME/cypress/screenshots cypress-screenshots16''', returnStatus: true + + archiveArtifacts artifacts: 'cypress-screenshots16/**', fingerprint: true, allowEmptyArchive: true + + if ( videos == 0 ) { + sh '''for file in $(find cypress-results16 -name *.xml); do if [ $(grep -E 'failures="[1-9].*"' $file | wc -l) -eq 0 ]; then testname=$(grep -E 'file=.*failures="0"' $file | sed 's#.* file=".*\\/\\(.*\\.[jsxt]\\+\\)" time.*#\\1#' ); rm -f cypress-videos16/videos/$testname.mp4; fi; done''' + archiveArtifacts artifacts: 'cypress-videos16/**/*.mp4', fingerprint: true, allowEmptyArchive: true + } + } + } finally { + catchError(buildResult: 'SUCCESS', stageResult: 'SUCCESS') { + junit testResults: 'cypress-results16/**/*.xml', allowEmptyResults: true + } + catchError(buildResult: 'SUCCESS', stageResult: 'SUCCESS') { + sh '''docker logs $IMAGE_NAME-cypress16''' + } + sh script: "docker stop $IMAGE_NAME-cypress16", returnStatus: true + sh script: "docker stop $IMAGE_NAME-plone16", returnStatus: true + sh script: "docker rm -v $IMAGE_NAME-plone16", returnStatus: true + sh script: "docker rm -v $IMAGE_NAME-cypress16", returnStatus: true + } + } + } + } + } + + } + } + } + post { + always { + sh script: "docker rmi $IMAGE_NAME-frontend", returnStatus: true + sh script: "docker rmi $IMAGE_NAME-frontend16", returnStatus: true + } + } + } + + stage('SonarQube compare to master') { when { anyOf { allOf { not { environment name: 'CHANGE_ID', value: '' } environment name: 'CHANGE_TARGET', value: 'develop' + environment name: 'SKIP_TESTS', value: '' } allOf { + environment name: 'SKIP_TESTS', value: '' environment name: 'CHANGE_ID', value: '' branch 'develop' not { changelog '.*^Automated release [0-9\\.]+$' } @@ -323,3 +413,4 @@ pipeline { } } } + diff --git a/Makefile b/Makefile index c583f3fe..522b5771 100644 --- a/Makefile +++ b/Makefile @@ -46,7 +46,7 @@ endif DIR=$(shell basename $$(pwd)) NODE_MODULES?="../../../node_modules" PLONE_VERSION?=6 -VOLTO_VERSION?=16 +VOLTO_VERSION?=17 ADDON_PATH="${DIR}" ADDON_NAME="@eeacms/${ADDON_PATH}" DOCKER_COMPOSE=PLONE_VERSION=${PLONE_VERSION} VOLTO_VERSION=${VOLTO_VERSION} ADDON_NAME=${ADDON_NAME} ADDON_PATH=${ADDON_PATH} docker compose @@ -98,7 +98,7 @@ test-update: ## Update jest tests snapshots .PHONY: stylelint stylelint: ## Stylelint - $(NODE_MODULES)/stylelint/bin/stylelint.js --allow-empty-input 'src/**/*.{css,less}' + $(NODE_MODULES)/.bin/stylelint --allow-empty-input 'src/**/*.{css,less}' .PHONY: stylelint-overrides stylelint-overrides: @@ -106,7 +106,7 @@ stylelint-overrides: .PHONY: stylelint-fix stylelint-fix: ## Fix stylelint - $(NODE_MODULES)/stylelint/bin/stylelint.js --allow-empty-input 'src/**/*.{css,less}' --fix + $(NODE_MODULES)/.bin/stylelint --allow-empty-input 'src/**/*.{css,less}' --fix $(NODE_MODULES)/.bin/stylelint --custom-syntax less --allow-empty-input 'theme/**/*.overrides' 'src/**/*.overrides' --fix .PHONY: prettier @@ -119,11 +119,11 @@ prettier-fix: ## Fix prettier .PHONY: lint lint: ## ES Lint - $(NODE_MODULES)/eslint/bin/eslint.js --max-warnings=0 'src/**/*.{js,jsx}' + $(NODE_MODULES)/.bin/eslint --max-warnings=0 'src/**/*.{js,jsx}' .PHONY: lint-fix lint-fix: ## Fix ES Lint - $(NODE_MODULES)/eslint/bin/eslint.js --fix 'src/**/*.{js,jsx}' + $(NODE_MODULES)/.bin/eslint --fix 'src/**/*.{js,jsx}' .PHONY: i18n i18n: ## i18n diff --git a/cypress/support/e2e.js b/cypress/support/e2e.js index 7739f913..9dc5a186 100644 --- a/cypress/support/e2e.js +++ b/cypress/support/e2e.js @@ -35,11 +35,7 @@ export const slateBeforeEach = (contentType = 'Document') => { path: 'cypress', }); cy.visit('/cypress/my-page'); - cy.waitForResourceToLoad('@navigation'); - // cy.waitForResourceToLoad('@breadcrumbs'); - cy.waitForResourceToLoad('@actions'); - cy.waitForResourceToLoad('@types'); - cy.waitForResourceToLoad('my-page'); + // cy.waitForResourceToLoad('my-page'); cy.navigate('/cypress/my-page/edit'); }; diff --git a/docker-compose.yml b/docker-compose.yml index 5d79f5cd..ae185753 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,7 +15,7 @@ services: args: ADDON_NAME: "${ADDON_NAME}" ADDON_PATH: "${ADDON_PATH}" - VOLTO_VERSION: ${VOLTO_VERSION:-16} + VOLTO_VERSION: ${VOLTO_VERSION:-17} ports: - "3000:3000" - "3001:3001" diff --git a/jest-addon.config.js b/jest-addon.config.js index 4148ea3f..8c5f28ad 100644 --- a/jest-addon.config.js +++ b/jest-addon.config.js @@ -14,6 +14,8 @@ module.exports = { '@package/(.*)$': '/node_modules/@plone/volto/src/$1', '@root/(.*)$': '/node_modules/@plone/volto/src/$1', '@plone/volto-quanta/(.*)$': '/src/addons/volto-quanta/src/$1', + '@eeacms/search/(.*)$': '/src/addons/volto-searchlib/searchlib/$1', + '@eeacms/search': '/src/addons/volto-searchlib/searchlib', '@eeacms/(.*?)/(.*)$': '/node_modules/@eeacms/$1/src/$2', '@plone/volto-slate$': '/node_modules/@plone/volto/packages/volto-slate/src', @@ -28,6 +30,7 @@ module.exports = { ], transform: { '^.+\\.js(x)?$': 'babel-jest', + '^.+\\.ts(x)?$': 'babel-jest', '^.+\\.(png)$': 'jest-file', '^.+\\.(jpg)$': 'jest-file', '^.+\\.(svg)$': './node_modules/@plone/volto/jest-svgsystem-transform.js', diff --git a/package.json b/package.json index 00d8af7c..ffdba8c1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@eeacms/volto-eea-website-theme", - "version": "1.33.2", + "version": "2.0.0", "description": "@eeacms/volto-eea-website-theme: Volto add-on", "main": "src/index.js", "author": "European Environment Agency: IDM2 A-Team", @@ -34,6 +34,7 @@ "babel-plugin-transform-class-properties": "^6.24.1", "dotenv": "^16.3.2", "husky": "^8.0.3", + "jsonwebtoken": "9.0.0", "lint-staged": "^14.0.1", "md5": "^2.3.0", "postcss-less": "6.0.0" diff --git a/src/components/manage/Blocks/LayoutSettings/index.js b/src/components/manage/Blocks/LayoutSettings/index.js index 3fb0a82e..5751aac4 100644 --- a/src/components/manage/Blocks/LayoutSettings/index.js +++ b/src/components/manage/Blocks/LayoutSettings/index.js @@ -3,7 +3,7 @@ import LayoutSettingsView from './LayoutSettingsView'; import LayoutSettingsEdit from './LayoutSettingsEdit'; import BlockSettingsSchema from '@plone/volto/components/manage/Blocks/Block/Schema'; -export default (config) => { +const applyConfig = (config) => { config.blocks.blocksConfig.layoutSettings = { id: 'layoutSettings', title: 'Layout settings', @@ -19,3 +19,5 @@ export default (config) => { return config; }; + +export default applyConfig; diff --git a/src/components/manage/Blocks/Title/index.js b/src/components/manage/Blocks/Title/index.js index 090b180c..614172d6 100644 --- a/src/components/manage/Blocks/Title/index.js +++ b/src/components/manage/Blocks/Title/index.js @@ -1,7 +1,7 @@ import Edit from './Edit'; import View from './View'; -export default (config) => { +const applyConfig = (config) => { config.blocks.blocksConfig.title = { ...config.blocks.blocksConfig.title, edit: Edit, @@ -12,3 +12,5 @@ export default (config) => { return config; }; + +export default applyConfig; diff --git a/src/components/manage/Blocks/Title/schema.js b/src/components/manage/Blocks/Title/schema.js index 95b7df79..ff1a9289 100644 --- a/src/components/manage/Blocks/Title/schema.js +++ b/src/components/manage/Blocks/Title/schema.js @@ -54,7 +54,7 @@ const RSSLink = { required: [], }; -export default { +const titleSchema = { title: 'Page header', fieldsets: [ { @@ -175,3 +175,5 @@ export default { required: [], }; + +export default titleSchema; diff --git a/src/components/theme/Banner/View.jsx b/src/components/theme/Banner/View.jsx index 63d0153d..e721fc93 100644 --- a/src/components/theme/Banner/View.jsx +++ b/src/components/theme/Banner/View.jsx @@ -89,6 +89,9 @@ const View = (props) => { const copyrightPrefix = config.blocks.blocksConfig.title.copyrightPrefix || ''; + const contentTypesWithoutHeaderImage = + config.settings?.eea?.contentTypesWithoutHeaderImage || []; + // Set dates const getDate = useCallback( (hidden, key) => { @@ -96,10 +99,10 @@ const View = (props) => { }, [metadata], ); - const creationDate = useMemo(() => getDate(hideCreationDate, 'created'), [ - getDate, - hideCreationDate, - ]); + const creationDate = useMemo( + () => getDate(hideCreationDate, 'created'), + [getDate, hideCreationDate], + ); const publishingDate = useMemo( () => getDate(hidePublishingDate, 'effective'), [getDate, hidePublishingDate], @@ -110,7 +113,11 @@ const View = (props) => { ); // Set image source - const image = getImageSource(metadata['image']); + const image = contentTypesWithoutHeaderImage.includes( + props.properties['@type'], + ) + ? false + : getImageSource(metadata['image']); // Get type const type = metadata.type_title || friendlyId(metadata['@type']); diff --git a/src/components/theme/DraftBackground/DraftBackground.jsx b/src/components/theme/DraftBackground/DraftBackground.jsx index 5eddbcfa..963e4814 100644 --- a/src/components/theme/DraftBackground/DraftBackground.jsx +++ b/src/components/theme/DraftBackground/DraftBackground.jsx @@ -7,18 +7,45 @@ import { compose } from 'redux'; import { flattenToAppURL } from '@plone/volto/helpers'; /** - * @param {Object} props - * @returns + * Removes any trailing slashes from the given string. + * + * @param {string} str - The input string to remove trailing slashes from. + * @returns {string} The input string with any trailing slashes removed. */ - const removeTrailingSlash = (str) => { return str.replace(/\/+$/, ''); }; -const checkIfPublished = (props) => { +/** + * Checks if the current content is published. + * + * This function checks the review state and effective date of the current content + * to determine if it should be considered published. It handles various cases, + * such as when the review state is null, when the content has a parent, and when + * the effective date is in the future. + * + * @param {object} props - The props object containing information about the current content. + * @param {string} props.contentId - The ID of the current content. + * @param {string} props.pathname - The current URL pathname. + * @param {object} props.content - The content object. + * @param {string} props.review_state - The review state of the current content. + * @returns {boolean} - True if the content is considered published, false otherwise. + */ +export const checkIfPublished = (props) => { //case 0: the state is not for the current content-type eg: Go to /contents from a page if (props.contentId !== removeTrailingSlash(props.pathname)) return true; + // set draft image if effective date is set and is in the future + // regardless of review_state + const effectiveDate = props?.content?.effective; + if ( + effectiveDate && + effectiveDate !== 'None' && + new Date(effectiveDate).getTime() > new Date().getTime() + ) { + return false; + } + //case 1 : review_state published if (props?.review_state === 'published') return true; @@ -41,6 +68,7 @@ const checkIfPublished = (props) => { return true; return false; }; + const DraftBackground = (props) => { let draftClass = 'wf-state-is-draft'; if (checkIfPublished(props)) { diff --git a/src/components/theme/DraftBackground/DraftBackground.test.jsx b/src/components/theme/DraftBackground/DraftBackground.test.jsx new file mode 100644 index 00000000..11d69ba4 --- /dev/null +++ b/src/components/theme/DraftBackground/DraftBackground.test.jsx @@ -0,0 +1,85 @@ +import { checkIfPublished } from './DraftBackground'; +describe('checkIfPublished', () => { + it('should return true if contentId does not match pathname', () => { + const props = { + contentId: '/page1', + pathname: '/page2', + }; + + expect(checkIfPublished(props)).toBe(true); + }); + + it('should return false if effective date is in the future', () => { + const futureDate = new Date(); + futureDate.setDate(futureDate.getDate() + 10); + const props = { + contentId: '/page1', + pathname: '/page1', + content: { + effective: futureDate.toISOString(), + }, + }; + expect(checkIfPublished(props)).toBe(false); + }); + + it('should return true if review_state is published', () => { + const props = { + contentId: '/page1', + pathname: '/page1', + review_state: 'published', + }; + expect(checkIfPublished(props)).toBe(true); + }); + + it('should return true if review_state is null and parent is published', () => { + const props = { + contentId: '/page1', + pathname: '/page1', + review_state: null, + content: { + parent: { + review_state: 'published', + }, + }, + }; + expect(checkIfPublished(props)).toBe(true); + }); + + it('should return true if review_state is null and parent is empty', () => { + const props = { + contentId: '/page1', + pathname: '/page1', + review_state: null, + content: { + parent: {}, + }, + }; + expect(checkIfPublished(props)).toBe(true); + }); + + it('should return true if review_state is null and parent review_state is null', () => { + const props = { + contentId: '/page1', + pathname: '/page1', + review_state: null, + content: { + parent: { + review_state: null, + }, + }, + }; + expect(checkIfPublished(props)).toBe(true); + }); + + it('should return false if review_state is not published and effective date is not in the future', () => { + const props = { + contentId: '/page1', + pathname: '/page1', + review_state: 'private', + content: { + effective: '2023-01-01T00:00:00Z', + }, + }; + expect(checkIfPublished(props)).toBe(false); + }); +}); diff --git a/src/config.js b/src/config.js index b251fa96..d7de50a5 100644 --- a/src/config.js +++ b/src/config.js @@ -342,3 +342,5 @@ export const colors = [ '#F9F9F9', '#FFFFFF', ]; + +export const contentTypesWithoutHeaderImage = ['Image']; diff --git a/src/customizations/@plone/volto-slate/blocks/Text/TextBlockView.jsx b/src/customizations/@plone/volto-slate/blocks/Text/TextBlockView.jsx new file mode 100644 index 00000000..2cbabc4a --- /dev/null +++ b/src/customizations/@plone/volto-slate/blocks/Text/TextBlockView.jsx @@ -0,0 +1,32 @@ +import { + serializeNodes, + serializeNodesToText, +} from '@plone/volto-slate/editor/render'; +import config from '@plone/volto/registry'; +import { isEqual } from 'lodash'; +import Slugger from 'github-slugger'; +import { normalizeString } from '@plone/volto/helpers'; + +const TextBlockView = (props) => { + const { id, data, styling = {} } = props; + const { value, override_toc } = data; + const metadata = props.metadata || props.properties; + const { topLevelTargetElements } = config.settings.slate; + + const getAttributes = (node, path) => { + const res = { ...styling }; + if (node.type && isEqual(path, [0])) { + if (topLevelTargetElements.includes(node.type) || override_toc) { + const text = serializeNodesToText([node] || []); + const slug = Slugger.slug(normalizeString(text)); + res.id = slug || id; + res['data-block'] = id; + } + } + return res; + }; + + return serializeNodes(value, getAttributes, { metadata: metadata }); +}; + +export default TextBlockView; diff --git a/src/customizations/@plone/volto-slate/editor/plugins/StyleMenu/README.txt b/src/customizations/@plone/volto-slate/editor/plugins/StyleMenu/README.txt deleted file mode 100644 index 30579d1b..00000000 --- a/src/customizations/@plone/volto-slate/editor/plugins/StyleMenu/README.txt +++ /dev/null @@ -1 +0,0 @@ -This customization fixes bugs with styleMenu not highlighting selected styles in some scenarios. This should be removed after https://github.com/plone/volto/pull/4852 \ No newline at end of file diff --git a/src/customizations/@plone/volto-slate/editor/plugins/StyleMenu/StyleMenu.jsx b/src/customizations/@plone/volto-slate/editor/plugins/StyleMenu/StyleMenu.jsx deleted file mode 100644 index fa21dbe4..00000000 --- a/src/customizations/@plone/volto-slate/editor/plugins/StyleMenu/StyleMenu.jsx +++ /dev/null @@ -1,157 +0,0 @@ -import React from 'react'; -import { useSlate } from 'slate-react'; -import { Dropdown } from 'semantic-ui-react'; -import { useIntl, defineMessages } from 'react-intl'; -import cx from 'classnames'; -import { omit } from 'lodash'; -import { isBlockStyleActive, isInlineStyleActive, toggleStyle } from './utils'; -import config from '@plone/volto/registry'; -import { ToolbarButton } from '@plone/volto-slate/editor/ui'; -import paintSVG from '@plone/volto/icons/paint.svg'; - -const messages = defineMessages({ - inlineStyle: { - id: 'Inline Style', - defaultMessage: 'Inline Style', - }, - paragraphStyle: { - id: 'Paragraph Style', - defaultMessage: 'Paragraph Style', - }, - additionalStyles: { - id: 'Additional Styles', - defaultMessage: 'Additional Styles', - }, -}); - -const StyleMenuButton = ({ icon, active, ...props }) => ( - -); - -const MenuOpts = ({ editor, toSelect, option, type }) => { - const isActive = toSelect.includes(option); - return ( - { - toggleStyle(editor, { - cssClass: selItem.value, - isBlock: selItem.isBlock, - }); - }} - /> - ); -}; - -const StylingsButton = (props) => { - const editor = useSlate(); - const intl = useIntl(); - - // Converting the settings to a format that is required by dropdowns. - const inlineOpts = [ - ...config.settings.slate.styleMenu.inlineStyles.map((def) => { - return { - value: def.cssClass, - text: def.label, - icon: def.icon, - isBlock: false, - }; - }), - ]; - const blockOpts = [ - ...config.settings.slate.styleMenu.blockStyles.map((def) => { - return { - value: def.cssClass, - text: def.label, - icon: def.icon, - isBlock: true, - }; - }), - ]; - - // Calculating the initial selection. - const toSelect = []; - // block styles - for (const val of blockOpts) { - const ia = isBlockStyleActive(editor, val.value); - if (ia) { - toSelect.push(val); - } - } - // inline styles - for (const val of inlineOpts) { - const ia = isInlineStyleActive(editor, val.value); - if (ia) { - toSelect.push(val); - } - } - - const menuItemProps = { - toSelect, - editor, - }; - const showMenu = inlineOpts.length || blockOpts.length; - return showMenu ? ( - 0} - /> - } - > - - {inlineOpts.length ? ( - <> - - {inlineOpts.map((option, index) => ( - - ))} - - ) : ( - '' - )} - - {blockOpts.length ? ( - <> - - {blockOpts.map((option, index) => ( - - ))} - - ) : ( - '' - )} - - - ) : ( - '' - ); -}; - -export default StylingsButton; diff --git a/src/customizations/@plone/volto-slate/editor/plugins/StyleMenu/utils.js b/src/customizations/@plone/volto-slate/editor/plugins/StyleMenu/utils.js deleted file mode 100644 index 4090c8d7..00000000 --- a/src/customizations/@plone/volto-slate/editor/plugins/StyleMenu/utils.js +++ /dev/null @@ -1,168 +0,0 @@ -/* eslint no-console: ["error", { allow: ["warn", "error"] }] */ -import { Editor, Transforms } from 'slate'; -import { isBlockActive } from '@plone/volto-slate/utils'; -import config from '@plone/volto/registry'; - -/** - * Toggles a style (e.g. in the StyleMenu plugin). - * @param {Editor} editor - * @param {object} options - * @param {boolean} options.isRequested Whether the given style is requested by - * the user. The style is only applied if it is requested and only removed if it - * is not requested. - */ -export const toggleStyle = (editor, { cssClass, isBlock, isRequested }) => { - if (isBlock) { - toggleBlockStyle(editor, cssClass); - } else { - toggleInlineStyle(editor, cssClass); - } -}; - -export const toggleBlockStyle = (editor, style) => { - // We have 6 boolean variables which need to be accounted for. - // See https://docs.google.com/spreadsheets/d/1mVeMuqSTMABV2BhoHPrPAFjn7zUksbNgZ9AQK_dcd3U/edit?usp=sharing - const { slate } = config.settings; - - const isListItem = isBlockActive(editor, slate.listItemType); - const isActive = isBlockStyleActive(editor, style); - const wantsList = false; - - if (isListItem && !wantsList) { - toggleBlockStyleAsListItem(editor, style); - } else if (isListItem && wantsList && !isActive) { - // switchListType(editor, format); // this will deconstruct to Volto blocks - } else if (!isListItem && wantsList) { - // changeBlockToList(editor, format); - } else if (!isListItem && !wantsList) { - internalToggleBlockStyle(editor, style); - } else { - console.warn('toggleBlockStyle case not covered, please examine:', { - wantsList, - isActive, - isListItem, - }); - } -}; - -export const toggleInlineStyle = (editor, style) => { - // We have 6 boolean variables which need to be accounted for. - // See https://docs.google.com/spreadsheets/d/1mVeMuqSTMABV2BhoHPrPAFjn7zUksbNgZ9AQK_dcd3U/edit?usp=sharing - const { slate } = config.settings; - - const isListItem = isBlockActive(editor, slate.listItemType); - const isActive = isInlineStyleActive(editor, style); - const wantsList = false; - - if (isListItem && !wantsList) { - toggleInlineStyleAsListItem(editor, style); - } else if (isListItem && wantsList && !isActive) { - // switchListType(editor, format); // this will deconstruct to Volto blocks - } else if (!isListItem && wantsList) { - // changeBlockToList(editor, format); - } else if (!isListItem && !wantsList) { - internalToggleInlineStyle(editor, style); - } else { - console.warn('toggleInlineStyle case not covered, please examine:', { - wantsList, - isActive, - isListItem, - }); - } -}; - -export const isBlockStyleActive = (editor, style) => { - const keyName = `style-${style}`; - const sn = Array.from( - Editor.nodes(editor, { - match: (n) => { - const isStyle = typeof n.styleName === 'string' || n[keyName]; - return !Editor.isEditor(n) && isStyle; - }, - mode: 'all', - }), - ); - - for (const [n] of sn) { - if (typeof n.styleName === 'string') { - if (n.styleName.split(' ').filter((x) => x === style).length > 0) { - return true; - } - } else if ( - n[keyName] && - keyName.split('-').filter((x) => x === style).length > 0 - ) - return true; - } - return false; -}; - -export const isInlineStyleActive = (editor, style) => { - const m = Editor.marks(editor); - const keyName = `style-${style}`; - if (m && m[keyName]) { - return true; - } - return false; -}; - -export const internalToggleBlockStyle = (editor, style) => { - toggleBlockStyleInSelection(editor, style); -}; - -export const internalToggleInlineStyle = (editor, style) => { - toggleInlineStyleInSelection(editor, style); -}; - -/* - * Applies a block format unto a list item. Will split the list and deconstruct the - * block - */ -export const toggleBlockStyleAsListItem = (editor, style) => { - toggleBlockStyleInSelection(editor, style); -}; - -/* - * Applies an inline style unto a list item. - */ -export const toggleInlineStyleAsListItem = (editor, style) => { - toggleInlineStyleInSelection(editor, style); -}; - -function toggleInlineStyleInSelection(editor, style) { - const m = Editor.marks(editor); - const keyName = 'style-' + style; - - if (m && m[keyName]) { - Editor.removeMark(editor, keyName); - } else { - Editor.addMark(editor, keyName, true); - } -} - -function toggleBlockStyleInSelection(editor, style) { - const sn = Array.from( - Editor.nodes(editor, { - mode: 'highest', - match: (n) => { - return !Editor.isEditor(n); - }, - }), - ); - - for (const [n, p] of sn) { - let cn = n.styleName; - if (typeof n.styleName !== 'string') { - cn = style; - } else if (n.styleName.split(' ').filter((x) => x === style).length > 0) { - cn = cn - .split(' ') - .filter((x) => x !== style) - .join(' '); - } else { - // the style is not set but other styles are set - cn = cn.split(' ').concat(style).join(' '); - } - Transforms.setNodes(editor, { styleName: cn }, { at: p }); - } -} diff --git a/src/customizations/@plone/volto-slate/editor/render.jsx b/src/customizations/@plone/volto-slate/editor/render.jsx index 91b81b0d..5e8b8c1f 100644 --- a/src/customizations/@plone/volto-slate/editor/render.jsx +++ b/src/customizations/@plone/volto-slate/editor/render.jsx @@ -1,9 +1,19 @@ import React from 'react'; import { renderToStaticMarkup } from 'react-dom/server'; +import { useLocation } from 'react-router-dom'; +import { toast } from 'react-toastify'; +import { useIntl } from 'react-intl'; +import { useSelector } from 'react-redux'; import { Node, Text } from 'slate'; import cx from 'classnames'; import { isEmpty, isEqual, omit } from 'lodash'; +import { UniversalLink, Toast } from '@plone/volto/components'; +import { messages, addAppURL } from '@plone/volto/helpers'; +import useClipboard from '@plone/volto/hooks/clipboard/useClipboard'; import config from '@plone/volto/registry'; +import linkSVG from '@plone/volto/icons/link.svg'; + +import '@plone/volto-slate/editor/less/slate.less'; const OMITTED = ['editor', 'path']; @@ -177,3 +187,68 @@ export const serializeNodesToText = (nodes) => { export const serializeNodesToHtml = (nodes) => renderToStaticMarkup(serializeNodes(nodes)); + +export const renderLinkElement = (tagName) => { + function LinkElement({ + attributes, + children, + mode = 'edit', + className = null, + }) { + const { slate = {} } = config.settings; + const Tag = tagName; + const slug = attributes.id || ''; + const location = useLocation(); + const token = useSelector((state) => state.userSession.token); + const appPathname = addAppURL(location.pathname); + // eslint-disable-next-line no-unused-vars + const [copied, copy, setCopied] = useClipboard( + appPathname.concat(`#${slug}`), + ); + const intl = useIntl(); + return !token || slate.useLinkedHeadings === false ? ( + + {children} + + ) : ( + + {children} + {mode === 'view' && slug && ( + + )} + + ); + } + LinkElement.displayName = `${tagName}LinkElement`; + return LinkElement; +}; diff --git a/src/customizations/@plone/volto-slate/elementEditor/utils.js b/src/customizations/@plone/volto-slate/elementEditor/utils.js index a219c359..8f0badae 100644 --- a/src/customizations/@plone/volto-slate/elementEditor/utils.js +++ b/src/customizations/@plone/volto-slate/elementEditor/utils.js @@ -158,88 +158,89 @@ export const _isActiveElement = (elementType) => (editor) => { * @param {string|Object[]} elementType - this can be a string or an array of strings * @returns {Object|null} - found node */ -export const _getActiveElement = (elementType) => ( - editor, - direction = 'any', -) => { - const selection = editor.selection || editor.getSavedSelection(); - let found = []; - - try { - found = Array.from( - Editor.nodes(editor, { - match: (n) => - Array.isArray(elementType) - ? elementType.includes(n.type) - : n.type === elementType, - at: selection, - }), - ); - } catch (e) { - return null; - } - - if (found.length) return found[0]; - - if (!selection) return null; +export const _getActiveElement = + (elementType) => + (editor, direction = 'any') => { + const selection = editor.selection || editor.getSavedSelection(); + let found = []; - if (direction === 'any' || direction === 'backward') { - const { path } = selection.anchor; - const isAtStart = - selection.anchor.offset === 0 && selection.focus.offset === 0; + try { + found = Array.from( + Editor.nodes(editor, { + match: (n) => + Array.isArray(elementType) + ? elementType.includes(n.type) + : n.type === elementType, + at: selection, + }), + ); + } catch (e) { + return null; + } - if (isAtStart) { - let found; - try { - found = Editor.previous(editor, { - at: path, - }); - } catch (ex) { - // eslint-disable-next-line no-console - console.warn('Unable to find previous node', editor, path); - return; - } - if (found && found[0] && found[0].type === elementType) { - if ( - (Array.isArray(elementType) && elementType.includes(found[0].type)) || - found[0].type === elementType - ) { - return found; + if (found.length) return found[0]; + + if (!selection) return null; + + if (direction === 'any' || direction === 'backward') { + const { path } = selection.anchor; + const isAtStart = + selection.anchor.offset === 0 && selection.focus.offset === 0; + + if (isAtStart) { + let found; + try { + found = Editor.previous(editor, { + at: path, + }); + } catch (ex) { + // eslint-disable-next-line no-console + console.warn('Unable to find previous node', editor, path); + return; + } + if (found && found[0] && found[0].type === elementType) { + if ( + (Array.isArray(elementType) && + elementType.includes(found[0].type)) || + found[0].type === elementType + ) { + return found; + } + } else { + return null; } - } else { - return null; } } - } - - if (direction === 'any' || direction === 'forward') { - const { path } = selection.anchor; - const isAtStart = - selection.anchor.offset === 0 && selection.focus.offset === 0; - if (isAtStart) { - let found; - try { - found = Editor.next(editor, { - at: path, - }); - } catch (e) { - // eslint-disable-next-line - console.warn('Unable to find next node', editor, path); - return; - } - if (found && found[0] && found[0].type === elementType) { - if ( - (Array.isArray(elementType) && elementType.includes(found[0].type)) || - found[0].type === elementType - ) { - return found; + if (direction === 'any' || direction === 'forward') { + const { path } = selection.anchor; + const isAtStart = + selection.anchor.offset === 0 && selection.focus.offset === 0; + + if (isAtStart) { + let found; + try { + found = Editor.next(editor, { + at: path, + }); + } catch (e) { + // eslint-disable-next-line + console.warn('Unable to find next node', editor, path); + return; + } + if (found && found[0] && found[0].type === elementType) { + if ( + (Array.isArray(elementType) && + elementType.includes(found[0].type)) || + found[0].type === elementType + ) { + return found; + } + } else { + return null; } - } else { - return null; } } - } - return null; -}; + return null; + }; diff --git a/src/customizations/volto/components/manage/Add/Add.jsx b/src/customizations/volto/components/manage/Add/Add.jsx deleted file mode 100644 index ca88c0d5..00000000 --- a/src/customizations/volto/components/manage/Add/Add.jsx +++ /dev/null @@ -1,498 +0,0 @@ -/** - * Add container. - * @module components/manage/Add/Add - */ - -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; -import { BodyClass, Helmet } from '@plone/volto/helpers'; -import { connect } from 'react-redux'; -import { compose } from 'redux'; -import { keys, isEmpty } from 'lodash'; -import { defineMessages, injectIntl } from 'react-intl'; -import { Button, Grid, Menu } from 'semantic-ui-react'; -import { Portal } from 'react-portal'; -import { v4 as uuid } from 'uuid'; -import qs from 'query-string'; -import { toast } from 'react-toastify'; - -import { createContent, getSchema, changeLanguage } from '@plone/volto/actions'; -import { - Form, - Icon, - Toolbar, - Sidebar, - Toast, - TranslationObject, -} from '@plone/volto/components'; -import { - getBaseUrl, - hasBlocksData, - flattenToAppURL, - getBlocksFieldname, - getBlocksLayoutFieldname, - getLanguageIndependentFields, - langmap, - toGettextLang, -} from '@plone/volto/helpers'; - -import { preloadLazyLibs } from '@plone/volto/helpers/Loadable'; -import { tryParseJSON } from '@plone/volto/helpers'; - -import config from '@plone/volto/registry'; - -import saveSVG from '@plone/volto/icons/save.svg'; -import clearSVG from '@plone/volto/icons/clear.svg'; - -const messages = defineMessages({ - add: { - id: 'Add {type}', - defaultMessage: 'Add {type}', - }, - save: { - id: 'Save', - defaultMessage: 'Save', - }, - cancel: { - id: 'Cancel', - defaultMessage: 'Cancel', - }, - error: { - id: 'Error', - defaultMessage: 'Error', - }, - translateTo: { - id: 'Translate to {lang}', - defaultMessage: 'Translate to {lang}', - }, - someErrors: { - id: 'There are some errors.', - defaultMessage: 'There are some errors.', - }, -}); - -/** - * Add class. - * @class Add - * @extends Component - */ -class Add extends Component { - /** - * Property types. - * @property {Object} propTypes Property types. - * @static - */ - static propTypes = { - createContent: PropTypes.func.isRequired, - getSchema: PropTypes.func.isRequired, - pathname: PropTypes.string.isRequired, - schema: PropTypes.objectOf(PropTypes.any), - content: PropTypes.shape({ - // eslint-disable-line react/no-unused-prop-types - '@id': PropTypes.string, - '@type': PropTypes.string, - }), - returnUrl: PropTypes.string, - createRequest: PropTypes.shape({ - loading: PropTypes.bool, - loaded: PropTypes.bool, - }).isRequired, - schemaRequest: PropTypes.shape({ - loading: PropTypes.bool, - loaded: PropTypes.bool, - }).isRequired, - type: PropTypes.string, - location: PropTypes.objectOf(PropTypes.any), - }; - - /** - * Default properties - * @property {Object} defaultProps Default properties. - * @static - */ - static defaultProps = { - schema: null, - content: null, - returnUrl: null, - type: 'Default', - }; - - /** - * Constructor - * @method constructor - * @param {Object} props Component properties - * @constructs WysiwygEditor - */ - constructor(props) { - super(props); - this.onCancel = this.onCancel.bind(this); - this.onSubmit = this.onSubmit.bind(this); - - if (config.blocks?.initialBlocks[props.type]) { - this.initialBlocksLayout = config.blocks.initialBlocks[ - props.type - ].map((item) => uuid()); - this.initialBlocks = this.initialBlocksLayout.reduce( - (acc, value, index) => ({ - ...acc, - [value]: { '@type': config.blocks.initialBlocks[props.type][index] }, - }), - {}, - ); - } - this.state = { - isClient: false, - error: null, - formSelected: 'addForm', - }; - } - - /** - * Component did mount - * @method componentDidMount - * @returns {undefined} - */ - componentDidMount() { - this.props.getSchema(this.props.type, getBaseUrl(this.props.pathname)); - this.setState({ isClient: true }); - } - - /** - * Component will receive props - * @method componentWillReceiveProps - * @param {Object} nextProps Next properties - * @returns {undefined} - */ - UNSAFE_componentWillReceiveProps(nextProps) { - if ( - this.props.createRequest.loading && - nextProps.createRequest.loaded && - nextProps.content['@type'] === this.props.type - ) { - this.props.history.push( - this.props.returnUrl || flattenToAppURL(nextProps.content['@id']), - ); - } - - if (this.props.createRequest.loading && nextProps.createRequest.error) { - const message = - nextProps.createRequest.error.response?.body?.message || - nextProps.createRequest.error.response?.text; - - const error = - new DOMParser().parseFromString(message, 'text/html')?.all[0] - ?.textContent || message; - - const errorsList = tryParseJSON(error); - let erroMessage; - if (Array.isArray(errorsList)) { - const invariantErrors = errorsList - .filter((errorItem) => !('field' in errorItem)) - .map((errorItem) => errorItem['message']); - if (invariantErrors.length > 0) { - // Plone invariant validation message. - erroMessage = invariantErrors.join(' - '); - } else { - // Error in specific field. - erroMessage = this.props.intl.formatMessage(messages.someErrors); - } - } else { - erroMessage = errorsList.error?.message || error; - } - - this.setState({ error: error }); - - if ( - document?.querySelector('.sidebar-container .tabs-wrapper a')?.click - ) { - document.querySelector('.sidebar-container .tabs-wrapper a').click(); - } - toast.error( - , - ); - } - } - - /** - * Submit handler - * @method onSubmit - * @param {object} data Form data. - * @returns {undefined} - */ - onSubmit(data) { - this.props.createContent(getBaseUrl(this.props.pathname), { - ...data, - '@static_behaviors': this.props.schema.definitions - ? keys(this.props.schema.definitions) - : null, - '@type': this.props.type, - ...(config.settings.isMultilingual && - this.props.location?.state?.translationOf && { - translation_of: this.props.location.state.translationOf, - language: this.props.location.state.language, - }), - }); - } - - /** - * Cancel handler - * @method onCancel - * @returns {undefined} - */ - onCancel() { - if (this.props.location?.state?.translationOf) { - const language = this.props.location.state.languageFrom; - const langFileName = toGettextLang(language); - import('@root/../locales/' + langFileName + '.json').then((locale) => { - this.props.changeLanguage(language, locale.default); - }); - this.props.history.push(this.props.location?.state?.translationOf); - } else { - this.props.history.push(getBaseUrl(this.props.pathname)); - } - } - - form = React.createRef(); - - /** - * Render method. - * @method render - * @returns {string} Markup for the component. - */ - render() { - if (this.props.schemaRequest.loaded) { - const visual = hasBlocksData(this.props.schema.properties); - const blocksFieldname = getBlocksFieldname(this.props.schema.properties); - const blocksLayoutFieldname = getBlocksLayoutFieldname( - this.props.schema.properties, - ); - const translationObject = this.props.location?.state?.translationObject; - - const translateTo = translationObject - ? langmap?.[this.props.location?.state?.language]?.nativeName - : null; - - // Lookup initialBlocks and initialBlocksLayout within schema - const schemaBlocks = this.props.schema.properties[blocksFieldname] - ?.default; - const schemaBlocksLayout = this.props.schema.properties[ - blocksLayoutFieldname - ]?.default?.items; - let initialBlocks = this.initialBlocks; - let initialBlocksLayout = this.initialBlocksLayout; - - if (!isEmpty(schemaBlocksLayout) && !isEmpty(schemaBlocks)) { - initialBlocks = {}; - initialBlocksLayout = []; - schemaBlocksLayout.forEach((value) => { - if (!isEmpty(schemaBlocks[value])) { - let newUid = uuid(); - initialBlocksLayout.push(newUid); - initialBlocks[newUid] = schemaBlocks[value]; - initialBlocks[newUid].block = newUid; - - // Layout ID - keep a reference to the original block id within layout - initialBlocks[newUid]['@layout'] = value; - } - }); - } - //copy blocks from translationObject - if (translationObject && blocksFieldname && blocksLayoutFieldname) { - initialBlocks = {}; - initialBlocksLayout = []; - const originalBlocks = JSON.parse( - JSON.stringify(translationObject[blocksFieldname]), - ); - const originalBlocksLayout = - translationObject[blocksLayoutFieldname].items; - - originalBlocksLayout.forEach((value) => { - if (!isEmpty(originalBlocks[value])) { - let newUid = uuid(); - initialBlocksLayout.push(newUid); - initialBlocks[newUid] = originalBlocks[value]; - initialBlocks[newUid].block = newUid; - - // Layout ID - keep a reference to the original block id within layout - initialBlocks[newUid]['@canonical'] = value; - } - }); - } - - const lifData = () => { - const data = {}; - if (translationObject) { - getLanguageIndependentFields(this.props.schema).forEach( - (lif) => (data[lif] = translationObject[lif]), - ); - } - return data; - }; - - const pageAdd = ( -
- -
{ - this.setState({ formSelected: 'addForm' }); - }} - global - /> - {this.state.isClient && ( - - - - - - } - /> - - )} - {visual && this.state.isClient && ( - - - - )} -
- ); - - return translationObject ? ( - <> - - - - { - this.setState({ - formSelected: 'translationObjectForm', - }); - }} - /> - - -
- - - {`${this.props.intl.formatMessage(messages.translateTo, { - lang: translateTo, - })}`} - - - {pageAdd} -
-
-
- - ) : ( - pageAdd - ); - } - return
; - } -} - -export default compose( - injectIntl, - connect( - (state, props) => ({ - createRequest: state.content.create, - schemaRequest: state.schema, - content: state.content.data, - schema: state.schema.schema, - pathname: props.location.pathname, - returnUrl: qs.parse(props.location.search).return_url, - type: qs.parse(props.location.search).type, - }), - { createContent, getSchema, changeLanguage }, - ), - preloadLazyLibs('cms'), -)(Add); diff --git a/src/customizations/volto/components/manage/Add/readme.md b/src/customizations/volto/components/manage/Add/readme.md deleted file mode 100644 index e685bfc2..00000000 --- a/src/customizations/volto/components/manage/Add/readme.md +++ /dev/null @@ -1 +0,0 @@ -This customization aims to resolve the issue of adding a page with an empty title. It is a temporary solution and should be removed upon upgrading to Volto 17. Please check this for more information: https://github.com/plone/volto/pull/5842 diff --git a/src/customizations/volto/components/manage/Blocks/Grid/Edit.jsx b/src/customizations/volto/components/manage/Blocks/Grid/Edit.jsx new file mode 100644 index 00000000..e300f4d4 --- /dev/null +++ b/src/customizations/volto/components/manage/Blocks/Grid/Edit.jsx @@ -0,0 +1,70 @@ +import PropTypes from 'prop-types'; +import cx from 'classnames'; +import { useState } from 'react'; +import ContainerEdit from '@plone/volto/components/manage/Blocks/Container/Edit'; + +const convertTeaserToGridIfNecessary = (data) => { + if (data?.['@type'] === 'teaserGrid') + return { + ...data, + '@type': 'gridBlock', + blocks_layout: { items: data?.columns.map((c) => c.id) }, + blocks: data?.columns?.reduce((acc, current) => { + return { + ...acc, + [current?.id]: current, + }; + }, {}), + }; + return data; +}; + +const GridBlockEdit = (props) => { + const { data } = props; + + const columnsLength = + data?.blocks_layout?.items?.length || data?.columns?.length || 0; + + const [selectedBlock, setSelectedBlock] = useState(null); + + //convert to gridBlock if necessary + if (data?.['@type'] === 'teaserGrid') { + props.onChangeBlock(props.block, convertTeaserToGridIfNecessary(data)); + } + + return ( +
= 4, + 'grid-items': true, + })} + // This is required to enabling a small "in-between" clickable area + // for bringing the Grid sidebar alive once you have selected an inner block + onClick={(e) => { + if (!e.block) setSelectedBlock(null); + }} + role="presentation" + > + +
+ ); +}; + +GridBlockEdit.propTypes = { + block: PropTypes.string.isRequired, + onChangeBlock: PropTypes.func.isRequired, + pathname: PropTypes.string.isRequired, + selected: PropTypes.bool.isRequired, + manage: PropTypes.bool.isRequired, +}; + +export default GridBlockEdit; diff --git a/src/customizations/volto/components/manage/Blocks/Grid/View.jsx b/src/customizations/volto/components/manage/Blocks/Grid/View.jsx new file mode 100644 index 00000000..2411fdb9 --- /dev/null +++ b/src/customizations/volto/components/manage/Blocks/Grid/View.jsx @@ -0,0 +1,61 @@ +import { Grid } from 'semantic-ui-react'; +import cx from 'classnames'; +import { RenderBlocks } from '@plone/volto/components'; +import { withBlockExtensions } from '@plone/volto/helpers'; +import config from '@plone/volto/registry'; + +const convertTeaserToGridIfNecessary = (data) => { + if (data?.['@type'] === 'teaserGrid') + return { + ...data, + '@type': 'gridBlock', + blocks_layout: { items: data?.columns.map((c) => c.id) }, + blocks: data?.columns?.reduce((acc, current) => { + return { + ...acc, + [current?.id]: current, + }; + }, {}), + }; + return data; +}; + +const GridBlockView = (props) => { + const { data, path, className, style } = props; + const metadata = props.metadata || props.properties; + const columns = data?.blocks_layout?.items || data?.columns; + const blocksConfig = + config.blocks.blocksConfig[data['@type']].blocksConfig || + props.blocksConfig; + const location = { + pathname: path, + }; + + return ( +
+ {data.headline &&

{data.headline}

} + + + + +
+ ); +}; + +export default withBlockExtensions(GridBlockView); diff --git a/src/customizations/volto/components/manage/Blocks/Grid/readme.md b/src/customizations/volto/components/manage/Blocks/Grid/readme.md new file mode 100644 index 00000000..8ab009d9 --- /dev/null +++ b/src/customizations/volto/components/manage/Blocks/Grid/readme.md @@ -0,0 +1 @@ +These two customizations ensure backward compatibility with the legacy @kitconcept/volto-blocks-grid. For more details, refer to the ticket here: https://taskman.eionet.europa.eu/issues/265726. diff --git a/src/customizations/volto/components/manage/Blocks/Image/Edit.jsx b/src/customizations/volto/components/manage/Blocks/Image/Edit.jsx index 3fc91532..55f2f505 100644 --- a/src/customizations/volto/components/manage/Blocks/Image/Edit.jsx +++ b/src/customizations/volto/components/manage/Blocks/Image/Edit.jsx @@ -16,7 +16,6 @@ import { isEqual } from 'lodash'; import { Icon, ImageSidebar, SidebarPortal } from '@plone/volto/components'; import { Icon as IconSemantic } from 'semantic-ui-react'; -import { withBlockExtensions } from '@plone/volto/helpers'; import { createContent } from '@plone/volto/actions'; import { Copyright } from '@eeacms/volto-eea-design-system/ui'; @@ -24,13 +23,33 @@ import { flattenToAppURL, getBaseUrl, isInternalURL, + withBlockExtensions, + validateFileUploadSize, } from '@plone/volto/helpers'; +import config from '@plone/volto/registry'; import imageBlockSVG from '@plone/volto/components/manage/Blocks/Image/block-image.svg'; import clearSVG from '@plone/volto/icons/clear.svg'; import navTreeSVG from '@plone/volto/icons/nav.svg'; import aheadSVG from '@plone/volto/icons/ahead.svg'; import uploadSVG from '@plone/volto/icons/upload.svg'; + +// please Volto 16 test +export const getImageBlockSizes = function (data) { + if (data.align === 'full') return '100vw'; + if (data.align === 'center') { + if (data.size === 'l') return '100vw'; + if (data.size === 'm') return '50vw'; + if (data.size === 's') return '25vw'; + } + if (data.align === 'left' || data.align === 'right') { + if (data.size === 'l') return '50vw'; + if (data.size === 'm') return '25vw'; + if (data.size === 's') return '15vw'; + } + return undefined; +}; + const Dropzone = loadable(() => import('react-dropzone')); const messages = defineMessages({ @@ -38,6 +57,10 @@ const messages = defineMessages({ id: 'Browse the site, drop an image, or type an URL', defaultMessage: 'Browse the site, drop an image, or type an URL', }, + uploadingImage: { + id: 'Uploading image', + defaultMessage: 'Uploading image', + }, }); /** @@ -56,7 +79,7 @@ class Edit extends Component { block: PropTypes.string.isRequired, index: PropTypes.number.isRequired, data: PropTypes.objectOf(PropTypes.any).isRequired, - content: PropTypes.objectOf(PropTypes.any).isRequired, + content: PropTypes.objectOf(PropTypes.any), request: PropTypes.shape({ loading: PropTypes.bool, loaded: PropTypes.bool, @@ -85,6 +108,7 @@ class Edit extends Component { * @returns {undefined} */ UNSAFE_componentWillReceiveProps(nextProps) { + // Update block data after upload finished if ( this.props.request.loading && nextProps.request.loaded && @@ -96,6 +120,8 @@ class Edit extends Component { this.props.onChangeBlock(this.props.block, { ...this.props.data, url: nextProps.content['@id'], + image_field: 'image', + image_scales: { image: [nextProps.content.image] }, alt: '', }); } @@ -123,6 +149,7 @@ class Edit extends Component { onUploadImage = (e) => { e.stopPropagation(); const file = e.target.files[0]; + if (!validateFileUploadSize(file, this.props.intl.formatMessage)) return; this.setState({ uploading: true, }); @@ -167,6 +194,8 @@ class Edit extends Component { this.props.onChangeBlock(this.props.block, { ...this.props.data, url: flattenToAppURL(this.state.url), + image_field: undefined, + image_scales: undefined, }); }; @@ -176,23 +205,25 @@ class Edit extends Component { * @param {array} files File objects * @returns {undefined} */ - onDrop = (file) => { - this.setState({ - uploading: true, - }); + onDrop = (files) => { + if (!validateFileUploadSize(files[0], this.props.intl.formatMessage)) { + this.setState({ dragging: false }); + return; + } + this.setState({ uploading: true }); - readAsDataURL(file[0]).then((data) => { + readAsDataURL(files[0]).then((data) => { const fields = data.match(/^data:(.*);(.*),(.*)$/); this.props.createContent( getBaseUrl(this.props.pathname), { '@type': 'Image', - title: file[0].name, + title: files[0].name, image: { data: fields[3], encoding: fields[2], 'content-type': fields[1], - filename: file[0].name, + filename: files[0].name, }, }, this.props.block, @@ -234,6 +265,7 @@ class Edit extends Component { * @returns {string} Markup for the component. */ render() { + const Image = config.getComponent({ name: 'Image' }).component; const { data } = this.props; const placeholder = this.props.data.placeholder || @@ -265,7 +297,7 @@ class Edit extends Component { > {data.url ? ( <> - { - if (data.align === 'full') - return `${flattenToAppURL( - data.url, - )}/@@images/image/huge`; if (data.size === 'l') - return `${flattenToAppURL( - data.url, - )}/@@images/image/great`; + return `${flattenToAppURL(data.url)}/@@images/image`; if (data.size === 'm') return `${flattenToAppURL( data.url, @@ -295,13 +332,14 @@ class Edit extends Component { return `${flattenToAppURL( data.url, )}/@@images/image/mini`; - return `${flattenToAppURL( - data.url, - )}/@@images/image/great`; + return `${flattenToAppURL(data.url)}/@@images/image`; })() : data.url } + sizes={config.blocks.blocksConfig.image.getSizes(data)} alt={data.alt || ''} + loading="lazy" + responsive={true} />
{copyright && showCopyright ? ( @@ -332,7 +370,11 @@ class Edit extends Component { {this.state.dragging && } {this.state.uploading && ( - Uploading image + + {this.props.intl.formatMessage( + messages.uploadingImage, + )} + )}
@@ -345,7 +387,24 @@ class Edit extends Component { onClick={(e) => { e.stopPropagation(); e.preventDefault(); - this.props.openObjectBrowser(); + this.props.openObjectBrowser({ + onSelectItem: ( + url, + { title, image_field, image_scales }, + ) => { + this.props.onChangeBlock( + this.props.block, + { + ...this.props.data, + url, + image_field, + image_scales, + alt: + this.props.data.alt || title || '', + }, + ); + }, + }); }} > diff --git a/src/customizations/volto/components/manage/Blocks/Image/Edit.test.jsx b/src/customizations/volto/components/manage/Blocks/Image/Edit.test.jsx index 8c999ed4..2f83624f 100644 --- a/src/customizations/volto/components/manage/Blocks/Image/Edit.test.jsx +++ b/src/customizations/volto/components/manage/Blocks/Image/Edit.test.jsx @@ -1,10 +1,16 @@ -import React from 'react'; +import config from '@plone/volto/registry'; +import '@testing-library/jest-dom/extend-expect'; import { render } from '@testing-library/react'; +import React from 'react'; import { Provider } from 'react-intl-redux'; import configureMockStore from 'redux-mock-store'; import Edit from './Edit'; -import config from '@plone/volto/registry'; -import '@testing-library/jest-dom/extend-expect'; +import { Image } from '@plone/volto/components'; +import { getImageBlockSizes } from './Edit'; + +config.set('components', { + Image: { component: Image }, +}); const mockStore = configureMockStore(); const { settings } = config; @@ -25,6 +31,7 @@ config.blocks.blocksConfig = { addPermission: [], view: [], }, + getSizes: getImageBlockSizes, }, }; const blockId = '1234'; diff --git a/src/customizations/volto/components/manage/Blocks/Image/View.jsx b/src/customizations/volto/components/manage/Blocks/Image/View.jsx index 3814856d..d42fed1f 100644 --- a/src/customizations/volto/components/manage/Blocks/Image/View.jsx +++ b/src/customizations/volto/components/manage/Blocks/Image/View.jsx @@ -9,8 +9,12 @@ import PropTypes from 'prop-types'; import { UniversalLink } from '@plone/volto/components'; import { Icon } from 'semantic-ui-react'; import cx from 'classnames'; -import { withBlockExtensions } from '@plone/volto/helpers'; -import { flattenToAppURL, isInternalURL } from '@plone/volto/helpers'; +import { + flattenToAppURL, + isInternalURL, + withBlockExtensions, +} from '@plone/volto/helpers'; +import config from '@plone/volto/registry'; import { Copyright } from '@eeacms/volto-eea-design-system/ui'; /** @@ -19,131 +23,126 @@ import { Copyright } from '@eeacms/volto-eea-design-system/ui'; * @extends Component */ export const View = (props) => { - const { data, detached } = props; - const href = data?.href?.[0]?.['@id'] || ''; + const { className, data, detached, style } = props; const { copyright, copyrightIcon, copyrightPosition } = data; - // const [hovering, setHovering] = React.useState(false); - const [viewLoaded, setViewLoaded] = React.useState(false); - + const href = data?.href?.[0]?.['@id'] || ''; const showCopyright = data?.size === 'l' || !data.size; - React.useEffect(() => { - setViewLoaded(true); - }, []); + const Image = config.getComponent({ name: 'Image' }).component; return ( <> - {viewLoaded && ( +
-
- {data.url && ( - <> - {(() => { - const image = ( - <> - { - if (data.align === 'full') - return `${flattenToAppURL( - data.url, - )}/@@images/image/huge`; - if (data.size === 'l') - return `${flattenToAppURL( - data.url, - )}/@@images/image/great`; - if (data.size === 'm') - return `${flattenToAppURL( - data.url, - )}/@@images/image/preview`; - if (data.size === 's') - return `${flattenToAppURL( - data.url, - )}/@@images/image/mini`; + {data.url && ( + <> + {(() => { + const image = ( + <> + { + if (data.size === 'l') + return `${flattenToAppURL( + data.url, + )}/@@images/image`; + if (data.size === 'm') + return `${flattenToAppURL( + data.url, + )}/@@images/image/preview`; + if (data.size === 's') return `${flattenToAppURL( data.url, - )}/@@images/image/great`; - })() - : data.url - } - alt={data.alt || ''} - loading="lazy" - /> -
setHovering(true)} - // onMouseLeave={() => setHovering(false)} - className={`copyright-wrapper ${ - copyrightPosition ? copyrightPosition : 'left' - }`} - > - {copyright && showCopyright ? ( - - - - - {/**/} - {copyright} - {/*
*/} - - ) : ( - '' - )} -
- + )}/@@images/image/mini`; + return `${flattenToAppURL( + data.url, + )}/@@images/image`; + })() + : data.url + } + sizes={config.blocks.blocksConfig.image.getSizes(data)} + alt={data.alt || ''} + loading="lazy" + responsive={true} + /> +
+ {copyright && showCopyright ? ( + + + + + {copyright} + + ) : ( + '' + )} +
+ + ); + if (href) { + return ( + + {image} + ); - if (href) { - return ( - - {image} - - ); - } else { - return image; - } - })()} - - )} -
+ } else { + return image; + } + })()} + + )}
- )} +
); }; diff --git a/src/customizations/volto/components/manage/Blocks/Image/schema.js b/src/customizations/volto/components/manage/Blocks/Image/schema.js index ec038a9c..63bc6d84 100644 --- a/src/customizations/volto/components/manage/Blocks/Image/schema.js +++ b/src/customizations/volto/components/manage/Blocks/Image/schema.js @@ -37,6 +37,10 @@ const messages = defineMessages({ id: 'Alt text hint link text', defaultMessage: 'Describe the purpose of the image.', }, + linkSettings: { + id: 'Link settings', + defaultMessage: 'Link settings', + }, }); export function ImageSchema({ formData, intl }) { @@ -61,7 +65,7 @@ export function ImageSchema({ formData, intl }) { ? [ { id: 'link_settings', - title: 'Link settings', + title: intl.formatMessage(messages.linkSettings), fields: ['href', 'openLinkInNewTab'], }, ] @@ -80,7 +84,7 @@ export function ImageSchema({ formData, intl }) { href="https://www.w3.org/WAI/tutorials/images/decision-tree/" title={intl.formatMessage(messages.openLinkInNewTab)} target="_blank" - rel="noopener" + rel="noopener noreferrer" > {intl.formatMessage(messages.AltTextHintLinkText)} {' '} @@ -136,3 +140,14 @@ export function ImageSchema({ formData, intl }) { required: [], }; } + +export const gridImageDisableSizeAndPositionHandlersSchema = ({ + schema, + formData, + intl, +}) => { + schema.fieldsets[0].fields = schema.fieldsets[0].fields.filter( + (item) => !['align', 'size'].includes(item), + ); + return schema; +}; diff --git a/src/customizations/volto/components/manage/Blocks/LeadImage/Edit.jsx b/src/customizations/volto/components/manage/Blocks/LeadImage/Edit.jsx index e68252a3..632345f9 100644 --- a/src/customizations/volto/components/manage/Blocks/LeadImage/Edit.jsx +++ b/src/customizations/volto/components/manage/Blocks/LeadImage/Edit.jsx @@ -10,10 +10,11 @@ import { defineMessages, injectIntl } from 'react-intl'; import cx from 'classnames'; import { Message } from 'semantic-ui-react'; import { isEqual } from 'lodash'; + import { Copyright } from '@eeacms/volto-eea-design-system/ui'; import { Icon } from 'semantic-ui-react'; import { LeadImageSidebar, SidebarPortal } from '@plone/volto/components'; -import { flattenToAppURL } from '@plone/volto/helpers'; +import config from '@plone/volto/registry'; import imageBlockSVG from '@plone/volto/components/manage/Blocks/Image/block-image.svg'; @@ -80,11 +81,18 @@ class Edit extends Component { * @returns {string} Markup for the component. */ render() { + const Image = config.getComponent({ name: 'Image' }).component; const { data, properties } = this.props; + const { copyright, copyrightIcon, copyrightPosition } = data; const placeholder = this.props.data.placeholder || this.props.intl.formatMessage(messages.ImageBlockInputPlaceholder); - const { copyright, copyrightIcon, copyrightPosition, styles } = data; + + const hasImage = !!properties.image; + const hasImageData = hasImage && !!properties.image.data; + const className = cx('responsive', { 'full-image': data.align === 'full' }); + const altText = data.image_caption || properties.image_caption || ''; + return (
- {!properties.image && ( + {!hasImage && (
@@ -108,19 +116,17 @@ class Edit extends Component {
)} - {properties.image && ( + {hasImage && hasImageData && (
{data.image_caption
{copyright ? ( @@ -136,6 +142,21 @@ class Edit extends Component {
)} + {hasImage && !hasImageData && ( + { + if (data.align === 'full' || data.align === 'center') + return '100vw'; + if (data.align === 'left' || data.align === 'right') + return '50vw'; + return undefined; + })()} + alt={altText} + /> + )}
diff --git a/src/customizations/volto/components/manage/Blocks/LeadImage/View.jsx b/src/customizations/volto/components/manage/Blocks/LeadImage/View.jsx index 48ad4f01..8c1f9723 100644 --- a/src/customizations/volto/components/manage/Blocks/LeadImage/View.jsx +++ b/src/customizations/volto/components/manage/Blocks/LeadImage/View.jsx @@ -7,101 +7,87 @@ import React from 'react'; import PropTypes from 'prop-types'; import { UniversalLink } from '@plone/volto/components'; import cx from 'classnames'; +import config from '@plone/volto/registry'; import { Copyright } from '@eeacms/volto-eea-design-system/ui'; import { Icon } from 'semantic-ui-react'; -import { flattenToAppURL } from '@plone/volto/helpers'; /** * View image block class. * @class View * @extends Component */ -const View = (props) => { - const { data, properties } = props; - const { copyright, copyrightIcon, copyrightPosition, styles } = data; - - // const [hovering, setHovering] = React.useState(false); - const [viewLoaded, setViewLoaded] = React.useState(false); - - React.useEffect(() => { - setViewLoaded(true); - }, []); +const View = ({ data, properties }) => { + const { copyright, copyrightIcon, copyrightPosition } = data; + const Image = config.getComponent({ name: 'Image' }).component; return ( <> - {viewLoaded && ( -

+

-
- {properties.image && ( - <> - {(() => { - const image = ( -
- {properties.image_caption -
setHovering(true)} - // onMouseLeave={() => setHovering(false)} - className={`copyright-wrapper ${ - copyrightPosition ? copyrightPosition : 'left' - }`} - > - {copyright ? ( - - - - - {/**/} - {copyright} - {/*
*/} - - ) : ( - '' - )} -
+ {properties.image && ( + <> + {(() => { + const image = ( +
+ {properties.image_caption +
+ {copyright ? ( + + + + + + ) : ( + '' + )}
+
+ ); + if (data.href) { + return ( + + {image} + ); - if (data.href) { - return ( - - {image} - - ); - } else { - return image; - } - })()} - - )} -
-

- )} + } else { + return image; + } + })()} + + )} +
+

); }; diff --git a/src/customizations/volto/components/manage/Contents/ContentsPropertiesModal.jsx b/src/customizations/volto/components/manage/Contents/ContentsPropertiesModal.jsx deleted file mode 100644 index d81e9345..00000000 --- a/src/customizations/volto/components/manage/Contents/ContentsPropertiesModal.jsx +++ /dev/null @@ -1,232 +0,0 @@ -/** - * Contents properties modal. - * @module components/manage/Contents/ContentsPropertiesModal - */ - -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; -import { connect } from 'react-redux'; -import { compose } from 'redux'; -import { isEmpty, map } from 'lodash'; -import { defineMessages, injectIntl } from 'react-intl'; - -import { updateContent } from '@plone/volto/actions'; -import { ModalForm } from '@plone/volto/components'; - -const messages = defineMessages({ - properties: { - id: 'Properties', - defaultMessage: 'Properties', - }, - default: { - id: 'Default', - defaultMessage: 'Default', - }, - effectiveTitle: { - id: 'Publishing Date', - defaultMessage: 'Publishing Date', - }, - effectiveDescription: { - id: - 'If this date is in the future, the content will not show up in listings and searches until this date.', - defaultMessage: - 'If this date is in the future, the content will not show up in listings and searches until this date.', - }, - expiresTitle: { - id: 'Expiration Date', - defaultMessage: 'Expiration Date', - }, - expiresDescription: { - id: - 'When this date is reached, the content will nolonger be visible in listings and searches.', - defaultMessage: - 'When this date is reached, the content will nolonger be visible in listings and searches.', - }, - rightsTitle: { - id: 'Rights', - defaultMessage: 'Rights', - }, - rightsDescription: { - id: 'Copyright statement or other rights information on this item.', - defaultMessage: - 'Copyright statement or other rights information on this item.', - }, - creatorsTitle: { - id: 'Creators', - defaultMessage: 'Creators', - }, - creatorsDescription: { - id: - 'Persons responsible for creating the content of this item. Please enter a list of user names, one per line. The principal creator should come first.', - defaultMessage: - 'Persons responsible for creating the content of this item. Please enter a list of user names, one per line. The principal creator should come first.', - }, - excludeFromNavTitle: { - id: 'Exclude from navigation', - defaultMessage: 'Exclude from navigation', - }, - excludeFromNavDescription: { - id: 'If selected, this item will not appear in the navigation tree', - defaultMessage: - 'If selected, this item will not appear in the navigation tree', - }, - yes: { - id: 'Yes', - defaultMessage: 'Yes', - }, - no: { - id: 'No', - defaultMessage: 'No', - }, -}); - -/** - * ContentsPropertiesModal class. - * @class ContentsPropertiesModal - * @extends Component - */ -class ContentsPropertiesModal extends Component { - /** - * Property types. - * @property {Object} propTypes Property types. - * @static - */ - static propTypes = { - updateContent: PropTypes.func.isRequired, - items: PropTypes.arrayOf(PropTypes.string).isRequired, - request: PropTypes.shape({ - loading: PropTypes.bool, - loaded: PropTypes.bool, - }).isRequired, - open: PropTypes.bool.isRequired, - onOk: PropTypes.func.isRequired, - onCancel: PropTypes.func.isRequired, - }; - - /** - * Constructor - * @method constructor - * @param {Object} props Component properties - * @constructs ContentsUploadModal - */ - constructor(props) { - super(props); - - this.onSubmit = this.onSubmit.bind(this); - } - - /** - * Component will receive props - * @method componentWillReceiveProps - * @param {Object} nextProps Next properties - * @returns {undefined} - */ - UNSAFE_componentWillReceiveProps(nextProps) { - if (this.props.request.loading && nextProps.request.loaded) { - this.props.onOk(); - } - } - - /** - * Submit handler - * @method onSubmit - * @param {Object} data Form data - * @returns {undefined} - */ - onSubmit(data) { - if (isEmpty(data)) { - this.props.onOk(); - } else { - this.props.updateContent( - this.props.items, - map(this.props.items, () => data), - ); - } - } - - /** - * Render method. - * @method render - * @returns {string} Markup for the component. - */ - render() { - return ( - this.props.open && ( - - ) - ); - } -} - -export default compose( - injectIntl, - connect( - (state) => ({ - request: state.content.update, - }), - { updateContent }, - ), -)(ContentsPropertiesModal); diff --git a/src/customizations/volto/components/manage/Display/Display.jsx b/src/customizations/volto/components/manage/Display/Display.jsx new file mode 100644 index 00000000..6d38c3c1 --- /dev/null +++ b/src/customizations/volto/components/manage/Display/Display.jsx @@ -0,0 +1,306 @@ +import React, { Component, Fragment } from 'react'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; +import { compose } from 'redux'; +import { injectLazyLibs } from '@plone/volto/helpers/Loadable/Loadable'; + +import jwtDecode from 'jwt-decode'; +import { + getSchema, + getUser, + updateContent, + getContent, +} from '@plone/volto/actions'; +import { getLayoutFieldname } from '@plone/volto/helpers'; +import { FormFieldWrapper, Icon } from '@plone/volto/components'; +import { defineMessages, injectIntl } from 'react-intl'; +import config from '@plone/volto/registry'; + +import downSVG from '@plone/volto/icons/down-key.svg'; +import upSVG from '@plone/volto/icons/up-key.svg'; +import checkSVG from '@plone/volto/icons/check.svg'; + +const messages = defineMessages({ + Viewmode: { + id: 'Viewmode', + defaultMessage: 'View', + }, +}); + +const Option = injectLazyLibs('reactSelect')((props) => { + const { Option } = props.reactSelect.components; + return ( + + ); +}); + +const DropdownIndicator = injectLazyLibs('reactSelect')((props) => { + const { DropdownIndicator } = props.reactSelect.components; + return ( + + {props.selectProps.menuIsOpen ? ( + + ) : ( + + )} + + ); +}); + +const selectTheme = (theme) => ({ + ...theme, + borderRadius: 0, + colors: { + ...theme.colors, + primary25: 'hotpink', + primary: '#b8c6c8', + }, +}); + +const customSelectStyles = { + control: (styles, state) => ({ + ...styles, + border: 'none', + borderBottom: '2px solid #b8c6c8', + boxShadow: 'none', + borderBottomStyle: state.menuIsOpen ? 'dotted' : 'solid', + }), + menu: (styles, state) => ({ + ...styles, + top: null, + marginTop: 0, + boxShadow: 'none', + borderBottom: '2px solid #b8c6c8', + }), + menuList: (styles, state) => ({ + ...styles, + maxHeight: '400px', + }), + indicatorSeparator: (styles) => ({ + ...styles, + width: null, + }), + valueContainer: (styles) => ({ + ...styles, + padding: 0, + }), + option: (styles, state) => ({ + ...styles, + backgroundColor: null, + minHeight: '50px', + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + padding: '12px 12px', + color: state.isSelected + ? '#007bc1' + : state.isFocused + ? '#4a4a4a' + : 'inherit', + ':active': { + backgroundColor: null, + }, + span: { + flex: '0 0 auto', + }, + svg: { + flex: '0 0 auto', + }, + }), +}; + +/** + * Display container class. + * @class Display + * @extends Component + */ +class DisplaySelect extends Component { + /** + * Property types. + * @property {Object} propTypes Property types. + * @static + */ + static propTypes = { + getSchema: PropTypes.func.isRequired, + updateContent: PropTypes.func.isRequired, + getContent: PropTypes.func.isRequired, + loaded: PropTypes.bool.isRequired, + pathname: PropTypes.string.isRequired, + layouts: PropTypes.arrayOf(PropTypes.string), + layout: PropTypes.string, + type: PropTypes.string.isRequired, + }; + + /** + * Default properties + * @property {Object} defaultProps Default properties. + * @static + */ + static defaultProps = { + layouts: [], + layout: '', + rolesWhoCanChangeLayout: [], + }; + + state = { + hasMatchingRole: false, + selectedOption: { + value: this.props.layout, + label: config.views.layoutViewsNamesMapping?.[this.props.layout] + ? this.props.intl.formatMessage({ + id: config.views.layoutViewsNamesMapping?.[this.props.layout], + defaultMessage: + config.views.layoutViewsNamesMapping?.[this.props.layout], + }) + : this.props.layout, + }, + }; + + componentDidMount() { + this.props.getSchema(this.props.type); + } + + UNSAFE_componentWillMount() { + if (!this.props.rolesWhoCanChangeLayout.length) { + this.props.rolesWhoCanChangeLayout.push( + ...(config?.settings?.eea?.rolesWhoCanChangeLayout || []), + ); + } + if (!this.props.layouts.length) { + this.props.getSchema(this.props.type); + } + if (Object.keys(this.props.user).length === 0) { + this.props.getUser(this.props.userId); + } else { + const hasMatchingRole = this.props.user.roles.some((role) => + this.props.rolesWhoCanChangeLayout.includes(role), + ); + if (hasMatchingRole !== this.state.hasMatchingRole) { + this.setState({ hasMatchingRole }); + } + } + } + + /** + * Component will receive props + * @method componentWillReceiveProps + * @param {Object} nextProps Next properties + * @returns {undefined} + */ + UNSAFE_componentWillReceiveProps(nextProps) { + if (nextProps.pathname !== this.props.pathname) { + this.props.getSchema(nextProps.type); + } + if (!this.props.loaded && nextProps.loaded) { + this.props.getContent(nextProps.pathname); + } + + if (Object.keys(nextProps.user).length !== 0) { + const hasMatchingRole = nextProps.user.roles.some((role) => + this.props.rolesWhoCanChangeLayout.includes(role), + ); + if (hasMatchingRole !== this.state.hasMatchingRole) { + this.setState({ hasMatchingRole }); + } + } + } + + /** + * On set layout handler + * @method setLayout + * @param {Object} event Event object + * @returns {undefined} + */ + setLayout = (selectedOption) => { + this.props.updateContent(this.props.pathname, { + layout: selectedOption.value, + }); + this.setState({ selectedOption }); + }; + + selectValue = (option) => ( + + {option.label} + + ); + + optionRenderer = (option) => ( + + {option.label} + + + ); + + render() { + if (!this.state.hasMatchingRole) { + return null; + } + const { selectedOption } = this.state; + const Select = this.props.reactSelect.default; + const layoutsNames = config.views.layoutViewsNamesMapping; + const layoutOptions = this.props.layouts + .filter( + (layout) => + Object.keys(config.views.contentTypesViews).includes(layout) || + Object.keys(config.views.layoutViews).includes(layout), + ) + .map((item) => ({ + value: item, + label: + this.props.intl.formatMessage({ + id: layoutsNames[item], + defaultMessage: layoutsNames[item], + }) || item, + })); + + return layoutOptions?.length > 1 ? ( + +