diff --git a/CHANGELOG.md b/CHANGELOG.md index 3742d31..8b7bb5f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,20 @@ -# Changelog +### Changelog -## 0.1.0 +All notable changes to this project will be documented in this file. Dates are displayed in UTC. -- Initial release +Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). + +#### [0.1.1](https://github.com/eea/volto-quote-block/compare/0.1.0...0.1.1) + +- Improvments [`15b8aac`](https://github.com/eea/volto-quote-block/commit/15b8aac6b575af5b6b05d32cbfd32d8c603b2eb3) +- Fix tests [`f1faba6`](https://github.com/eea/volto-quote-block/commit/f1faba692370b519c690a831efe80f26d1e9a8cc) +- Add slate add-on [`57a1e1b`](https://github.com/eea/volto-quote-block/commit/57a1e1bd5d68ec08afb24d6a51346ad54fa891b3) +- Lint fix [`a1b29b0`](https://github.com/eea/volto-quote-block/commit/a1b29b0ceee5606ec14edd8d60e3196019b07506) +- Remove uneeded code [`ff26f0d`](https://github.com/eea/volto-quote-block/commit/ff26f0d60193f70e993522f5f3f1328ce3ce6e11) +- Add quote blocks and volto-slate quote plugin [`7089995`](https://github.com/eea/volto-quote-block/commit/7089995a87c475742986eec09f0e4f67d411a711) + +#### 0.1.0 + +> 12 April 2022 + +- Initial commit [`f748002`](https://github.com/eea/volto-quote-block/commit/f7480020346c8069fc825a439183b8f73445d62c) diff --git a/DEVELOP.md b/DEVELOP.md index 9c01eb6..9afc79f 100644 --- a/DEVELOP.md +++ b/DEVELOP.md @@ -1,4 +1,4 @@ -# volto-addon-template +# volto-quote-block ## Develop @@ -10,15 +10,15 @@ Before starting make sure your development environment is properly set. See [Vol 1. Create new volto app - yo @plone/volto my-volto-project --addon @eeacms/volto-addon-template --skip-install + yo @plone/volto my-volto-project --addon @eeacms/volto-quote-block --skip-install cd my-volto-project 1. Add the following to `mrs.developer.json`: { - "volto-addon-template": { - "url": "https://github.com/eea/volto-addon-template.git", - "package": "@eeacms/volto-addon-template", + "volto-quote-block": { + "url": "https://github.com/eea/volto-quote-block.git", + "package": "@eeacms/volto-quote-block", "branch": "develop", "path": "src" } @@ -48,4 +48,5 @@ Before starting make sure your development environment is properly set. See [Vol 1. Happy hacking! - cd src/addons/volto-addon-template/ + cd src/addons/volto-quote-block/ +o-addon-template/ diff --git a/DEVELOP.md.tpl b/DEVELOP.md.tpl deleted file mode 100644 index cf7da06..0000000 --- a/DEVELOP.md.tpl +++ /dev/null @@ -1,51 +0,0 @@ -# <%= name %> - -## Develop - -Before starting make sure your development environment is properly set. See [Volto Developer Documentation](https://docs.voltocms.com/getting-started/install/) - -1. Make sure you have installed `yo`, `@plone/generator-volto` and `mrs-developer` - - npm install -g yo @plone/generator-volto mrs-developer - -1. Create new volto app - - yo @plone/volto my-volto-project --addon <%= addonName %> --skip-install - cd my-volto-project - -1. Add the following to `mrs.developer.json`: - - { - "<%= name %>": { - "url": "https://github.com/eea/<%= name %>.git", - "package": "<%= addonName %>", - "branch": "develop", - "path": "src" - } - } - -1. Install - - yarn develop - yarn - -1. Start backend - - docker pull plone - docker run -d --name plone -p 8080:8080 -e SITE=Plone -e PROFILES="profile-plone.restapi:blocks" plone - - ...wait for backend to setup and start - `Ready to handle requests`: - - docker logs -f plone - - ...you can also check http://localhost:8080/Plone - -1. Start frontend - - yarn start - -1. Go to http://localhost:3000 - -1. Happy hacking! - - cd src/addons/<%= name %>/ diff --git a/Jenkinsfile b/Jenkinsfile index 36a2afd..8583d36 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -2,7 +2,7 @@ pipeline { agent any environment { - GIT_NAME = "volto-addon-template" + GIT_NAME = "volto-quote-block" NAMESPACE = "@eeacms" SONARQUBE_TAGS = "volto.eea.europa.eu" DEPENDENCIES = "" @@ -184,10 +184,10 @@ pipeline { unstash "xunit-reports" unstash "cypress-coverage" def scannerHome = tool 'SonarQubeScanner'; - def nodeJS = tool 'NodeJS11'; + def nodeJS = tool 'NodeJS'; withSonarQubeEnv('Sonarqube') { sh '''sed -i "s#/opt/frontend/my-volto-project/src/addons/${GIT_NAME}/##g" xunit-reports/coverage/lcov.info''' - sh "export PATH=$PATH:${scannerHome}/bin:${nodeJS}/bin; sonar-scanner -Dsonar.javascript.lcov.reportPaths=./xunit-reports/coverage/lcov.info,./cypress-coverage/coverage/lcov.info -Dsonar.sources=./src -Dsonar.projectKey=$GIT_NAME-$BRANCH_NAME -Dsonar.projectVersion=$BRANCH_NAME-$BUILD_NUMBER" + sh "export PATH=${scannerHome}/bin:${nodeJS}/bin:$PATH; sonar-scanner -Dsonar.javascript.lcov.reportPaths=./xunit-reports/coverage/lcov.info,./cypress-coverage/coverage/lcov.info -Dsonar.sources=./src -Dsonar.projectKey=$GIT_NAME-$BRANCH_NAME -Dsonar.projectVersion=$BRANCH_NAME-$BUILD_NUMBER" sh '''try=2; while [ \$try -gt 0 ]; do curl -s -XPOST -u "${SONAR_AUTH_TOKEN}:" "${SONAR_HOST_URL}api/project_tags/set?project=${GIT_NAME}-${BRANCH_NAME}&tags=${SONARQUBE_TAGS},${BRANCH_NAME}" > set_tags_result; if [ \$(grep -ic error set_tags_result ) -eq 0 ]; then try=0; else cat set_tags_result; echo "... Will retry"; sleep 60; try=\$(( \$try - 1 )); fi; done''' } } diff --git a/Jenkinsfile.tpl b/Jenkinsfile.tpl deleted file mode 100644 index 0fedbdc..0000000 --- a/Jenkinsfile.tpl +++ /dev/null @@ -1,241 +0,0 @@ -pipeline { - agent any - - environment { - GIT_NAME = "<%= name %>" - NAMESPACE = "@eeacms" - SONARQUBE_TAGS = "volto.eea.europa.eu" - DEPENDENCIES = "" - } - - stages { - - stage('Release') { - when { - allOf { - environment name: 'CHANGE_ID', value: '' - branch 'master' - } - } - steps { - node(label: 'docker') { - withCredentials([string(credentialsId: 'eea-jenkins-token', variable: 'GITHUB_TOKEN'),string(credentialsId: 'eea-jenkins-npm-token', variable: 'NPM_TOKEN')]) { - sh '''docker pull eeacms/gitflow''' - sh '''docker run -i --rm --name="$BUILD_TAG-gitflow-master" -e GIT_BRANCH="$BRANCH_NAME" -e GIT_NAME="$GIT_NAME" -e GIT_TOKEN="$GITHUB_TOKEN" -e NPM_TOKEN="$NPM_TOKEN" -e LANGUAGE=javascript eeacms/gitflow''' - } - } - } - } - - stage('Code') { - when { - allOf { - environment name: 'CHANGE_ID', value: '' - not { changelog '.*^Automated release [0-9\\.]+$' } - not { branch 'master' } - } - } - steps { - parallel( - - "ES lint": { - node(label: 'docker') { - sh '''docker run -i --rm --name="$BUILD_TAG-eslint" -e NAMESPACE="$NAMESPACE" -e GIT_NAME=$GIT_NAME -e GIT_BRANCH="$BRANCH_NAME" -e GIT_CHANGE_ID="$CHANGE_ID" plone/volto-addon-ci eslint''' - } - }, - - "Style lint": { - node(label: 'docker') { - sh '''docker run -i --rm --name="$BUILD_TAG-stylelint" -e NAMESPACE="$NAMESPACE" -e GIT_NAME=$GIT_NAME -e GIT_BRANCH="$BRANCH_NAME" -e GIT_CHANGE_ID="$CHANGE_ID" plone/volto-addon-ci stylelint''' - } - }, - - "Prettier": { - node(label: 'docker') { - sh '''docker run -i --rm --name="$BUILD_TAG-prettier" -e NAMESPACE="$NAMESPACE" -e GIT_NAME=$GIT_NAME -e GIT_BRANCH="$BRANCH_NAME" -e GIT_CHANGE_ID="$CHANGE_ID" plone/volto-addon-ci prettier''' - } - } - ) - } - } - - stage('Tests') { - when { - allOf { - environment name: 'CHANGE_ID', value: '' - anyOf { - not { changelog '.*^Automated release [0-9\\.]+$' } - branch 'master' - } - } - } - steps { - parallel( - - "Volto": { - node(label: 'docker') { - script { - try { - sh '''docker pull plone/volto-addon-ci''' - sh '''docker run -i --name="$BUILD_TAG-volto" -e NAMESPACE="$NAMESPACE" -e GIT_NAME=$GIT_NAME -e GIT_BRANCH="$BRANCH_NAME" -e GIT_CHANGE_ID="$CHANGE_ID" plone/volto-addon-ci''' - sh '''rm -rf xunit-reports''' - sh '''mkdir -p xunit-reports''' - sh '''docker cp $BUILD_TAG-volto:/opt/frontend/my-volto-project/coverage xunit-reports/''' - sh '''docker cp $BUILD_TAG-volto:/opt/frontend/my-volto-project/junit.xml xunit-reports/''' - sh '''docker cp $BUILD_TAG-volto:/opt/frontend/my-volto-project/unit_tests_log.txt xunit-reports/''' - stash name: "xunit-reports", includes: "xunit-reports/**" - archiveArtifacts artifacts: "xunit-reports/unit_tests_log.txt", fingerprint: true - publishHTML (target : [ - allowMissing: false, - alwaysLinkToLastBuild: true, - keepAll: true, - reportDir: 'xunit-reports/coverage/lcov-report', - reportFiles: 'index.html', - reportName: 'UTCoverage', - reportTitles: 'Unit Tests Code Coverage' - ]) - } finally { - catchError(buildResult: 'SUCCESS', stageResult: 'SUCCESS') { - junit testResults: 'xunit-reports/junit.xml', allowEmptyResults: true - } - sh script: '''docker rm -v $BUILD_TAG-volto''', returnStatus: true - } - } - } - } - ) - } - } - - stage('Integration tests') { - when { - allOf { - environment name: 'CHANGE_ID', value: '' - anyOf { - not { changelog '.*^Automated release [0-9\\.]+$' } - branch 'master' - } - } - } - steps { - parallel( - - "Cypress": { - node(label: 'docker') { - script { - try { - sh '''docker pull plone; docker run -d --rm --name="$BUILD_TAG-plone" -e SITE="Plone" -e PROFILES="profile-plone.restapi:blocks" plone fg''' - sh '''docker pull plone/volto-addon-ci; docker run -i --name="$BUILD_TAG-cypress" --link $BUILD_TAG-plone:plone -e NAMESPACE="$NAMESPACE" -e GIT_NAME=$GIT_NAME -e GIT_BRANCH="$BRANCH_NAME" -e GIT_CHANGE_ID="$CHANGE_ID" -e DEPENDENCIES="$DEPENDENCIES" plone/volto-addon-ci cypress''' - } finally { - try { - sh '''rm -rf cypress-reports cypress-results cypress-coverage''' - sh '''mkdir -p cypress-reports cypress-results cypress-coverage''' - sh '''docker cp $BUILD_TAG-cypress:/opt/frontend/my-volto-project/src/addons/$GIT_NAME/cypress/videos cypress-reports/''' - sh '''docker cp $BUILD_TAG-cypress:/opt/frontend/my-volto-project/src/addons/$GIT_NAME/cypress/reports cypress-results/''' - coverage = sh script: '''docker cp $BUILD_TAG-cypress:/opt/frontend/my-volto-project/src/addons/$GIT_NAME/coverage cypress-coverage/''', returnStatus: true - if ( coverage == 0 ) { - publishHTML (target : [allowMissing: false, - alwaysLinkToLastBuild: true, - keepAll: true, - reportDir: 'cypress-coverage/coverage/lcov-report', - reportFiles: 'index.html', - reportName: 'CypressCoverage', - reportTitles: 'Integration Tests Code Coverage']) - } - sh '''touch empty_file; for ok_test in $(grep -E 'file=.*failures="0"' $(grep 'testsuites .*failures="0"' $(find cypress-results -name *.xml) empty_file | awk -F: '{print $1}') empty_file | sed 's/.* file="\\(.*\\)" time.*/\\1/' | sed 's#^cypress/integration/##g' | sed 's#^../../../node_modules/@eeacms/##g'); do rm -f cypress-reports/videos/$ok_test.mp4; rm -f cypress-reports/$ok_test.mp4; done''' - archiveArtifacts artifacts: 'cypress-reports/**/*.mp4', fingerprint: true, allowEmptyArchive: true - stash name: "cypress-coverage", includes: "cypress-coverage/**", allowEmpty: true - } - finally { - catchError(buildResult: 'SUCCESS', stageResult: 'SUCCESS') { - junit testResults: 'cypress-results/**/*.xml', allowEmptyResults: true - } - sh script: "docker stop $BUILD_TAG-plone", returnStatus: true - sh script: "docker rm -v $BUILD_TAG-plone", returnStatus: true - sh script: "docker rm -v $BUILD_TAG-cypress", returnStatus: true - - } - } - } - } - } - - ) - } - } - - stage('Report to SonarQube') { - when { - allOf { - environment name: 'CHANGE_ID', value: '' - anyOf { - branch 'master' - allOf { - branch 'develop' - not { changelog '.*^Automated release [0-9\\.]+$' } - } - } - } - } - steps { - node(label: 'swarm') { - script{ - checkout scm - unstash "xunit-reports" - unstash "cypress-coverage" - def scannerHome = tool 'SonarQubeScanner'; - def nodeJS = tool 'NodeJS11'; - withSonarQubeEnv('Sonarqube') { - sh '''sed -i "s#/opt/frontend/my-volto-project/src/addons/${GIT_NAME}/##g" xunit-reports/coverage/lcov.info''' - sh "export PATH=$PATH:${scannerHome}/bin:${nodeJS}/bin; sonar-scanner -Dsonar.javascript.lcov.reportPaths=./xunit-reports/coverage/lcov.info,./cypress-coverage/coverage/lcov.info -Dsonar.sources=./src -Dsonar.projectKey=$GIT_NAME-$BRANCH_NAME -Dsonar.projectVersion=$BRANCH_NAME-$BUILD_NUMBER" - sh '''try=2; while [ \$try -gt 0 ]; do curl -s -XPOST -u "${SONAR_AUTH_TOKEN}:" "${SONAR_HOST_URL}api/project_tags/set?project=${GIT_NAME}-${BRANCH_NAME}&tags=${SONARQUBE_TAGS},${BRANCH_NAME}" > set_tags_result; if [ \$(grep -ic error set_tags_result ) -eq 0 ]; then try=0; else cat set_tags_result; echo "... Will retry"; sleep 60; try=\$(( \$try - 1 )); fi; done''' - } - } - } - } - } - - stage('Pull Request') { - when { - not { - environment name: 'CHANGE_ID', value: '' - } - environment name: 'CHANGE_TARGET', value: 'master' - } - steps { - node(label: 'docker') { - script { - if ( env.CHANGE_BRANCH != "develop" ) { - error "Pipeline aborted due to PR not made from develop branch" - } - withCredentials([string(credentialsId: 'eea-jenkins-token', variable: 'GITHUB_TOKEN')]) { - sh '''docker pull eeacms/gitflow''' - sh '''docker run -i --rm --name="$BUILD_TAG-gitflow-pr" -e GIT_CHANGE_TARGET="$CHANGE_TARGET" -e GIT_CHANGE_BRANCH="$CHANGE_BRANCH" -e GIT_CHANGE_AUTHOR="$CHANGE_AUTHOR" -e GIT_CHANGE_TITLE="$CHANGE_TITLE" -e GIT_TOKEN="$GITHUB_TOKEN" -e GIT_BRANCH="$BRANCH_NAME" -e GIT_CHANGE_ID="$CHANGE_ID" -e GIT_ORG="$GIT_ORG" -e GIT_NAME="$GIT_NAME" -e LANGUAGE=javascript eeacms/gitflow''' - } - } - } - } - } - - } - - post { - always { - cleanWs(cleanWhenAborted: true, cleanWhenFailure: true, cleanWhenNotBuilt: true, cleanWhenSuccess: true, cleanWhenUnstable: true, deleteDirs: true) - } - changed { - script { - def details = """

