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 ? (
-
- ) : (
- ''
- );
-};
-
-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 && (
+
+
+ {
+ copy();
+
+ toast.info(
+ ,
+ );
+ }}
+ >
+
+ )}
+
+ );
+ }
+ 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 = (
-
- );
-
- 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 && (
{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 = (
-
-
-
setHovering(true)}
- // onMouseLeave={() => setHovering(false)}
- className={`copyright-wrapper ${
- copyrightPosition ? copyrightPosition : 'left'
- }`}
- >
- {copyright ? (
-
-
-
-
- {/**/}
- {copyright}
- {/*
*/}
-
- ) : (
- ''
- )}
-
+ {properties.image && (
+ <>
+ {(() => {
+ const image = (
+
+
+
+ {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 (
+
+ {props.label}
+ {props.isFocused && !props.isSelected && (
+
+ )}
+ {props.isSelected && }
+
+ );
+});
+
+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 ? (
+
+
+
+ ) : null;
+ }
+}
+
+export default compose(
+ injectIntl,
+ injectLazyLibs('reactSelect'),
+ connect(
+ (state) => ({
+ loaded: state.content.update.loaded,
+ layouts: state.schema.schema ? state.schema.schema.layouts : [],
+ layout: state.content.data
+ ? state.content.data[getLayoutFieldname(state.content.data)]
+ : '',
+ layout_fieldname: state.content.data
+ ? getLayoutFieldname(state.content.data)
+ : '',
+ type: state.content.data ? state.content.data['@type'] : '',
+ user: state.users.user,
+ userId: state.userSession.token
+ ? jwtDecode(state.userSession.token).sub
+ : '',
+ }),
+ { getSchema, getUser, updateContent, getContent },
+ ),
+)(DisplaySelect);
diff --git a/src/customizations/volto/components/manage/Display/Readme.md b/src/customizations/volto/components/manage/Display/Readme.md
new file mode 100644
index 00000000..3d4fb7b1
--- /dev/null
+++ b/src/customizations/volto/components/manage/Display/Readme.md
@@ -0,0 +1 @@
+Added customization to condition the display of the layout options based on the user's roles.
diff --git a/src/customizations/volto/components/manage/Form/Form.jsx b/src/customizations/volto/components/manage/Form/Form.jsx
deleted file mode 100644
index 696f3cae..00000000
--- a/src/customizations/volto/components/manage/Form/Form.jsx
+++ /dev/null
@@ -1,810 +0,0 @@
-/**
- * Form component.
- * @module components/manage/Form/Form
- */
-
-import { BlocksForm, Field, Icon, Toast } from '@plone/volto/components';
-import {
- difference,
- FormValidation,
- getBlocksFieldname,
- getBlocksLayoutFieldname,
- messages,
-} from '@plone/volto/helpers';
-import aheadSVG from '@plone/volto/icons/ahead.svg';
-import clearSVG from '@plone/volto/icons/clear.svg';
-import {
- findIndex,
- isEmpty,
- keys,
- map,
- mapValues,
- pickBy,
- without,
- cloneDeep,
-} from 'lodash';
-import isBoolean from 'lodash/isBoolean';
-import PropTypes from 'prop-types';
-import React, { Component } from 'react';
-import { injectIntl } from 'react-intl';
-import { Portal } from 'react-portal';
-import { connect } from 'react-redux';
-import {
- Button,
- Container,
- Form as UiForm,
- Message,
- Segment,
- Tab,
-} from 'semantic-ui-react';
-import { v4 as uuid } from 'uuid';
-import { toast } from 'react-toastify';
-import { BlocksToolbar, UndoToolbar } from '@plone/volto/components';
-import { setSidebarTab } from '@plone/volto/actions';
-import { compose } from 'redux';
-import config from '@plone/volto/registry';
-
-/**
- * Form container class.
- * @class Form
- * @extends Component
- */
-class Form extends Component {
- /**
- * Property types.
- * @property {Object} propTypes Property types.
- * @static
- */
- static propTypes = {
- schema: PropTypes.shape({
- fieldsets: PropTypes.arrayOf(
- PropTypes.shape({
- fields: PropTypes.arrayOf(PropTypes.string),
- id: PropTypes.string,
- title: PropTypes.string,
- }),
- ),
- properties: PropTypes.objectOf(PropTypes.any),
- definitions: PropTypes.objectOf(PropTypes.any),
- required: PropTypes.arrayOf(PropTypes.string),
- }),
- formData: PropTypes.objectOf(PropTypes.any),
- pathname: PropTypes.string,
- onSubmit: PropTypes.func,
- onCancel: PropTypes.func,
- submitLabel: PropTypes.string,
- resetAfterSubmit: PropTypes.bool,
- resetOnCancel: PropTypes.bool,
- isEditForm: PropTypes.bool,
- isAdminForm: PropTypes.bool,
- title: PropTypes.string,
- error: PropTypes.shape({
- message: PropTypes.string,
- }),
- loading: PropTypes.bool,
- hideActions: PropTypes.bool,
- description: PropTypes.string,
- visual: PropTypes.bool,
- blocks: PropTypes.arrayOf(PropTypes.object),
- isFormSelected: PropTypes.bool,
- onSelectForm: PropTypes.func,
- editable: PropTypes.bool,
- onChangeFormData: PropTypes.func,
- requestError: PropTypes.string,
- allowedBlocks: PropTypes.arrayOf(PropTypes.string),
- showRestricted: PropTypes.bool,
- };
-
- /**
- * Default properties.
- * @property {Object} defaultProps Default properties.
- * @static
- */
- static defaultProps = {
- formData: null,
- onSubmit: null,
- onCancel: null,
- submitLabel: null,
- resetAfterSubmit: false,
- resetOnCancel: false,
- isEditForm: false,
- isAdminForm: false,
- title: null,
- description: null,
- error: null,
- loading: null,
- hideActions: false,
- visual: false,
- blocks: [],
- pathname: '',
- schema: {},
- isFormSelected: true,
- onSelectForm: null,
- editable: true,
- requestError: null,
- allowedBlocks: null,
- };
-
- /**
- * Constructor
- * @method constructor
- * @param {Object} props Component properties
- * @constructs Form
- */
- constructor(props) {
- super(props);
- const ids = {
- title: uuid(),
- text: uuid(),
- };
- let { formData } = props;
- // TODO Tiberiu: customized here
- formData = formData || {}; // when coming from login screen, formData is null
- // this fixes a bug where, if you go to an /edit page, it will show login (you need to wait the 5 seconds timeout), after login you get redirected back to the edit, then it crashes
- // end customized
- const blocksFieldname = getBlocksFieldname(formData);
- const blocksLayoutFieldname = getBlocksLayoutFieldname(formData);
-
- if (!props.isEditForm) {
- // It's a normal (add form), get defaults from schema
- formData = {
- ...mapValues(props.schema.properties, 'default'),
- ...formData,
- };
- }
- // defaults for block editor; should be moved to schema on server side
- // Adding fallback in case the fields are empty, so we are sure that the edit form
- // shows at least the default blocks
- if (
- formData.hasOwnProperty(blocksFieldname) &&
- formData.hasOwnProperty(blocksLayoutFieldname)
- ) {
- if (
- !formData[blocksLayoutFieldname] ||
- isEmpty(formData[blocksLayoutFieldname].items)
- ) {
- formData[blocksLayoutFieldname] = {
- items: [ids.title, ids.text],
- };
- }
- if (!formData[blocksFieldname] || isEmpty(formData[blocksFieldname])) {
- formData[blocksFieldname] = {
- [ids.title]: {
- '@type': 'title',
- },
- [ids.text]: {
- '@type': config.settings.defaultBlockType,
- },
- };
- }
- }
-
- let selectedBlock = null;
- if (
- formData.hasOwnProperty(blocksLayoutFieldname) &&
- formData[blocksLayoutFieldname].items.length > 0
- ) {
- if (config.blocks?.initialBlocksFocus === null) {
- selectedBlock = null;
- } else if (this.props.type in config.blocks?.initialBlocksFocus) {
- // Default selected is not the first block, but the one from config.
- // TODO Select first block and not an arbitrary one.
- Object.keys(formData[blocksFieldname]).forEach((b_key) => {
- if (
- formData[blocksFieldname][b_key]['@type'] ===
- config.blocks?.initialBlocksFocus?.[this.props.type]
- ) {
- selectedBlock = b_key;
- }
- });
- } else {
- selectedBlock = formData[blocksLayoutFieldname].items[0];
- }
- }
-
- this.state = {
- formData,
- initialFormData: cloneDeep(formData),
- errors: {},
- selected: selectedBlock,
- multiSelected: [],
- isClient: false,
- // Ensure focus remain in field after change
- inFocus: {},
- };
- this.onChangeField = this.onChangeField.bind(this);
- this.onSelectBlock = this.onSelectBlock.bind(this);
- this.onSubmit = this.onSubmit.bind(this);
- this.onCancel = this.onCancel.bind(this);
- this.onTabChange = this.onTabChange.bind(this);
- this.onBlurField = this.onBlurField.bind(this);
- this.onClickInput = this.onClickInput.bind(this);
- }
-
- /**
- * On updates caused by props change
- * if errors from Backend come, these will be shown to their corresponding Fields
- * also the first Tab to have any errors will be selected
- * @param {Object} prevProps
- */
- async componentDidUpdate(prevProps, prevState) {
- let { requestError } = this.props;
- let errors = {};
- let activeIndex = 0;
-
- if (requestError && prevProps.requestError !== requestError) {
- errors = FormValidation.giveServerErrorsToCorrespondingFields(
- requestError,
- );
- activeIndex = FormValidation.showFirstTabWithErrors({
- errors,
- schema: this.props.schema,
- });
-
- this.setState({
- errors,
- activeIndex,
- });
- }
-
- if (this.props.onChangeFormData) {
- if (
- // TODO: use fast-deep-equal
- JSON.stringify(prevState?.formData) !==
- JSON.stringify(this.state.formData)
- ) {
- this.props.onChangeFormData(this.state.formData);
- }
- }
- }
-
- /**
- * Tab selection is done only by setting activeIndex in state
- */
- onTabChange(e, { activeIndex }) {
- const defaultFocus = this.props.schema.fieldsets[activeIndex].fields[0];
- this.setState({
- activeIndex,
- ...(defaultFocus ? { inFocus: { [defaultFocus]: true } } : {}),
- });
- }
-
- /**
- * If user clicks on input, the form will be not considered pristine
- * this will avoid onBlur effects without interraction with the form
- * @param {Object} e event
- */
- onClickInput(e) {
- this.setState({ isFormPristine: false });
- }
-
- /**
- * Validate fields on blur
- * @method onBlurField
- * @param {string} id Id of the field
- * @param {*} value Value of the field
- * @returns {undefined}
- */
- onBlurField(id, value) {
- if (!this.state.isFormPristine) {
- const errors = FormValidation.validateFieldsPerFieldset({
- schema: this.props.schema,
- formData: this.state.formData,
- formatMessage: this.props.intl.formatMessage,
- touchedField: { [id]: value },
- });
-
- this.setState({
- errors,
- });
- }
- }
-
- /**
- * Component did mount
- * @method componentDidMount
- * @returns {undefined}
- */
- componentDidMount() {
- this.setState({ isClient: true });
- }
-
- static getDerivedStateFromProps(props, state) {
- let newState = { ...state };
- if (!props.isFormSelected) {
- newState.selected = null;
- }
-
- return newState;
- }
-
- /**
- * Change field handler
- * Remove errors for changed field
- * @method onChangeField
- * @param {string} id Id of the field
- * @param {*} value Value of the field
- * @returns {undefined}
- */
- onChangeField(id, value) {
- this.setState((prevState) => {
- const { errors, formData } = prevState;
- delete errors[id];
- return {
- errors,
- formData: {
- ...formData,
- // We need to catch also when the value equals false this fixes #888
- [id]:
- value || (value !== undefined && isBoolean(value)) ? value : null,
- },
- // Changing the form data re-renders the select widget which causes the
- // focus to get lost. To circumvent this, we set the focus back to
- // the input.
- // This could fix other widgets too but currently targeted
- // against the select widget only.
- // Ensure field to be in focus after the change
- inFocus: { [id]: true },
- };
- });
- }
-
- /**
- * Select block handler
- * @method onSelectBlock
- * @param {string} id Id of the field
- * @param {string} isMultipleSelection true if multiple blocks are selected
- * @returns {undefined}
- */
- onSelectBlock(id, isMultipleSelection, event) {
- let multiSelected = [];
- let selected = id;
-
- if (isMultipleSelection) {
- selected = null;
- const blocksLayoutFieldname = getBlocksLayoutFieldname(
- this.state.formData,
- );
-
- const blocks_layout = this.state.formData[blocksLayoutFieldname].items;
-
- if (event.shiftKey) {
- const anchor =
- this.state.multiSelected.length > 0
- ? blocks_layout.indexOf(this.state.multiSelected[0])
- : blocks_layout.indexOf(this.state.selected);
- const focus = blocks_layout.indexOf(id);
-
- if (anchor === focus) {
- multiSelected = [id];
- } else if (focus > anchor) {
- multiSelected = [...blocks_layout.slice(anchor, focus + 1)];
- } else {
- multiSelected = [...blocks_layout.slice(focus, anchor + 1)];
- }
- }
-
- if ((event.ctrlKey || event.metaKey) && !event.shiftKey) {
- multiSelected = this.state.multiSelected || [];
- if (!this.state.multiSelected.includes(this.state.selected)) {
- multiSelected = [...multiSelected, this.state.selected];
- selected = null;
- }
- if (this.state.multiSelected.includes(id)) {
- selected = null;
- multiSelected = without(multiSelected, id);
- } else {
- multiSelected = [...multiSelected, id];
- }
- }
- }
-
- this.setState({
- selected,
- multiSelected,
- });
-
- if (this.props.onSelectForm) {
- if (event) event.nativeEvent.stopImmediatePropagation();
- this.props.onSelectForm();
- }
- }
-
- /**
- * Cancel handler
- * It prevents event from triggering submit, reset form if props.resetAfterSubmit
- * and calls this.props.onCancel
- * @method onCancel
- * @param {Object} event Event object.
- * @returns {undefined}
- */
- onCancel(event) {
- if (event) {
- event.preventDefault();
- }
- if (this.props.resetOnCancel || this.props.resetAfterSubmit) {
- this.setState({
- formData: this.props.formData,
- });
- }
- this.props.onCancel(event);
- }
-
- /**
- * Submit handler also validate form and collect errors
- * @method onSubmit
- * @param {Object} event Event object.
- * @returns {undefined}
- */
- onSubmit(event) {
- if (event) {
- event.preventDefault();
- }
-
- const errors = this.props.schema
- ? FormValidation.validateFieldsPerFieldset({
- schema: this.props.schema,
- formData: this.state.formData,
- formatMessage: this.props.intl.formatMessage,
- })
- : {};
-
- if (keys(errors).length > 0) {
- const activeIndex = FormValidation.showFirstTabWithErrors({
- errors,
- schema: this.props.schema,
- });
- this.setState(
- {
- errors,
- activeIndex,
- },
- () => {
- Object.keys(errors).forEach((err) =>
- toast.error(
-
,
- ),
- );
- },
- );
- // Changes the focus to the metadata tab in the sidebar if error
- this.props.setSidebarTab(0);
- } else {
- // Get only the values that have been modified (Edit forms), send all in case that
- // it's an add form
- if (this.props.isEditForm) {
- this.props.onSubmit(this.getOnlyFormModifiedValues());
- } else {
- this.props.onSubmit(this.state.formData);
- }
- if (this.props.resetAfterSubmit) {
- this.setState({
- formData: this.props.formData,
- });
- }
- }
- }
-
- /**
- * getOnlyFormModifiedValues handler
- * It returns only the values of the fields that are have really changed since the
- * form was loaded. Useful for edit forms and PATCH operations, when we only want to
- * send the changed data.
- * @method getOnlyFormModifiedValues
- * @param {Object} event Event object.
- * @returns {undefined}
- */
- getOnlyFormModifiedValues = () => {
- const fieldsModified = Object.keys(
- difference(this.state.formData, this.state.initialFormData),
- );
- return {
- ...pickBy(this.state.formData, (value, key) =>
- fieldsModified.includes(key),
- ),
- ...(this.state.formData['@static_behaviors'] && {
- '@static_behaviors': this.state.formData['@static_behaviors'],
- }),
- };
- };
-
- /**
- * Removed blocks and blocks_layout fields from the form.
- * @method removeBlocksLayoutFields
- * @param {object} schema The schema definition of the form.
- * @returns A modified copy of the given schema.
- */
- removeBlocksLayoutFields = (schema) => {
- const newSchema = { ...schema };
- const layoutFieldsetIndex = findIndex(
- newSchema.fieldsets,
- (fieldset) => fieldset.id === 'layout',
- );
- if (layoutFieldsetIndex > -1) {
- const layoutFields = newSchema.fieldsets[layoutFieldsetIndex].fields;
- newSchema.fieldsets[layoutFieldsetIndex].fields = layoutFields.filter(
- (field) => field !== 'blocks' && field !== 'blocks_layout',
- );
- if (newSchema.fieldsets[layoutFieldsetIndex].fields.length === 0) {
- newSchema.fieldsets = [
- ...newSchema.fieldsets.slice(0, layoutFieldsetIndex),
- ...newSchema.fieldsets.slice(layoutFieldsetIndex + 1),
- ];
- }
- }
- return newSchema;
- };
-
- /**
- * Render method.
- * @method render
- * @returns {string} Markup for the component.
- */
- render() {
- const { settings } = config;
- const { schema: originalSchema, onCancel, onSubmit } = this.props;
- const { formData } = this.state;
- const schema = this.removeBlocksLayoutFields(originalSchema);
-
- return this.props.visual ? (
- // Removing this from SSR is important, since react-beautiful-dnd supports SSR,
- // but draftJS don't like it much and the hydration gets messed up
- this.state.isClient && (
-
-
- this.setState({
- formData: {
- ...formData,
- ...newBlockData,
- },
- })
- }
- onSetSelectedBlocks={(blockIds) =>
- this.setState({ multiSelected: blockIds })
- }
- onSelectBlock={this.onSelectBlock}
- />
- this.setState(state)}
- />
-
- this.setState({
- formData: {
- ...formData,
- ...newFormData,
- },
- })
- }
- onChangeField={this.onChangeField}
- onSelectBlock={this.onSelectBlock}
- properties={formData}
- pathname={this.props.pathname}
- selectedBlock={this.state.selected}
- multiSelected={this.state.multiSelected}
- manage={this.props.isAdminForm}
- allowedBlocks={this.props.allowedBlocks}
- showRestricted={this.props.showRestricted}
- editable={this.props.editable}
- isMainForm={this.props.editable}
- />
- {this.state.isClient && this.props.editable && (
-
- 0}
- >
- {schema &&
- map(schema.fieldsets, (item) => [
-
- {item.title}
- ,
-
- {map(item.fields, (field, index) => (
-
- ))}
- ,
- ])}
-
-
- )}
-
- )
- ) : (
-
- 0}
- className={settings.verticalFormTabs ? 'vertical-form' : ''}
- >
-
-
- {schema && schema.fieldsets.length > 1 && (
- <>
- {settings.verticalFormTabs && this.props.title && (
-
- {this.props.title}
-
- )}
- ({
- menuItem: item.title,
- render: () => [
- !settings.verticalFormTabs && this.props.title && (
-
- {this.props.title}
-
- ),
- item.description && (
-
- {item.description}
-
- ),
- ...map(item.fields, (field, index) => (
-
- )),
- ],
- }))}
- />
- >
- )}
- {schema && schema.fieldsets.length === 1 && (
-
- {this.props.title && (
-
- {this.props.title}
-
- )}
- {this.props.description && (
- {this.props.description}
- )}
- {keys(this.state.errors).length > 0 && (
-
- )}
- {this.props.error && (
-
- )}
- {map(schema.fieldsets[0].fields, (field) => (
-
- ))}
-
- )}
- {!this.props.hideActions && (
-
- {onSubmit && (
-
-
-
- )}
- {onCancel && (
-
-
-
- )}
-
- )}
-
-
-
-
- );
- }
-}
-
-const FormIntl = injectIntl(Form, { forwardRef: true });
-
-export default compose(
- connect(null, { setSidebarTab }, null, { forwardRef: true }),
-)(FormIntl);
diff --git a/src/customizations/volto/components/manage/Form/Form.test.jsx b/src/customizations/volto/components/manage/Form/Form.test.jsx
deleted file mode 100644
index 36803d20..00000000
--- a/src/customizations/volto/components/manage/Form/Form.test.jsx
+++ /dev/null
@@ -1,1124 +0,0 @@
-import React from 'react';
-import { render, fireEvent } from '@testing-library/react';
-import { Provider } from 'react-intl-redux';
-import configureMockStore from 'redux-mock-store';
-import Form from './Form';
-import config from '@plone/volto/registry';
-import { FormValidation } from '@plone/volto/helpers';
-import '@testing-library/jest-dom/extend-expect';
-
-const mockStore = configureMockStore();
-let store;
-
-jest.mock('@plone/volto/components/manage/Form/Field', () => (props) => {
- return (
-
- {props.id}
- {props.description}
-
- );
-});
-
-jest.mock(
- '@plone/volto/components/manage/Form/BlocksToolbar',
- () => (props) => {
- return (
-
{
- props.onSetSelectedBlocks(
- target.target.value ? [...target.target.value.split(',')] : [],
- );
- }}
- onChange={props.onChangeBlocks}
- onSelect={(target) => {
- props.onSelectBlock(
- target.target.id,
- target.target.isMultipleSelection,
- target.target.event,
- );
- }}
- />
- );
- },
-);
-
-jest.mock('@plone/volto/components/manage/Form/UndoToolbar', () => (props) => {
- return
UndoToolbar
;
-});
-
-jest.mock(
- '@plone/volto/components/manage/Blocks/Block/BlocksForm',
- () => (props) => {
- return
;
- },
-);
-
-describe('Form', () => {
- beforeEach(() => {
- store = mockStore({
- intl: {
- locale: 'en',
- messages: {},
- },
- });
- });
-
- it('renders "Test title" and has the correct structure without formData without crashing', () => {
- config.blocks = {
- initialBlocksFocus: {
- typeB: 'typeB',
- },
- };
- config.settings = {
- verticalFormTabs: false,
- };
- const props = {
- isFormSelected: false,
- schema: {
- fieldsets: [{ fields: [], id: 'default', title: 'Default' }],
- properties: {},
- definitions: {},
- required: [],
- },
- formData: {
- blocks: undefined,
- blocks_layout: undefined,
- },
- type: 'typeB',
- title: 'Test title',
- };
-
- const { container, getByText } = render(
-
-
- ,
- );
- expect(container).toBeTruthy();
- expect(
- container.querySelector('.ui.form .invisible .ui.raised.segments'),
- ).toBeInTheDocument();
- expect(getByText('Test title')).toBeInTheDocument();
- });
-
- it('renders "Test title" and has the correct structure with formData without crashing with same types', () => {
- config.blocks = {
- initialBlocksFocus: {
- typeB: 'typeB',
- },
- };
- config.settings = {
- verticalFormTabs: false,
- };
- const props = {
- schema: {
- fieldsets: [{ fields: [], id: 'default', title: 'Default' }],
- properties: {},
- definitions: {},
- required: [],
- },
- formData: {
- blocks: {
- id1: {
- '@type': 'typeB',
- plaintext: 'Block A',
- override_toc: false,
- },
- },
- blocks_layout: {
- items: ['id1'],
- },
- },
- type: 'typeB',
- title: 'Test title',
- };
-
- const { container, getByText } = render(
-
-
- ,
- );
- expect(container).toBeTruthy();
- expect(
- container.querySelector('.ui.form .invisible .ui.raised.segments'),
- ).toBeInTheDocument();
- expect(getByText('Test title')).toBeInTheDocument();
- });
-
- it('renders "Test title" and has the correct structure with formData without crashing with different types and isEditForm true', () => {
- config.blocks = {
- initialBlocksFocus: {
- typeA: 'typeA',
- },
- };
- config.settings = {
- verticalFormTabs: false,
- };
- const props = {
- schema: {
- fieldsets: [{ fields: [], id: 'default', title: 'Default' }],
- properties: {},
- definitions: {},
- required: [],
- },
- formData: {
- blocks: {
- id1: {
- '@type': 'typeB',
- plaintext: 'Block A',
- override_toc: false,
- },
- },
- blocks_layout: {
- items: ['id1'],
- },
- },
- type: 'typeB',
- title: 'Test title',
- isEditForm: true,
- };
-
- const { container, getByText } = render(
-
-
- ,
- );
- expect(container).toBeTruthy();
- expect(
- container.querySelector('.ui.form .invisible .ui.raised.segments'),
- ).toBeInTheDocument();
- expect(getByText('Test title')).toBeInTheDocument();
- });
-
- it('renders "Test title" and has the correct structure with formData without crashing with no focused block', () => {
- config.blocks = {
- initialBlocksFocus: null,
- };
- config.settings = {
- verticalFormTabs: false,
- };
- const props = {
- schema: {
- fieldsets: [{ fields: [], id: 'default', title: 'Default' }],
- properties: {},
- definitions: {},
- required: [],
- },
- formData: {
- blocks: {
- id1: {
- '@type': 'typeB',
- plaintext: 'Block A',
- override_toc: false,
- },
- },
- blocks_layout: {
- items: ['id1'],
- },
- },
- type: 'typeB',
- title: 'Test title',
- };
-
- const { container, getByText } = render(
-
-
- ,
- );
- expect(container).toBeTruthy();
- expect(
- container.querySelector('.ui.form .invisible .ui.raised.segments'),
- ).toBeInTheDocument();
- expect(getByText('Test title')).toBeInTheDocument();
- });
-
- it('should display the correct fields on the currently selected fieldset', () => {
- config.blocks = {
- initialBlocksFocus: null,
- };
- config.settings = {
- verticalFormTabs: false,
- };
- const props = {
- schema: {
- fieldsets: [
- {
- fields: ['field1', 'field2'],
- id: 'fieldset1',
- title: 'Fieldset 1',
- description: 'Fieldset 1 description',
- },
- {
- fields: ['field3', 'field4'],
- id: 'fieldset2',
- title: 'Fieldset 2',
- },
- ],
- properties: {
- field1: {
- title: 'Field 1',
- description: 'Field 1 description',
- items: ['field4'],
- },
- field2: { title: 'Field 2' },
- field3: { title: 'Field 3' },
- field4: { title: 'Field 4' },
- },
- definitions: {},
- required: [],
- },
- formData: {
- testBlocks: {
- id1: {
- '@type': 'typeB',
- plaintext: 'Block A',
- override_toc: false,
- },
- },
- TestBlocks_layout: {
- items: ['id1'],
- },
- },
- type: 'typeB',
- title: 'Test title',
- description: 'Test description',
- onChangeFormData: jest.fn(),
- };
-
- const prevProps = { requestError: null };
- const prevState = { formData: {}, errors: {}, activeIndex: 0 };
- const giveServerErrorsToCorrespondingFieldsMock = jest.spyOn(
- FormValidation,
- 'giveServerErrorsToCorrespondingFields',
- );
- giveServerErrorsToCorrespondingFieldsMock.mockImplementation(() => [
- { message: 'Sample error message' },
- { message: 'Sample error message' },
- ]);
- const requestError = 'Sample error message';
-
- const { container, getByText, rerender } = render(
-
-
- ,
- );
-
- expect(getByText('Fieldset 1')).toBeInTheDocument();
- expect(getByText('Fieldset 2')).toBeInTheDocument();
- expect(getByText('Fieldset 1 description')).toBeInTheDocument();
- expect(getByText('Test title')).toBeInTheDocument();
- expect(container.querySelector('#mocked-field-field1')).toBeInTheDocument();
- expect(container.querySelector('#mocked-field-field2')).toBeInTheDocument();
- expect(
- container.querySelector('#mocked-field-field3'),
- ).not.toBeInTheDocument();
- expect(
- container.querySelector('#mocked-field-field4'),
- ).not.toBeInTheDocument();
-
- fireEvent.click(container.querySelector('#mocked-field-field2'));
- fireEvent.click(getByText('Fieldset 2'));
-
- expect(
- container.querySelector('#mocked-field-field1'),
- ).not.toBeInTheDocument();
- expect(
- container.querySelector('#mocked-field-field2'),
- ).not.toBeInTheDocument();
- expect(container.querySelector('#mocked-field-field3')).toBeInTheDocument();
- expect(container.querySelector('#mocked-field-field4')).toBeInTheDocument();
-
- rerender(
-
-
- ,
- );
-
- expect(giveServerErrorsToCorrespondingFieldsMock).toHaveBeenCalledWith(
- requestError,
- );
- });
-
- it('renders without crashing and selecting Submit/Cancel button with resetAfterSubmit and errors', () => {
- config.blocks = {
- initialBlocksFocus: null,
- };
- config.settings = {
- verticalFormTabs: true,
- };
-
- const props = {
- schema: {
- fieldsets: [
- {
- fields: ['field1', 'field2'],
- id: 'fieldset1',
- title: 'Fieldset 1',
- },
- {
- fields: ['field3', 'field4'],
- id: 'fieldset2',
- title: 'Fieldset 2',
- },
- ],
- properties: {
- field1: {
- title: 'Field 1',
- description: 'Field 1 description',
- items: ['field4'],
- widget: 'textarea',
- },
- field2: { widget: 'textarea' },
- },
- definitions: {},
- required: [],
- },
- formData: {
- blocks: {
- id1: {
- '@type': 'typeB',
- plaintext: 'Block A',
- override_toc: false,
- },
- },
- blocks_layout: {
- items: ['id1'],
- },
- },
- type: 'typeB',
- isClient: true,
- title: 'Test title',
- description: 'Test description',
- error: {
- message: 'Sample error message',
- },
- onSubmit: jest.fn(),
- onCancel: jest.fn(),
- resetAfterSubmit: true,
- submitLabel: 'Submit',
- };
-
- const validateFieldsPerFieldsetMock = jest.spyOn(
- FormValidation,
- 'validateFieldsPerFieldset',
- );
- validateFieldsPerFieldsetMock.mockImplementation(() => ({
- field1: [],
- field2: [],
- }));
-
- const { container, getByText } = render(
-
-
- ,
- );
-
- expect(getByText('Fieldset 1')).toBeInTheDocument();
- expect(getByText('Fieldset 2')).toBeInTheDocument();
- expect(getByText('Test title')).toBeInTheDocument();
- expect(container.querySelector('#mocked-field-field1')).toBeInTheDocument();
- expect(container.querySelector('#mocked-field-field2')).toBeInTheDocument();
- expect(
- container.querySelector('#mocked-field-field3'),
- ).not.toBeInTheDocument();
- expect(
- container.querySelector('#mocked-field-field4'),
- ).not.toBeInTheDocument();
-
- fireEvent.click(container.querySelector('button[aria-label="Submit"]'));
- fireEvent.click(container.querySelector('button[aria-label="Cancel"]'));
- });
-
- it('renders without crashing and selecting Submit/Cancel button with resetAfterSubmit and isaEditForm and no errors', () => {
- config.blocks = {
- initialBlocksFocus: null,
- };
- config.settings = {
- verticalFormTabs: true,
- };
-
- const props = {
- schema: {
- fieldsets: [
- {
- fields: ['field1', 'field2'],
- id: 'fieldset1',
- title: 'Fieldset 1',
- },
- {
- fields: ['field3', 'field4'],
- id: 'fieldset2',
- title: 'Fieldset 2',
- },
- ],
- properties: {
- field1: {
- title: 'Field 1',
- description: 'Field 1 description',
- items: ['field4'],
- widget: 'textarea',
- },
- field2: { widget: 'textarea' },
- },
- definitions: {},
- required: [],
- },
- formData: {
- blocks: {
- id1: {
- '@type': 'typeB',
- plaintext: 'Block A',
- override_toc: false,
- },
- },
- blocks_layout: {
- items: ['id1'],
- },
- },
- type: 'typeB',
- isClient: true,
- title: 'Test title',
- description: 'Test description',
- error: {
- message: 'Sample error message',
- },
- onSubmit: jest.fn(),
- onCancel: jest.fn(),
- resetAfterSubmit: true,
- isEditForm: true,
- submitLabel: 'Submit',
- };
-
- const validateFieldsPerFieldsetMock = jest.spyOn(
- FormValidation,
- 'validateFieldsPerFieldset',
- );
- validateFieldsPerFieldsetMock.mockImplementation(() => ({}));
-
- const { container, getByText } = render(
-
-
- ,
- );
-
- expect(getByText('Fieldset 1')).toBeInTheDocument();
- expect(getByText('Fieldset 2')).toBeInTheDocument();
- expect(getByText('Test title')).toBeInTheDocument();
- expect(container.querySelector('#mocked-field-field1')).toBeInTheDocument();
- expect(container.querySelector('#mocked-field-field2')).toBeInTheDocument();
- expect(
- container.querySelector('#mocked-field-field3'),
- ).not.toBeInTheDocument();
- expect(
- container.querySelector('#mocked-field-field4'),
- ).not.toBeInTheDocument();
-
- fireEvent.click(container.querySelector('button[aria-label="Submit"]'));
- fireEvent.click(container.querySelector('button[aria-label="Cancel"]'));
- });
-
- it('renders without crashing and selecting Submit/Cancel button with no errors', () => {
- config.blocks = {
- initialBlocksFocus: null,
- };
- config.settings = {
- verticalFormTabs: true,
- };
-
- const props = {
- schema: {
- fieldsets: [
- {
- fields: ['field1', 'field2'],
- id: 'fieldset1',
- title: 'Fieldset 1',
- },
- {
- fields: ['field3', 'field4'],
- id: 'fieldset2',
- title: 'Fieldset 2',
- },
- ],
- properties: {
- field1: {
- title: 'Field 1',
- description: 'Field 1 description',
- items: ['field4'],
- widget: 'textarea',
- },
- field2: { widget: 'textarea' },
- },
- definitions: {},
- required: [],
- },
- formData: {
- blocks: {
- id1: {
- '@type': 'typeB',
- plaintext: 'Block A',
- override_toc: false,
- },
- },
- blocks_layout: {
- items: ['id1'],
- },
- },
- type: 'typeB',
- isClient: true,
- title: 'Test title',
- description: 'Test description',
- error: {
- message: 'Sample error message',
- },
- onSubmit: jest.fn(),
- onCancel: jest.fn(),
- submitLabel: 'Submit',
- };
-
- const validateFieldsPerFieldsetMock = jest.spyOn(
- FormValidation,
- 'validateFieldsPerFieldset',
- );
- validateFieldsPerFieldsetMock.mockImplementation(() => ({}));
-
- const { container, getByText } = render(
-
-
- ,
- );
-
- expect(getByText('Fieldset 1')).toBeInTheDocument();
- expect(getByText('Fieldset 2')).toBeInTheDocument();
- expect(getByText('Test title')).toBeInTheDocument();
- expect(container.querySelector('#mocked-field-field1')).toBeInTheDocument();
- expect(container.querySelector('#mocked-field-field2')).toBeInTheDocument();
- expect(
- container.querySelector('#mocked-field-field3'),
- ).not.toBeInTheDocument();
- expect(
- container.querySelector('#mocked-field-field4'),
- ).not.toBeInTheDocument();
-
- fireEvent.click(container.querySelector('button[aria-label="Submit"]'));
- fireEvent.click(container.querySelector('button[aria-label="Cancel"]'));
- });
-
- it('renders only one fieldset and the actions to save/cancel', () => {
- config.blocks = {
- initialBlocksFocus: null,
- };
- config.settings = {
- verticalFormTabs: true,
- };
-
- const props = {
- schema: {
- fieldsets: [
- {
- fields: ['field1', 'field2'],
- id: 'fieldset1',
- title: 'Fieldset 1',
- },
- ],
- properties: {
- field1: {
- title: 'Field 1',
- description: 'Field 1 description',
- items: ['field4'],
- widget: 'textarea',
- },
- field2: { title: 'Field 2', widget: 'textarea' },
- },
- definitions: {},
- required: [],
- },
- formData: {
- blocks: {
- id1: {
- '@type': 'typeB',
- plaintext: 'Block A',
- override_toc: false,
- },
- },
- blocks_layout: {
- items: ['id1'],
- },
- },
- type: 'typeB',
- isClient: true,
- title: 'Test title',
- description: 'Test description',
- error: {
- message: 'Sample error message',
- },
- onSubmit: jest.fn(),
- onCancel: jest.fn(),
- };
-
- const validateFieldsPerFieldsetMock = jest.spyOn(
- FormValidation,
- 'validateFieldsPerFieldset',
- );
- validateFieldsPerFieldsetMock.mockImplementation(() => [
- 'field1',
- 'field2',
- ]);
-
- const { container, getByText } = render(
-
-
- ,
- );
-
- expect(getByText('Error')).toBeInTheDocument();
- expect(getByText('Sample error message')).toBeInTheDocument();
-
- expect(container.querySelector('#mocked-field-field1')).toBeInTheDocument();
- expect(container.querySelector('#mocked-field-field2')).toBeInTheDocument();
-
- fireEvent.click(container.querySelector('#mocked-field-field2 textarea'));
- fireEvent.blur(container.querySelector('#mocked-field-field2 textarea'));
- fireEvent.change(container.querySelector('#mocked-field-field2 textarea'), {
- target: { value: 'test change' },
- });
- });
-
- it('triggers the onSubmit with the shiftKey and multiple selected blocks and multiple erorrs', () => {
- config.blocks = {
- initialBlocksFocus: null,
- };
- config.settings = {
- verticalFormTabs: true,
- };
-
- const props = {
- schema: {
- fieldsets: [
- {
- fields: ['field1', 'field2'],
- id: 'fieldset1',
- title: 'Fieldset 1',
- },
- ],
- properties: {
- field1: {
- title: 'Field 1',
- description: 'Field 1 description',
- items: ['field4'],
- widget: 'textarea',
- },
- field2: { title: 'Field 2', widget: 'textarea' },
- },
- definitions: {},
- required: [],
- },
- formData: {
- blocks: {
- id1: {
- '@type': 'typeB',
- plaintext: 'Block A',
- override_toc: false,
- },
- },
- blocks_layout: {
- items: ['id1'],
- },
- },
- type: 'typeB',
- isClient: true,
- visual: true,
- title: 'Test title',
- description: 'Test description',
- error: {
- message: 'Sample error message',
- },
- onSubmit: jest.fn(),
- onCancel: jest.fn(),
- };
-
- const validateFieldsPerFieldsetMock = jest.spyOn(
- FormValidation,
- 'validateFieldsPerFieldset',
- );
- validateFieldsPerFieldsetMock.mockImplementation(() => [
- 'field1',
- 'field2',
- ]);
-
- const { container } = render(
-
-
- ,
- );
-
- fireEvent.click(container.querySelector('#blocks-toolbar'), {
- target: { value: 'id1,id2' },
- });
- fireEvent.change(container.querySelector('#blocks-toolbar'), {
- target: { value: 'test change' },
- });
- fireEvent.select(container.querySelector('#blocks-toolbar'), {
- target: {
- id: 'id1',
- isMultipleSelection: true,
- event: { shiftKey: true },
- },
- });
-
- fireEvent.change(container.querySelector('#blocks-form'), {
- target: { value: 'test change' },
- });
- });
-
- it('triggers the onSubmit with the shiftKey and multiple selected blocks when the selected blocks are in another order and multiple errors', () => {
- config.blocks = {
- initialBlocksFocus: null,
- };
- config.settings = {
- verticalFormTabs: true,
- };
-
- const props = {
- schema: {
- fieldsets: [
- {
- fields: ['field1', 'field2'],
- id: 'fieldset1',
- title: 'Fieldset 1',
- },
- ],
- properties: {
- field1: {
- title: 'Field 1',
- description: 'Field 1 description',
- items: ['field4'],
- widget: 'textarea',
- },
- field2: { title: 'Field 2', widget: 'textarea' },
- },
- definitions: {},
- required: [],
- },
- formData: {
- blocks: {
- id1: {
- '@type': 'typeB',
- plaintext: 'Block A',
- override_toc: false,
- },
- id2: {
- '@type': 'typeB',
- plaintext: 'Block A',
- override_toc: false,
- },
- },
- blocks_layout: {
- items: ['id1', 'id2'],
- },
- },
- type: 'typeB',
- isClient: true,
- visual: true,
- title: 'Test title',
- description: 'Test description',
- error: {
- message: 'Sample error message',
- },
- onSubmit: jest.fn(),
- onCancel: jest.fn(),
- };
-
- const validateFieldsPerFieldsetMock = jest.spyOn(
- FormValidation,
- 'validateFieldsPerFieldset',
- );
- validateFieldsPerFieldsetMock.mockImplementation(() => [
- 'field1',
- 'field2',
- ]);
-
- const { container } = render(
-
-
- ,
- );
-
- fireEvent.click(container.querySelector('#blocks-toolbar'), {
- target: { value: 'id2,id1' },
- });
- fireEvent.change(container.querySelector('#blocks-toolbar'), {
- target: { value: 'test change' },
- });
- fireEvent.select(container.querySelector('#blocks-toolbar'), {
- target: {
- id: 'id1',
- isMultipleSelection: true,
- event: { shiftKey: true },
- },
- });
- });
-
- it('triggers the onSubmit with the shiftKey and multiple selected blocks when there are no selected blocks and multiple errors', () => {
- config.blocks = {
- initialBlocksFocus: null,
- };
- config.settings = {
- verticalFormTabs: true,
- };
-
- const props = {
- schema: {
- fieldsets: [
- {
- fields: ['field1', 'field2'],
- id: 'fieldset1',
- title: 'Fieldset 1',
- },
- ],
- properties: {
- field1: {
- title: 'Field 1',
- description: 'Field 1 description',
- items: ['field4'],
- widget: 'textarea',
- },
- field2: { title: 'Field 2', widget: 'textarea' },
- },
- definitions: {},
- required: [],
- },
- formData: {
- blocks: {
- id1: {
- '@type': 'typeB',
- plaintext: 'Block A',
- override_toc: false,
- },
- },
- blocks_layout: {
- items: ['id1'],
- },
- },
- type: 'typeB',
- isClient: true,
- visual: true,
- title: 'Test title',
- description: 'Test description',
- error: {
- message: 'Sample error message',
- },
- onSubmit: jest.fn(),
- onCancel: jest.fn(),
- };
-
- const validateFieldsPerFieldsetMock = jest.spyOn(
- FormValidation,
- 'validateFieldsPerFieldset',
- );
- validateFieldsPerFieldsetMock.mockImplementation(() => [
- 'field1',
- 'field2',
- ]);
-
- const { container } = render(
-
-
- ,
- );
-
- fireEvent.click(container.querySelector('#blocks-toolbar'), {
- target: { value: undefined },
- });
- fireEvent.change(container.querySelector('#blocks-toolbar'), {
- target: { value: 'test change' },
- });
- fireEvent.select(container.querySelector('#blocks-toolbar'), {
- target: {
- id: 'id1',
- isMultipleSelection: true,
- event: { shiftKey: true },
- },
- });
- });
-
- it('triggers the onSubmit with the ctrlKey and multiple selected blocks and multiple errors', () => {
- config.blocks = {
- initialBlocksFocus: null,
- };
- config.settings = {
- verticalFormTabs: true,
- };
-
- const props = {
- schema: {
- fieldsets: [
- {
- fields: ['field1', 'field2'],
- id: 'fieldset1',
- title: 'Fieldset 1',
- },
- ],
- properties: {
- field1: {
- title: 'Field 1',
- description: 'Field 1 description',
- items: ['field4'],
- widget: 'textarea',
- },
- field2: { title: 'Field 2', widget: 'textarea' },
- },
- definitions: {},
- required: [],
- },
- formData: {
- blocks: {
- id1: {
- '@type': 'typeB',
- plaintext: 'Block A',
- override_toc: false,
- },
- },
- blocks_layout: {
- items: ['id1'],
- },
- },
- type: 'typeB',
- isClient: true,
- visual: true,
- title: 'Test title',
- description: 'Test description',
- error: {
- message: 'Sample error message',
- },
- onSubmit: jest.fn(),
- onCancel: jest.fn(),
- };
-
- const validateFieldsPerFieldsetMock = jest.spyOn(
- FormValidation,
- 'validateFieldsPerFieldset',
- );
- validateFieldsPerFieldsetMock.mockImplementation(() => [
- 'field1',
- 'field2',
- ]);
-
- const { container } = render(
-
-
- ,
- );
-
- fireEvent.click(container.querySelector('#blocks-toolbar'), {
- target: { value: 'id1,id2' },
- });
- fireEvent.change(container.querySelector('#blocks-toolbar'), {
- target: { value: 'test change' },
- });
- fireEvent.select(container.querySelector('#blocks-toolbar'), {
- target: {
- id: 'id1',
- isMultipleSelection: true,
- event: { ctrlKey: true },
- },
- });
- });
-
- it('triggers the onSubmit with the ctrlKey and multiple selected blocks and multiple errors when the selected block in not in the list of selected blocks', () => {
- config.blocks = {
- initialBlocksFocus: null,
- };
- config.settings = {
- verticalFormTabs: true,
- };
-
- const props = {
- schema: {
- fieldsets: [
- {
- fields: ['field1', 'field2'],
- id: 'fieldset1',
- title: 'Fieldset 1',
- },
- ],
- properties: {
- field1: {
- title: 'Field 1',
- description: 'Field 1 description',
- items: ['field4'],
- widget: 'textarea',
- },
- field2: { title: 'Field 2', widget: 'textarea' },
- },
- definitions: {},
- required: [],
- },
- formData: {
- blocks: {
- id1: {
- '@type': 'typeB',
- plaintext: 'Block A',
- override_toc: false,
- },
- },
- blocks_layout: {
- items: ['id1'],
- },
- },
- type: 'typeB',
- isClient: true,
- visual: true,
- title: 'Test title',
- description: 'Test description',
- error: {
- message: 'Sample error message',
- },
- onSubmit: jest.fn(),
- onCancel: jest.fn(),
- };
-
- const validateFieldsPerFieldsetMock = jest.spyOn(
- FormValidation,
- 'validateFieldsPerFieldset',
- );
- validateFieldsPerFieldsetMock.mockImplementation(() => [
- 'field1',
- 'field2',
- ]);
-
- const { container } = render(
-
-
- ,
- );
-
- fireEvent.click(container.querySelector('#blocks-toolbar'), {
- target: { value: 'id1,id2' },
- });
- fireEvent.change(container.querySelector('#blocks-toolbar'), {
- target: { value: 'test change' },
- });
- fireEvent.select(container.querySelector('#blocks-toolbar'), {
- target: {
- id: 'id3',
- isMultipleSelection: true,
- event: { ctrlKey: true },
- },
- });
- });
-});
diff --git a/src/customizations/volto/components/manage/Form/ModalForm.jsx b/src/customizations/volto/components/manage/Form/ModalForm.jsx
deleted file mode 100644
index c460cd16..00000000
--- a/src/customizations/volto/components/manage/Form/ModalForm.jsx
+++ /dev/null
@@ -1,326 +0,0 @@
-/**
- * Modal form component.
- * @module components/manage/Form/ModalForm
- */
-
-import React, { Component } from 'react';
-import PropTypes from 'prop-types';
-import { keys, map } from 'lodash';
-import {
- Button,
- Form as UiForm,
- Header,
- Menu,
- Message,
- Modal,
- Dimmer,
- Loader,
-} from 'semantic-ui-react';
-import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
-import { FormValidation } from '@plone/volto/helpers';
-import { Field, Icon } from '@plone/volto/components';
-import aheadSVG from '@plone/volto/icons/ahead.svg';
-import clearSVG from '@plone/volto/icons/clear.svg';
-
-const messages = defineMessages({
- required: {
- id: 'Required input is missing.',
- defaultMessage: 'Required input is missing.',
- },
- minLength: {
- id: 'Minimum length is {len}.',
- defaultMessage: 'Minimum length is {len}.',
- },
- uniqueItems: {
- id: 'Items must be unique.',
- defaultMessage: 'Items must be unique.',
- },
- save: {
- id: 'Save',
- defaultMessage: 'Save',
- },
- cancel: {
- id: 'Cancel',
- defaultMessage: 'Cancel',
- },
-});
-
-/**
- * Modal form container class.
- * @class ModalForm
- * @extends Component
- */
-class ModalForm extends Component {
- /**
- * Property types.
- * @property {Object} propTypes Property types.
- * @static
- */
- static propTypes = {
- schema: PropTypes.shape({
- fieldsets: PropTypes.arrayOf(
- PropTypes.shape({
- fields: PropTypes.arrayOf(PropTypes.string),
- id: PropTypes.string,
- title: PropTypes.string,
- }),
- ),
- properties: PropTypes.objectOf(PropTypes.any),
- required: PropTypes.arrayOf(PropTypes.string),
- }).isRequired,
- title: PropTypes.string.isRequired,
- formData: PropTypes.objectOf(PropTypes.any),
- submitError: PropTypes.string,
- onSubmit: PropTypes.func.isRequired,
- onCancel: PropTypes.func,
- open: PropTypes.bool,
- submitLabel: PropTypes.string,
- loading: PropTypes.bool,
- loadingMessage: PropTypes.string,
- className: PropTypes.string,
- };
-
- /**
- * Default properties.
- * @property {Object} defaultProps Default properties.
- * @static
- */
- static defaultProps = {
- submitLabel: null,
- onCancel: null,
- formData: {},
- open: true,
- loading: null,
- loadingMessage: null,
- submitError: null,
- className: null,
- dimmer: null,
- };
-
- /**
- * Constructor
- * @method constructor
- * @param {Object} props Component properties
- * @constructs ModalForm
- */
- constructor(props) {
- super(props);
- this.state = {
- currentTab: 0,
- errors: {},
- isFormPristine: true,
- formData: props.formData,
- };
- this.selectTab = this.selectTab.bind(this);
- this.onChangeField = this.onChangeField.bind(this);
- this.onBlurField = this.onBlurField.bind(this);
- this.onClickInput = this.onClickInput.bind(this);
- this.onSubmit = this.onSubmit.bind(this);
- }
-
- /**
- * Change field handler
- * @method onChangeField
- * @param {string} id Id of the field
- * @param {*} value Value of the field
- * @returns {undefined}
- */
- onChangeField(id, value) {
- this.setState({
- formData: {
- ...this.state.formData,
- [id]: value,
- },
- });
- }
-
- /**
- * If user clicks on input, the form will be not considered pristine
- * this will avoid onBlur effects without interraction with the form
- * @param {Object} e event
- */
- onClickInput(e) {
- this.setState({ isFormPristine: false });
- }
-
- /**
- * Validate fields on blur
- * @method onBlurField
- * @param {string} id Id of the field
- * @param {*} value Value of the field
- * @returns {undefined}
- */
- onBlurField(id, value) {
- if (!this.state.isFormPristine) {
- const errors = FormValidation.validateFieldsPerFieldset({
- schema: this.props.schema,
- formData: this.state.formData,
- formatMessage: this.props.intl.formatMessage,
- touchedField: { [id]: value },
- });
-
- this.setState({
- errors,
- });
- }
- }
-
- /**
- * Submit handler
- * @method onSubmit
- * @param {Object} event Event object.
- * @returns {undefined}
- */
- onSubmit(event) {
- event.preventDefault();
- const errors = FormValidation.validateFieldsPerFieldset({
- schema: this.props.schema,
- formData: this.state.formData,
- formatMessage: this.props.intl.formatMessage,
- });
-
- if (keys(errors).length > 0) {
- this.setState({
- errors,
- });
- } else {
- let setFormDataCallback = (formData) => {
- this.setState({ formData: formData, errors: {} });
- };
- this.props.onSubmit(this.state.formData, setFormDataCallback);
- }
- }
-
- /**
- * Select tab handler
- * @method selectTab
- * @param {Object} event Event object.
- * @param {number} index Selected tab index.
- * @returns {undefined}
- */
- selectTab(event, { index }) {
- this.setState({
- currentTab: index,
- });
- }
-
- /**
- * Render method.
- * @method render
- * @returns {string} Markup for the component.
- */
- render() {
- const { schema, onCancel } = this.props;
- const currentFieldset = schema.fieldsets[this.state.currentTab];
-
- const fields = map(currentFieldset.fields, (field) => ({
- ...schema.properties[field],
- id: field,
- value: this.state.formData[field],
- required: schema.required.indexOf(field) !== -1,
- onChange: this.onChangeField,
- onBlur: this.onBlurField,
- onClick: this.onClickInput,
- }));
-
- const state_errors = keys(this.state.errors).length > 0;
- return (
-
-
-
-
- {this.props.loadingMessage || (
-
- )}
-
-
-
-
-
- {state_errors ? (
-
- ) : (
- ''
- )}
- {this.props.submitError}
-
- {schema.fieldsets.length > 1 && (
-
- {map(schema.fieldsets, (item, index) => (
-
- {item.title}
-
- ))}
-
- )}
- {fields.map((field) => (
-
- ))}
-
-
-
-
-
-
- {onCancel && (
-
-
-
- )}
-
-
- );
- }
-}
-
-export default injectIntl(ModalForm);
diff --git a/src/customizations/volto/components/manage/Sharing/Sharing.jsx b/src/customizations/volto/components/manage/Sharing/Sharing.jsx
deleted file mode 100644
index 8a92094e..00000000
--- a/src/customizations/volto/components/manage/Sharing/Sharing.jsx
+++ /dev/null
@@ -1,528 +0,0 @@
-/**
- * Sharing container.
- * @module components/manage/Sharing/Sharing
- */
-import React, { Component } from 'react';
-import PropTypes from 'prop-types';
-import { Plug, Pluggable } from '@plone/volto/components/manage/Pluggable';
-import { Helmet } from '@plone/volto/helpers';
-import { connect } from 'react-redux';
-import { compose } from 'redux';
-import { Link, withRouter } from 'react-router-dom';
-import { find, isEqual, map } from 'lodash';
-import { Portal } from 'react-portal';
-import {
- Button,
- Checkbox,
- Container as SemanticContainer,
- Form,
- Icon as IconOld,
- Input,
- Segment,
- Table,
-} from 'semantic-ui-react';
-import jwtDecode from 'jwt-decode';
-import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
-
-import { updateSharing, getSharing } from '@plone/volto/actions';
-import { getBaseUrl } from '@plone/volto/helpers';
-import { Icon, Toolbar, Toast } from '@plone/volto/components';
-import { toast } from 'react-toastify';
-import config from '@plone/volto/registry';
-
-import aheadSVG from '@plone/volto/icons/ahead.svg';
-import clearSVG from '@plone/volto/icons/clear.svg';
-import backSVG from '@plone/volto/icons/back.svg';
-
-const messages = defineMessages({
- searchForUserOrGroup: {
- id: 'Search for user or group',
- defaultMessage: 'Search for user or group',
- },
- inherit: {
- id: 'Inherit permissions from higher levels',
- defaultMessage: 'Inherit permissions from higher levels',
- },
- save: {
- id: 'Save',
- defaultMessage: 'Save',
- },
- cancel: {
- id: 'Cancel',
- defaultMessage: 'Cancel',
- },
- back: {
- id: 'Back',
- defaultMessage: 'Back',
- },
- sharing: {
- id: 'Sharing',
- defaultMessage: 'Sharing',
- },
- user: {
- id: 'User',
- defaultMessage: 'User',
- },
- group: {
- id: 'Group',
- defaultMessage: 'Group',
- },
- globalRole: {
- id: 'Global role',
- defaultMessage: 'Global role',
- },
- inheritedValue: {
- id: 'Inherited value',
- defaultMessage: 'Inherited value',
- },
- permissionsUpdated: {
- id: 'Permissions updated',
- defaultMessage: 'Permissions updated',
- },
- permissionsUpdatedSuccessfully: {
- id: 'Permissions have been updated successfully',
- defaultMessage: 'Permissions have been updated successfully',
- },
-});
-
-/**
- * SharingComponent class.
- * @class SharingComponent
- * @extends Component
- */
-class SharingComponent extends Component {
- /**
- * Property types.
- * @property {Object} propTypes Property types.
- * @static
- */
- static propTypes = {
- updateSharing: PropTypes.func.isRequired,
- getSharing: PropTypes.func.isRequired,
- updateRequest: PropTypes.shape({
- loading: PropTypes.bool,
- loaded: PropTypes.bool,
- }).isRequired,
- pathname: PropTypes.string.isRequired,
- entries: PropTypes.arrayOf(
- PropTypes.shape({
- id: PropTypes.string,
- login: PropTypes.string,
- roles: PropTypes.object,
- title: PropTypes.string,
- type: PropTypes.string,
- }),
- ).isRequired,
- available_roles: PropTypes.arrayOf(PropTypes.object).isRequired,
- inherit: PropTypes.bool,
- title: PropTypes.string.isRequired,
- login: PropTypes.string,
- };
-
- /**
- * Default properties
- * @property {Object} defaultProps Default properties.
- * @static
- */
- static defaultProps = {
- inherit: null,
- login: '',
- };
-
- /**
- * Constructor
- * @method constructor
- * @param {Object} props Component properties
- * @constructs Sharing
- */
- constructor(props) {
- super(props);
- this.onCancel = this.onCancel.bind(this);
- this.onChange = this.onChange.bind(this);
- this.onChangeSearch = this.onChangeSearch.bind(this);
- this.onSearch = this.onSearch.bind(this);
- this.onSubmit = this.onSubmit.bind(this);
- this.onToggleInherit = this.onToggleInherit.bind(this);
- this.state = {
- search: '',
- isLoading: false,
- inherit: props.inherit,
- entries: props.entries,
- isClient: false,
- };
- }
-
- /**
- * Component did mount
- * @method componentDidMount
- * @returns {undefined}
- */
- componentDidMount() {
- this.props.getSharing(getBaseUrl(this.props.pathname), this.state.search);
- this.setState({ isClient: true });
- }
-
- /**
- * Component will receive props
- * @method componentWillReceiveProps
- * @param {Object} nextProps Next properties
- * @returns {undefined}
- */
- UNSAFE_componentWillReceiveProps(nextProps) {
- if (this.props.updateRequest.loading && nextProps.updateRequest.loaded) {
- this.props.getSharing(getBaseUrl(this.props.pathname), this.state.search);
- toast.success(
-
,
- );
- }
- this.setState({
- inherit:
- this.props.inherit === null ? nextProps.inherit : this.state.inherit,
- entries: map(nextProps.entries, (entry) => {
- const values = find(this.state.entries, { id: entry.id });
- return {
- ...entry,
- roles: values ? values.roles : entry.roles,
- };
- }),
- });
- }
-
- /**
- * Submit handler
- * @method onSubmit
- * @param {object} event Event object.
- * @returns {undefined}
- */
- onSubmit(event) {
- const data = { entries: [] };
- event.preventDefault();
- if (this.props.inherit !== this.state.inherit) {
- data.inherit = this.state.inherit;
- }
- for (let i = 0; i < this.props.entries.length; i += 1) {
- if (!isEqual(this.props.entries[i].roles, this.state.entries[i].roles)) {
- data.entries.push({
- id: this.state.entries[i].id,
- type: this.state.entries[i].type,
- roles: this.state.entries[i].roles,
- });
- }
- }
- this.props.updateSharing(getBaseUrl(this.props.pathname), data);
- }
-
- /**
- * Search handler
- * @method onSearch
- * @param {object} event Event object.
- * @returns {undefined}
- */
- onSearch(event) {
- event.preventDefault();
- this.setState({ isLoading: true });
- this.props
- .getSharing(getBaseUrl(this.props.pathname), this.state.search)
- .then(() => {
- this.setState({ isLoading: false });
- })
- .catch((error) => {
- this.setState({ isLoading: false });
- // eslint-disable-next-line no-console
- console.error('Error searching users or groups', error);
- });
- }
-
- /**
- * On change search handler
- * @method onChangeSearch
- * @param {object} event Event object.
- * @returns {undefined}
- */
- onChangeSearch(event) {
- this.setState({
- search: event.target.value,
- });
- }
-
- /**
- * On toggle inherit handler
- * @method onToggleInherit
- * @returns {undefined}
- */
- onToggleInherit() {
- this.setState((state) => ({
- inherit: !state.inherit,
- }));
- }
-
- /**
- * On change handler
- * @method onChange
- * @param {object} event Event object
- * @param {string} value Entry value
- * @returns {undefined}
- */
- onChange(event, { value }) {
- const [principal, role] = value.split(':');
- this.setState({
- entries: map(this.state.entries, (entry) => ({
- ...entry,
- roles:
- entry.id === principal
- ? {
- ...entry.roles,
- [role]: !entry.roles[role],
- }
- : entry.roles,
- })),
- });
- }
-
- /**
- * Cancel handler
- * @method onCancel
- * @returns {undefined}
- */
- onCancel() {
- this.props.history.push(getBaseUrl(this.props.pathname));
- }
-
- /**
- * Render method.
- * @method render
- * @returns {string} Markup for the component.
- */
- render() {
- const Container =
- config.getComponent({ name: 'Container' }).component || SemanticContainer;
-
- return (
-
-
-
-
-
-
- {this.props.title} }}
- />
-
-
-
-
-
-
-
-
- {({ isLoading }) => {
- return (
-
-
-
-
-
-
- );
- }}
-
-
-
-
-
- {this.state.isClient && (
-
-
-
-
- }
- />
-
- )}
-
- );
- }
-}
-
-export default compose(
- withRouter,
- injectIntl,
- connect(
- (state, props) => ({
- entries: state.sharing.data.entries,
- inherit: state.sharing.data.inherit,
- available_roles: state.sharing.data.available_roles,
- updateRequest: state.sharing.update,
- pathname: props.location.pathname,
- title: state.content.data.title,
- login: state.userSession.token
- ? jwtDecode(state.userSession.token).sub
- : '',
- }),
- { updateSharing, getSharing },
- ),
-)(SharingComponent);
diff --git a/src/customizations/volto/components/manage/Sharing/Sharing.test.jsx b/src/customizations/volto/components/manage/Sharing/Sharing.test.jsx
deleted file mode 100644
index 9e0cafbf..00000000
--- a/src/customizations/volto/components/manage/Sharing/Sharing.test.jsx
+++ /dev/null
@@ -1,72 +0,0 @@
-import React from 'react';
-import renderer from 'react-test-renderer';
-import configureStore from 'redux-mock-store';
-import { Provider } from 'react-intl-redux';
-import jwt from 'jsonwebtoken';
-import { MemoryRouter } from 'react-router-dom';
-import { PluggablesProvider } from '@plone/volto/components/manage/Pluggable';
-
-import Sharing from './Sharing';
-
-const mockStore = configureStore();
-
-jest.mock('react-portal', () => ({
- Portal: jest.fn(() =>
),
-}));
-
-describe('Sharing', () => {
- it('renders a sharing component', () => {
- const store = mockStore({
- userSession: {
- token: jwt.sign({ sub: 'john-doe' }, 'secret'),
- },
- sharing: {
- data: {
- entries: [
- {
- id: 'john-doe',
- disabled: false,
- login: 'john-doe',
- roles: {
- Contributer: true,
- },
- title: 'John Doe',
- type: 'user',
- },
- ],
- inherit: true,
- available_roles: [
- {
- id: 'Contributor',
- title: 'Can add',
- },
- ],
- },
- update: {
- loading: false,
- loaded: true,
- },
- },
- content: {
- data: {
- title: 'Blog',
- },
- },
- intl: {
- locale: 'en',
- messages: {},
- },
- });
- const component = renderer.create(
-
-
-
-
-
-
- ,
- );
- const json = component.toJSON();
- expect(json).toMatchSnapshot();
- });
-});
diff --git a/src/customizations/volto/components/manage/Sharing/__snapshots__/Sharing.test.jsx.snap b/src/customizations/volto/components/manage/Sharing/__snapshots__/Sharing.test.jsx.snap
deleted file mode 100644
index d1a1bcd4..00000000
--- a/src/customizations/volto/components/manage/Sharing/__snapshots__/Sharing.test.jsx.snap
+++ /dev/null
@@ -1,216 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`Sharing renders a sharing component 1`] = `
-
-
-
- Sharing for
-
- Blog
-
-
-
- You can control who can view and edit your item using the list below.
-
-
-
-
-
-
-`;
diff --git a/src/customizations/volto/components/manage/Sidebar/SidebarPopup copy.jsx b/src/customizations/volto/components/manage/Sidebar/SidebarPopup copy.jsx
new file mode 100644
index 00000000..9142544e
--- /dev/null
+++ b/src/customizations/volto/components/manage/Sidebar/SidebarPopup copy.jsx
@@ -0,0 +1,82 @@
+// Check this https://github.com/plone/volto/pull/5520
+import React from 'react';
+import { Portal } from 'react-portal';
+import { CSSTransition } from 'react-transition-group';
+import PropTypes from 'prop-types';
+import { doesNodeContainClick } from 'semantic-ui-react/dist/commonjs/lib';
+
+const DEFAULT_TIMEOUT = 500;
+
+const SidebarPopup = (props) => {
+ const { children, open, onClose, overlay } = props;
+
+ const asideElement = React.useRef();
+
+ const handleClickOutside = (e) => {
+ if (asideElement && doesNodeContainClick(asideElement.current, e)) return;
+ onClose();
+ };
+
+ React.useEffect(() => {
+ document.addEventListener('mousedown', handleClickOutside, false);
+ return () => {
+ document.removeEventListener('mousedown', handleClickOutside, false);
+ };
+ });
+
+ return (
+ <>
+ {overlay && (
+
+
+
+
+
+ )}
+
+
+ {
+ e.stopPropagation();
+ }}
+ onKeyDown={(e) => {
+ e.stopPropagation();
+ }}
+ ref={asideElement}
+ key="sidebarpopup"
+ className="sidebar-container"
+ style={{ overflowY: 'auto' }}
+ >
+ {children}
+
+
+
+ >
+ );
+};
+
+SidebarPopup.propTypes = {
+ open: PropTypes.bool,
+ onClose: PropTypes.func,
+ overlay: PropTypes.bool,
+};
+
+SidebarPopup.defaultProps = {
+ open: false,
+ onClose: () => {},
+ overlay: false,
+};
+
+export default SidebarPopup;
diff --git a/src/customizations/volto/components/manage/Toolbar/More.jsx b/src/customizations/volto/components/manage/Toolbar/More.jsx
new file mode 100644
index 00000000..afe86a7c
--- /dev/null
+++ b/src/customizations/volto/components/manage/Toolbar/More.jsx
@@ -0,0 +1,541 @@
+/**
+ * More component.
+ * @module components/manage/Toolbar/More
+ */
+
+import React, { Component } from 'react';
+import { defineMessages, injectIntl } from 'react-intl';
+import PropTypes from 'prop-types';
+import { connect } from 'react-redux';
+import { compose } from 'redux';
+import { Link, withRouter } from 'react-router-dom';
+import { find } from 'lodash';
+import { toast } from 'react-toastify';
+import { Toast } from '@plone/volto/components';
+import { Pluggable, Plug } from '@plone/volto/components/manage/Pluggable';
+import {
+ FormattedDate,
+ Icon,
+ Display,
+ Workflow,
+} from '@plone/volto/components';
+import {
+ applyWorkingCopy,
+ createWorkingCopy,
+ removeWorkingCopy,
+} from '@plone/volto/actions';
+import { flattenToAppURL, getBaseUrl } from '@plone/volto/helpers';
+import config from '@plone/volto/registry';
+
+import rightArrowSVG from '@plone/volto/icons/right-key.svg';
+import userSVG from '@plone/volto/icons/user.svg';
+import applySVG from '@plone/volto/icons/ready.svg';
+import removeSVG from '@plone/volto/icons/circle-dismiss.svg';
+
+const messages = defineMessages({
+ personalTools: {
+ id: 'Personal tools',
+ defaultMessage: 'Personal tools',
+ },
+ history: {
+ id: 'History',
+ defaultMessage: 'History',
+ },
+ sharing: {
+ id: 'Sharing',
+ defaultMessage: 'Sharing',
+ },
+ rules: {
+ id: 'Rules',
+ defaultMessage: 'Rules',
+ },
+ aliases: {
+ id: 'URL Management',
+ defaultMessage: 'URL Management',
+ },
+ ManageTranslations: {
+ id: 'Manage Translations',
+ defaultMessage: 'Manage Translations',
+ },
+ manageContent: {
+ id: 'Manage content…',
+ defaultMessage: 'Manage content…',
+ },
+ CreateWorkingCopy: {
+ id: 'Create working copy',
+ defaultMessage: 'Create working copy',
+ },
+ applyWorkingCopy: {
+ id: 'Apply working copy',
+ defaultMessage: 'Apply working copy',
+ },
+ removeWorkingCopy: {
+ id: 'Remove working copy',
+ defaultMessage: 'Remove working copy',
+ },
+ viewWorkingCopy: {
+ id: 'View working copy',
+ defaultMessage: 'View working copy',
+ },
+ workingAppliedTitle: {
+ id: 'Changes applied.',
+ defaultMessage: 'Changes applied',
+ },
+ workingCopyAppliedBy: {
+ id: 'Made by {creator} on {date}. This is not a working copy anymore, but the main content.',
+ defaultMessage:
+ 'Made by {creator} on {date}. This is not a working copy anymore, but the main content.',
+ },
+ workingCopyRemovedTitle: {
+ id: 'The working copy was discarded',
+ defaultMessage: 'The working copy was discarded',
+ },
+ Unauthorized: {
+ id: 'Unauthorized',
+ defaultMessage: 'Unauthorized',
+ },
+ workingCopyErrorUnauthorized: {
+ id: 'workingCopyErrorUnauthorized',
+ defaultMessage: 'You are not authorized to perform this operation.',
+ },
+ Error: {
+ id: 'Error',
+ defaultMessage: 'Error',
+ },
+ workingCopyGenericError: {
+ id: 'workingCopyGenericError',
+ defaultMessage: 'An error occurred while performing this operation.',
+ },
+});
+
+/**
+ * More container class.
+ * @class More
+ * @extends Component
+ */
+class More extends Component {
+ static propTypes = {
+ actions: PropTypes.shape({
+ object: PropTypes.arrayOf(PropTypes.object),
+ object_buttons: PropTypes.arrayOf(PropTypes.object),
+ user: PropTypes.arrayOf(PropTypes.object),
+ }),
+ pathname: PropTypes.string.isRequired,
+ content: PropTypes.shape({
+ title: PropTypes.string,
+ '@type': PropTypes.string,
+ is_folderish: PropTypes.bool,
+ review_state: PropTypes.string,
+ }),
+ loadComponent: PropTypes.func.isRequired,
+ closeMenu: PropTypes.func.isRequired,
+ };
+
+ /**
+ * Default properties.
+ * @property {Object} defaultProps Default properties.
+ * @static
+ */
+ static defaultProps = {
+ actions: null,
+ content: null,
+ };
+ state = {
+ openManageTranslations: false,
+ pushed: false,
+ };
+
+ push = (selector) => {
+ this.setState(() => ({
+ pushed: true,
+ }));
+ this.props.loadComponent(selector);
+ document.removeEventListener('mousedown', this.handleClickOutside, false);
+ };
+
+ componentDidUpdate(prevProps, prevState) {
+ let erroredAction = '';
+ if (
+ prevProps.workingCopy.apply.loading &&
+ this.props.workingCopy.apply.error
+ ) {
+ erroredAction = 'apply';
+ } else if (
+ prevProps.workingCopy.create.loading &&
+ this.props.workingCopy.create.error
+ ) {
+ erroredAction = 'create';
+ } else if (
+ prevProps.workingCopy.remove.loading &&
+ this.props.workingCopy.remove.error
+ ) {
+ erroredAction = 'remove';
+ }
+
+ if (erroredAction) {
+ const errorStatus = this.props.workingCopy[erroredAction].error.status;
+ if (errorStatus === 401 || errorStatus === 403) {
+ toast.error(
+
,
+ {
+ toastId: 'workingCopyErrorUnauthorized',
+ autoClose: 10000,
+ },
+ );
+ } else {
+ toast.error(
+
,
+ {
+ toastId: 'workingCopyGenericError',
+ autoClose: 10000,
+ },
+ );
+ }
+ }
+ }
+
+ /**
+ * Render method.
+ * @method render
+ * @returns {string} Markup for the component.
+ */
+ render() {
+ const path = getBaseUrl(this.props.pathname);
+ const editAction = find(this.props.actions.object, { id: 'edit' });
+ const historyAction = find(this.props.actions.object, { id: 'history' });
+ const sharingAction = find(this.props.actions.object, {
+ id: 'local_roles',
+ });
+
+ const rulesAction = find(this.props.actions.object, {
+ id: 'contentrules',
+ });
+
+ const aliasesAction = find(this.props.actions.object_buttons, {
+ id: 'redirection',
+ });
+ const { content, intl } = this.props;
+
+ const dateOptions = {
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric',
+ };
+
+ return (
+
+
+ {this.props.content.title}
+ this.push('personalTools')}
+ tabIndex={0}
+ >
+
+
+
+
+
+
+
+ {this.props.content['@type'] !== 'Plone Site' && (
+ // Plone Site does not have workflow
+
+
+
+ )}
+
+
+ {this.props.content['@type'] !== 'Plone Site' && (
+ // Plone Site does not have view (yet)
+
+ {editAction && }
+
+ )}
+
+
+ {this.props.content['@type'] !== 'Plone Site' && (
+ // Plone Site does not have history (yet)
+
+
+
+
+ {historyAction?.title ||
+ this.props.intl.formatMessage(messages.history)}
+
+
+
+
+
+
+ )}
+
+
+ {sharingAction && (
+
+
+ {this.props.intl.formatMessage(messages.sharing)}
+
+
+
+ )}
+
+
+ {aliasesAction && (
+
+
+ {this.props.intl.formatMessage(messages.aliases)}
+
+
+
+ )}
+
+
+ {rulesAction && (
+
+
+ {this.props.intl.formatMessage(messages.rules)}
+
+
+
+ )}
+
+
+
+
+ {(pluggables) => (
+ <>
+ {pluggables.length > 0 && (
+ <>
+
+
+ {this.props.intl.formatMessage(messages.manageContent)}
+
+
+
+
+ {pluggables.map((p) => (
+ {p()}
+ ))}
+
+
+ >
+ )}
+ >
+ )}
+
+ {config.settings.hasWorkingCopySupport &&
+ this.props.content['@type'] !== 'Plone Site' &&
+ this.props.content['@type'] !== 'ims_indicator' && (
+ <>
+ {!this.props.content.working_copy && (
+
+
+ {
+ this.props.createWorkingCopy(path).then((response) => {
+ this.props.history.push(
+ flattenToAppURL(response['@id']),
+ );
+ this.props.closeMenu();
+ });
+ }}
+ >
+ {this.props.intl.formatMessage(
+ messages.CreateWorkingCopy,
+ )}
+
+
+
+
+
+ )}
+ {this.props.content.working_copy &&
+ this.props.content.working_copy_of && (
+
+
+ {
+ this.props.applyWorkingCopy(path).then((response) => {
+ this.props.history.push(
+ flattenToAppURL(
+ this.props.content.working_copy_of['@id'],
+ ),
+ );
+ this.props.closeMenu();
+ toast.info(
+
+ ),
+ },
+ )}
+ />,
+ {
+ toastId: 'workingcopyapplyinfo',
+ autoClose: 10000,
+ },
+ );
+ });
+ }}
+ >
+ {this.props.intl.formatMessage(
+ messages.applyWorkingCopy,
+ )}
+
+
+
+
+
+ {
+ this.props
+ .removeWorkingCopy(path)
+ .then((response) => {
+ this.props.history.push(
+ flattenToAppURL(
+ this.props.content.working_copy_of['@id'],
+ ),
+ );
+ this.props.closeMenu();
+ toast.info(
+ ,
+ {
+ toastId: 'workingcopyremovednotice',
+ autoClose: 10000,
+ },
+ );
+ });
+ }}
+ >
+ {this.props.intl.formatMessage(
+ messages.removeWorkingCopy,
+ )}
+
+
+
+
+
+ )}
+ {this.props.content.working_copy &&
+ !this.props.content.working_copy_of && (
+
+
+ this.props.closeMenu()}
+ >
+ {this.props.intl.formatMessage(
+ messages.viewWorkingCopy,
+ )}
+
+
+
+
+ )}
+ >
+ )}
+ {editAction &&
+ config.settings.isMultilingual &&
+ this.props.content['@type'] !== 'ims_indicator' && (
+
+
+
+ {this.props.intl.formatMessage(messages.ManageTranslations)}
+
+
+
+
+
+ )}
+
+ );
+ }
+}
+
+export default compose(
+ injectIntl,
+ withRouter,
+ connect(
+ (state, props) => ({
+ actions: state.actions.actions,
+ pathname: props.pathname,
+ content: state.content.data,
+ lang: state.intl.locale,
+ workingCopy: state.workingCopy,
+ }),
+ { applyWorkingCopy, createWorkingCopy, removeWorkingCopy },
+ ),
+)(More);
diff --git a/src/customizations/volto/components/manage/UniversalLink/UniversalLink.jsx b/src/customizations/volto/components/manage/UniversalLink/UniversalLink.jsx
index e764abeb..34b50b76 100644
--- a/src/customizations/volto/components/manage/UniversalLink/UniversalLink.jsx
+++ b/src/customizations/volto/components/manage/UniversalLink/UniversalLink.jsx
@@ -74,7 +74,9 @@ const UniversalLink = ({
const checkedURL = URLUtils.checkAndNormalizeUrl(url);
- url = checkedURL.url;
+ // we can receive an item with a linkWithHash property set from ObjectBrowserWidget
+ // if so, we use that instead of the url prop
+ url = (item && item['linkWithHash']) || checkedURL.url;
let tag = (
{
if (flattenToAppURL(_item['@id']) === flattenToAppURL(item['@id'])) {
exists = true;
@@ -181,6 +183,7 @@ export class ObjectBrowserWidgetComponent extends Component {
...this.props.selectedItemAttrs,
// Add the required attributes for the widget to work
'@id',
+ 'linkWithHash', // add linkWithHash to the allowed attributes
'title',
];
resultantItem = Object.keys(item)
@@ -222,16 +225,28 @@ export class ObjectBrowserWidgetComponent extends Component {
}
};
+ /**
+ * Splits a URL into its link and hash components.
+ * @param {string} url - The URL to split.
+ * @returns {[string, string]} - An array containing the link and hash components of the URL.
+ */
+ getHashAndLinkFromUrl = (url) => {
+ return url.split('#');
+ };
+
onSubmitManualLink = () => {
if (this.validateManualLink(this.state.manualLinkInput)) {
if (isInternalURL(this.state.manualLinkInput)) {
- const link = this.state.manualLinkInput;
+ const [link, hash] = this.getHashAndLinkFromUrl(
+ this.state.manualLinkInput,
+ );
+ const relative_link = flattenToAppURL(link);
// convert it into an internal on if possible
this.props
.searchContent(
'/',
{
- 'path.query': flattenToAppURL(this.state.manualLinkInput),
+ 'path.query': relative_link,
'path.depth': '0',
sort_on: 'getObjPositionInParent',
metadata_fields: '_all',
@@ -241,6 +256,10 @@ export class ObjectBrowserWidgetComponent extends Component {
)
.then((resp) => {
if (resp.items?.length > 0) {
+ // if there is a hash within the url, add it to the item as linkWithHash
+ if (hash) {
+ resp.items[0]['linkWithHash'] = `${relative_link}#${hash}`;
+ }
this.onChange(resp.items[0]);
} else {
this.props.onChange(this.props.id, [
@@ -312,15 +331,8 @@ export class ObjectBrowserWidgetComponent extends Component {
* @returns {string} Markup for the component.
*/
render() {
- const {
- id,
- description,
- fieldSet,
- value,
- mode,
- onChange,
- isDisabled,
- } = this.props;
+ const { id, description, fieldSet, value, mode, onChange, isDisabled } =
+ this.props;
let items = compact(!isArray(value) && value ? [value] : value || []);
@@ -379,7 +391,6 @@ export class ObjectBrowserWidgetComponent extends Component {
{
e.stopPropagation();
@@ -390,7 +401,6 @@ export class ObjectBrowserWidgetComponent extends Component {
{
diff --git a/src/customizations/volto/components/manage/Widgets/ObjectBrowserWidget.test.jsx b/src/customizations/volto/components/manage/Widgets/ObjectBrowserWidget.test.jsx
deleted file mode 100644
index 0a8d1a24..00000000
--- a/src/customizations/volto/components/manage/Widgets/ObjectBrowserWidget.test.jsx
+++ /dev/null
@@ -1,193 +0,0 @@
-import React from 'react';
-import { render, fireEvent } from '@testing-library/react';
-import '@testing-library/jest-dom/extend-expect';
-import { Provider } from 'react-intl-redux';
-import configureStore from 'redux-mock-store';
-import { Router } from 'react-router-dom';
-import { createMemoryHistory } from 'history';
-import ObjectBrowserWidgetComponent from './ObjectBrowserWidget';
-import '@testing-library/jest-dom/extend-expect';
-
-const mockStore = configureStore();
-let store;
-let history;
-
-describe('ObjectBrowserWidgetComponent', () => {
- beforeEach(() => {
- store = mockStore({
- search: {
- subrequests: {
- 'testBlock-multiple': {},
- 'testBlock-link': {},
- },
- },
- intl: {
- locale: 'en',
- messages: {},
- },
- });
- history = createMemoryHistory();
- });
-
- it('renders without crashing', () => {
- const { container } = render(
-
-
- {}}
- openObjectBrowser={() => {}}
- />
-
- ,
- );
-
- expect(container).toBeTruthy();
- });
-
- it('renders without crashing with values, mode different than multiple, and description', () => {
- const { container } = render(
-
-
- {}}
- openObjectBrowser={() => {}}
- />
-
- ,
- );
-
- expect(container).toBeTruthy();
- });
-
- it('renders without crashing with values, mode different than multiple and triggers the cancel function', () => {
- const { container, getByPlaceholderText } = render(
-
-
- {}}
- openObjectBrowser={() => {}}
- allowExternals={true}
- placeholder="My placeholder"
- />
-
- ,
- );
-
- fireEvent.change(getByPlaceholderText('My placeholder'), {
- target: { value: 'http://localhost:8080/Plone/test' },
- });
-
- expect(container.querySelector('button.cancel')).toBeInTheDocument();
- fireEvent.click(container.querySelector('button.cancel'));
-
- expect(container.querySelector('button.action')).toBeInTheDocument();
- fireEvent.click(container.querySelector('button.action'));
- });
-
- it('renders without crashing with values, mode different than multiple with placeholder and triggers the cancel function', () => {
- const { container, getByPlaceholderText } = render(
-
-
- {}}
- openObjectBrowser={() => {}}
- allowExternals={true}
- />
-
- ,
- );
-
- expect(getByPlaceholderText('No items selected')).toBeInTheDocument();
- fireEvent.change(getByPlaceholderText('No items selected'), {
- target: { value: 'test' },
- });
-
- expect(container.querySelector('button.cancel')).toBeInTheDocument();
- fireEvent.click(container.querySelector('button.cancel'));
-
- expect(container.querySelector('button.action')).toBeInTheDocument();
-
- expect(container).toBeTruthy();
- });
-
- it('renders without crashing with values, mode different than multiple and triggers for keydown, change and submit', () => {
- const { container, getByPlaceholderText } = render(
-
-
- {}}
- openObjectBrowser={() => {}}
- allowExternals={true}
- />
-
- ,
- );
-
- expect(getByPlaceholderText('No items selected')).toBeInTheDocument();
- fireEvent.keyDown(getByPlaceholderText('No items selected'), {
- key: 'Enter',
- code: 'Enter',
- charCode: 13,
- });
- fireEvent.keyDown(getByPlaceholderText('No items selected'), {
- key: 'Escape',
- code: 'Escape',
- charCode: 27,
- });
- fireEvent.keyDown(getByPlaceholderText('No items selected'), {
- key: 'A',
- code: 'KeyA',
- });
-
- fireEvent.change(getByPlaceholderText('No items selected'), {
- target: { value: 'http://localhost:3000/Plone/test' },
- });
-
- expect(container.querySelector('button.primary')).toBeInTheDocument();
- fireEvent.click(container.querySelector('button.primary'));
-
- fireEvent.click(container.querySelector('button.action'));
-
- expect(container).toBeTruthy();
- });
-
- it('renders without crashing with values, mode different than multiple and triggers for click on the Popup', () => {
- const { container } = render(
-
-
- {}}
- openObjectBrowser={() => {}}
- />
-
- ,
- );
-
- expect(container.querySelector('.icon.right')).toBeInTheDocument();
- fireEvent.click(container.querySelector('.icon.right'));
- expect(container).toBeTruthy();
- });
-});
diff --git a/src/customizations/volto/components/manage/Widgets/README.md b/src/customizations/volto/components/manage/Widgets/README.md
new file mode 100644
index 00000000..b52d0898
--- /dev/null
+++ b/src/customizations/volto/components/manage/Widgets/README.md
@@ -0,0 +1 @@
+Customized ObjectBrowserWidget to preserve anchor links in the manually pasted internal URL.
diff --git a/src/customizations/volto/components/manage/Workflow/README.txt b/src/customizations/volto/components/manage/Workflow/README.txt
new file mode 100644
index 00000000..869e377d
--- /dev/null
+++ b/src/customizations/volto/components/manage/Workflow/README.txt
@@ -0,0 +1 @@
+Workflow.jsx - copied from @plone/volto 16.22.2 - refs #256563
diff --git a/src/customizations/volto/components/manage/Workflow/Workflow.jsx b/src/customizations/volto/components/manage/Workflow/Workflow.jsx
new file mode 100644
index 00000000..f743fce9
--- /dev/null
+++ b/src/customizations/volto/components/manage/Workflow/Workflow.jsx
@@ -0,0 +1,324 @@
+import React, { useEffect } from 'react';
+import PropTypes from 'prop-types';
+import { compose } from 'redux';
+import { useDispatch, useSelector, shallowEqual } from 'react-redux';
+import { uniqBy } from 'lodash';
+import { toast } from 'react-toastify';
+import { defineMessages, useIntl } from 'react-intl';
+import { useHistory } from 'react-router-dom';
+import { Icon, Toast } from '@plone/volto/components';
+import { FormFieldWrapper } from '@plone/volto/components';
+import {
+ flattenToAppURL,
+ getWorkflowOptions,
+ getCurrentStateMapping,
+} from '@plone/volto/helpers';
+import { injectLazyLibs } from '@plone/volto/helpers/Loadable/Loadable';
+
+import {
+ getContent,
+ getWorkflow,
+ transitionWorkflow,
+} from '@plone/volto/actions';
+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({
+ messageUpdated: {
+ id: 'Workflow updated.',
+ defaultMessage: 'Workflow updated.',
+ },
+ messageNoWorkflow: {
+ id: 'No workflow',
+ defaultMessage: 'No workflow',
+ },
+ notAllowedToUpdateWorkflow: {
+ id: 'notAllowedToUpdateWorkflow',
+ defaultMessage: 'Please fill out all the required fields',
+ },
+ state: {
+ id: 'State',
+ defaultMessage: 'State',
+ },
+});
+
+const SingleValue = injectLazyLibs('reactSelect')(({ children, ...props }) => {
+ const stateDecorator = {
+ marginRight: '10px',
+ display: 'inline-block',
+ backgroundColor: props.selectProps.value.color || null,
+ content: ' ',
+ height: '10px',
+ width: '10px',
+ borderRadius: '50%',
+ };
+ const { SingleValue } = props.reactSelect.components;
+ return (
+
+
+ {children}
+
+ );
+});
+
+const Option = injectLazyLibs('reactSelect')((props) => {
+ const stateDecorator = {
+ marginRight: '10px',
+ display: 'inline-block',
+ backgroundColor:
+ props.selectProps.value.value === props.data.value
+ ? props.selectProps.value.color
+ : null,
+ content: ' ',
+ height: '10px',
+ width: '10px',
+ borderRadius: '50%',
+ border:
+ props.selectProps.value.value !== props.data.value
+ ? `1px solid ${props.data.color}`
+ : null,
+ };
+
+ const { Option } = props['reactSelect'].components;
+ return (
+
+
+ {props.label}
+ {props.isFocused && !props.isSelected && (
+
+ )}
+ {props.isSelected && }
+
+ );
+});
+
+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',
+ }),
+ 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',
+ },
+ }),
+};
+
+function useWorkflow() {
+ const history = useSelector((state) => state.workflow.history, shallowEqual);
+ const transitions = useSelector(
+ (state) => state.workflow.transitions,
+ shallowEqual,
+ );
+ const editingProgressSteps = useSelector((state) =>
+ state?.editingProgress?.editing?.loaded === true
+ ? state?.editingProgress?.result?.steps
+ : [],
+ );
+ const workflowLoaded = useSelector((state) => state.workflow.get?.loaded);
+ const loaded = useSelector((state) => state.workflow.transition.loaded);
+ const currentStateValue = useSelector(
+ (state) => getCurrentStateMapping(state.workflow.currentState),
+ shallowEqual,
+ );
+
+ return {
+ loaded,
+ history,
+ transitions,
+ currentStateValue,
+ workflowLoaded,
+ editingProgressSteps,
+ };
+}
+
+const filter_remaining_steps = (values, key) => {
+ return values.filter((value) => {
+ const is_not_ready = !value.is_ready;
+
+ if (!is_not_ready) {
+ return false;
+ }
+
+ const states = value.states;
+ const required_for_all = states?.indexOf('all') !== -1;
+
+ return (
+ (is_not_ready && required_for_all) ||
+ (is_not_ready && states?.indexOf(key) !== -1)
+ );
+ });
+};
+
+const Workflow = (props) => {
+ const intl = useIntl();
+ const history = useHistory();
+ const dispatch = useDispatch();
+ const {
+ loaded,
+ currentStateValue,
+ transitions,
+ workflowLoaded,
+ editingProgressSteps,
+ } = useWorkflow();
+ const content = useSelector((state) => state.content?.data, shallowEqual);
+ const [selectedOption, setSelectedOption] = React.useState(currentStateValue);
+ const { pathname } = props;
+
+ useEffect(() => {
+ dispatch(getWorkflow(pathname));
+ dispatch(getContent(pathname));
+ }, [dispatch, pathname, loaded]);
+
+ const transition = (selectedOption) => {
+ if (
+ filter_remaining_steps(
+ editingProgressSteps,
+ props?.content?.review_state || '',
+ ).length === 0
+ ) {
+ dispatch(transitionWorkflow(flattenToAppURL(selectedOption.url)));
+ setSelectedOption(selectedOption);
+ toast.success(
+ ,
+ );
+ } else {
+ toast.error(
+ ,
+ );
+ }
+ };
+
+ useEffect(() => {
+ if (selectedOption?.value === 'createNewVersion' && workflowLoaded) {
+ history.push(`${pathname}.1`);
+ }
+ }, [history, pathname, selectedOption?.value, workflowLoaded]);
+
+ const { Placeholder } = props.reactSelect.components;
+ const Select = props.reactSelect.default;
+
+ const filterd_transitions = transitions.filter((transition) => {
+ if (
+ transition?.['@id']?.endsWith('markForDeletion') &&
+ props.content?.review_state === 'published'
+ ) {
+ return false;
+ }
+ return true;
+ });
+
+ return (
+
+
+ getWorkflowOptions(transition),
+ ),
+ 'label',
+ ).concat(currentStateValue)}
+ styles={customSelectStyles}
+ theme={selectTheme}
+ components={{
+ DropdownIndicator,
+ Placeholder,
+ Option,
+ SingleValue,
+ }}
+ onChange={transition}
+ value={
+ content.review_state
+ ? currentStateValue
+ : {
+ label: intl.formatMessage(messages.messageNoWorkflow),
+ value: 'noworkflow',
+ }
+ }
+ isSearchable={false}
+ />
+
+ );
+};
+
+Workflow.propTypes = {
+ pathname: PropTypes.string.isRequired,
+};
+
+export default compose(injectLazyLibs(['reactSelect']))(Workflow);
diff --git a/src/customizations/volto/components/manage/Workflow/Workflow.test.jsx b/src/customizations/volto/components/manage/Workflow/Workflow.test.jsx
new file mode 100644
index 00000000..8a48086c
--- /dev/null
+++ b/src/customizations/volto/components/manage/Workflow/Workflow.test.jsx
@@ -0,0 +1,81 @@
+import React from 'react';
+import configureStore from 'redux-mock-store';
+import { Provider } from 'react-intl-redux';
+import { waitFor, render, screen } from '@testing-library/react';
+import { MemoryRouter } from 'react-router-dom';
+import config from '@plone/volto/registry';
+
+import Workflow from './Workflow';
+
+const mockStore = configureStore();
+
+jest.mock('@plone/volto/helpers/Loadable/Loadable');
+beforeAll(
+ async () =>
+ await require('@plone/volto/helpers/Loadable/Loadable').__setLoadables(),
+);
+
+beforeEach(() => {
+ config.settings.workflowMapping = {
+ published: { value: 'published', color: '#007bc1' },
+ publish: { value: 'publish', color: '#007bc1' },
+ private: { value: 'private', color: '#ed4033' },
+ pending: { value: 'pending', color: '#f6a808' },
+ send_back: { value: 'private', color: '#ed4033' },
+ retract: { value: 'private', color: '#ed4033' },
+ submit: { value: 'review', color: '#f4e037' },
+ };
+});
+
+describe('Workflow', () => {
+ it('renders an empty workflow component', async () => {
+ const store = mockStore({
+ workflow: {
+ currentState: { id: 'published', title: 'Published' },
+ history: [],
+ transition: { loaded: true },
+ transitions: [],
+ },
+ intl: {
+ locale: 'en',
+ messages: {},
+ },
+ content: { data: { review_state: 'published' } },
+ });
+ const { container } = render(
+
+
+
+
+ ,
+ );
+ await waitFor(() => screen.getByText(/Published/));
+ expect(container).toMatchSnapshot();
+ });
+
+ it('renders a workflow component', async () => {
+ const store = mockStore({
+ workflow: {
+ currentState: { id: 'private', title: 'Private' },
+ history: [{ review_state: 'private' }],
+ transition: { loaded: true },
+ transitions: [{ '@id': 'http://publish', title: 'Publish' }],
+ },
+ intl: {
+ locale: 'en',
+ messages: {},
+ },
+ content: { data: { review_state: 'private' } },
+ });
+
+ const { container } = render(
+
+
+
+
+ ,
+ );
+ await waitFor(() => screen.getByText('Private'));
+ expect(container).toMatchSnapshot();
+ });
+});
diff --git a/src/customizations/volto/components/manage/Workflow/__snapshots__/Workflow.test.jsx.snap b/src/customizations/volto/components/manage/Workflow/__snapshots__/Workflow.test.jsx.snap
new file mode 100644
index 00000000..f688149c
--- /dev/null
+++ b/src/customizations/volto/components/manage/Workflow/__snapshots__/Workflow.test.jsx.snap
@@ -0,0 +1,181 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Workflow renders a workflow component 1`] = `
+
+`;
+
+exports[`Workflow renders an empty workflow component 1`] = `
+
+`;
diff --git a/src/customizations/volto/components/theme/AppExtras/AppExtras.jsx b/src/customizations/volto/components/theme/AppExtras/AppExtras.jsx
deleted file mode 100644
index 7fd492fe..00000000
--- a/src/customizations/volto/components/theme/AppExtras/AppExtras.jsx
+++ /dev/null
@@ -1,27 +0,0 @@
-//this should be deleted when upgraded to a volto version that supports App Extras exceptions
-import React from 'react';
-import { matchPath } from 'react-router';
-import config from '@plone/volto/registry';
-
-const AppExtras = (props) => {
- const { settings } = config;
- const { appExtras = [] } = settings;
- const { pathname } = props;
- const active = appExtras
- .map((reg) => {
- const excluded = matchPath(pathname, reg.exclude);
- if (excluded) return null;
- const match = matchPath(pathname, reg.match);
- return match ? { reg, match } : null;
- })
- .filter((reg) => reg);
-
- return active.map(({ reg: { component, props: extraProps }, match }, i) => {
- const Insert = component;
- return (
-
- );
- });
-};
-
-export default AppExtras;
diff --git a/src/customizations/volto/components/theme/Comments/Comments.jsx b/src/customizations/volto/components/theme/Comments/Comments.jsx
index 58251352..3d75814f 100644
--- a/src/customizations/volto/components/theme/Comments/Comments.jsx
+++ b/src/customizations/volto/components/theme/Comments/Comments.jsx
@@ -33,8 +33,7 @@ const messages = defineMessages({
defaultMessage: 'Comments',
},
commentDescription: {
- id:
- 'You can add a comment by filling out the form below. Plain text formatting.',
+ id: 'You can add a comment by filling out the form below. Plain text formatting.',
defaultMessage:
'You can add a comment by filling out the form below. Plain text formatting.',
},
diff --git a/src/customizations/volto/components/theme/Comments/__snapshots__/Comments.test.jsx.snap b/src/customizations/volto/components/theme/Comments/__snapshots__/Comments.test.jsx.snap
index 2ffd4afd..43499865 100644
--- a/src/customizations/volto/components/theme/Comments/__snapshots__/Comments.test.jsx.snap
+++ b/src/customizations/volto/components/theme/Comments/__snapshots__/Comments.test.jsx.snap
@@ -142,7 +142,7 @@ exports[`Comments renders a comments component 1`] = `
- 6 years ago
+ 7 years ago
@@ -242,7 +242,7 @@ exports[`Comments renders a comments component 1`] = `
- 6 years ago
+ 7 years ago
diff --git a/src/customizations/volto/components/theme/ContactForm/ContactForm.jsx b/src/customizations/volto/components/theme/ContactForm/ContactForm.jsx
index eeb61974..1f1b6f4a 100644
--- a/src/customizations/volto/components/theme/ContactForm/ContactForm.jsx
+++ b/src/customizations/volto/components/theme/ContactForm/ContactForm.jsx
@@ -12,7 +12,7 @@ export class ContactFormComponent extends Component {
}
render() {
- const remoteUrl = config.settings.contactForm;
+ const remoteUrl = config.settings.contactForm || '/';
return (
diff --git a/src/customizations/volto/components/theme/ContactForm/__snapshots__/ContactForm.test.js.snap b/src/customizations/volto/components/theme/ContactForm/__snapshots__/ContactForm.test.js.snap
index ef103bf0..91fc6ac0 100644
--- a/src/customizations/volto/components/theme/ContactForm/__snapshots__/ContactForm.test.js.snap
+++ b/src/customizations/volto/components/theme/ContactForm/__snapshots__/ContactForm.test.js.snap
@@ -10,10 +10,13 @@ exports[`Contact form renders a contact form 1`] = `
+ >
+ /
+
`;
@@ -28,10 +31,13 @@ exports[`Contact form renders a contact form with error message 1`] = `
+ >
+ /
+
`;
diff --git a/src/customizations/volto/components/theme/EventDetails/EventDetails.jsx b/src/customizations/volto/components/theme/EventDetails/EventDetails.jsx
index 4cff18e1..f291740e 100644
--- a/src/customizations/volto/components/theme/EventDetails/EventDetails.jsx
+++ b/src/customizations/volto/components/theme/EventDetails/EventDetails.jsx
@@ -143,6 +143,7 @@ const EventDetails = ({ content, display_as = 'aside' }) => {
className="ics-download"
target="_blank"
href={`${expandToBackendURL(content['@id'])}/ics_view`}
+ rel="noopener"
>
{intl.formatMessage(messages.downloadEvent)}
diff --git a/src/index.js b/src/index.js
index 93d56634..049f8229 100644
--- a/src/index.js
+++ b/src/index.js
@@ -35,11 +35,7 @@ import { v4 as uuid } from 'uuid';
import * as eea from './config';
import React from 'react';
-const restrictedBlocks = [
- '__grid', // Grid/Teaser block (kitconcept)
- 'imagesGrid',
- 'teaser',
-];
+const restrictedBlocks = ['imagesGrid', 'teaser', 'dataFigure', 'plotly_chart'];
/**
* Customizes the variations of a tabs block by modifying their schema and semantic icons.
@@ -131,6 +127,12 @@ const applyConfig = (config) => {
...(config.settings.eea || {}),
};
+ //include site title in
+ if (!config.settings.siteTitleFormat) {
+ config.settings.siteTitleFormat = { includeSiteTitle: true };
+ } else config.settings.siteTitleFormat.includeSiteTitle = true;
+ config.settings.titleAndSiteTitleSeparator = '|';
+
// #160689 Redirect contact-form to contact-us
config.settings.contactForm = '/contact';
@@ -185,11 +187,13 @@ const applyConfig = (config) => {
//Apply the image position style for image and leadimage blocks
if (config.blocks.blocksConfig.leadimage) {
- config.blocks.blocksConfig.leadimage.schemaEnhancer = addStylingFieldsetSchemaEnhancerImagePosition;
+ config.blocks.blocksConfig.leadimage.schemaEnhancer =
+ addStylingFieldsetSchemaEnhancerImagePosition;
}
if (config.blocks.blocksConfig.image) {
- config.blocks.blocksConfig.image.schemaEnhancer = addStylingFieldsetSchemaEnhancerImagePosition;
+ config.blocks.blocksConfig.image.schemaEnhancer =
+ addStylingFieldsetSchemaEnhancerImagePosition;
}
// Set Languages in nextcloud-video-block
@@ -197,9 +201,8 @@ const applyConfig = (config) => {
config?.blocks?.blocksConfig?.nextCloudVideo?.subtitlesLanguages &&
config?.settings?.eea?.languages?.length > 0
)
- config.blocks.blocksConfig.nextCloudVideo.subtitlesLanguages = config.settings.eea.languages.map(
- (el) => [el.code, el.name],
- );
+ config.blocks.blocksConfig.nextCloudVideo.subtitlesLanguages =
+ config.settings.eea.languages.map((el) => [el.code, el.name]);
// Enable Title block
config.blocks.blocksConfig.title.restricted = false;
@@ -224,7 +227,7 @@ const applyConfig = (config) => {
};
config.views.errorViews = {
...config.views.errorViews,
- '404': NotFound,
+ 404: NotFound,
};
// Apply slate text block customization
if (config.blocks.blocksConfig.slate) {
@@ -467,19 +470,22 @@ const applyConfig = (config) => {
// Group
if (config.blocks.blocksConfig.group) {
- config.blocks.blocksConfig.group.schemaEnhancer = addStylingFieldsetSchemaEnhancer;
+ config.blocks.blocksConfig.group.schemaEnhancer =
+ addStylingFieldsetSchemaEnhancer;
}
// Columns
if (config.blocks.blocksConfig.columnsBlock) {
config.blocks.blocksConfig.columnsBlock.mostUsed = true;
- config.blocks.blocksConfig.columnsBlock.schemaEnhancer = addStylingFieldsetSchemaEnhancer;
+ config.blocks.blocksConfig.columnsBlock.schemaEnhancer =
+ addStylingFieldsetSchemaEnhancer;
}
// Listing
if (config.blocks.blocksConfig.listing) {
config.blocks.blocksConfig.listing.title = 'Listing (Content)';
- config.blocks.blocksConfig.listing.schemaEnhancer = addStylingFieldsetSchemaEnhancer;
+ config.blocks.blocksConfig.listing.schemaEnhancer =
+ addStylingFieldsetSchemaEnhancer;
}
// Block chooser
diff --git a/src/middleware/ok.js b/src/middleware/ok.js
index 0c9c23e6..20764f53 100644
--- a/src/middleware/ok.js
+++ b/src/middleware/ok.js
@@ -7,9 +7,11 @@ const ok = function (req, res, next) {
res.send('ok');
};
-export default function (express) {
+const okMiddleware = function (express) {
const middleware = express.Router();
middleware.all(config?.settings?.okRoute || '/ok', ok);
middleware.id = 'ok';
return middleware;
-}
+};
+
+export default okMiddleware;
diff --git a/src/middleware/voltoCustom.js b/src/middleware/voltoCustom.js
index ac5e5c73..5fbf77a2 100644
--- a/src/middleware/voltoCustom.js
+++ b/src/middleware/voltoCustom.js
@@ -29,9 +29,11 @@ function voltoCustomMiddleware(req, res, next) {
});
}
-export default function (express) {
+const registervoltoCustomMiddleware = function (express) {
const middleware = express.Router();
middleware.all(['**/voltoCustom.css$'], voltoCustomMiddleware);
middleware.id = 'voltoCustom.css';
return middleware;
-}
+};
+
+export default registervoltoCustomMiddleware;
diff --git a/src/slate.js b/src/slate.js
index 0a0559f6..1bd99f26 100644
--- a/src/slate.js
+++ b/src/slate.js
@@ -150,8 +150,8 @@ export default function installSlate(config) {
config = installCallout(config);
try {
- renderLinkElement = require('@eeacms/volto-anchors/helpers')
- .renderLinkElement;
+ renderLinkElement =
+ require('@eeacms/volto-anchors/helpers').renderLinkElement;
} catch {}
installSlateToolbarButton({
@@ -190,14 +190,16 @@ export default function installSlate(config) {
);
// Remove blockquote, italic, strikethrough slate button from toolbarButtons
- config.settings.slate.toolbarButtons = config.settings.slate.toolbarButtons.filter(
- (item) => !['blockquote', 'italic', 'strikethrough'].includes(item),
- );
+ config.settings.slate.toolbarButtons =
+ config.settings.slate.toolbarButtons.filter(
+ (item) => !['blockquote', 'italic', 'strikethrough'].includes(item),
+ );
// Remove blockquote, italic, strikethrough slate button from expandedToolbarButtons
- config.settings.slate.expandedToolbarButtons = config.settings.slate.expandedToolbarButtons.filter(
- (item) => !['blockquote', 'italic', 'strikethrough'].includes(item),
- );
+ config.settings.slate.expandedToolbarButtons =
+ config.settings.slate.expandedToolbarButtons.filter(
+ (item) => !['blockquote', 'italic', 'strikethrough'].includes(item),
+ );
// Remove 'underline' and 'italic' hotkeys
config.settings.slate.hotkeys = Object.keys(config.settings.slate.hotkeys)