diff --git a/.env b/.env new file mode 100644 index 00000000..ef282fa3 --- /dev/null +++ b/.env @@ -0,0 +1,3 @@ +# Jest configuration variables +# - possible values: ON, OFF +JEST_USE_SETUP=OFF \ No newline at end of file diff --git a/.eslintrc.js b/.eslintrc.js index 3c8f6a2f..52469170 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,40 +1,43 @@ const fs = require('fs'); const path = require('path'); - -const projectRootPath = fs.existsSync('./project') - ? fs.realpathSync('./project') - : fs.realpathSync(__dirname + '/../../../'); -const packageJson = require(path.join(projectRootPath, 'package.json')); -const jsConfig = require(path.join(projectRootPath, 'jsconfig.json')).compilerOptions; - -const pathsConfig = jsConfig.paths; +const projectRootPath = fs.realpathSync(__dirname + '/../../../'); let voltoPath = path.join(projectRootPath, 'node_modules/@plone/volto'); +let configFile; +if (fs.existsSync(`${projectRootPath}/tsconfig.json`)) + configFile = `${projectRootPath}/tsconfig.json`; +else if (fs.existsSync(`${projectRootPath}/jsconfig.json`)) + configFile = `${projectRootPath}/jsconfig.json`; + +if (configFile) { + const jsConfig = require(configFile).compilerOptions; + const pathsConfig = jsConfig.paths; + if (pathsConfig['@plone/volto']) + voltoPath = `./${jsConfig.baseUrl}/${pathsConfig['@plone/volto'][0]}`; +} -Object.keys(pathsConfig).forEach(pkg => { - if (pkg === '@plone/volto') { - voltoPath = `./${jsConfig.baseUrl}/${pathsConfig[pkg][0]}`; - } -}); const AddonConfigurationRegistry = require(`${voltoPath}/addon-registry.js`); const reg = new AddonConfigurationRegistry(projectRootPath); // Extends ESlint configuration for adding the aliases to `src` directories in Volto addons -const addonAliases = Object.keys(reg.packages).map(o => [ +const addonAliases = Object.keys(reg.packages).map((o) => [ o, reg.packages[o].modulePath, ]); +const addonExtenders = reg.getEslintExtenders().map((m) => require(m)); -module.exports = { - extends: `${projectRootPath}/node_modules/@plone/volto/.eslintrc`, +const defaultConfig = { + extends: `${voltoPath}/.eslintrc`, settings: { 'import/resolver': { alias: { map: [ ['@plone/volto', '@plone/volto/src'], + ['@plone/volto-slate', '@plone/volto/packages/volto-slate/src'], ...addonAliases, ['@package', `${__dirname}/src`], + ['@root', `${__dirname}/src`], ['~', `${__dirname}/src`], ], extensions: ['.js', '.jsx', '.json'], @@ -51,6 +54,12 @@ module.exports = { allowReferrer: true, }, ], - } + }, }; +const config = addonExtenders.reduce( + (acc, extender) => extender.modify(acc), + defaultConfig, +); + +module.exports = config; diff --git a/.gitignore b/.gitignore index 53b9801b..f1be4ff0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,5 @@ .vscode/ .history -.eslintrc.js .nyc_output project coverage diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a9a0bfd..22e30fbb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,16 +4,27 @@ 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.27.2](https://github.com/eea/volto-eea-website-theme/compare/1.27.1...1.27.2) - 23 January 2024 +### [1.28.0](https://github.com/eea/volto-eea-website-theme/compare/1.27.2...1.28.0) - 16 February 2024 #### :bug: Bug Fixes +- fix(toc): make toc work, refs #265201 [Razvan - [`507adc2`](https://github.com/eea/volto-eea-website-theme/commit/507adc29f0a2e144933b06dfcf0856f1ac7efc98)] +- fix: volto slate when used in metadata block with SlateJSONField - refs #264239 [Miu Razvan - [`51682c4`](https://github.com/eea/volto-eea-website-theme/commit/51682c42001f6aa3433feff62c5f8536283de990)] - fix(lint): service so that it work with editor and command line tool [David Ichim - [`ad43bc2`](https://github.com/eea/volto-eea-website-theme/commit/ad43bc2f9bfc3e272d30b35db9d4b160a8edcbec)] +#### :house: Internal changes + +- chore: package.json [Alin Voinea - [`08beb70`](https://github.com/eea/volto-eea-website-theme/commit/08beb706d9021a89c80acc5aa7c94350195f7de7)] + #### :hammer_and_wrench: Others +- 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)] +- 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)] +### [1.27.2](https://github.com/eea/volto-eea-website-theme/compare/1.27.1...1.27.2) - 24 January 2024 + ### [1.27.1](https://github.com/eea/volto-eea-website-theme/compare/1.27.0...1.27.1) - 18 January 2024 #### :bug: Bug Fixes @@ -103,11 +114,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 @@ -120,7 +126,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)] @@ -306,7 +311,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 @@ -462,7 +466,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 @@ -609,7 +612,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 @@ -643,7 +645,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 @@ -653,7 +654,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 @@ -664,7 +664,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 @@ -705,7 +704,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 @@ -753,7 +751,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 @@ -1021,7 +1018,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)] @@ -1063,5 +1059,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 05d08feb..c6d92fb1 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -9,7 +9,7 @@ pipeline { environment { GIT_NAME = "volto-eea-website-theme" NAMESPACE = "@eeacms" - SONARQUBE_TAGS = "volto.eea.europa.eu,demo-www.eea.europa.eu,prod-www.eea.europa.eu,climate-adapt.eea.europa.eu,climate-advisory-board.devel4cph.eea.europa.eu,climate-advisory-board.europa.eu,www.eea.europa.eu-en" + SONARQUBE_TAGS = "volto.eea.europa.eu,demo-www.eea.europa.eu,prod-www.eea.europa.eu,climate-adapt.eea.europa.eu,climate-advisory-board.devel4cph.eea.europa.eu,climate-advisory-board.europa.eu,www.eea.europa.eu-en,insitu-frontend.eionet.europa.eu,water.europa.eu-freshwater" DEPENDENCIES = "" BACKEND_PROFILES = "eea.kitkat:testing" BACKEND_ADDONS = "" @@ -162,10 +162,16 @@ pipeline { script { try { sh '''docker run --pull always --rm -d --name="$IMAGE_NAME-plone" -e SITE="Plone" -e PROFILES="$BACKEND_PROFILES" -e ADDONS="$BACKEND_ADDONS" eeacms/plone-backend''' - sh '''docker run -d --shm-size=3g --link $IMAGE_NAME-plone:plone --name="$IMAGE_NAME-cypress" -e "RAZZLE_INTERNAL_API_PATH=http://plone:8080/Plone" --entrypoint=make --workdir=/app/src/addons/$GIT_NAME $IMAGE_NAME-frontend start-ci''' + sh '''docker run -d --shm-size=4g --link $IMAGE_NAME-plone:plone --name="$IMAGE_NAME-cypress" -e "RAZZLE_INTERNAL_API_PATH=http://plone:8080/Plone" --entrypoint=make --workdir=/app/src/addons/$GIT_NAME $IMAGE_NAME-frontend start-ci''' + frontend = sh script:'''docker exec --workdir=/app/src/addons/${GIT_NAME} $IMAGE_NAME-cypress make check-ci''', returnStatus: true + if ( frontend != 0 ) { + sh '''docker logs $IMAGE_NAME-cypress; exit 1''' + } + sh '''timeout -s 9 1800 docker exec --workdir=/app/src/addons/${GIT_NAME} $IMAGE_NAME-cypress make cypress-ci''' } finally { try { + if ( frontend == 0 ) { sh '''rm -rf cypress-videos cypress-results cypress-coverage cypress-screenshots''' sh '''mkdir -p cypress-videos cypress-results cypress-coverage cypress-screenshots''' videos = sh script: '''docker cp $IMAGE_NAME-cypress:/app/src/addons/$GIT_NAME/cypress/videos cypress-videos/''', returnStatus: true @@ -189,6 +195,7 @@ pipeline { sh '''for file in $(find cypress-results -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-videos/videos/$testname.mp4; fi; done''' archiveArtifacts artifacts: 'cypress-videos/**/*.mp4', fingerprint: true, allowEmptyArchive: true } + } } finally { catchError(buildResult: 'SUCCESS', stageResult: 'SUCCESS') { junit testResults: 'cypress-results/**/*.xml', allowEmptyResults: true diff --git a/Makefile b/Makefile index efbf2fbd..c583f3fe 100644 --- a/Makefile +++ b/Makefile @@ -86,7 +86,7 @@ cypress-open: ## Open cypress integration tests .PHONY: cypress-run cypress-run: ## Run cypress integration tests - CYPRESS_API_PATH="${RAZZLE_DEV_PROXY_API_PATH}" NODE_ENV=development $(NODE_MODULES)/cypress/bin/cypress run --browser chromium + CYPRESS_API_PATH="${RAZZLE_DEV_PROXY_API_PATH}" NODE_ENV=development $(NODE_MODULES)/cypress/bin/cypress run .PHONY: test test: ## Run jest tests @@ -155,8 +155,11 @@ start-ci: cd ../.. yarn start +.PHONY: check-ci +check-ci: + $(NODE_MODULES)/.bin/wait-on -t 240000 http://localhost:3000 + .PHONY: cypress-ci cypress-ci: $(NODE_MODULES)/.bin/wait-on -t 240000 http://localhost:3000 - NODE_ENV=development make cypress-run - + CYPRESS_API_PATH="${RAZZLE_DEV_PROXY_API_PATH}" NODE_ENV=development $(NODE_MODULES)/cypress/bin/cypress run --browser chromium diff --git a/README.md b/README.md index aa9e5ce5..51ad7d1b 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,8 @@ See [Storybook](https://eea.github.io/eea-storybook/). ## Volto customizations +- `volto-slate/editor/SlateEditor` -> [ref](https://taskman.eionet.europa.eu/issues/264239#note-11) When two slates looks at the same prop changing one slate and updating the other should be handled properly. This change makes replacing the old value of slate work in sync with the other slates that watches the same prop. + - `volto/components/manage/Sidebar/SidebarPopup` -> https://github.com/plone/volto/pull/5520 ## Getting started diff --git a/jest-addon.config.js b/jest-addon.config.js index 89df8364..4148ea3f 100644 --- a/jest-addon.config.js +++ b/jest-addon.config.js @@ -1,3 +1,5 @@ +require('dotenv').config({ path: __dirname + '/.env' }) + module.exports = { testMatch: ['**/src/addons/**/?(*.)+(spec|test).[jt]s?(x)'], collectCoverageFrom: [ @@ -9,31 +11,38 @@ module.exports = { '@plone/volto/cypress': '/node_modules/@plone/volto/cypress', '@plone/volto/babel': '/node_modules/@plone/volto/babel', '@plone/volto/(.*)$': '/node_modules/@plone/volto/src/$1', - '@package/(.*)$': '/src/$1', - '@root/(.*)$': '/src/$1', + '@package/(.*)$': '/node_modules/@plone/volto/src/$1', + '@root/(.*)$': '/node_modules/@plone/volto/src/$1', '@plone/volto-quanta/(.*)$': '/src/addons/volto-quanta/src/$1', '@eeacms/(.*?)/(.*)$': '/node_modules/@eeacms/$1/src/$2', - '@plone/volto-slate': + '@plone/volto-slate$': '/node_modules/@plone/volto/packages/volto-slate/src', + '@plone/volto-slate/(.*)$': + '/node_modules/@plone/volto/packages/volto-slate/src/$1', '~/(.*)$': '/src/$1', 'load-volto-addons': '/node_modules/@plone/volto/jest-addons-loader.js', }, + transformIgnorePatterns: [ + '/node_modules/(?!(@plone|@root|@package|@eeacms)/).*/', + ], transform: { '^.+\\.js(x)?$': 'babel-jest', '^.+\\.(png)$': 'jest-file', '^.+\\.(jpg)$': 'jest-file', '^.+\\.(svg)$': './node_modules/@plone/volto/jest-svgsystem-transform.js', }, - transformIgnorePatterns: [ - 'node_modules/(?!@eeacms)/volto-eea-design-system/ui', - ], coverageThreshold: { global: { - branches: 0, - functions: 0, - lines: 0, - statements: 0, + branches: 5, + functions: 5, + lines: 5, + statements: 5, }, }, -}; + ...(process.env.JEST_USE_SETUP === 'ON' && { + setupFilesAfterEnv: [ + '/node_modules/@eeacms/volto-eea-website-theme/jest.setup.js', + ], + }), +} diff --git a/jest.setup.js b/jest.setup.js new file mode 100644 index 00000000..85b16f79 --- /dev/null +++ b/jest.setup.js @@ -0,0 +1,65 @@ +import { jest } from '@jest/globals'; +import configureStore from 'redux-mock-store'; +import thunk from 'redux-thunk'; +import { blocksConfig } from '@plone/volto/config/Blocks'; +import installSlate from '@plone/volto-slate/index'; + +var mockSemanticComponents = jest.requireActual('semantic-ui-react'); +var mockComponents = jest.requireActual('@plone/volto/components'); +var config = jest.requireActual('@plone/volto/registry').default; + +config.blocks.blocksConfig = { + ...blocksConfig, + ...config.blocks.blocksConfig, +}; + +jest.doMock('semantic-ui-react', () => ({ + __esModule: true, + ...mockSemanticComponents, + Popup: ({ content, trigger }) => { + return ( +
+
{trigger}
+
{content}
+
+ ); + }, +})); + +jest.doMock('@plone/volto/components', () => { + return { + __esModule: true, + ...mockComponents, + SidebarPortal: ({ children }) => , + }; +}); + +jest.doMock('@plone/volto/registry', () => + [installSlate].reduce((acc, apply) => apply(acc), config), +); + +const mockStore = configureStore([thunk]); + +global.fetch = jest.fn(() => + Promise.resolve({ + json: () => Promise.resolve({}), + }), +); + +global.store = mockStore({ + intl: { + locale: 'en', + messages: {}, + formatMessage: jest.fn(), + }, + content: { + create: {}, + subrequests: [], + }, + connected_data_parameters: {}, + screen: { + page: { + width: 768, + }, + }, +}); diff --git a/package.json b/package.json index e5da6da4..c6e99454 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@eeacms/volto-eea-website-theme", - "version": "1.27.2", + "version": "1.28.0", "description": "@eeacms/volto-eea-website-theme: Volto add-on", "main": "src/index.js", "author": "European Environment Agency: IDM2 A-Team", @@ -32,6 +32,7 @@ "@cypress/code-coverage": "^3.10.0", "@plone/scripts": "*", "babel-plugin-transform-class-properties": "^6.24.1", + "dotenv": "^16.3.2", "husky": "^8.0.3", "lint-staged": "^14.0.1", "md5": "^2.3.0", @@ -78,4 +79,4 @@ "cypress:open": "make cypress-open", "prepare": "husky install" } -} +} \ No newline at end of file diff --git a/src/customizations/@plone/volto-slate/editor/SlateEditor.jsx b/src/customizations/@plone/volto-slate/editor/SlateEditor.jsx new file mode 100644 index 00000000..021648bf --- /dev/null +++ b/src/customizations/@plone/volto-slate/editor/SlateEditor.jsx @@ -0,0 +1,404 @@ +import ReactDOM from 'react-dom'; +import cx from 'classnames'; +import { isEqual, cloneDeep } from 'lodash'; +import { Transforms, Editor, Point } from 'slate'; // , Transforms +import { Slate, Editable, ReactEditor } from 'slate-react'; +import React, { Component } from 'react'; // , useState +import { v4 as uuid } from 'uuid'; + +import config from '@plone/volto/registry'; + +import { Element, Leaf } from './render'; + +import withTestingFeatures from '@plone/volto-slate/editor/extensions/withTestingFeatures'; +import { + makeEditor, + toggleInlineFormat, + toggleMark, + parseDefaultSelection, +} from '@plone/volto-slate/utils'; +import { InlineToolbar } from '@plone/volto-slate/editor/ui'; +import EditorContext from '@plone/volto-slate/editor/EditorContext'; + +import isHotkey from 'is-hotkey'; + +import '@plone/volto-slate/editor/less/editor.less'; + +import Toolbar from '@plone/volto-slate/editor/ui/Toolbar'; + +const handleHotKeys = (editor, event, config) => { + let wasHotkey = false; + + for (const hk of Object.entries(config.hotkeys)) { + const [shortcut, { format, type }] = hk; + if (isHotkey(shortcut, event)) { + event.preventDefault(); + + if (type === 'inline') { + toggleInlineFormat(editor, format); + } else { + // type === 'mark' + toggleMark(editor, format); + } + + wasHotkey = true; + } + } + + return wasHotkey; +}; + +function resetNodes(editor, options = {}) { + const children = [...editor.children]; + + children.forEach((node) => + editor.apply({ type: 'remove_node', path: [0], node }), + ); + + if (options.nodes) { + options.nodes.forEach((node, i) => + editor.apply({ type: 'insert_node', path: [i], node: node }), + ); + } + + const point = + options.at && Point.isPoint(options.at) + ? options.at + : Editor.end(editor, []); + + if (point) { + Transforms.select(editor, point); + } +} + +// TODO: implement onFocus +class SlateEditor extends Component { + constructor(props) { + super(props); + + this.createEditor = this.createEditor.bind(this); + this.multiDecorator = this.multiDecorator.bind(this); + this.handleChange = this.handleChange.bind(this); + this.getSavedSelection = this.getSavedSelection.bind(this); + this.setSavedSelection = this.setSavedSelection.bind(this); + + this.savedSelection = null; + + const uid = uuid(); // used to namespace the editor's plugins + + this.slateSettings = props.slateSettings || config.settings.slate; + + this.initialValue = cloneDeep( + this.props.value || this.slateSettings.defaultValue(), + ); + + this.state = { + editor: this.createEditor(uid), + showExpandedToolbar: config.settings.slate.showExpandedToolbar, + internalValue: this.initialValue, + uid, + }; + + this.editor = null; + this.selectionTimeout = null; + } + + getSavedSelection() { + return this.savedSelection; + } + setSavedSelection(selection) { + this.savedSelection = selection; + } + + createEditor(uid) { + // extensions are "editor plugins" or "editor wrappers". It's a similar + // similar to OOP inheritance, where a callable creates a new copy of the + // editor, while replacing or adding new capabilities to that editor. + // Extensions are purely JS, no React components. + const editor = makeEditor({ extensions: this.props.extensions }); + + // When the editor loses focus it no longer has a valid selections. This + // makes it impossible to have complex types of interactions (like filling + // in another text box, operating a select menu, etc). For this reason we + // save the active selection + + editor.getSavedSelection = this.getSavedSelection; + editor.setSavedSelection = this.setSavedSelection; + editor.uid = uid || this.state.uid; + + return editor; + } + + handleChange(value) { + ReactDOM.unstable_batchedUpdates(() => { + const newValue = cloneDeep(value); + this.setState({ internalValue: newValue }); + if (this.props.onChange && !isEqual(newValue, this.props.value)) { + this.props.onChange(newValue, this.editor); + } + }); + } + + multiDecorator([node, path]) { + // Decorations (such as higlighting node types, selection, etc). + const { runtimeDecorators = [] } = this.slateSettings; + return runtimeDecorators.reduce( + (acc, deco) => deco(this.state.editor, [node, path], acc), + [], + ); + } + + componentDidMount() { + // watch the dom change + + if (this.props.selected) { + let focused = true; + try { + focused = ReactEditor.isFocused(this.state.editor); + } catch {} + if (!focused) { + setTimeout(() => { + try { + ReactEditor.focus(this.state.editor); + } catch {} + }, 100); // flush + } + + this.state.editor.normalize({ force: true }); + } + } + + componentWillUnmount() { + this.isUnmounted = true; + } + + componentDidUpdate(prevProps) { + if (!isEqual(prevProps.extensions, this.props.extensions)) { + this.setState({ editor: this.createEditor() }); + return; + } + + if ( + this.props.value && + !isEqual(this.props.value, this.state.internalValue) + ) { + const newValue = cloneDeep(this.props.value); + const { editor } = this.state; + + resetNodes(editor, { nodes: newValue }); + + this.setState({ + internalValue: newValue, + }); + + if (this.props.defaultSelection) { + const selection = parseDefaultSelection( + editor, + this.props.defaultSelection, + ); + + ReactEditor.focus(editor); + Transforms.select(editor, selection); + } else { + Transforms.select(editor, Editor.end(editor, [])); + } + return; + } + + const { editor } = this.state; + + if (!prevProps.selected && this.props.selected) { + // if the SlateEditor becomes selected from unselected + + if (window.getSelection().type === 'None') { + // TODO: why is this condition checked? + Transforms.select( + this.state.editor, + Editor.range(this.state.editor, Editor.start(this.state.editor, [])), + ); + } + + ReactEditor.focus(this.state.editor); + } + + if (this.props.selected && this.props.onUpdate) { + this.props.onUpdate(editor); + } + } + + shouldComponentUpdate(nextProps, nextState) { + const { selected = true, value, readOnly } = nextProps; + const res = + selected || + this.props.selected !== selected || + this.props.readOnly !== readOnly || + !isEqual(value, this.props.value); + return res; + } + + render() { + const { + selected, + placeholder, + onKeyDown, + testingEditorRef, + readOnly, + className, + renderExtensions = [], + editableProps = {}, + } = this.props; + const slateSettings = this.slateSettings; + + // renderExtensions is needed because the editor is memoized, so if these + // extensions need an updated state (for example to insert updated + // blockProps) then we need to always wrap the editor with them + const editor = renderExtensions.reduce( + (acc, apply) => apply(acc), + this.state.editor, + ); + + // Reset selection if field is reset + if ( + editor.selection && + this.props.value?.length === 1 && + this.props.value[0].children.length === 1 && + this.props.value[0].children[0].text === '' + ) { + Transforms.select(editor, { + anchor: { path: [0, 0], offset: 0 }, + focus: { path: [0, 0], offset: 0 }, + }); + } + this.editor = editor; + + if (testingEditorRef) { + testingEditorRef.current = editor; + } + + // debug-values are `data-` HTML attributes in withTestingFeatures HOC + + return ( +
+ + + {selected ? ( + <> + + {Object.keys(slateSettings.elementToolbarButtons).map( + (t, i) => { + return ( + + {slateSettings.elementToolbarButtons[t].map( + (Btn, b) => { + return ; + }, + )} + + ); + }, + )} + + ) : ( + '' + )} + } + renderLeaf={(props) => } + decorate={this.multiDecorator} + spellCheck={false} + scrollSelectionIntoView={ + slateSettings.scrollIntoView ? undefined : () => null + } + onBlur={() => { + this.props.onBlur && this.props.onBlur(); + return null; + }} + onClick={this.props.onClick} + onSelect={(e) => { + if (!selected && this.props.onFocus) { + // we can't overwrite the onFocus of Editable, as the onFocus + // in Slate has too much builtin behaviour that's not + // accessible otherwise. Instead we try to detect such an + // event based on observing selected state + if (!editor.selection) { + setTimeout(() => { + this.props.onFocus(); + }, 100); // TODO: why 100 is chosen here? + } + } + + if (this.selectionTimeout) clearTimeout(this.selectionTimeout); + this.selectionTimeout = setTimeout(() => { + if ( + editor.selection && + !isEqual(editor.selection, this.savedSelection) && + !this.isUnmounted + ) { + this.setState((state) => ({ update: !this.state.update })); + this.setSavedSelection( + JSON.parse(JSON.stringify(editor.selection)), + ); + } + }, 200); + }} + onKeyDown={(event) => { + const handled = handleHotKeys(editor, event, slateSettings); + if (handled) return; + onKeyDown && onKeyDown({ editor, event }); + }} + {...editableProps} + /> + {selected && + slateSettings.persistentHelpers.map((Helper, i) => { + return ; + })} + {this.props.debug ? ( +
    +
  • {selected ? 'selected' : 'no-selected'}
  • +
  • + {ReactEditor.isFocused(editor) ? 'focused' : 'unfocused'} +
  • +
  • + savedSelection: {JSON.stringify(editor.getSavedSelection())} +
  • +
  • live selection: {JSON.stringify(editor.selection)}
  • +
  • children: {JSON.stringify(editor.children)}
  • +
+ ) : ( + '' + )} + {this.props.children} +
+
+
+ ); + } +} + +SlateEditor.defaultProps = { + extensions: [], + className: '', +}; + +// May be needed to wrap in React.memo(), it used to be wrapped in connect() +export default __CLIENT__ && window?.Cypress + ? withTestingFeatures(SlateEditor) + : SlateEditor; diff --git a/src/index.js b/src/index.js index b08ca632..70f82432 100644 --- a/src/index.js +++ b/src/index.js @@ -8,6 +8,7 @@ import { TokenWidget } from '@eeacms/volto-eea-website-theme/components/theme/Wi import { TopicsWidget } from '@eeacms/volto-eea-website-theme/components/theme/Widgets/TopicsWidget'; import { Icon } from '@plone/volto/components'; import { getBlocks } from '@plone/volto/helpers'; +import { serializeNodesToText } from '@plone/volto-slate/editor/render'; import Tag from '@eeacms/volto-eea-design-system/ui/Tag/Tag'; import { @@ -224,6 +225,20 @@ const applyConfig = (config) => { ...config.views.errorViews, '404': NotFound, }; + // Apply slate text block customization + if (config.blocks.blocksConfig.slate) { + config.blocks.blocksConfig.slate.tocEntry = (block = {}) => { + const { value, override_toc, entry_text, level } = block; + const plaintext = + serializeNodesToText(block.value || []) || block.plaintext; + const type = value?.[0]?.type; + return override_toc && level + ? [parseInt(level.slice(1)), entry_text] + : config.settings.slate.topLevelTargetElements.includes(type) + ? [parseInt(type.slice(1)), plaintext] + : null; + }; + } // Apply accordion block customization if (config.blocks.blocksConfig.accordion) { config.blocks.blocksConfig.accordion.titleIcons = {