${env.JOB_NAME} - Build #${env.BUILD_NUMBER} - ${currentBuild.currentResult}

-

Check console output at ${env.JOB_BASE_NAME} - #${env.BUILD_NUMBER}

- """ - emailext( - subject: '$DEFAULT_SUBJECT', - body: details, - attachLog: true, - compressLog: true, - recipientProviders: [[$class: 'DevelopersRecipientProvider'], [$class: 'CulpritsRecipientProvider']] - ) - } - } - } -} diff --git a/Makefile b/Makefile index add0ada..bfdb33d 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ SHELL=/bin/bash DIR=$(shell basename $$(pwd)) -ADDON ?= "@eeacms/volto-addon-template" +ADDON ?= "@eeacms/volto-quote-block" # We like colors # From: https://coderwall.com/p/izxssa/colored-makefile-for-golang-projects @@ -47,4 +47,5 @@ test-update: .PHONY: help help: ## Show this help. @echo -e "$$(grep -hE '^\S+:.*##' $(MAKEFILE_LIST) | sed -e 's/:.*##\s*/:/' -e 's/^\(.\+\):\(.*\)/\\x1b[36m\1\\x1b[m:\2/' | column -c2 -t -s :)" +)" .*\)/\\x1b[36m\1\\x1b[m:\2/' | column -c2 -t -s :)" diff --git a/Makefile.tpl b/Makefile.tpl deleted file mode 100644 index 2040f7a..0000000 --- a/Makefile.tpl +++ /dev/null @@ -1,49 +0,0 @@ -SHELL=/bin/bash - -DIR=$(shell basename $$(pwd)) -ADDON ?= "<%= addonName %>" - -# We like colors -# From: https://coderwall.com/p/izxssa/colored-makefile-for-golang-projects -RED=`tput setaf 1` -GREEN=`tput setaf 2` -RESET=`tput sgr0` -YELLOW=`tput setaf 3` - -project: - npm install -g yo - npm install -g @plone/generator-volto - npm install -g mrs-developer - yo @plone/volto project --addon ${ADDON} --workspace "src/addons/${DIR}" --no-interactive - ln -sf $$(pwd) project/src/addons/ - cp .project.eslintrc.js .eslintrc.js - cd project && yarn - @echo "-------------------" - @echo "$(GREEN)Volto project is ready!$(RESET)" - @echo "$(RED)Now run: cd project && yarn start$(RESET)" - -all: project - -.PHONY: start-test-backend -start-test-backend: ## Start Test Plone Backend - @echo "$(GREEN)==> Start Test Plone Backend$(RESET)" - docker run -i --rm -e ZSERVER_HOST=0.0.0.0 -e ZSERVER_PORT=55001 -p 55001:55001 -e SITE=plone -e APPLY_PROFILES=plone.app.contenttypes:plone-content,plone.restapi:default,kitconcept.volto:default-homepage -e CONFIGURE_PACKAGES=plone.app.contenttypes,plone.restapi,kitconcept.volto,kitconcept.volto.cors -e ADDONS='plone.app.robotframework plone.app.contenttypes plone.restapi kitconcept.volto' plone ./bin/robot-server plone.app.robotframework.testing.PLONE_ROBOT_TESTING - -.PHONY: start-backend-docker -start-backend-docker: ## Starts a Docker-based backend - @echo "$(GREEN)==> Start Docker-based Plone Backend$(RESET)" - docker run -it --rm --name=plone -p 8080:8080 -e SITE=Plone -e ADDONS="kitconcept.volto" -e ZCML="kitconcept.volto.cors" plone - -.PHONY: test -test: - docker pull plone/volto-addon-ci - docker run -it --rm -e NAMESPACE="@eeacms" -e GIT_NAME="${DIR}" -e RAZZLE_JEST_CONFIG=jest-addon.config.js -v "$$(pwd):/opt/frontend/my-volto-project/src/addons/${DIR}" plone/volto-addon-ci yarn test --watchAll=false - -.PHONY: test-update -test-update: - docker pull plone/volto-addon-ci - docker run -it --rm -e NAMESPACE="@eeacms" -e GIT_NAME="${DIR}" -e RAZZLE_JEST_CONFIG=jest-addon.config.js -v "$$(pwd):/opt/frontend/my-volto-project/src/addons/${DIR}" plone/volto-addon-ci yarn test --watchAll=false -u - -.PHONY: help -help: ## Show this help. - @echo -e "$$(grep -hE '^\S+:.*##' $(MAKEFILE_LIST) | sed -e 's/:.*##\s*/:/' -e 's/^\(.\+\):\(.*\)/\\x1b[36m\1\\x1b[m:\2/' | column -c2 -t -s :)" diff --git a/README.md b/README.md index 7a39e0c..4a10b63 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,18 @@ -# volto-addon-template +# volto-quote-block -[![Releases](https://img.shields.io/github/v/release/eea/volto-addon-template)](https://github.com/eea/volto-addon-template/releases) +[![Releases](https://img.shields.io/github/v/release/eea/volto-quote-block)](https://github.com/eea/volto-quote-block/releases) -[![Pipeline](https://ci.eionet.europa.eu/buildStatus/icon?job=volto-addons%2Fvolto-addon-template%2Fmaster&subject=master)](https://ci.eionet.europa.eu/view/Github/job/volto-addons/job/volto-addon-template/job/master/display/redirect) -[![Lines of Code](https://sonarqube.eea.europa.eu/api/project_badges/measure?project=volto-addon-template-master&metric=ncloc)](https://sonarqube.eea.europa.eu/dashboard?id=volto-addon-template-master) -[![Coverage](https://sonarqube.eea.europa.eu/api/project_badges/measure?project=volto-addon-template-master&metric=coverage)](https://sonarqube.eea.europa.eu/dashboard?id=volto-addon-template-master) -[![Bugs](https://sonarqube.eea.europa.eu/api/project_badges/measure?project=volto-addon-template-master&metric=bugs)](https://sonarqube.eea.europa.eu/dashboard?id=volto-addon-template-master) -[![Duplicated Lines (%)](https://sonarqube.eea.europa.eu/api/project_badges/measure?project=volto-addon-template-master&metric=duplicated_lines_density)](https://sonarqube.eea.europa.eu/dashboard?id=volto-addon-template-master) +[![Pipeline](https://ci.eionet.europa.eu/buildStatus/icon?job=volto-addons%2Fvolto-quote-block%2Fmaster&subject=master)](https://ci.eionet.europa.eu/view/Github/job/volto-addons/job/volto-quote-block/job/master/display/redirect) +[![Lines of Code](https://sonarqube.eea.europa.eu/api/project_badges/measure?project=volto-quote-block-master&metric=ncloc)](https://sonarqube.eea.europa.eu/dashboard?id=volto-quote-block-master) +[![Coverage](https://sonarqube.eea.europa.eu/api/project_badges/measure?project=volto-quote-block-master&metric=coverage)](https://sonarqube.eea.europa.eu/dashboard?id=volto-quote-block-master) +[![Bugs](https://sonarqube.eea.europa.eu/api/project_badges/measure?project=volto-quote-block-master&metric=bugs)](https://sonarqube.eea.europa.eu/dashboard?id=volto-quote-block-master) +[![Duplicated Lines (%)](https://sonarqube.eea.europa.eu/api/project_badges/measure?project=volto-quote-block-master&metric=duplicated_lines_density)](https://sonarqube.eea.europa.eu/dashboard?id=volto-quote-block-master) -[![Pipeline](https://ci.eionet.europa.eu/buildStatus/icon?job=volto-addons%2Fvolto-addon-template%2Fdevelop&subject=develop)](https://ci.eionet.europa.eu/view/Github/job/volto-addons/job/volto-addon-template/job/develop/display/redirect) -[![Lines of Code](https://sonarqube.eea.europa.eu/api/project_badges/measure?project=volto-addon-template-develop&metric=ncloc)](https://sonarqube.eea.europa.eu/dashboard?id=volto-addon-template-develop) -[![Coverage](https://sonarqube.eea.europa.eu/api/project_badges/measure?project=volto-addon-template-develop&metric=coverage)](https://sonarqube.eea.europa.eu/dashboard?id=volto-addon-template-develop) -[![Bugs](https://sonarqube.eea.europa.eu/api/project_badges/measure?project=volto-addon-template-develop&metric=bugs)](https://sonarqube.eea.europa.eu/dashboard?id=volto-addon-template-develop) -[![Duplicated Lines (%)](https://sonarqube.eea.europa.eu/api/project_badges/measure?project=volto-addon-template-develop&metric=duplicated_lines_density)](https://sonarqube.eea.europa.eu/dashboard?id=volto-addon-template-develop) +[![Pipeline](https://ci.eionet.europa.eu/buildStatus/icon?job=volto-addons%2Fvolto-quote-block%2Fdevelop&subject=develop)](https://ci.eionet.europa.eu/view/Github/job/volto-addons/job/volto-quote-block/job/develop/display/redirect) +[![Lines of Code](https://sonarqube.eea.europa.eu/api/project_badges/measure?project=volto-quote-block-develop&metric=ncloc)](https://sonarqube.eea.europa.eu/dashboard?id=volto-quote-block-develop) +[![Coverage](https://sonarqube.eea.europa.eu/api/project_badges/measure?project=volto-quote-block-develop&metric=coverage)](https://sonarqube.eea.europa.eu/dashboard?id=volto-quote-block-develop) +[![Bugs](https://sonarqube.eea.europa.eu/api/project_badges/measure?project=volto-quote-block-develop&metric=bugs)](https://sonarqube.eea.europa.eu/dashboard?id=volto-quote-block-develop) +[![Duplicated Lines (%)](https://sonarqube.eea.europa.eu/api/project_badges/measure?project=volto-quote-block-develop&metric=duplicated_lines_density)](https://sonarqube.eea.europa.eu/dashboard?id=volto-quote-block-develop) [Volto](https://github.com/plone/volto) add-on @@ -23,7 +23,7 @@ Demo GIF ## Getting started -### Try volto-addon-template with Docker +### Try volto-quote-block with Docker 1. Get the latest Docker images @@ -40,12 +40,12 @@ Demo GIF 1. Start Volto frontend ``` - docker run -it --rm -p 3000:3000 --link plone -e ADDONS="@eeacms/volto-addon-template" plone/volto + docker run -it --rm -p 3000:3000 --link plone -e ADDONS="@eeacms/volto-quote-block" plone/volto ``` 1. Go to http://localhost:3000 -### Add volto-addon-template to your Volto project +### Add volto-quote-block to your Volto project 1. Make sure you have a [Plone backend](https://plone.org/download) up-and-running at http://localhost:8080/Plone @@ -55,11 +55,11 @@ Demo GIF ```JSON "addons": [ - "@eeacms/volto-addon-template" + "@eeacms/volto-quote-block" ], "dependencies": { - "@eeacms/volto-addon-template": "^1.0.0" + "@eeacms/volto-quote-block": "^1.0.0" } ``` @@ -67,7 +67,7 @@ Demo GIF ``` npm install -g yo @plone/generator-volto - yo @plone/volto my-volto-project --addon @eeacms/volto-addon-template + yo @plone/volto my-volto-project --addon @eeacms/volto-quote-block cd my-volto-project ``` @@ -84,18 +84,23 @@ Demo GIF ## Release -See [RELEASE.md](https://github.com/eea/volto-addon-template/blob/master/RELEASE.md). +See [RELEASE.md](https://github.com/eea/volto-quote-block/blob/master/RELEASE.md). ## How to contribute -See [DEVELOP.md](https://github.com/eea/volto-addon-template/blob/master/DEVELOP.md). +See [DEVELOP.md](https://github.com/eea/volto-quote-block/blob/master/DEVELOP.md). ## Copyright and license The Initial Owner of the Original Code is European Environment Agency (EEA). All Rights Reserved. -See [LICENSE.md](https://github.com/eea/volto-addon-template/blob/master/LICENSE.md) for details. +See [LICENSE.md](https://github.com/eea/volto-quote-block/blob/master/LICENSE.md) for details. + +## Funding + +[European Environment Agency (EU)](http://eea.europa.eu) +/LICENSE.md) for details. ## Funding diff --git a/README.md.tpl b/README.md.tpl deleted file mode 100644 index 27df35f..0000000 --- a/README.md.tpl +++ /dev/null @@ -1,102 +0,0 @@ -# <%= name %> - -[![Releases](https://img.shields.io/github/v/release/eea/<%= name %>)](https://github.com/eea/<%= name %>/releases) - -[![Pipeline](https://ci.eionet.europa.eu/buildStatus/icon?job=volto-addons%2F<%= name %>%2Fmaster&subject=master)](https://ci.eionet.europa.eu/view/Github/job/volto-addons/job/<%= name %>/job/master/display/redirect) -[![Lines of Code](https://sonarqube.eea.europa.eu/api/project_badges/measure?project=<%= name %>-master&metric=ncloc)](https://sonarqube.eea.europa.eu/dashboard?id=<%= name %>-master) -[![Coverage](https://sonarqube.eea.europa.eu/api/project_badges/measure?project=<%= name %>-master&metric=coverage)](https://sonarqube.eea.europa.eu/dashboard?id=<%= name %>-master) -[![Bugs](https://sonarqube.eea.europa.eu/api/project_badges/measure?project=<%= name %>-master&metric=bugs)](https://sonarqube.eea.europa.eu/dashboard?id=<%= name %>-master) -[![Duplicated Lines (%)](https://sonarqube.eea.europa.eu/api/project_badges/measure?project=<%= name %>-master&metric=duplicated_lines_density)](https://sonarqube.eea.europa.eu/dashboard?id=<%= name %>-master) - -[![Pipeline](https://ci.eionet.europa.eu/buildStatus/icon?job=volto-addons%2F<%= name %>%2Fdevelop&subject=develop)](https://ci.eionet.europa.eu/view/Github/job/volto-addons/job/<%= name %>/job/develop/display/redirect) -[![Lines of Code](https://sonarqube.eea.europa.eu/api/project_badges/measure?project=<%= name %>-develop&metric=ncloc)](https://sonarqube.eea.europa.eu/dashboard?id=<%= name %>-develop) -[![Coverage](https://sonarqube.eea.europa.eu/api/project_badges/measure?project=<%= name %>-develop&metric=coverage)](https://sonarqube.eea.europa.eu/dashboard?id=<%= name %>-develop) -[![Bugs](https://sonarqube.eea.europa.eu/api/project_badges/measure?project=<%= name %>-develop&metric=bugs)](https://sonarqube.eea.europa.eu/dashboard?id=<%= name %>-develop) -[![Duplicated Lines (%)](https://sonarqube.eea.europa.eu/api/project_badges/measure?project=<%= name %>-develop&metric=duplicated_lines_density)](https://sonarqube.eea.europa.eu/dashboard?id=<%= name %>-develop) - - -[Volto](https://github.com/plone/volto) add-on - -## Features - -Demo GIF - -## Getting started - -### Try <%= name %> with Docker - -1. Get the latest Docker images - - ``` - docker pull plone - docker pull plone/volto - ``` - -1. Start Plone backend - ``` - docker run -d --name plone -p 8080:8080 -e SITE=Plone -e PROFILES="profile-plone.restapi:blocks" plone - ``` - -1. Start Volto frontend - - ``` - docker run -it --rm -p 3000:3000 --link plone -e ADDONS="<%= addonName %>" plone/volto - ``` - -1. Go to http://localhost:3000 - -### Add <%= name %> to your Volto project - -1. Make sure you have a [Plone backend](https://plone.org/download) up-and-running at http://localhost:8080/Plone - -1. Start Volto frontend - -* If you already have a volto project, just update `package.json`: - - ```JSON - "addons": [ - "<%= addonName %>" - ], - - "dependencies": { - "<%= addonName %>": "^1.0.0" - } - ``` - -* If not, create one: - - ``` - npm install -g yo @plone/generator-volto - yo @plone/volto my-volto-project --addon <%= addonName %> - cd my-volto-project - ``` - -1. Install new add-ons and restart Volto: - - ``` - yarn - yarn start - ``` - -1. Go to http://localhost:3000 - -1. Happy editing! - -## Release - -See [RELEASE.md](https://github.com/eea/<%= name %>/blob/master/RELEASE.md). - -## How to contribute - -See [DEVELOP.md](https://github.com/eea/<%= name %>/blob/master/DEVELOP.md). - -## Copyright and license - -The Initial Owner of the Original Code is European Environment Agency (EEA). -All Rights Reserved. - -See [LICENSE.md](https://github.com/eea/<%= name %>/blob/master/LICENSE.md) for details. - -## Funding - -[European Environment Agency (EU)](http://eea.europa.eu) diff --git a/package.json b/package.json index 707eeff..75ccc36 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,11 @@ { - "name": "@eeacms/volto-addon-template", - "version": "0.1.0", - "description": "@eeacms/volto-addon-template: Volto add-on", + "name": "@eeacms/volto-quote-block", + "version": "0.1.1", + "description": "@eeacms/volto-quote-block: Volto add-on", "main": "src/index.js", "author": "European Environment Agency: IDM2 A-Team", "license": "MIT", - "homepage": "https://github.com/eea/volto-addon-template", + "homepage": "https://github.com/eea/volto-quote-block", "keywords": [ "volto-addon", "volto", @@ -14,10 +14,14 @@ ], "repository": { "type": "git", - "url": "git@github.com:eea/volto-addon-template.git" + "url": "git@github.com:eea/volto-quote-block.git" }, + "addons": [ + "volto-slate" + ], "dependencies": { - "@plone/scripts": "*" + "@plone/scripts": "*", + "volto-slate": "*" }, "devDependencies": { "@cypress/code-coverage": "^3.9.5", @@ -25,14 +29,12 @@ }, "scripts": { "release": "release-it", - "release-major-beta": "release-it major --preRelease=beta", - "release-beta": "release-it --preRelease=beta", "bootstrap": "npm install -g ejs; npm link ejs; node bootstrap", "test": "make test", "test:fix": "make test-update", "pre-commit": "yarn stylelint:fix && yarn prettier:fix && yarn lint:fix", - "stylelint": "if [ -d ./project ]; then ./project/node_modules/stylelint/bin/stylelint.js --allow-empty-input 'src/**/*.{css,less}'; else ../../../node_modules/stylelint/bin/stylelint.js --allow-empty-input 'src/**/*.{css,less}'; fi", - "stylelint:overrides": "if [ -d ./project ]; then ./project/node_modules/.bin/stylelint --syntax less --allow-empty-input 'theme/**/*.overrides' 'src/**/*.overrides'; else ../../../node_modules/.bin/stylelint --syntax less --allow-empty-input 'theme/**/*.overrides' 'src/**/*.overrides'; fi", + "stylelint": "../../../node_modules/.bin/stylelint 'theme/**/*.{css,less}' 'src/**/*.{css,less}'", + "stylelint:overrides": "../../../node_modules/.bin/stylelint --allow-empty-input 'theme/**/*.overrides' 'src/**/*.overrides'", "stylelint:fix": "yarn stylelint --fix && yarn stylelint:overrides --fix", "prettier": "if [ -d ./project ]; then ./project/node_modules/.bin/prettier --single-quote --check 'src/**/*.{js,jsx,json,css,less,md}'; else ../../../node_modules/.bin/prettier --single-quote --check 'src/**/*.{js,jsx,json,css,less,md}'; fi", "prettier:fix": "if [ -d ./project ]; then ./project/node_modules/.bin/prettier --single-quote --write 'src/**/*.{js,jsx,json,css,less,md}'; else ../../../node_modules/.bin/prettier --single-quote --write 'src/**/*.{js,jsx,json,css,less,md}'; fi", diff --git a/package.json.tpl b/package.json.tpl deleted file mode 100644 index e8ac5cf..0000000 --- a/package.json.tpl +++ /dev/null @@ -1,43 +0,0 @@ -{ - "name": "<%= addonName %>", - "version": "0.1.0", - "description": "<%= addonName %>: Volto add-on", - "main": "src/index.js", - "author": "European Environment Agency: IDM2 A-Team", - "license": "MIT", - "homepage": "https://github.com/eea/<%= name %>", - "keywords": [ - "volto-addon", - "volto", - "plone", - "react" - ], - "repository": { - "type": "git", - "url": "git@github.com:eea/<%= name %>.git" - }, - "dependencies": { - "@plone/scripts": "*" - }, - "devDependencies": { - "@cypress/code-coverage": "^3.9.5", - "babel-plugin-transform-class-properties": "^6.24.1" - }, - "scripts": { - "release": "release-it", - "bootstrap": "npm install -g ejs; npm link ejs; node bootstrap", - "test": "make test", - "test:fix": "make test-update", - "pre-commit": "yarn stylelint:fix && yarn prettier:fix && yarn lint:fix", - "stylelint": "if [ -d ./project ]; then ./project/node_modules/stylelint/bin/stylelint.js --allow-empty-input 'src/**/*.{css,less}'; else ../../../node_modules/stylelint/bin/stylelint.js --allow-empty-input 'src/**/*.{css,less}'; fi", - "stylelint:overrides": "if [ -d ./project ]; then ./project/node_modules/.bin/stylelint --syntax less --allow-empty-input 'theme/**/*.overrides' 'src/**/*.overrides'; else ../../../node_modules/.bin/stylelint --syntax less --allow-empty-input 'theme/**/*.overrides' 'src/**/*.overrides'; fi", - "stylelint:fix": "yarn stylelint --fix && yarn stylelint:overrides --fix", - "prettier": "if [ -d ./project ]; then ./project/node_modules/.bin/prettier --single-quote --check 'src/**/*.{js,jsx,json,css,less,md}'; else ../../../node_modules/.bin/prettier --single-quote --check 'src/**/*.{js,jsx,json,css,less,md}'; fi", - "prettier:fix": "if [ -d ./project ]; then ./project/node_modules/.bin/prettier --single-quote --write 'src/**/*.{js,jsx,json,css,less,md}'; else ../../../node_modules/.bin/prettier --single-quote --write 'src/**/*.{js,jsx,json,css,less,md}'; fi", - "lint": "if [ -d ./project ]; then ./project/node_modules/eslint/bin/eslint.js --max-warnings=0 'src/**/*.{js,jsx}'; else ../../../node_modules/eslint/bin/eslint.js --max-warnings=0 'src/**/*.{js,jsx}'; fi", - "lint:fix": "if [ -d ./project ]; then ./project/node_modules/eslint/bin/eslint.js --fix 'src/**/*.{js,jsx}'; else ../../../node_modules/eslint/bin/eslint.js --fix 'src/**/*.{js,jsx}'; fi", - "i18n": "rm -rf build/messages && NODE_ENV=production i18n --addon", - "cypress:run": "if [ -d ./project ]; then NODE_ENV=development ./project/node_modules/cypress/bin/cypress run; else NODE_ENV=development ../../../node_modules/cypress/bin/cypress run; fi", - "cypress:open": "if [ -d ./project ]; then NODE_ENV=development ./project/node_modules/cypress/bin/cypress open; else NODE_ENV=development ../../../node_modules/cypress/bin/cypress open; fi" - } -} diff --git a/src/Blocks/Quote/Edit.jsx b/src/Blocks/Quote/Edit.jsx new file mode 100644 index 0000000..91eb511 --- /dev/null +++ b/src/Blocks/Quote/Edit.jsx @@ -0,0 +1,35 @@ +import React from 'react'; +import { SidebarPortal } from '@plone/volto/components'; +import InlineForm from '@plone/volto/components/manage/Form/InlineForm'; +import View from './View'; +import getSchema from './schema'; + +import './styles.less'; + +const Edit = (props) => { + const { data = {}, block = null, selected = false, onChangeBlock } = props; + + const schema = getSchema(props); + + return ( + <> + + + + { + onChangeBlock(block, { + ...data, + [id]: value, + }); + }} + formData={data} + /> + + + ); +}; + +export default Edit; diff --git a/src/Blocks/Quote/View.jsx b/src/Blocks/Quote/View.jsx new file mode 100644 index 0000000..1a18bb7 --- /dev/null +++ b/src/Blocks/Quote/View.jsx @@ -0,0 +1,26 @@ +import React from 'react'; +import config from '@plone/volto/registry'; + +import './styles.less'; + +const View = (props) => { + const { data, mode, block, onChangeBlock } = props; + const { template = 'default' } = data; + + const Quote = + config.blocks.blocksConfig.quote.templates[template]?.view || (() => ''); + + React.useEffect(() => { + if (mode === 'edit' && !template) { + onChangeBlock(block, { + ...data, + template: 'default', + }); + } + /* eslint-disable-next-line */ + }, []); + + return ; +}; + +export default View; diff --git a/src/Blocks/Quote/index.js b/src/Blocks/Quote/index.js new file mode 100644 index 0000000..865ee3e --- /dev/null +++ b/src/Blocks/Quote/index.js @@ -0,0 +1,40 @@ +import quoteSVG from '@plone/volto/icons/quote.svg'; +import QuoteEdit from './Edit'; +import QuoteView from './View'; + +import BlockquoteView from './templates/Blockquote/Blockquote'; +import PullquoteView from './templates/Pullquote/Pullquote'; + +export default (config) => { + config.blocks.blocksConfig.quote = { + id: 'quote', + title: 'Quote', + icon: quoteSVG, + group: 'text', + edit: QuoteEdit, + view: QuoteView, + templates: { + default: { + title: 'Quote', + view: PullquoteView, + icons: { + openQuote: 'quote left', + closeQuote: 'quote right', + }, + }, + blockquote: { + title: 'Blockquote', + view: BlockquoteView, + }, + }, + blockHasOwnFocusManagement: true, + restricted: false, + mostUsed: false, + sidebarTab: 1, + security: { + addPermission: [], + view: [], + }, + }; + return config; +}; diff --git a/src/Blocks/Quote/schema.js b/src/Blocks/Quote/schema.js new file mode 100644 index 0000000..d373936 --- /dev/null +++ b/src/Blocks/Quote/schema.js @@ -0,0 +1,68 @@ +import config from '@plone/volto/registry'; + +export default (props) => { + const { position } = props.data; + const templatesConfig = config.blocks.blocksConfig.quote?.templates; + const templates = Object.keys(templatesConfig).map((template) => [ + template, + templatesConfig[template].title || template, + ]); + const schema = + templatesConfig[props.data?.template || 'default']?.schema || []; + const templateSchema = typeof schema === 'function' ? schema(props) : schema; + const defaultFieldset = + templateSchema.fieldsets?.filter( + (fieldset) => fieldset.id === 'default', + )[0] || {}; + return { + title: templateSchema.title || 'Quote', + fieldsets: [ + { + id: 'default', + title: 'Default', + fields: [ + 'template', + ...(position && ['left', 'right'].includes(position) + ? ['quote'] + : []), + 'reversed', + 'position', + 'source', + 'metadata', + ...(defaultFieldset?.fields || []), + ], + }, + ...(templateSchema.fieldsets?.filter( + (fieldset) => fieldset.id !== 'default', + ) || []), + ], + properties: { + template: { + title: 'Template', + choices: templates, + }, + reversed: { + title: 'Reversed', + type: 'boolean', + }, + position: { + title: 'Alignment', + widget: 'align', + }, + quote: { + title: 'Quote', + widget: 'slate_richtext', + }, + source: { + title: 'Source', + widget: 'slate_richtext', + }, + metadata: { + title: 'Extra info', + widget: 'slate_richtext', + }, + ...(templateSchema.properties || {}), + }, + required: [...(templateSchema.required || [])], + }; +}; diff --git a/src/Blocks/Quote/styles.less b/src/Blocks/Quote/styles.less new file mode 100644 index 0000000..edbaf13 --- /dev/null +++ b/src/Blocks/Quote/styles.less @@ -0,0 +1,7 @@ +.eea.blockquote .quote > *:not(.meta) { + margin-bottom: 0; +} + +.eea.pullquote .quotes { + flex-flow: column; +} diff --git a/src/Blocks/Quote/templates/Blockquote/Blockquote.jsx b/src/Blocks/Quote/templates/Blockquote/Blockquote.jsx new file mode 100644 index 0000000..650cb3c --- /dev/null +++ b/src/Blocks/Quote/templates/Blockquote/Blockquote.jsx @@ -0,0 +1,157 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import cx from 'classnames'; +import { Message } from 'semantic-ui-react'; +import config from '@plone/volto/registry'; +import SlateEditor from 'volto-slate/editor/SlateEditor'; +import { handleKey } from 'volto-slate/blocks/Text/keyboard'; +import { saveSlateBlockSelection } from 'volto-slate/actions'; +import { + createSlateParagraph, + isFloated, + serializeText, + textNotEmpty, +} from '@eeacms/volto-quote-block/helpers'; + +import '@eeacms/volto-quote-block/less/blockquote.less'; + +const BlockquoteWrapper = (props) => { + const { children, index, block, mode, handleKeyDown } = props; + return mode === 'edit' ? ( +
{ + handleKeyDown(e, index, block, props.blockNode.current); + }} + style={{ outline: 'none' }} + // The tabIndex is required for the keyboard navigation + /* eslint-disable jsx-a11y/no-noninteractive-tabindex */ + tabIndex={0} + > + {children} +
+ ) : ( + children + ); +}; + +const Blockquote = (props) => { + const { + data, + mode, + index, + block, + selected, + properties, + onAddBlock, + onChangeBlock, + onFocusNextBlock, + onFocusPreviousBlock, + onSelectBlock, + } = props; + const { slate } = config.settings; + const { quote, source, metadata, position = null, reversed = false } = data; + const floated = isFloated(position); + const withInfo = textNotEmpty(source || metadata); + + const withBlockProperties = React.useCallback( + (editor) => { + editor.getBlockProps = () => props; + return editor; + }, + [props], + ); + + const handleFocus = React.useCallback(() => { + if (!selected) { + onSelectBlock(block); + } + }, [onSelectBlock, selected, block]); + + const handleKeyDown = React.useCallback( + ( + e, + index, + block, + node, + { + disableEnter = false, + disableArrowUp = false, + disableArrowDown = false, + } = {}, + ) => { + if (mode === 'edit' && !floated) return; + const isMultipleSelection = e.shiftKey; + if (e.key === 'ArrowUp' && !disableArrowUp) { + onFocusPreviousBlock(block, node, isMultipleSelection); + e.preventDefault(); + } + if (e.key === 'ArrowDown' && !disableArrowDown) { + onFocusNextBlock(block, node, isMultipleSelection); + e.preventDefault(); + } + if ((e.key === 'Return' || e.key === 'Enter') && !disableEnter) { + onAddBlock(config.settings.defaultBlockType, index + 1); + e.preventDefault(); + } + }, + [onAddBlock, onFocusPreviousBlock, onFocusNextBlock, mode, floated], + ); + + return ( + + {mode === 'edit' && floated && ( + + Click here to edit blockquote. + + )} +
+

+ {mode === 'edit' && !floated ? ( + { + onChangeBlock(block, { + ...data, + quote, + }); + }} + block={block} + onFocus={handleFocus} + onKeyDown={handleKey} + selected={selected} + placeholder="Add quote" + slateSettings={slate} + /> + ) : ( + serializeText(quote) + )} +

+ {withInfo && ( +
+ {source &&

{serializeText(source)}

} + {metadata &&

{serializeText(metadata)}

} +
+ )} +
+
+ ); +}; + +export default connect( + () => { + return {}; + }, + { + saveSlateBlockSelection, + }, +)(Blockquote); diff --git a/src/Blocks/Quote/templates/Pullquote/Pullquote.jsx b/src/Blocks/Quote/templates/Pullquote/Pullquote.jsx new file mode 100644 index 0000000..14b12b3 --- /dev/null +++ b/src/Blocks/Quote/templates/Pullquote/Pullquote.jsx @@ -0,0 +1,173 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import cx from 'classnames'; +import { Icon, Message } from 'semantic-ui-react'; +import config from '@plone/volto/registry'; +import SlateEditor from 'volto-slate/editor/SlateEditor'; +import { handleKey } from 'volto-slate/blocks/Text/keyboard'; +import { saveSlateBlockSelection } from 'volto-slate/actions'; +import { + createSlateParagraph, + isFloated, + serializeText, + textNotEmpty, +} from '@eeacms/volto-quote-block/helpers'; + +import '@eeacms/volto-quote-block/less/pullquote.less'; + +const PullquoteWrapper = (props) => { + const { children, index, block, mode, handleKeyDown } = props; + return mode === 'edit' ? ( +
{ + handleKeyDown(e, index, block, props.blockNode.current); + }} + style={{ outline: 'none' }} + // The tabIndex is required for the keyboard navigation + /* eslint-disable jsx-a11y/no-noninteractive-tabindex */ + tabIndex={0} + > + {children} +
+ ) : ( + children + ); +}; + +const Pullquote = (props) => { + const { slate } = config.settings; + const { icons } = config.blocks.blocksConfig.quote.templates.default || {}; + const { + data, + mode, + index, + block, + selected, + properties, + onAddBlock, + onChangeBlock, + onFocusNextBlock, + onFocusPreviousBlock, + onSelectBlock, + } = props; + const { quote, source, metadata, position = null, reversed = false } = data; + const floated = isFloated(position); + const withInfo = textNotEmpty(source || metadata); + + const withBlockProperties = React.useCallback( + (editor) => { + editor.getBlockProps = () => props; + return editor; + }, + [props], + ); + + const handleFocus = React.useCallback(() => { + if (!selected) { + onSelectBlock(block); + } + }, [onSelectBlock, selected, block]); + + const handleKeyDown = React.useCallback( + ( + e, + index, + block, + node, + { + disableEnter = false, + disableArrowUp = false, + disableArrowDown = false, + } = {}, + ) => { + if (mode === 'edit' && !floated) return; + const isMultipleSelection = e.shiftKey; + if (e.key === 'ArrowUp' && !disableArrowUp) { + onFocusPreviousBlock(block, node, isMultipleSelection); + e.preventDefault(); + } + if (e.key === 'ArrowDown' && !disableArrowDown) { + onFocusNextBlock(block, node, isMultipleSelection); + e.preventDefault(); + } + if ((e.key === 'Return' || e.key === 'Enter') && !disableEnter) { + onAddBlock(config.settings.defaultBlockType, index + 1); + } + }, + [onAddBlock, onFocusPreviousBlock, onFocusNextBlock, mode, floated], + ); + + return ( + + {mode === 'edit' && floated && ( + + Click here to edit quote. + + )} +
+ {mode === 'edit' && !floated ? ( + + { + onChangeBlock(block, { + ...data, + quote, + }); + }} + block={block} + onFocus={handleFocus} + onKeyDown={handleKey} + selected={selected} + placeholder="Add quote" + slateSettings={slate} + /> + + ) : ( + + {serializeText(quote)} + + )} + {withInfo && ( +
+ {source &&

{serializeText(source)}

} + {metadata &&

{serializeText(metadata)}

} +
+ )} +
+
+ ); +}; + +Pullquote.Quote = ({ children, icons, as: As, ...rest }) => ( +
+ + {As ? ( + + {children} + + ) : ( +

{children}

+ )} + +
+); + +export default connect( + () => { + return {}; + }, + { + saveSlateBlockSelection, + }, +)(Pullquote); diff --git a/src/SlatePlugins/Quote/Quote.jsx b/src/SlatePlugins/Quote/Quote.jsx new file mode 100644 index 0000000..1033cc7 --- /dev/null +++ b/src/SlatePlugins/Quote/Quote.jsx @@ -0,0 +1,11 @@ +import React from 'react'; +import templates from './templates'; + +function Quote(props) { + const data = props.element.data || {}; + const QuoteElement = templates[data.template || 'default'].view; + + return ; +} + +export default Quote; diff --git a/src/SlatePlugins/Quote/extensions.js b/src/SlatePlugins/Quote/extensions.js new file mode 100644 index 0000000..9b80c5b --- /dev/null +++ b/src/SlatePlugins/Quote/extensions.js @@ -0,0 +1,45 @@ +import { nanoid } from 'volto-slate/utils'; +import { Transforms } from 'slate'; + +export const withBlockquote = (editor) => { + const { normalizeNode, isInline } = editor; + + editor.isInline = (element) => { + return element.type === 'blockquote' ? true : isInline(element); + }; + + editor.normalizeNode = (entry) => { + const [node, path] = entry; + + if (node.type === 'blockquote' && !node.data?.uid) { + Transforms.setNodes( + editor, + { + data: { + uid: nanoid(5), + }, + }, + { + at: path, + }, + ); + } + return normalizeNode(entry); + }; + + return editor; +}; + +// will replace existing uid with a new one +// this will be usefull when copy/pase items have the same uid +export const withBeforeInsertFragment = (editor) => { + const { beforeInsertFragment } = editor; + editor.beforeInsertFragment = (parsed) => { + if (parsed?.[0]?.children?.[0]?.data?.uid) { + parsed[0].children[0].data.uid = nanoid(5); + } + return beforeInsertFragment ? beforeInsertFragment(parsed) : parsed; + }; + + return editor; +}; diff --git a/src/SlatePlugins/Quote/index.js b/src/SlatePlugins/Quote/index.js new file mode 100644 index 0000000..6f32613 --- /dev/null +++ b/src/SlatePlugins/Quote/index.js @@ -0,0 +1,45 @@ +import { defineMessages } from 'react-intl'; +import { makeInlineElementPlugin } from 'volto-slate/components/ElementEditor'; +import Quote from './Quote'; +import quoteSchema from './schema'; +import { withBlockquote, withBeforeInsertFragment } from './extensions'; + +import quoteIcon from '@plone/volto/icons/quote.svg'; + +const messages = defineMessages({ + edit: { + id: 'Edit quote', + defaultMessage: 'Edit quote', + }, + delete: { + id: 'Remove quote', + defaultMessage: 'Remove quote', + }, +}); + +export default (config) => { + const opts = { + title: 'Quote', + pluginId: 'blockquote', + elementType: 'blockquote', + element: Quote, + isInlineElement: true, + editSchema: quoteSchema, + extensions: [withBlockquote, withBeforeInsertFragment], + hasValue: (formData) => !!formData, + toolbarButtonIcon: quoteIcon, + messages, + }; + + const [installQuote] = makeInlineElementPlugin(opts); + config = installQuote(config); + + config.settings.slateQuote = { + icons: { + openQuote: 'quote left', + closeQuote: 'quote right', + }, + }; + + return config; +}; diff --git a/src/SlatePlugins/Quote/schema.js b/src/SlatePlugins/Quote/schema.js new file mode 100644 index 0000000..d938f90 --- /dev/null +++ b/src/SlatePlugins/Quote/schema.js @@ -0,0 +1,34 @@ +export default { + title: 'Quote', + fieldsets: [ + { + id: 'default', + title: 'Properties', + fields: ['reversed', 'position', 'source', 'metadata'], + }, + ], + properties: { + // template: { + // title: 'Template', + // choices: [['default', 'Default']], + // default: 'default', + // }, + reversed: { + title: 'Reversed', + type: 'boolean', + }, + position: { + title: 'Alignment', + widget: 'align', + }, + source: { + title: 'Source', + widget: 'slate_richtext', + }, + metadata: { + title: 'Extra info', + widget: 'slate_richtext', + }, + }, + required: [], +}; diff --git a/src/SlatePlugins/Quote/templates/default/View.jsx b/src/SlatePlugins/Quote/templates/default/View.jsx new file mode 100644 index 0000000..29434df --- /dev/null +++ b/src/SlatePlugins/Quote/templates/default/View.jsx @@ -0,0 +1,40 @@ +import React from 'react'; +import cx from 'classnames'; +import { Icon } from 'semantic-ui-react'; +import config from '@plone/volto/registry'; +import { serializeText } from '@eeacms/volto-quote-block/helpers'; + +import '@eeacms/volto-quote-block/less/pullquote.less'; + +const Pullquote = ({ children, element }) => { + const { source, metadata, position = null, reversed = false } = + element?.data || {}; + + return ( +
+ {children} + {(source || metadata) && ( +
+ {source &&

{serializeText(source)}

} + {metadata &&

{serializeText(metadata)}

} +
+ )} +
+ ); +}; + +Pullquote.Quote = ({ children, as: As, ...rest }) => ( +
+ + {As ? ( + + {children} + + ) : ( +

{children}

+ )} + +
+); + +export default Pullquote; diff --git a/src/SlatePlugins/Quote/templates/index.js b/src/SlatePlugins/Quote/templates/index.js new file mode 100644 index 0000000..73f81f1 --- /dev/null +++ b/src/SlatePlugins/Quote/templates/index.js @@ -0,0 +1,7 @@ +import DefaultQuote from './default/View'; + +export default { + default: { + view: DefaultQuote, + }, +}; diff --git a/src/SlatePlugins/index.js b/src/SlatePlugins/index.js new file mode 100644 index 0000000..7c259ea --- /dev/null +++ b/src/SlatePlugins/index.js @@ -0,0 +1,5 @@ +import installQuotePlugin from './Quote'; + +export default (config) => { + return [installQuotePlugin].reduce((acc, apply) => apply(acc), config); +}; diff --git a/src/helpers.js b/src/helpers.js new file mode 100644 index 0000000..c29d4fe --- /dev/null +++ b/src/helpers.js @@ -0,0 +1,25 @@ +import { isArray } from 'lodash'; +import config from '@plone/volto/registry'; +import { + serializeNodes, + serializeNodesToText, +} from 'volto-slate/editor/render'; + +export const createSlateParagraph = (text) => { + return isArray(text) ? text : config.settings.slate.defaultValue(); +}; + +export const serializeText = (text) => { + return isArray(text) ? serializeNodes(text) : text; +}; + +export const isFloated = (position) => { + return position && ['left', 'right'].includes(position); +}; + +export const textNotEmpty = (text) => { + if (text && isArray(text) && serializeNodesToText(text).length > 0) + return true; + if (text && typeof text === 'string' && text.length > 0) return true; + return false; +}; diff --git a/src/index.js b/src/index.js index cb042f0..3cf32b0 100644 --- a/src/index.js +++ b/src/index.js @@ -1,5 +1,7 @@ +import installQuoteBlock from './Blocks/Quote'; + const applyConfig = (config) => { - return config; + return [installQuoteBlock].reduce((acc, apply) => apply(acc), config); }; export default applyConfig; diff --git a/src/less/blockquote.less b/src/less/blockquote.less new file mode 100644 index 0000000..e98b761 --- /dev/null +++ b/src/less/blockquote.less @@ -0,0 +1,132 @@ +@import './globals.less'; + +@addon: 'volto-addons'; +@addontype: 'quoteBlock'; +@addonelement: 'blockquote'; + +.loadAddonVariables(); + +/******************************* + Blockquote +*******************************/ + +#page-edit { + .eea.blockquote { + .author, + .meta { + margin-bottom: 0; + + > * { + margin-bottom: 0; + } + } + } +} + +.eea.blockquote { + display: flex; + flex-flow: column; + padding: @mobileQuotePadding; + border: @calloutBorder; + border-left: @mobileBlockquoteBorderLeft; + margin: 0; + border-radius: @calloutBorderRadius; + box-shadow: @calloutBoxShadow; + + .quote { + margin: @quoteMargin; + color: @quoteColor; + font-size: @mobileQuoteFontSize; + } + + .author { + margin: @authorMargin; + color: @authorTextColor; + font-size: @mobileAuthorFontSize; + font-weight: @authorFontWeight; + text-align: @authorTextAlign; + } + + .meta { + font-size: @mobileMetaFontSize; + font-weight: @metaFontWeight; + text-align: @metaTextAlign; + } + + &.with-info { + .quote { + margin: @quoteWithInfoMargin; + } + } + + &.reversed { + flex-flow: column-reverse; + + .quote { + margin: @quoteReversedMargin; + } + + .info.wrapper { + margin-bottom: 1.25rem; + } + + .author { + text-align: @authorReversedTextAlign; + } + + .meta { + text-align: @metaReversedTextAlign; + } + } +} + +@media only screen and (min-width: @tabletBreakpoint) { + .eea.blockquote { + padding: @tabletQuotePadding; + border-left: @tabletBlockquoteBorderLeft; + + .quote { + font-size: @tabletQuoteFontSize; + } + + .author { + font-size: @tabletAuthorFontSize; + } + + .meta { + font-size: @tabletMetaFontSize; + } + } + + .eea.blockquote.left { + width: @maxWidthOnFloat; + margin: @blockquoteFloatLeftMargin; + float: left; + } + + .eea.blockquote.right { + width: @maxWidthOnFloat; + margin: @blockquoteFloatRightMargin; + float: right; + } +} + +@media only screen and (min-width: @computerBreakpoint) { + .eea.blockquote { + padding: @computerQuotePadding; + + .quote { + font-size: @computerQuoteFontSize; + } + + .author { + font-size: @computerAuthorFontSize; + } + + .meta { + font-size: @computerMetaFontSize; + } + } +} + +.loadAddonVariables(); diff --git a/src/less/blockquote.variables b/src/less/blockquote.variables new file mode 100644 index 0000000..ed438f6 --- /dev/null +++ b/src/less/blockquote.variables @@ -0,0 +1,46 @@ +/******************************* + Blockquote +*******************************/ +/* Body */ +@mobileBlockquoteBorderLeft : @5px solid @secondaryColor; +@tabletBlockquoteBorderLeft : @10px solid @secondaryColor; +@blockquoteFloatLeftMargin : @largeGap @largeGap @largeGap 0; +@blockquoteFloatRightMargin : @largeGap 0 @largeGap @largeGap; +@maxWidthOnFloat : 50%; + +/* Quote */ +@mobileQuotePadding : 0 0.5rem; +@tabletQuotePadding : 0 1rem; +@computerQuotePadding : 0 1.25rem; +@quoteMargin : 0; +@quoteWithInfoMargin : 0 0 1.25rem 0; +@quoteReversedMargin : 0; +@quoteColor : #2E3E4C; +@mobileQuoteFontSize : 1rem; +@tabletQuoteFontSize : @h6; +@computerQuoteFontSize : @h5; + +/* Author */ +@authorTextColor : #00928F; +@authorTextAlign : left; +@authorReversedTextAlign : left; +@mobileAuthorFontSize : 1rem; +@tabletAuthorFontSize : @h6; +@computerAuthorFontSize : @h5; +@authorFontWeight : @bold; +@authorMargin : 0; + +/* Meta */ +@metaTextAlign : left; +@metaReversedTextAlign : left; +@mobileMetaFontSize : 1rem; +@tabletMetaFontSize : @h6; +@computerMetaFontSize : @h5; +@metaFontWeight : 400; +@metaMargin : 0; + +/* Callout */ +@calloutBorder : none; +@calloutBoxShadow : none; +@calloutBorderRadius : 0; +@calloutPadding : 0; diff --git a/src/less/globals.less b/src/less/globals.less new file mode 100644 index 0000000..4271fc7 --- /dev/null +++ b/src/less/globals.less @@ -0,0 +1,81 @@ +@import (multiple, reference, optional) '../../theme.config'; + +/* Enables customization of addons */ +.loadAddonOverrides() { + @import (optional) + '@{siteFolder}/../addons/@{addon}/@{addontype}s/@{addonelement}.overrides'; +} + +/* Helper to load variables */ +.loadAddonVariables() { + @import (optional) '@{addonelement}.variables'; + @import (optional) + '@{siteFolder}/../addons/@{addon}/@{addontype}s/@{addonelement}.variables'; +} + +@type: extra; +@element: custom; + +/*------------------- + Exact Pixel Values +--------------------*/ +/* + These are used to specify exact pixel values in em + for things like borders that remain constantly + sized as emSize adjusts + + Since there are many more sizes than names for sizes, + these are named by their original pixel values. + +*/ + +@1px: unit((1 / @emSize), rem); +@2px: unit((2 / @emSize), rem); +@3px: unit((3 / @emSize), rem); +@4px: unit((4 / @emSize), rem); +@5px: unit((5 / @emSize), rem); +@6px: unit((6 / @emSize), rem); +@7px: unit((7 / @emSize), rem); +@8px: unit((8 / @emSize), rem); +@9px: unit((9 / @emSize), rem); +@10px: unit((10 / @emSize), rem); +@11px: unit((11 / @emSize), rem); +@12px: unit((12 / @emSize), rem); +@13px: unit((13 / @emSize), rem); +@14px: unit((14 / @emSize), rem); +@15px: unit((15 / @emSize), rem); +@16px: unit((16 / @emSize), rem); +@17px: unit((17 / @emSize), rem); +@18px: unit((18 / @emSize), rem); +@19px: unit((19 / @emSize), rem); +@20px: unit((20 / @emSize), rem); + +@h1: unit((48 / 16), rem); +@h2: unit((40 / 16), rem); +@h3: unit((36 / 16), rem); +@h4: unit((24 / 16), rem); +@h5: unit((20 / 16), rem); +@h6: unit((18 / 16), rem); + +@bold: 600; +@normal: normal; + +/*-------------------------- + Gaps / Paddings / Margins +---------------------------*/ + +@tinyGap: unit((12 / @emSize), rem); +@mediumGap: unit((16 / @emSize), rem); +@largeGap: unit((20 / @emSize), rem); +@bigGap: unit((24 / @emSize), rem); +@hugeGap: unit((28 / @emSize), rem); + +@squareTiny: @tinyGap @tinyGap; +@squareMedium: @mediumGap @mediumGap; +@squareLarge: @largeGap @largeGap; +@squareBig: @bigGap @bigGap; + +@rectangleTiny: @tinyGap @mediumGap; +@rectangleMedium: @mediumGap @largeGap; +@rectangleLarge: @largeGap @bigGap; +@rectangleBig: @bigGap @hugeGap; diff --git a/src/less/pullquote.less b/src/less/pullquote.less new file mode 100644 index 0000000..1051151 --- /dev/null +++ b/src/less/pullquote.less @@ -0,0 +1,125 @@ +@import './globals.less'; + +@addon: 'volto-addons'; +@addontype: 'quoteBlock'; +@addonelement: 'pullquote'; + +.loadAddonVariables(); + +/******************************* + Pullquote +*******************************/ + +#page-edit { + .eea.pullquote { + .author, + .meta { + > * { + margin-bottom: 0; + } + } + } +} + +.eea.pullquote { + display: flex; + flex-flow: column; + padding: @pullquotePadding; + margin: @pullquoteMargin; + hyphens: @contentWordBreakHyphens; + word-break: @contentWordBreak; + + .quotes.wrapper { + display: flex; + + .quote:not(.icon) { + padding: @mobileQuotePadding; + margin: 0; + text-indent: @mobileQuoteIconSize; + } + + i.icon { + display: flex; + align-items: flex-end; + color: @quoteIconColor; + font-size: @mobileQuoteIconSize; + line-height: @mobileQuoteIconSize; + } + + i.icomn:first-child { + align-self: flex-start; + } + + i.icon:last-child { + align-self: flex-end; + } + } + + .author { + margin: @authorMargin; + color: @authorTextColor; + font-size: @authorFontSize; + font-weight: @authorFontWeight; + text-align: @authorTextAlign; + } + + .meta { + font-size: @metaFontSize; + font-weight: @metaFontWeight; + text-align: @metaTextAlign; + } + + i.icon.quote.left:before { + content: @openQuote; + } + + i.icon.quote.right:before { + content: @closeQuote; + } + + &.reversed { + flex-flow: column-reverse; + + .info.wrapper { + margin-bottom: 1rem; + } + + .author { + text-align: left; + } + + .meta { + text-align: left; + } + } +} + +@media only screen and (min-width: @tabletBreakpoint) { + .eea.pullquote { + .quotes.wrapper { + .quote:not(.icon) { + padding: @tabletQuotePadding; + text-indent: @tabletQuoteIconSize; + } + + i.icon { + font-size: @tabletQuoteIconSize; + line-height: @tabletQuoteIconSize; + } + } + } + + .eea.pullquote.left { + width: @maxWidthOnFloat; + padding: @pullquoteFloatLeftPadding; + float: left; + } + + .eea.pullquote.right { + width: @maxWidthOnFloat; + padding: @pullquoteFloatRightPadding; + float: right; + } +} + +.loadAddonVariables(); diff --git a/src/less/pullquote.variables b/src/less/pullquote.variables new file mode 100644 index 0000000..e69207d --- /dev/null +++ b/src/less/pullquote.variables @@ -0,0 +1,38 @@ +/******************************* + Pullquote +*******************************/ + +/* Body */ +@pullquotePadding : @largeGap 0; +@pullquoteFloatLeftPadding : @largeGap @largeGap @largeGap 0; +@pullquoteFloatRightPadding : @largeGap 0 @largeGap @largeGap; +@pullquoteMargin : 0; +@maxWidthOnFloat : 50%; +@contentWordBreak : break-word; +@contentWordBreakHyphens : manual; + +/* Quote paragraph */ +@mobileQuotePadding : 0; +@tabletQuotePadding : 0.5rem 0 0.5rem; + +/* Author */ +@authorTextColor : #00928F; +@authorTextAlign : right; +@authorReversedTextAlign : left; +@authorFontWeight : @bold; +@authorFontSize : 1.25rem; +@authorMargin : 0; + +/* Metadata */ +@metaTextAlign : right; +@metaReversedTextAlign : left; +@metaFontWeight : @normal; +@metaFontSize : 1rem; + +/* Icon */ +@mobileQuoteIconSize : 2rem; +@tabletQuoteIconSize : 3rem; +@quoteIconColor : @secondaryColor; +@quoteDownIconFloat : right; +@openQuote : open-quote; +@closeQuote : close-quote; \ No newline at end of file