From 2639e00a0457046fc43e932146c19af7c9ed07e7 Mon Sep 17 00:00:00 2001 From: Tiberiu Ichim Date: Fri, 10 Sep 2021 15:38:32 +0300 Subject: [PATCH] Initial commit --- .coverage.babel.config.js | 9 + .gitignore | 25 +++ .project.eslintrc.js | 46 ++++ .release-it.json | 17 ++ CHANGELOG.md | 5 + DEVELOP.md | 51 +++++ DEVELOP.md.tpl | 51 +++++ Jenkinsfile | 209 ++++++++++++++++++ Jenkinsfile.tpl | 209 ++++++++++++++++++ LICENSE.md | 9 + Makefile | 50 +++++ Makefile.tpl | 49 +++++ README.md | 86 ++++++++ README.md.tpl | 86 ++++++++ bootstrap | 41 ++++ cypress.json | 12 ++ cypress/fixtures/example.json | 5 + cypress/integration/block-basics.js | 32 +++ cypress/plugins/index.js | 26 +++ cypress/support/commands.js | 315 ++++++++++++++++++++++++++++ cypress/support/index.js | 53 +++++ jest-addon.config.js | 36 ++++ locales/volto.pot | 0 package.json | 42 ++++ package.json.tpl | 42 ++++ src/i18n.js | 180 ++++++++++++++++ src/index.js | 5 + 27 files changed, 1691 insertions(+) create mode 100644 .coverage.babel.config.js create mode 100644 .gitignore create mode 100644 .project.eslintrc.js create mode 100644 .release-it.json create mode 100644 CHANGELOG.md create mode 100644 DEVELOP.md create mode 100644 DEVELOP.md.tpl create mode 100644 Jenkinsfile create mode 100644 Jenkinsfile.tpl create mode 100644 LICENSE.md create mode 100644 Makefile create mode 100644 Makefile.tpl create mode 100644 README.md create mode 100644 README.md.tpl create mode 100644 bootstrap create mode 100644 cypress.json create mode 100644 cypress/fixtures/example.json create mode 100644 cypress/integration/block-basics.js create mode 100644 cypress/plugins/index.js create mode 100644 cypress/support/commands.js create mode 100644 cypress/support/index.js create mode 100644 jest-addon.config.js create mode 100644 locales/volto.pot create mode 100644 package.json create mode 100644 package.json.tpl create mode 100644 src/i18n.js create mode 100644 src/index.js diff --git a/.coverage.babel.config.js b/.coverage.babel.config.js new file mode 100644 index 000000000..e8b54d3c9 --- /dev/null +++ b/.coverage.babel.config.js @@ -0,0 +1,9 @@ +const defaultBabel = require('@plone/volto/babel'); + +function applyDefault(api) { + const voltoBabel = defaultBabel(api); + voltoBabel.plugins.push('@babel/plugin-transform-modules-commonjs', 'transform-class-properties', 'istanbul'); + return voltoBabel; +} + +module.exports = applyDefault; diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..cdcaf4610 --- /dev/null +++ b/.gitignore @@ -0,0 +1,25 @@ +.vscode/ +.history +.eslintrc.js +project +logs +*.log +npm-debug.log* +.DS_Store +*.swp +yarn-error.log +yarn.lock +package-lock.json + +node_modules +build +dist +cypress/videos +cypress/reports +screenshots +videos +.env.local +.env.development.local +.env.test.local +.env.production.local +*~ diff --git a/.project.eslintrc.js b/.project.eslintrc.js new file mode 100644 index 000000000..cfefd890c --- /dev/null +++ b/.project.eslintrc.js @@ -0,0 +1,46 @@ +const fs = require('fs'); +const path = require('path'); + +const projectRootPath = fs.realpathSync('./project'); // __dirname +const packageJson = require(path.join(projectRootPath, 'package.json')); +const jsConfig = require(path.join(projectRootPath, 'jsconfig.json')).compilerOptions; + +const pathsConfig = jsConfig.paths; + +let voltoPath = path.join(projectRootPath, 'node_modules/@plone/volto'); + +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 => [ + o, + reg.packages[o].modulePath, +]); + + +module.exports = { + extends: `${projectRootPath}/node_modules/@plone/volto/.eslintrc`, + settings: { + 'import/resolver': { + alias: { + map: [ + ['@plone/volto', '@plone/volto/src'], + ...addonAliases, + ['@package', `${__dirname}/src`], + ['~', `${__dirname}/src`], + ], + extensions: ['.js', '.jsx', '.json'], + }, + 'babel-plugin-root-import': { + rootPathSuffix: 'src', + }, + }, + }, +}; + diff --git a/.release-it.json b/.release-it.json new file mode 100644 index 000000000..a02990038 --- /dev/null +++ b/.release-it.json @@ -0,0 +1,17 @@ +{ + "npm": { + "publish": false + }, + "git": { + "changelog": "npx auto-changelog --stdout --commit-limit false -u --template https://github.com/raw/release-it/release-it/master/templates/changelog-compact.hbs", + "tagName": "${version}" + }, + "github": { + "release": true, + "releaseName": "${version}", + "releaseNotes": "npx auto-changelog --stdout --commit-limit false -u --template https://github.com/raw/release-it/release-it/master/templates/changelog-compact.hbs" + }, + "hooks": { + "after:bump": "npx auto-changelog --commit-limit false -p" + } +} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..3742d31d0 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + +## 0.1.0 + +- Initial release diff --git a/DEVELOP.md b/DEVELOP.md new file mode 100644 index 000000000..9c01eb677 --- /dev/null +++ b/DEVELOP.md @@ -0,0 +1,51 @@ +# volto-addon-template + +## 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 @eeacms/volto-addon-template --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", + "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/volto-addon-template/ diff --git a/DEVELOP.md.tpl b/DEVELOP.md.tpl new file mode 100644 index 000000000..cf7da0681 --- /dev/null +++ b/DEVELOP.md.tpl @@ -0,0 +1,51 @@ +# <%= 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 new file mode 100644 index 000000000..fdda77afd --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,209 @@ +pipeline { + agent any + + environment { + GIT_NAME = "volto-addon-template" + NAMESPACE = "@eeacms" + SONARQUBE_TAGS = "volto.eea.europa.eu" + DEPENDENCIES = "" + } + + stages { + + stage('Code') { + 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') { + 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') { + steps { + parallel( + + "Cypress": { + node(label: 'docker') { + script { + try { + sh '''docker pull plone; docker run -d --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']) + } + archiveArtifacts artifacts: 'cypress-reports/videos/*.mp4', fingerprint: 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') { + // Exclude Pull-Requests + when { + allOf { + environment name: 'CHANGE_ID', value: '' + } + } + 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" && !( env.CHANGE_BRANCH.startsWith("hotfix")) ) { + error "Pipeline aborted due to PR not made from develop or hotfix 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''' + } + } + } + } + } + + 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''' + } + } + } + } + + } + + 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/Jenkinsfile.tpl b/Jenkinsfile.tpl new file mode 100644 index 000000000..c6d1bcb4f --- /dev/null +++ b/Jenkinsfile.tpl @@ -0,0 +1,209 @@ +pipeline { + agent any + + environment { + GIT_NAME = "<%= name %>" + NAMESPACE = "@eeacms" + SONARQUBE_TAGS = "volto.eea.europa.eu" + DEPENDENCIES = "" + } + + stages { + + stage('Code') { + 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') { + 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') { + steps { + parallel( + + "Cypress": { + node(label: 'docker') { + script { + try { + sh '''docker pull plone; docker run -d --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']) + } + archiveArtifacts artifacts: 'cypress-reports/videos/*.mp4', fingerprint: 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') { + // Exclude Pull-Requests + when { + allOf { + environment name: 'CHANGE_ID', value: '' + } + } + 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" && !( env.CHANGE_BRANCH.startsWith("hotfix")) ) { + error "Pipeline aborted due to PR not made from develop or hotfix 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''' + } + } + } + } + } + + 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''' + } + } + } + } + + } + + 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/LICENSE.md b/LICENSE.md new file mode 100644 index 000000000..0992c000a --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,9 @@ +MIT License + +Copyright (c) 2020 European Environment Agency + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 000000000..add0adaf5 --- /dev/null +++ b/Makefile @@ -0,0 +1,50 @@ +SHELL=/bin/bash + +DIR=$(shell basename $$(pwd)) +ADDON ?= "@eeacms/volto-addon-template" + +# 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 :)" +.*\)/\\x1b[36m\1\\x1b[m:\2/' | column -c2 -t -s :)" diff --git a/Makefile.tpl b/Makefile.tpl new file mode 100644 index 000000000..2040f7aa1 --- /dev/null +++ b/Makefile.tpl @@ -0,0 +1,49 @@ +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 new file mode 100644 index 000000000..80ce56ee6 --- /dev/null +++ b/README.md @@ -0,0 +1,86 @@ +# volto-addon-template +[![Releases](https://img.shields.io/github/v/release/eea/volto-addon-template)](https://github.com/eea/volto-addon-template/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) +[![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) + +[Volto](https://github.com/plone/volto) add-on + +## Features + +Demo GIF + +## Getting started + +### Try volto-addon-template 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="@eeacms/volto-addon-template" plone/volto + ``` + +1. Go to http://localhost:3000 + +### Add volto-addon-template 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": [ + "@eeacms/volto-addon-template" + ], + + "dependencies": { + "@eeacms/volto-addon-template": "^1.0.0" + } + ``` + +* If not, create one: + + ``` + npm install -g yo @plone/generator-volto + yo @plone/volto my-volto-project --addon @eeacms/volto-addon-template + cd my-volto-project + ``` + +1. Install new add-ons and restart Volto: + + ``` + yarn + yarn start + ``` + +1. Go to http://localhost:3000 + +1. Happy editing! + +## How to contribute + +See [DEVELOP.md](https://github.com/eea/volto-addon-template/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. + +## Funding + +[European Environment Agency (EU)](http://eea.europa.eu) diff --git a/README.md.tpl b/README.md.tpl new file mode 100644 index 000000000..b914d1558 --- /dev/null +++ b/README.md.tpl @@ -0,0 +1,86 @@ +# <%= 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) +[![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) + +[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! + +## 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/bootstrap b/bootstrap new file mode 100644 index 000000000..8613750a8 --- /dev/null +++ b/bootstrap @@ -0,0 +1,41 @@ +const path = require('path'); +const fs = require('fs'); +const ejs = require('ejs'); + +const currentDir = path.basename(process.cwd()); + +const bootstrap = function (ofile) { + fs.readFile(ofile, 'utf8', function (err, data) { + if (err) { + return console.log(err); + } + const result = ejs.render(data, { + addonName: `@eeacms/${currentDir}`, + name: currentDir + }); + const output = ofile.replace('.tpl', ''); + fs.writeFile(output, result, 'utf8', function (err) { + if (err) { + return console.log(err); + } + }); + if (ofile.includes('.tpl')) { + fs.unlink(ofile, (err) => { + if (err) { + return console.error(err); + } + }); + } + }); +} + +fs.readdir(".", { withFileTypes: true }, (err, dirents) => { + const files = dirents + .filter(dirent => dirent.isFile()) + .map(dirent => dirent.name); + files.forEach(function (file) { + if (file != 'bootstrap') { + bootstrap(file); + } + }); +}); diff --git a/cypress.json b/cypress.json new file mode 100644 index 000000000..aef675e80 --- /dev/null +++ b/cypress.json @@ -0,0 +1,12 @@ +{ + "baseUrl": "http://localhost:3000", + "viewportWidth": 1280, + "defaultCommandTimeout": 15000, + "reporter": "junit", + "video": true, + "reporterOptions": { + "mochaFile": "cypress/reports/cypress-[hash].xml", + "jenkinsMode": true, + "toConsole": true + } +} diff --git a/cypress/fixtures/example.json b/cypress/fixtures/example.json new file mode 100644 index 000000000..da18d9352 --- /dev/null +++ b/cypress/fixtures/example.json @@ -0,0 +1,5 @@ +{ + "name": "Using fixtures to represent data", + "email": "hello@cypress.io", + "body": "Fixtures are a great way to mock data for responses to routes" +} \ No newline at end of file diff --git a/cypress/integration/block-basics.js b/cypress/integration/block-basics.js new file mode 100644 index 000000000..454084ce8 --- /dev/null +++ b/cypress/integration/block-basics.js @@ -0,0 +1,32 @@ +import { setupBeforeEach, tearDownAfterEach } from '../support'; + +describe('Blocks Tests', () => { + beforeEach(setupBeforeEach); + afterEach(tearDownAfterEach); + + it('Add Block: Empty', () => { + // Change page title + cy.get('.documentFirstHeading > .public-DraftStyleDefault-block') + .clear() + .type('My Add-on Page') + .get('.documentFirstHeading span[data-text]') + .contains('My Add-on Page'); + + cy.get('.documentFirstHeading > .public-DraftStyleDefault-block').type( + '{enter}', + ); + + // Add block + cy.get('.ui.basic.icon.button.block-add-button').first().click(); + cy.get('.blocks-chooser .title').contains('Media').click(); + cy.get('.content.active.media .button.image').contains('Image').click(); + + // Save + cy.get('#toolbar-save').click(); + cy.url().should('eq', Cypress.config().baseUrl + '/cypress/my-page'); + + // then the page view should contain our changes + cy.contains('My Add-on Page'); + cy.get('.block.image'); + }); +}); diff --git a/cypress/plugins/index.js b/cypress/plugins/index.js new file mode 100644 index 000000000..27a31a548 --- /dev/null +++ b/cypress/plugins/index.js @@ -0,0 +1,26 @@ +/// +// *********************************************************** +// This example plugins/index.js can be used to load plugins +// +// You can change the location of this file or turn off loading +// the plugins file with the 'pluginsFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/plugins-guide +// *********************************************************** + +// This function is called when a project is opened or re-opened (e.g. due to +// the project's config changing) + +/** + * @type {Cypress.PluginConfig} + */ +module.exports = (on, config) => { + // `on` is used to hook into various events Cypress emits + // `config` is the resolved Cypress config + /* coverage-start + require('@cypress/code-coverage/task')(on, config) + on('file:preprocessor', require('@cypress/code-coverage/use-babelrc')) + return config + coverage-end */ +}; diff --git a/cypress/support/commands.js b/cypress/support/commands.js new file mode 100644 index 000000000..ac4846100 --- /dev/null +++ b/cypress/support/commands.js @@ -0,0 +1,315 @@ +/* eslint no-console: ["error", { allow: ["log"] }] */ + +// --- AUTOLOGIN ------------------------------------------------------------- +Cypress.Commands.add('autologin', () => { + let api_url, user, password; + api_url = Cypress.env('API_PATH') || 'http://localhost:8080/Plone'; + user = 'admin'; + password = 'admin'; + + return cy + .request({ + method: 'POST', + url: `${api_url}/@login`, + headers: { Accept: 'application/json' }, + body: { login: user, password: password }, + }) + .then((response) => cy.setCookie('auth_token', response.body.token)); +}); + +// --- CREATE CONTENT -------------------------------------------------------- +Cypress.Commands.add( + 'createContent', + ({ + contentType, + contentId, + contentTitle, + path = '', + allow_discussion = false, + }) => { + let api_url, auth; + api_url = Cypress.env('API_PATH') || 'http://localhost:8080/Plone'; + auth = { + user: 'admin', + pass: 'admin', + }; + if (contentType === 'File') { + return cy.request({ + method: 'POST', + url: `${api_url}/${path}`, + headers: { + Accept: 'application/json', + }, + auth: auth, + body: { + '@type': contentType, + id: contentId, + title: contentTitle, + file: { + data: 'dGVzdGZpbGUK', + encoding: 'base64', + filename: 'lorem.txt', + 'content-type': 'text/plain', + }, + allow_discussion: allow_discussion, + }, + }); + } + if (contentType === 'Image') { + return cy.request({ + method: 'POST', + url: `${api_url}/${path}`, + headers: { + Accept: 'application/json', + }, + auth: auth, + body: { + '@type': contentType, + id: contentId, + title: contentTitle, + image: { + data: + 'iVBORw0KGgoAAAANSUhEUgAAANcAAAA4CAMAAABZsZ3QAAAAM1BMVEX29fK42OU+oMvn7u9drtIPisHI4OhstdWZyt4fkcXX5+sAg74umMhNp86p0eJ7vNiKw9v/UV4wAAAAAXRSTlMAQObYZgAABBxJREFUeF7tmuty4yAMhZG4X2zn/Z92J5tsBJwWXG/i3XR6frW2Y/SBLIRAfaQUDNt8E5tLUt9BycfcKfq3R6Mlfyimtx4rzp+K3dtibXkor99zsEqLYZltblTecciogoh+TXfY1Ve4dn07rCDGG9dHSEEOg/GmXl0U1XDxTKxNK5De7BxsyyBr6gGm2/vPxKJ8F6f7BXKfRMp1xIWK9A+5ks25alSb353dWnDJN1k35EL5f8dVGifTf/4tjUuuFq7u4srmXC60yAmldLXIWbg65RKU87lcGxJCFqUPv0IacW0PmSivOZFLE908inPToMmii/roG+MRV/O8FU88i8tFsxV3a06MFUw0Qu7RmAtdV5/HVVaOVMTWNOWSwMljLhzhcB6XIS7OK5V6AvRDNN7t5VJWQs1J40UmalbK56usBG/CuCHSYuc+rkUGeMCViNRARPrzW52N3oQLe6WifNliSuuGaH3czbVNudI9s7ZLUCLHVwWlyES522o1t14uvmbblmVTKqFjaZYJFSTPP4dLL1kU1z7p0lzdbRulmEWLxoQX+z9ce7A8GqEEucllLxePuZwdJl1Lezu0hoswvTPt61DrFcRuujV/2cmlxaGBC7Aw6cpovGANwRiSdOAWJ5AGy4gLL64dl0QhUEAuEUNws+XxV+OKGPdw/hESGYF9XEGaFC7sNLMSXWJjHsnanYi87VK428N2uxpOjOFANcagLM5l+7mSycM8KknZpKLcGi6jmzWGr/vLurZ/0g4u9AZuAoeb5r1ceQhyiTPY1E4wUR6u/F3H2ojSpXMMriBPT9cezTto8Cx+MsglHL4fv1Rxrb1LVw9yvyQpJ3AhFnLZfuRLH2QsOG3FGGD20X/th/u5bFAt16Bt308KjF+MNOXgl/SquIEySX3GhaZvc67KZbDxcCDORz2N8yCWPaY5lyQZO7lQ29fnZbt3Xu6qoge4+DjXl/MocySPOp9rlvdyznahRyHEYd77v3LhugOXDv4J65QXfl803BDAdaWBEDhfVx7nKofjoVCgxnUAqw/UAUDPn788BDvQuG4TDtdtUPvzjSlXAB8DvaDOhhrmhwbywylXAm8CvaouikJTL93gs3y7Yy4VYbIxOHrcMizPqWOjqO9l3Uz52kibQy4xxOgqhJvD+w5rvokOcAlGvNCfeqCv1ste1stzLm0f71Iq3ZfTrPfuE5nhPtF+LvQE2lffQC7pYtQy3tdzdrKvd5TLVVzDetScS3nEKmmwDyt1Cev1kX3YfbvzNK4fzrlw+cB6vm+uiUgf2zdXI62241LawCb7Pi5FXFPF8KpzDoF/Sw2lg+GrHNbno1mhPu+VCF/vfMnw06PnUl6j48dVHD3jHNHPua+fc3o/5yp/zsGi0vYtzi3Pz5mHd4T6BWMIlewacd63AAAAAElFTkSuQmCC', + encoding: 'base64', + filename: 'image.png', + 'content-type': 'image/png', + }, + }, + }); + } + if (['Document', 'Folder', 'CMSFolder'].includes(contentType)) { + return cy + .request({ + method: 'POST', + url: `${api_url}/${path}`, + headers: { + Accept: 'application/json', + }, + auth: auth, + body: { + '@type': contentType, + id: contentId, + title: contentTitle, + blocks: { + 'd3f1c443-583f-4e8e-a682-3bf25752a300': { '@type': 'title' }, + '7624cf59-05d0-4055-8f55-5fd6597d84b0': { '@type': 'text' }, + }, + blocks_layout: { + items: [ + 'd3f1c443-583f-4e8e-a682-3bf25752a300', + '7624cf59-05d0-4055-8f55-5fd6597d84b0', + ], + }, + allow_discussion: allow_discussion, + }, + }) + .then(() => console.log(`${contentType} created`)); + } else { + return cy + .request({ + method: 'POST', + url: `${api_url}/${path}`, + headers: { + Accept: 'application/json', + }, + auth: auth, + body: { + '@type': contentType, + id: contentId, + title: contentTitle, + allow_discussion: allow_discussion, + }, + }) + .then(() => console.log(`${contentType} created`)); + } + }, +); + +// --- REMOVE CONTENT -------------------------------------------------------- +Cypress.Commands.add('removeContent', (path) => { + let api_url, auth; + api_url = Cypress.env('API_PATH') || 'http://localhost:8080/Plone'; + auth = { + user: 'admin', + pass: 'admin', + }; + return cy + .request({ + method: 'DELETE', + url: `${api_url}/${path}`, + headers: { + Accept: 'application/json', + }, + auth: auth, + body: {}, + }) + .then(() => console.log(`${path} removed`)); +}); + +// --- SET WORKFLOW ---------------------------------------------------------- +Cypress.Commands.add( + 'setWorkflow', + ({ + path = '/', + actor = 'admin', + review_state = 'publish', + time = '1995-07-31T18:30:00', + title = '', + comment = '', + effective = '2018-01-21T08:00:00', + expires = '2019-01-21T08:00:00', + include_children = true, + }) => { + let api_url, auth; + api_url = Cypress.env('API_PATH') || 'http://localhost:8080/Plone'; + auth = { + user: 'admin', + pass: 'admin', + }; + return cy.request({ + method: 'POST', + url: `${api_url}/${path}/@workflow/${review_state}`, + headers: { + Accept: 'application/json', + }, + auth: auth, + body: { + actor: actor, + review_state: review_state, + time: time, + title: title, + comment: comment, + effective: effective, + expires: expires, + include_children: include_children, + }, + }); + }, +); + +// --- waitForResourceToLoad ---------------------------------------------------------- +Cypress.Commands.add('waitForResourceToLoad', (fileName, type) => { + const resourceCheckInterval = 40; + + return new Cypress.Promise((resolve) => { + const checkIfResourceHasBeenLoaded = () => { + const resource = cy + .state('window') + .performance.getEntriesByType('resource') + .filter((entry) => !type || entry.initiatorType === type) + .find((entry) => entry.name.includes(fileName)); + + if (resource) { + resolve(); + + return; + } + + setTimeout(checkIfResourceHasBeenLoaded, resourceCheckInterval); + }; + + checkIfResourceHasBeenLoaded(); + }); +}); + +// Low level command reused by `setSelection` and low level command `setCursor` +Cypress.Commands.add('selection', { prevSubject: true }, (subject, fn) => { + cy.wrap(subject).trigger('mousedown').then(fn).trigger('mouseup'); + + cy.document().trigger('selectionchange'); + return cy.wrap(subject); +}); + +Cypress.Commands.add( + 'setSelection', + { prevSubject: true }, + (subject, query, endQuery) => { + return cy.wrap(subject).selection(($el) => { + if (typeof query === 'string') { + const anchorNode = getTextNode($el[0], query); + const focusNode = endQuery ? getTextNode($el[0], endQuery) : anchorNode; + const anchorOffset = anchorNode.wholeText.indexOf(query); + const focusOffset = endQuery + ? focusNode.wholeText.indexOf(endQuery) + endQuery.length + : anchorOffset + query.length; + setBaseAndExtent(anchorNode, anchorOffset, focusNode, focusOffset); + } else if (typeof query === 'object') { + const el = $el[0]; + const anchorNode = getTextNode(el.querySelector(query.anchorQuery)); + const anchorOffset = query.anchorOffset || 0; + const focusNode = query.focusQuery + ? getTextNode(el.querySelector(query.focusQuery)) + : anchorNode; + const focusOffset = query.focusOffset || 0; + setBaseAndExtent(anchorNode, anchorOffset, focusNode, focusOffset); + } + }); + }, +); + +// Low level command reused by `setCursorBefore` and `setCursorAfter`, equal to `setCursorAfter` +Cypress.Commands.add( + 'setCursor', + { prevSubject: true }, + (subject, query, atStart) => { + return cy.wrap(subject).selection(($el) => { + const node = getTextNode($el[0], query); + const offset = + node.wholeText.indexOf(query) + (atStart ? 0 : query.length); + const document = node.ownerDocument; + document.getSelection().removeAllRanges(); + document.getSelection().collapse(node, offset); + }); + // Depending on what you're testing, you may need to chain a `.click()` here to ensure + // further commands are picked up by whatever you're testing (this was required for Slate, for example). + }, +); + +Cypress.Commands.add( + 'setCursorBefore', + { prevSubject: true }, + (subject, query) => { + cy.wrap(subject).setCursor(query, true); + }, +); + +Cypress.Commands.add( + 'setCursorAfter', + { prevSubject: true }, + (subject, query) => { + cy.wrap(subject).setCursor(query); + }, +); + +// Helper functions +function getTextNode(el, match) { + const walk = document.createTreeWalker(el, NodeFilter.SHOW_TEXT, null, false); + if (!match) { + return walk.nextNode(); + } + + let node; + while ((node = walk.nextNode())) { + if (node.wholeText.includes(match)) { + return node; + } + } +} + +function setBaseAndExtent(...args) { + const document = args[0].ownerDocument; + document.getSelection().removeAllRanges(); + document.getSelection().setBaseAndExtent(...args); +} + +Cypress.Commands.add('navigate', (route = '') => { + return cy.window().its('appHistory').invoke('push', route); +}); + +Cypress.Commands.add('store', () => { + return cy.window().its('store').invoke('getStore', ''); +}); + +Cypress.Commands.add('settings', (key, value) => { + return cy.window().its('settings'); +}); diff --git a/cypress/support/index.js b/cypress/support/index.js new file mode 100644 index 000000000..a3fd9352f --- /dev/null +++ b/cypress/support/index.js @@ -0,0 +1,53 @@ +// *********************************************************** +// This example support/index.js is processed and +// loaded automatically before your test files. +// +// This is a great place to put global configuration and +// behavior that modifies Cypress. +// +// You can change the location of this file or turn off +// automatically serving support files with the +// 'supportFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/configuration +// *********************************************************** + +// Import commands.js using ES2015 syntax: +import './commands'; + +// Alternatively you can use CommonJS syntax: +// require('./commands') + +/* coverage-start +//Generate code-coverage +import '@cypress/code-coverage/support'; +coverage-end */ + +export const setupBeforeEach = () => { + cy.autologin(); + cy.createContent({ + contentType: 'Folder', + contentId: 'cypress', + contentTitle: 'Cypress', + }); + cy.createContent({ + contentType: 'Document', + contentId: 'my-page', + contentTitle: 'My Page', + path: 'cypress', + }); + cy.visit('/cypress/my-page'); + cy.waitForResourceToLoad('@navigation'); + cy.waitForResourceToLoad('@breadcrumbs'); + cy.waitForResourceToLoad('@actions'); + cy.waitForResourceToLoad('@types'); + cy.waitForResourceToLoad('my-page'); + cy.navigate('/cypress/my-page/edit'); + cy.get(`.block.title [data-contents]`); +}; + +export const tearDownAfterEach = () => { + cy.autologin(); + cy.removeContent('cypress'); +}; diff --git a/jest-addon.config.js b/jest-addon.config.js new file mode 100644 index 000000000..7c1549932 --- /dev/null +++ b/jest-addon.config.js @@ -0,0 +1,36 @@ +module.exports = { + testMatch: ['**/src/addons/**/?(*.)+(spec|test).[jt]s?(x)'], + collectCoverageFrom: [ + 'src/addons/**/src/**/*.{js,jsx,ts,tsx}', + '!src/**/*.d.ts', + ], + moduleNameMapper: { + '@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', + '@plone/volto-quanta/(.*)$': '/src/addons/volto-quanta/src/$1', + '@eeacms/(.*?)/(.*)$': '/src/addons/$1/src/$2', + 'volto-slate/(.*)$': '/src/addons/volto-slate/src/$1', + '~/(.*)$': '/src/$1', + 'load-volto-addons': + '/node_modules/@plone/volto/jest-addons-loader.js', + }, + transform: { + '^.+\\.js(x)?$': 'babel-jest', + '^.+\\.css$': 'jest-css-modules', + '^.+\\.less$': 'jest-css-modules', + '^.+\\.scss$': 'jest-css-modules', + '^.+\\.(png)$': 'jest-file', + '^.+\\.(jpg)$': 'jest-file', + '^.+\\.(svg)$': './node_modules/@plone/volto/jest-svgsystem-transform.js', + }, + coverageThreshold: { + global: { + branches: 5, + functions: 5, + lines: 5, + statements: 5, + }, + }, +}; diff --git a/locales/volto.pot b/locales/volto.pot new file mode 100644 index 000000000..e69de29bb diff --git a/package.json b/package.json new file mode 100644 index 000000000..dfa6b4563 --- /dev/null +++ b/package.json @@ -0,0 +1,42 @@ +{ + "name": "@eeacms/volto-addon-template", + "version": "0.1.0", + "description": "@eeacms/volto-addon-template: Volto add-on", + "main": "src/index.js", + "author": "European Environment Agency: IDM2 A-Team", + "license": "MIT", + "homepage": "https://github.com/eea/volto-addon-template", + "keywords": [ + "volto-addon", + "volto", + "plone", + "react" + ], + "repository": { + "type": "git", + "url": "git@github.com:eea/volto-addon-template.git" + }, + "dependencies": { + }, + "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", + "i18n": "rm -rf build/messages && NODE_ENV=production node src/i18n.js", + "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", + "cypress:run": "if [ -d ./project ]; then ./project/node_modules/cypress/bin/cypress run; else ../../../node_modules/cypress/bin/cypress run; fi", + "cypress:open": "if [ -d ./project ]; then ./project/node_modules/cypress/bin/cypress open; else ../../../node_modules/cypress/bin/cypress open; fi" + } +} diff --git a/package.json.tpl b/package.json.tpl new file mode 100644 index 000000000..1e6a6226a --- /dev/null +++ b/package.json.tpl @@ -0,0 +1,42 @@ +{ + "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": { + }, + "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", + "i18n": "rm -rf build/messages && NODE_ENV=production node src/i18n.js", + "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", + "cypress:run": "if [ -d ./project ]; then ./project/node_modules/cypress/bin/cypress run; else ../../../node_modules/cypress/bin/cypress run; fi", + "cypress:open": "if [ -d ./project ]; then ./project/node_modules/cypress/bin/cypress open; else ../../../node_modules/cypress/bin/cypress open; fi" + } +} diff --git a/src/i18n.js b/src/i18n.js new file mode 100644 index 000000000..7f3a27559 --- /dev/null +++ b/src/i18n.js @@ -0,0 +1,180 @@ +/* eslint no-console: 0 */ +/** + * i18n script. + * @module scripts/i18n + */ + +const { find, keys, map, concat, reduce } = require('lodash'); +const glob = require('glob').sync; +const fs = require('fs'); +const Pofile = require('pofile'); +const babel = require('@babel/core'); + +/** + * Extract messages into separate JSON files + * @function extractMessages + * @return {undefined} + */ +function extractMessages() { + map(glob('src/**/*.js?(x)'), (filename) => { + babel.transformFileSync(filename, {}, (err) => { + if (err) { + console.log(err); + } + }); + }); +} + +/** + * Get messages from separate JSON files + * @function getMessages + * @return {Object} Object with messages + */ +function getMessages() { + return reduce( + concat( + {}, + ...map( + // We ignore the existing customized shadowed components ones, since most + // probably we won't be overriding them + // If so, we should do it in the config object or somewhere else + glob('build/messages/src/**/*.json', { + ignore: 'build/messages/src/customizations/**', + }), + (filename) => + map(JSON.parse(fs.readFileSync(filename, 'utf8')), (message) => ({ + ...message, + filename: filename.match(/build\/messages\/src\/(.*).json$/)[1], + })), + ), + ), + (current, value) => { + let result = current; + if (current.id) { + result = { + [current.id]: { + defaultMessage: current.defaultMessage, + filenames: [current.filename], + }, + }; + } + + if (result[value.id]) { + result[value.id].filenames.push(value.filename); + } else { + result[value.id] = { + defaultMessage: value.defaultMessage, + filenames: [value.filename], + }; + } + return result; + }, + ); +} + +/** + * Convert messages to pot format + * @function messagesToPot + * @param {Object} messages Messages + * @return {string} Formatted pot string + */ +function messagesToPot(messages) { + return map(keys(messages).sort(), (key) => + [ + ...map(messages[key].filenames, (filename) => `#: ${filename}`), + `# defaultMessage: ${messages[key].defaultMessage}`, + `msgid "${key}"`, + 'msgstr ""', + ].join('\n'), + ).join('\n\n'); +} + +/** + * Pot header + * @function potHeader + * @return {string} Formatted pot header + */ +function potHeader() { + return `msgid "" +msgstr "" +"Project-Id-Version: Plone\\n" +"POT-Creation-Date: ${new Date().toISOString()}\\n" +"Last-Translator: Plone i18n \\n" +"Language-Team: Plone i18n \\n" +"MIME-Version: 1.0\\n" +"Content-Type: text/plain; charset=utf-8\\n" +"Content-Transfer-Encoding: 8bit\\n" +"Plural-Forms: nplurals=1; plural=0;\\n" +"Language-Code: en\\n" +"Language-Name: English\\n" +"Preferred-Encodings: utf-8\\n" +"Domain: volto\\n" + +`; +} + +/** + * Format header + * @function formatHeader + * @param {Array} comments Array of comments + * @param {Object} headers Object of header items + * @return {string} Formatted header + */ +function formatHeader(comments, headers) { + return [ + ...map(comments, (comment) => `# ${comment}`), + 'msgid ""', + 'msgstr ""', + ...map(keys(headers), (key) => `"${key}: ${headers[key]}\\n"`), + '', + ].join('\n'); +} + +/** + * Sync po by the pot file + * @function syncPoByPot + * @return {undefined} + */ +function syncPoByPot() { + const pot = Pofile.parse(fs.readFileSync('locales/volto.pot', 'utf8')); + + map(glob('locales/**/*.po'), (filename) => { + const po = Pofile.parse(fs.readFileSync(filename, 'utf8')); + + fs.writeFileSync( + filename, + `${formatHeader(po.comments, po.headers)} +${map(pot.items, (item) => { + const poItem = find(po.items, { msgid: item.msgid }); + return [ + `${map(item.references, (ref) => `#: ${ref}`).join('\n')}`, + `msgid "${item.msgid}"`, + `msgstr "${poItem ? poItem.msgstr : ''}"`, + ].join('\n'); +}).join('\n\n')}\n`, + ); + }); +} + +// Main tasks +console.log('Extracting messages from source files...'); +extractMessages(); +console.log('Synchronizing messages to pot file...'); +// We only write the pot file if it's really different +const newPot = `${potHeader()}${messagesToPot(getMessages())}\n`.replace( + /"POT-Creation-Date:(.*)\\n"/, + '', +); +const oldPot = fs + .readFileSync('locales/volto.pot', 'utf8') + .replace(/"POT-Creation-Date:(.*)\\n"/, ''); + +if (newPot !== oldPot) { + fs.writeFileSync( + 'locales/volto.pot', + `${potHeader()}${messagesToPot(getMessages())}\n`, + ); +} +console.log('Synchronizing messages to po files...'); +syncPoByPot(); +console.log('done!'); diff --git a/src/index.js b/src/index.js new file mode 100644 index 000000000..cb042f0db --- /dev/null +++ b/src/index.js @@ -0,0 +1,5 @@ +const applyConfig = (config) => { + return config; +}; + +export default applyConfig;