From 39b017be98a168214bfd249c66e68c9667ecbff9 Mon Sep 17 00:00:00 2001 From: Alin Voinea Date: Wed, 13 Mar 2024 14:24:42 +0200 Subject: [PATCH] Initial commit --- .coverage.babel.config.js | 9 + .gitignore | 27 ++ .husky/pre-commit | 2 + .npmignore | 96 ++++++ .project.eslintrc.js | 48 +++ .release-it.json | 17 + CHANGELOG.md | 5 + DEVELOP.md | 106 ++++++ DEVELOP.md.tpl | 106 ++++++ Dockerfile | 14 + Jenkinsfile | 310 +++++++++++++++++ Jenkinsfile.tpl | 310 +++++++++++++++++ LICENSE.md | 9 + Makefile | 164 +++++++++ Makefile.tpl | 164 +++++++++ README.md | 93 ++++++ README.md.tpl | 93 ++++++ RELEASE.md | 74 +++++ babel.config.js | 17 + bootstrap | 41 +++ cypress.config.js | 27 ++ cypress/e2e/01-block-basics.cy.js | 29 ++ cypress/fixtures/example.json | 5 + cypress/support/commands.js | 536 ++++++++++++++++++++++++++++++ cypress/support/e2e.js | 128 +++++++ docker-compose.yml | 32 ++ jest-addon.config.js | 36 ++ locales/de/LC_MESSAGES/volto.po | 14 + locales/en/LC_MESSAGES/volto.po | 14 + locales/it/LC_MESSAGES/volto.po | 14 + locales/ro/LC_MESSAGES/volto.po | 14 + locales/volto.pot | 16 + package.json | 70 ++++ package.json.tpl | 70 ++++ src/index.js | 5 + 35 files changed, 2715 insertions(+) create mode 100644 .coverage.babel.config.js create mode 100644 .gitignore create mode 100755 .husky/pre-commit create mode 100644 .npmignore 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 Dockerfile 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 RELEASE.md create mode 100644 babel.config.js create mode 100644 bootstrap create mode 100644 cypress.config.js create mode 100644 cypress/e2e/01-block-basics.cy.js create mode 100644 cypress/fixtures/example.json create mode 100644 cypress/support/commands.js create mode 100644 cypress/support/e2e.js create mode 100644 docker-compose.yml create mode 100644 jest-addon.config.js create mode 100644 locales/de/LC_MESSAGES/volto.po create mode 100644 locales/en/LC_MESSAGES/volto.po create mode 100644 locales/it/LC_MESSAGES/volto.po create mode 100644 locales/ro/LC_MESSAGES/volto.po create mode 100644 locales/volto.pot create mode 100644 package.json create mode 100644 package.json.tpl create mode 100644 src/index.js diff --git a/.coverage.babel.config.js b/.coverage.babel.config.js new file mode 100644 index 0000000..37219fd --- /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('istanbul'); + return voltoBabel; +} + +module.exports = applyDefault; diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..53b9801 --- /dev/null +++ b/.gitignore @@ -0,0 +1,27 @@ +.vscode/ +.history +.eslintrc.js +.nyc_output +project +coverage +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/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 0000000..9183f9d --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,2 @@ +[ -n "$CI" ] && exit 0 +yarn lint-staged diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..080322a --- /dev/null +++ b/.npmignore @@ -0,0 +1,96 @@ +# https://docs.npmjs.com/using-npm/developers.html#keeping-files-out-of-your-package + +# Directories +api/ +bin/ +build/ +lib/ +g-api/ +tests/ + +# Docs +docs/ + +# Cypress +cypress/ + +# Tests +__tests__/ +*.snap + +# Files +.travis.yml +requirements-docs.txt +requirements-tests.txt +yarn.lock +.dockerignore +.gitattributes +.yarnrc +.nvmrc +changelogupdater.js +pip-selfcheck.json +Dockerfile +CNAME +entrypoint.sh +Jenkinsfile +Makefile + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# TypeScript v1 declaration files +typings/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env + +# next.js build output +.next + +styleguide.config +.vscode +packages diff --git a/.project.eslintrc.js b/.project.eslintrc.js new file mode 100644 index 0000000..765070f --- /dev/null +++ b/.project.eslintrc.js @@ -0,0 +1,48 @@ +const fs = require('fs'); +const path = require('path'); + +const projectRootPath = fs.existsSync('./project') + ? fs.realpathSync('./project') + : fs.realpathSync('./../../../'); +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 0000000..a029900 --- /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://raw.githubusercontent.com/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://raw.githubusercontent.com/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 0000000..3742d31 --- /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 0000000..fc46213 --- /dev/null +++ b/DEVELOP.md @@ -0,0 +1,106 @@ +# volto-addon-template + +## Develop + +1. Make sure you have `docker` and `docker compose` installed and running on your machine: + + ```Bash + git clone https://github.com/eea/volto-addon-template.git + cd volto-addon-template + git checkout -b bugfix-123456 develop + make + make start + ``` + +1. Wait for `Volto started at 0.0.0.0:3000` meesage + +1. Go to http://localhost:3000 + +1. Initialize git hooks + + ```Bash + yarn prepare + ``` + +1. Happy hacking! + +### Or add @eeacms/volto-addon-template to your Volto project + +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 + + make develop + yarn + +1. Start backend + + docker run --pull always -it --rm --name plone -p 8080:8080 -e SITE=Plone plone/plone-backend + + ...wait for backend to setup and start - `Ready to handle requests`: + + ...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/ + +## Cypress + +To run cypress locally, first make sure you don't have any Volto/Plone running on ports `8080` and `3000`. + +You don't have to be in a `clean-volto-project`, you can be in any Volto Frontend +project where you added `volto-addon-template` to `mrs.developer.json` + +Go to: + + ```BASH + cd src/addons/volto-addon-template/ + ``` + +Start: + + ```Bash + make + make start + ``` + +This will build and start with Docker a clean `Plone backend` and `Volto Frontend` with `volto-addon-template` block installed. + +Open Cypress Interface: + + ```Bash + make cypress-open + ``` + +Or run it: + + ```Bash + make cypress-run + ``` diff --git a/DEVELOP.md.tpl b/DEVELOP.md.tpl new file mode 100644 index 0000000..3f1d543 --- /dev/null +++ b/DEVELOP.md.tpl @@ -0,0 +1,106 @@ +# <%= name %> + +## Develop + +1. Make sure you have `docker` and `docker compose` installed and running on your machine: + + ```Bash + git clone https://github.com/eea/<%= name %>.git + cd <%= name %> + git checkout -b bugfix-123456 develop + make + make start + ``` + +1. Wait for `Volto started at 0.0.0.0:3000` meesage + +1. Go to http://localhost:3000 + +1. Initialize git hooks + + ```Bash + yarn prepare + ``` + +1. Happy hacking! + +### Or add <%= addonName %> to your Volto project + +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 + + make develop + yarn + +1. Start backend + + docker run --pull always -it --rm --name plone -p 8080:8080 -e SITE=Plone plone/plone-backend + + ...wait for backend to setup and start - `Ready to handle requests`: + + ...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 %>/ + +## Cypress + +To run cypress locally, first make sure you don't have any Volto/Plone running on ports `8080` and `3000`. + +You don't have to be in a `clean-volto-project`, you can be in any Volto Frontend +project where you added `<%= name %>` to `mrs.developer.json` + +Go to: + + ```BASH + cd src/addons/<%= name %>/ + ``` + +Start: + + ```Bash + make + make start + ``` + +This will build and start with Docker a clean `Plone backend` and `Volto Frontend` with `<%= name %>` block installed. + +Open Cypress Interface: + + ```Bash + make cypress-open + ``` + +Or run it: + + ```Bash + make cypress-run + ``` diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..b1ce603 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,14 @@ +# syntax=docker/dockerfile:1 +ARG VOLTO_VERSION +FROM eeacms/frontend-builder:${VOLTO_VERSION} + +ARG ADDON_NAME +ARG ADDON_PATH + +COPY --chown=node:node ./ /app/src/addons/${ADDON_PATH}/ + +RUN /setupAddon +RUN yarn install + +ENTRYPOINT ["yarn"] +CMD ["start"] diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 0000000..969aada --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,310 @@ +pipeline { + agent { + node { label 'docker-host' } + } + + environment { + GIT_NAME = "volto-addon-template" + NAMESPACE = "@eeacms" + SONARQUBE_TAGS = "volto.eea.europa.eu" + DEPENDENCIES = "" + BACKEND_PROFILES = "eea.kitkat:testing" + BACKEND_ADDONS = "" + VOLTO = "16" + IMAGE_NAME = BUILD_TAG.toLowerCase() + } + + 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 run -i --rm --pull always --name="$IMAGE_NAME-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('Check if testing needed') { + when { + allOf { + not { branch 'master' } + not { branch 'develop' } + environment name: 'CHANGE_ID', value: '' + } + } + steps { + script { + withCredentials([string(credentialsId: 'eea-jenkins-token', variable: 'GITHUB_TOKEN')]) { + check_result = sh script: '''docker run --pull always -i --rm --name="$IMAGE_NAME-gitflow-check" -e GIT_TOKEN="$GITHUB_TOKEN" -e GIT_BRANCH="$BRANCH_NAME" -e GIT_ORG="$GIT_ORG" -e GIT_NAME="$GIT_NAME" eeacms/gitflow /check_if_testing_needed.sh''', returnStatus: true + + if (check_result == 0) { + env.SKIP_TESTS = 'yes' + } + } + } + } + } + + stage('Testing') { + when { + anyOf { + allOf { + not { environment name: 'CHANGE_ID', value: '' } + environment name: 'CHANGE_TARGET', value: 'develop' + environment name: 'SKIP_TESTS', value: '' + } + allOf { + environment name: 'CHANGE_ID', value: '' + anyOf { + not { changelog '.*^Automated release [0-9\\.]+$' } + branch 'master' + } + environment name: 'SKIP_TESTS', value: '' + } + } + } + stages { + stage('Build test image') { + steps { + checkout scm + sh '''docker build --pull --build-arg="VOLTO_VERSION=$VOLTO" --build-arg="ADDON_NAME=$NAMESPACE/$GIT_NAME" --build-arg="ADDON_PATH=$GIT_NAME" . -t $IMAGE_NAME-frontend''' + } + } + + stage('Fix code') { + when { + environment name: 'CHANGE_ID', value: '' + not { branch 'master' } + } + steps { + script { + fix_result = sh(script: '''docker run --name="$IMAGE_NAME-fix" --entrypoint=make --workdir=/app/src/addons/$GIT_NAME $IMAGE_NAME-frontend ci-fix''', returnStatus: true) + sh '''docker cp $IMAGE_NAME-fix:/app/src/addons/$GIT_NAME/src .''' + sh '''docker rm -v $IMAGE_NAME-fix''' + FOUND_FIX = sh(script: '''git diff | wc -l''', returnStdout: true).trim() + + if (FOUND_FIX != '0') { + withCredentials([string(credentialsId: 'eea-jenkins-token', variable: 'GITHUB_TOKEN')]) { + sh '''sed -i "s|url = .*|url = https://eea-jenkins:$GITHUB_TOKEN@github.com/eea/$GIT_NAME.git|" .git/config''' + } + sh '''git fetch origin $GIT_BRANCH:$GIT_BRANCH''' + sh '''git checkout $GIT_BRANCH''' + sh '''git add src/''' + sh '''git commit -m "style: Automated code fix" ''' + sh '''git push --set-upstream origin $GIT_BRANCH''' + sh '''exit 1''' + } + } + } + } + + stage('ES lint') { + steps { + sh '''docker run --rm --name="$IMAGE_NAME-eslint" --entrypoint=make --workdir=/app/src/addons/$GIT_NAME $IMAGE_NAME-frontend lint''' + } + } + + stage('Style lint') { + steps { + sh '''docker run --rm --name="$IMAGE_NAME-stylelint" --entrypoint=make --workdir=/app/src/addons/$GIT_NAME $IMAGE_NAME-frontend stylelint''' + } + } + + stage('Prettier') { + steps { + sh '''docker run --rm --name="$IMAGE_NAME-prettier" --entrypoint=make --workdir=/app/src/addons/$GIT_NAME $IMAGE_NAME-frontend prettier''' + } + } + + stage('Coverage Tests') { + parallel { + + stage('Unit tests') { + steps { + script { + try { + sh '''docker run --name="$IMAGE_NAME-volto" --entrypoint=make --workdir=/app/src/addons/$GIT_NAME $IMAGE_NAME-frontend test-ci''' + sh '''rm -rf xunit-reports''' + sh '''mkdir -p xunit-reports''' + sh '''docker cp $IMAGE_NAME-volto:/app/coverage xunit-reports/''' + sh '''docker cp $IMAGE_NAME-volto:/app/junit.xml xunit-reports/''' + 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 $IMAGE_NAME-volto''', returnStatus: true + } + } + } + } + + stage('Integration tests') { + steps { + script { + try { + sh '''docker run --pull always --rm -d --name="$IMAGE_NAME-plone" -e SITE="Plone" -e PROFILES="$BACKEND_PROFILES" -e ADDONS="$BACKEND_ADDONS" eeacms/plone-backend''' + sh '''timeout 3600 docker run --link $IMAGE_NAME-plone:plone --entrypoint=make --name="$IMAGE_NAME-cypress" --workdir=/app/src/addons/${GIT_NAME} -e "RAZZLE_INTERNAL_API_PATH=http://plone:8080/Plone" $IMAGE_NAME-frontend cypress-ci''' + } finally { + try { + sh '''rm -rf cypress-videos cypress-results cypress-coverage cypress-screenshots''' + sh '''mkdir -p cypress-videos cypress-results cypress-coverage cypress-screenshots''' + videos = sh script: '''docker cp $IMAGE_NAME-cypress:/app/src/addons/$GIT_NAME/cypress/videos cypress-videos/''', returnStatus: true + sh '''docker cp $IMAGE_NAME-cypress:/app/src/addons/$GIT_NAME/cypress/reports cypress-results/''' + screenshots = sh script: '''docker cp $IMAGE_NAME-cypress:/app/src/addons/$GIT_NAME/cypress/screenshots cypress-screenshots''', returnStatus: true + + archiveArtifacts artifacts: 'cypress-screenshots/**', fingerprint: true, allowEmptyArchive: true + + coverage = sh script: '''docker cp $IMAGE_NAME-cypress:/app/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']) + } + if ( videos == 0 ) { + sh '''for file in $(find cypress-results -name *.xml); do if [ $(grep -E 'failures="[1-9].*"' $file | wc -l) -eq 0 ]; then testname=$(grep -E 'file=.*failures="0"' $file | sed 's#.* file=".*\\/\\(.*\\.[jsxt]\\+\\)" time.*#\\1#' ); rm -f cypress-videos/videos/$testname.mp4; fi; done''' + archiveArtifacts artifacts: 'cypress-videos/**/*.mp4', fingerprint: true, allowEmptyArchive: true + } + } finally { + catchError(buildResult: 'SUCCESS', stageResult: 'SUCCESS') { + junit testResults: 'cypress-results/**/*.xml', allowEmptyResults: true + } + sh script: "docker stop $IMAGE_NAME-plone", returnStatus: true + sh script: "docker rm -v $IMAGE_NAME-plone", returnStatus: true + sh script: "docker rm -v $IMAGE_NAME-cypress", returnStatus: true + } + } + } + } + } + } + } + } + post { + always { + sh script: "docker rmi $IMAGE_NAME-frontend", returnStatus: true + } + } + } + + stage('Report to SonarQube') { + when { + anyOf { + allOf { + not { environment name: 'CHANGE_ID', value: '' } + environment name: 'CHANGE_TARGET', value: 'develop' + } + allOf { + environment name: 'CHANGE_ID', value: '' + anyOf { + allOf { + branch 'develop' + not { changelog '.*^Automated release [0-9\\.]+$' } + } + branch 'master' + } + } + } + } + steps { + script { + def scannerHome = tool 'SonarQubeScanner' + def nodeJS = tool 'NodeJS' + withSonarQubeEnv('Sonarqube') { + sh '''sed -i "s#/app/src/addons/${GIT_NAME}/##g" xunit-reports/coverage/lcov.info''' + sh '''sed -i "s#src/addons/${GIT_NAME}/##g" xunit-reports/coverage/lcov.info''' + 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=5; 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 15; try=\$(( \$try - 1 )); fi; done''' + } + } + } + } + + stage('SonarQube compare to master') { + when { + anyOf { + allOf { + not { environment name: 'CHANGE_ID', value: '' } + environment name: 'CHANGE_TARGET', value: 'develop' + } + allOf { + environment name: 'CHANGE_ID', value: '' + branch 'develop' + not { changelog '.*^Automated release [0-9\\.]+$' } + } + } + } + steps { + script { + sh '''echo "Error" > checkresult.txt''' + catchError(buildResult: 'FAILURE', stageResult: 'FAILURE') { + sh '''set -o pipefail; docker run -i --rm --pull always --name="$IMAGE_NAME-gitflow-sn" -e GIT_BRANCH="$BRANCH_NAME" -e GIT_NAME="$GIT_NAME" eeacms/gitflow /checkSonarqubemaster.sh | grep -v "Found script" | tee checkresult.txt''' + } + + publishChecks name: 'SonarQube', title: 'Sonarqube Code Quality Check', summary: 'Quality check on the SonarQube metrics from branch develop, comparing it with the ones from master branch. No bugs are allowed', + text: readFile(file: 'checkresult.txt'), conclusion: "${currentBuild.currentResult}", + detailsURL: "${env.BUILD_URL}display/redirect" + } + } + } + + stage('Pull Request') { + when { + not { + environment name: 'CHANGE_ID', value: '' + } + environment name: 'CHANGE_TARGET', value: 'master' + } + steps { + 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 run --pull always -i --rm --name="$IMAGE_NAME-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/Jenkinsfile.tpl b/Jenkinsfile.tpl new file mode 100644 index 0000000..c78a195 --- /dev/null +++ b/Jenkinsfile.tpl @@ -0,0 +1,310 @@ +pipeline { + agent { + node { label 'docker-host' } + } + + environment { + GIT_NAME = "<%= name %>" + NAMESPACE = "@eeacms" + SONARQUBE_TAGS = "volto.eea.europa.eu" + DEPENDENCIES = "" + BACKEND_PROFILES = "eea.kitkat:testing" + BACKEND_ADDONS = "" + VOLTO = "16" + IMAGE_NAME = BUILD_TAG.toLowerCase() + } + + 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 run -i --rm --pull always --name="$IMAGE_NAME-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('Check if testing needed') { + when { + allOf { + not { branch 'master' } + not { branch 'develop' } + environment name: 'CHANGE_ID', value: '' + } + } + steps { + script { + withCredentials([string(credentialsId: 'eea-jenkins-token', variable: 'GITHUB_TOKEN')]) { + check_result = sh script: '''docker run --pull always -i --rm --name="$IMAGE_NAME-gitflow-check" -e GIT_TOKEN="$GITHUB_TOKEN" -e GIT_BRANCH="$BRANCH_NAME" -e GIT_ORG="$GIT_ORG" -e GIT_NAME="$GIT_NAME" eeacms/gitflow /check_if_testing_needed.sh''', returnStatus: true + + if (check_result == 0) { + env.SKIP_TESTS = 'yes' + } + } + } + } + } + + stage('Testing') { + when { + anyOf { + allOf { + not { environment name: 'CHANGE_ID', value: '' } + environment name: 'CHANGE_TARGET', value: 'develop' + environment name: 'SKIP_TESTS', value: '' + } + allOf { + environment name: 'CHANGE_ID', value: '' + anyOf { + not { changelog '.*^Automated release [0-9\\.]+$' } + branch 'master' + } + environment name: 'SKIP_TESTS', value: '' + } + } + } + stages { + stage('Build test image') { + steps { + checkout scm + sh '''docker build --pull --build-arg="VOLTO_VERSION=$VOLTO" --build-arg="ADDON_NAME=$NAMESPACE/$GIT_NAME" --build-arg="ADDON_PATH=$GIT_NAME" . -t $IMAGE_NAME-frontend''' + } + } + + stage('Fix code') { + when { + environment name: 'CHANGE_ID', value: '' + not { branch 'master' } + } + steps { + script { + fix_result = sh(script: '''docker run --name="$IMAGE_NAME-fix" --entrypoint=make --workdir=/app/src/addons/$GIT_NAME $IMAGE_NAME-frontend ci-fix''', returnStatus: true) + sh '''docker cp $IMAGE_NAME-fix:/app/src/addons/$GIT_NAME/src .''' + sh '''docker rm -v $IMAGE_NAME-fix''' + FOUND_FIX = sh(script: '''git diff | wc -l''', returnStdout: true).trim() + + if (FOUND_FIX != '0') { + withCredentials([string(credentialsId: 'eea-jenkins-token', variable: 'GITHUB_TOKEN')]) { + sh '''sed -i "s|url = .*|url = https://eea-jenkins:$GITHUB_TOKEN@github.com/eea/$GIT_NAME.git|" .git/config''' + } + sh '''git fetch origin $GIT_BRANCH:$GIT_BRANCH''' + sh '''git checkout $GIT_BRANCH''' + sh '''git add src/''' + sh '''git commit -m "style: Automated code fix" ''' + sh '''git push --set-upstream origin $GIT_BRANCH''' + sh '''exit 1''' + } + } + } + } + + stage('ES lint') { + steps { + sh '''docker run --rm --name="$IMAGE_NAME-eslint" --entrypoint=make --workdir=/app/src/addons/$GIT_NAME $IMAGE_NAME-frontend lint''' + } + } + + stage('Style lint') { + steps { + sh '''docker run --rm --name="$IMAGE_NAME-stylelint" --entrypoint=make --workdir=/app/src/addons/$GIT_NAME $IMAGE_NAME-frontend stylelint''' + } + } + + stage('Prettier') { + steps { + sh '''docker run --rm --name="$IMAGE_NAME-prettier" --entrypoint=make --workdir=/app/src/addons/$GIT_NAME $IMAGE_NAME-frontend prettier''' + } + } + + stage('Coverage Tests') { + parallel { + + stage('Unit tests') { + steps { + script { + try { + sh '''docker run --name="$IMAGE_NAME-volto" --entrypoint=make --workdir=/app/src/addons/$GIT_NAME $IMAGE_NAME-frontend test-ci''' + sh '''rm -rf xunit-reports''' + sh '''mkdir -p xunit-reports''' + sh '''docker cp $IMAGE_NAME-volto:/app/coverage xunit-reports/''' + sh '''docker cp $IMAGE_NAME-volto:/app/junit.xml xunit-reports/''' + 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 $IMAGE_NAME-volto''', returnStatus: true + } + } + } + } + + stage('Integration tests') { + steps { + script { + try { + sh '''docker run --pull always --rm -d --name="$IMAGE_NAME-plone" -e SITE="Plone" -e PROFILES="$BACKEND_PROFILES" -e ADDONS="$BACKEND_ADDONS" eeacms/plone-backend''' + sh '''timeout 3600 docker run --link $IMAGE_NAME-plone:plone --entrypoint=make --name="$IMAGE_NAME-cypress" --workdir=/app/src/addons/${GIT_NAME} -e "RAZZLE_INTERNAL_API_PATH=http://plone:8080/Plone" $IMAGE_NAME-frontend cypress-ci''' + } finally { + try { + sh '''rm -rf cypress-videos cypress-results cypress-coverage cypress-screenshots''' + sh '''mkdir -p cypress-videos cypress-results cypress-coverage cypress-screenshots''' + videos = sh script: '''docker cp $IMAGE_NAME-cypress:/app/src/addons/$GIT_NAME/cypress/videos cypress-videos/''', returnStatus: true + sh '''docker cp $IMAGE_NAME-cypress:/app/src/addons/$GIT_NAME/cypress/reports cypress-results/''' + screenshots = sh script: '''docker cp $IMAGE_NAME-cypress:/app/src/addons/$GIT_NAME/cypress/screenshots cypress-screenshots''', returnStatus: true + + archiveArtifacts artifacts: 'cypress-screenshots/**', fingerprint: true, allowEmptyArchive: true + + coverage = sh script: '''docker cp $IMAGE_NAME-cypress:/app/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']) + } + if ( videos == 0 ) { + sh '''for file in $(find cypress-results -name *.xml); do if [ $(grep -E 'failures="[1-9].*"' $file | wc -l) -eq 0 ]; then testname=$(grep -E 'file=.*failures="0"' $file | sed 's#.* file=".*\\/\\(.*\\.[jsxt]\\+\\)" time.*#\\1#' ); rm -f cypress-videos/videos/$testname.mp4; fi; done''' + archiveArtifacts artifacts: 'cypress-videos/**/*.mp4', fingerprint: true, allowEmptyArchive: true + } + } finally { + catchError(buildResult: 'SUCCESS', stageResult: 'SUCCESS') { + junit testResults: 'cypress-results/**/*.xml', allowEmptyResults: true + } + sh script: "docker stop $IMAGE_NAME-plone", returnStatus: true + sh script: "docker rm -v $IMAGE_NAME-plone", returnStatus: true + sh script: "docker rm -v $IMAGE_NAME-cypress", returnStatus: true + } + } + } + } + } + } + } + } + post { + always { + sh script: "docker rmi $IMAGE_NAME-frontend", returnStatus: true + } + } + } + + stage('Report to SonarQube') { + when { + anyOf { + allOf { + not { environment name: 'CHANGE_ID', value: '' } + environment name: 'CHANGE_TARGET', value: 'develop' + } + allOf { + environment name: 'CHANGE_ID', value: '' + anyOf { + allOf { + branch 'develop' + not { changelog '.*^Automated release [0-9\\.]+$' } + } + branch 'master' + } + } + } + } + steps { + script { + def scannerHome = tool 'SonarQubeScanner' + def nodeJS = tool 'NodeJS' + withSonarQubeEnv('Sonarqube') { + sh '''sed -i "s#/app/src/addons/${GIT_NAME}/##g" xunit-reports/coverage/lcov.info''' + sh '''sed -i "s#src/addons/${GIT_NAME}/##g" xunit-reports/coverage/lcov.info''' + 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=5; 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 15; try=\$(( \$try - 1 )); fi; done''' + } + } + } + } + + stage('SonarQube compare to master') { + when { + anyOf { + allOf { + not { environment name: 'CHANGE_ID', value: '' } + environment name: 'CHANGE_TARGET', value: 'develop' + } + allOf { + environment name: 'CHANGE_ID', value: '' + branch 'develop' + not { changelog '.*^Automated release [0-9\\.]+$' } + } + } + } + steps { + script { + sh '''echo "Error" > checkresult.txt''' + catchError(buildResult: 'FAILURE', stageResult: 'FAILURE') { + sh '''set -o pipefail; docker run -i --rm --pull always --name="$IMAGE_NAME-gitflow-sn" -e GIT_BRANCH="$BRANCH_NAME" -e GIT_NAME="$GIT_NAME" eeacms/gitflow /checkSonarqubemaster.sh | grep -v "Found script" | tee checkresult.txt''' + } + + publishChecks name: 'SonarQube', title: 'Sonarqube Code Quality Check', summary: 'Quality check on the SonarQube metrics from branch develop, comparing it with the ones from master branch. No bugs are allowed', + text: readFile(file: 'checkresult.txt'), conclusion: "${currentBuild.currentResult}", + detailsURL: "${env.BUILD_URL}display/redirect" + } + } + } + + stage('Pull Request') { + when { + not { + environment name: 'CHANGE_ID', value: '' + } + environment name: 'CHANGE_TARGET', value: 'master' + } + steps { + 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 run --pull always -i --rm --name="$IMAGE_NAME-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/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..0992c00 --- /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 0000000..30b44f0 --- /dev/null +++ b/Makefile @@ -0,0 +1,164 @@ +############################################################################## +# Run: +# make +# make start +# +# Go to: +# +# http://localhost:3000 +# +# Cypress: +# +# make cypress-open +# +############################################################################## +# SETUP MAKE +# +## Defensive settings for make: https://tech.davis-hansson.com/p/make/ +SHELL:=bash +.ONESHELL: +# for Makefile debugging purposes add -x to the .SHELLFLAGS +.SHELLFLAGS:=-eu -o pipefail -O inherit_errexit -c +.SILENT: +.DELETE_ON_ERROR: +MAKEFLAGS+=--warn-undefined-variables +MAKEFLAGS+=--no-builtin-rules + +# Colors +# OK=Green, warn=yellow, error=red +ifeq ($(TERM),) +# no colors if not in terminal + MARK_COLOR= + OK_COLOR= + WARN_COLOR= + ERROR_COLOR= + NO_COLOR= +else + MARK_COLOR=`tput setaf 6` + OK_COLOR=`tput setaf 2` + WARN_COLOR=`tput setaf 3` + ERROR_COLOR=`tput setaf 1` + NO_COLOR=`tput sgr0` +endif + +############################################################################## +# SETTINGS AND VARIABLE +DIR=$(shell basename $$(pwd)) +NODE_MODULES?="../../../node_modules" +PLONE_VERSION?=6 +VOLTO_VERSION?=16 +ADDON_PATH="${DIR}" +ADDON_NAME="@eeacms/${ADDON_PATH}" +DOCKER_COMPOSE=PLONE_VERSION=${PLONE_VERSION} VOLTO_VERSION=${VOLTO_VERSION} ADDON_NAME=${ADDON_NAME} ADDON_PATH=${ADDON_PATH} docker compose +RAZZLE_INTERNAL_API_PATH?="http://localhost:8080/Plone" +RAZZLE_DEV_PROXY_API_PATH?="${RAZZLE_INTERNAL_API_PATH}" +CYPRESS_API_PATH="${RAZZLE_DEV_PROXY_API_PATH}" + + + +# Top-level targets +.PHONY: all +all: clean install + +.PHONY: clean +clean: ## Cleanup development environment + ${DOCKER_COMPOSE} down --volumes --remove-orphans + +.PHONY: install +install: ## Build and install development environment + echo "Running: ${DOCKER_COMPOSE} build" + ${DOCKER_COMPOSE} pull + ${DOCKER_COMPOSE} build + +.PHONY: start +start: ## Start development environment + echo "Running: ${DOCKER_COMPOSE} up" + ${DOCKER_COMPOSE} up + +.PHONY: shell +shell: ## Start a shell in the frontend container + echo "Running: ${DOCKER_COMPOSE} run frontend bash" + ${DOCKER_COMPOSE} run --entrypoint=bash frontend + +.PHONY: cypress-open +cypress-open: ## Open cypress integration tests + CYPRESS_API_PATH="${RAZZLE_DEV_PROXY_API_PATH}" NODE_ENV=development $(NODE_MODULES)/cypress/bin/cypress open + +.PHONY: cypress-run +cypress-run: ## Run cypress integration tests + CYPRESS_API_PATH="${RAZZLE_DEV_PROXY_API_PATH}" NODE_ENV=development $(NODE_MODULES)/cypress/bin/cypress run --browser chromium + +.PHONY: test +test: ## Run jest tests + ${DOCKER_COMPOSE} run -e CI=1 frontend test + +.PHONY: test-update +test-update: ## Update jest tests snapshots + ${DOCKER_COMPOSE} run -e CI=1 frontend test -u + +.PHONY: stylelint +stylelint: ## Stylelint + $(NODE_MODULES)/stylelint/bin/stylelint.js --allow-empty-input 'src/**/*.{css,less}' + +.PHONY: stylelint-overrides +stylelint-overrides: + $(NODE_MODULES)/.bin/stylelint --custom-syntax less --allow-empty-input 'theme/**/*.overrides' 'src/**/*.overrides' + +.PHONY: stylelint-fix +stylelint-fix: ## Fix stylelint + $(NODE_MODULES)/stylelint/bin/stylelint.js --allow-empty-input 'src/**/*.{css,less}' --fix + $(NODE_MODULES)/.bin/stylelint --custom-syntax less --allow-empty-input 'theme/**/*.overrides' 'src/**/*.overrides' --fix + +.PHONY: prettier +prettier: ## Prettier + $(NODE_MODULES)/.bin/prettier --single-quote --check 'src/**/*.{js,jsx,json,css,less,md}' + +.PHONY: prettier-fix +prettier-fix: ## Fix prettier + $(NODE_MODULES)/.bin/prettier --single-quote --write 'src/**/*.{js,jsx,json,css,less,md}' + +.PHONY: lint +lint: ## ES Lint + $(NODE_MODULES)/eslint/bin/eslint.js --max-warnings=0 'src/**/*.{js,jsx}' + +.PHONY: lint-fix +lint-fix: ## Fix ES Lint + $(NODE_MODULES)/eslint/bin/eslint.js --fix 'src/**/*.{js,jsx}' + +.PHONY: i18n +i18n: ## i18n + rm -rf build/messages + NODE_ENV=development $(NODE_MODULES)/.bin/i18n --addon + +.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 :)" + head -n 14 Makefile + +.PHONY: ci-fix +ci-fix: + echo "Running lint-fix" + make lint-fix + echo "Running prettier-fix" + make prettier-fix + echo "Running stylelint-fix" + make stylelint-fix + +.PHONY: test-ci +test-ci: + cd /app + RAZZLE_JEST_CONFIG=src/addons/${ADDON_PATH}/jest-addon.config.js CI=true yarn test src/addons/${ADDON_PATH}/src --watchAll=false --reporters=default --reporters=jest-junit --collectCoverage --coverageReporters lcov cobertura text + +.PHONY: start-ci +start-ci: + cd ../.. + yarn start & + +.PHONY: cypress-ci +cypress-ci: + cp .coverage.babel.config.js /app/babel.config.js + make start-ci + $(NODE_MODULES)/.bin/wait-on -t 240000 http://localhost:3000 + NODE_ENV=development make cypress-run + + diff --git a/Makefile.tpl b/Makefile.tpl new file mode 100644 index 0000000..30b44f0 --- /dev/null +++ b/Makefile.tpl @@ -0,0 +1,164 @@ +############################################################################## +# Run: +# make +# make start +# +# Go to: +# +# http://localhost:3000 +# +# Cypress: +# +# make cypress-open +# +############################################################################## +# SETUP MAKE +# +## Defensive settings for make: https://tech.davis-hansson.com/p/make/ +SHELL:=bash +.ONESHELL: +# for Makefile debugging purposes add -x to the .SHELLFLAGS +.SHELLFLAGS:=-eu -o pipefail -O inherit_errexit -c +.SILENT: +.DELETE_ON_ERROR: +MAKEFLAGS+=--warn-undefined-variables +MAKEFLAGS+=--no-builtin-rules + +# Colors +# OK=Green, warn=yellow, error=red +ifeq ($(TERM),) +# no colors if not in terminal + MARK_COLOR= + OK_COLOR= + WARN_COLOR= + ERROR_COLOR= + NO_COLOR= +else + MARK_COLOR=`tput setaf 6` + OK_COLOR=`tput setaf 2` + WARN_COLOR=`tput setaf 3` + ERROR_COLOR=`tput setaf 1` + NO_COLOR=`tput sgr0` +endif + +############################################################################## +# SETTINGS AND VARIABLE +DIR=$(shell basename $$(pwd)) +NODE_MODULES?="../../../node_modules" +PLONE_VERSION?=6 +VOLTO_VERSION?=16 +ADDON_PATH="${DIR}" +ADDON_NAME="@eeacms/${ADDON_PATH}" +DOCKER_COMPOSE=PLONE_VERSION=${PLONE_VERSION} VOLTO_VERSION=${VOLTO_VERSION} ADDON_NAME=${ADDON_NAME} ADDON_PATH=${ADDON_PATH} docker compose +RAZZLE_INTERNAL_API_PATH?="http://localhost:8080/Plone" +RAZZLE_DEV_PROXY_API_PATH?="${RAZZLE_INTERNAL_API_PATH}" +CYPRESS_API_PATH="${RAZZLE_DEV_PROXY_API_PATH}" + + + +# Top-level targets +.PHONY: all +all: clean install + +.PHONY: clean +clean: ## Cleanup development environment + ${DOCKER_COMPOSE} down --volumes --remove-orphans + +.PHONY: install +install: ## Build and install development environment + echo "Running: ${DOCKER_COMPOSE} build" + ${DOCKER_COMPOSE} pull + ${DOCKER_COMPOSE} build + +.PHONY: start +start: ## Start development environment + echo "Running: ${DOCKER_COMPOSE} up" + ${DOCKER_COMPOSE} up + +.PHONY: shell +shell: ## Start a shell in the frontend container + echo "Running: ${DOCKER_COMPOSE} run frontend bash" + ${DOCKER_COMPOSE} run --entrypoint=bash frontend + +.PHONY: cypress-open +cypress-open: ## Open cypress integration tests + CYPRESS_API_PATH="${RAZZLE_DEV_PROXY_API_PATH}" NODE_ENV=development $(NODE_MODULES)/cypress/bin/cypress open + +.PHONY: cypress-run +cypress-run: ## Run cypress integration tests + CYPRESS_API_PATH="${RAZZLE_DEV_PROXY_API_PATH}" NODE_ENV=development $(NODE_MODULES)/cypress/bin/cypress run --browser chromium + +.PHONY: test +test: ## Run jest tests + ${DOCKER_COMPOSE} run -e CI=1 frontend test + +.PHONY: test-update +test-update: ## Update jest tests snapshots + ${DOCKER_COMPOSE} run -e CI=1 frontend test -u + +.PHONY: stylelint +stylelint: ## Stylelint + $(NODE_MODULES)/stylelint/bin/stylelint.js --allow-empty-input 'src/**/*.{css,less}' + +.PHONY: stylelint-overrides +stylelint-overrides: + $(NODE_MODULES)/.bin/stylelint --custom-syntax less --allow-empty-input 'theme/**/*.overrides' 'src/**/*.overrides' + +.PHONY: stylelint-fix +stylelint-fix: ## Fix stylelint + $(NODE_MODULES)/stylelint/bin/stylelint.js --allow-empty-input 'src/**/*.{css,less}' --fix + $(NODE_MODULES)/.bin/stylelint --custom-syntax less --allow-empty-input 'theme/**/*.overrides' 'src/**/*.overrides' --fix + +.PHONY: prettier +prettier: ## Prettier + $(NODE_MODULES)/.bin/prettier --single-quote --check 'src/**/*.{js,jsx,json,css,less,md}' + +.PHONY: prettier-fix +prettier-fix: ## Fix prettier + $(NODE_MODULES)/.bin/prettier --single-quote --write 'src/**/*.{js,jsx,json,css,less,md}' + +.PHONY: lint +lint: ## ES Lint + $(NODE_MODULES)/eslint/bin/eslint.js --max-warnings=0 'src/**/*.{js,jsx}' + +.PHONY: lint-fix +lint-fix: ## Fix ES Lint + $(NODE_MODULES)/eslint/bin/eslint.js --fix 'src/**/*.{js,jsx}' + +.PHONY: i18n +i18n: ## i18n + rm -rf build/messages + NODE_ENV=development $(NODE_MODULES)/.bin/i18n --addon + +.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 :)" + head -n 14 Makefile + +.PHONY: ci-fix +ci-fix: + echo "Running lint-fix" + make lint-fix + echo "Running prettier-fix" + make prettier-fix + echo "Running stylelint-fix" + make stylelint-fix + +.PHONY: test-ci +test-ci: + cd /app + RAZZLE_JEST_CONFIG=src/addons/${ADDON_PATH}/jest-addon.config.js CI=true yarn test src/addons/${ADDON_PATH}/src --watchAll=false --reporters=default --reporters=jest-junit --collectCoverage --coverageReporters lcov cobertura text + +.PHONY: start-ci +start-ci: + cd ../.. + yarn start & + +.PHONY: cypress-ci +cypress-ci: + cp .coverage.babel.config.js /app/babel.config.js + make start-ci + $(NODE_MODULES)/.bin/wait-on -t 240000 http://localhost:3000 + NODE_ENV=development make cypress-run + + diff --git a/README.md b/README.md new file mode 100644 index 0000000..93d7177 --- /dev/null +++ b/README.md @@ -0,0 +1,93 @@ +# 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) +[![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-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) + + +[Volto](https://github.com/plone/volto) add-on + +## Features + +Demo GIF + +## Getting started + +### Try volto-addon-template with Docker + + git clone https://github.com/eea/volto-addon-template.git + cd volto-addon-template + make + make start + +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 + + ```Bash + docker compose up backend + ``` + +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": "*" + } + ``` + +* If not, create one: + + ``` + npm install -g yo @plone/generator-volto + yo @plone/volto my-volto-project --canary --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! + +## Release + +See [RELEASE.md](https://github.com/eea/volto-addon-template/blob/master/RELEASE.md). + +## 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 0000000..e4ec495 --- /dev/null +++ b/README.md.tpl @@ -0,0 +1,93 @@ +# <%= 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 + + git clone https://github.com/eea/<%= name %>.git + cd <%= name %> + make + make start + +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 + + ```Bash + docker compose up backend + ``` + +1. Start Volto frontend + +* If you already have a volto project, just update `package.json`: + + ```JSON + "addons": [ + "<%= addonName %>" + ], + + "dependencies": { + "<%= addonName %>": "*" + } + ``` + +* If not, create one: + + ``` + npm install -g yo @plone/generator-volto + yo @plone/volto my-volto-project --canary --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/RELEASE.md b/RELEASE.md new file mode 100644 index 0000000..4e311a0 --- /dev/null +++ b/RELEASE.md @@ -0,0 +1,74 @@ +## Release + +### Automatic release using Jenkins + +* The automatic release is started by creating a [Pull Request](../../compare/master...develop) from `develop` to `master`. The pull request status checks correlated to the branch and PR Jenkins jobs need to be processed successfully. 1 review from a github user with rights is mandatory. +* It runs on every commit on `master` branch, which is protected from direct commits, only allowing pull request merge commits. +* The automatic release is done by [Jenkins](https://ci.eionet.europa.eu). The status of the release job can be seen both in the Readme.md badges and the green check/red cross/yellow circle near the last commit information. If you click on the icon, you will have the list of checks that were run. The `continuous-integration/jenkins/branch` link goes to the Jenkins job execution webpage. +* Automated release scripts are located in the `eeacms/gitflow` docker image, specifically [js-release.sh](https://github.com/eea/eea.docker.gitflow/blob/master/src/js-release.sh) script. It uses the `release-it` tool. +* As long as a PR request is open from develop to master, the PR Jenkins job will automatically re-create the CHANGELOG.md and package.json files to be production-ready. +* The version format must be MAJOR.MINOR.PATCH. By default, next release is set to next minor version (with patch 0). +* You can manually change the version in `package.json`. The new version must not be already present in the tags/releases of the repository, otherwise it will be automatically increased by the script. Any changes to the version will trigger a `CHANGELOG.md` re-generation. +* Automated commits and commits with [JENKINS] or [YARN] in the commit log are excluded from `CHANGELOG.md` file. + +### Manual release from the develop branch ( beta release ) + +#### Installation and configuration of release-it + +You need to first install the [release-it](https://github.com/release-it/release-it) client. + + ``` + npm install -g release-it + ``` + +Release-it uses the configuration written in the [`.release-it.json`](./.release-it.json) file located in the root of the repository. + +Release-it is a tool that automates 4 important steps in the release process: + +1. Version increase in `package.json` ( increased from the current version in `package.json`) +2. `CHANGELOG.md` automatic generation from commit messages ( grouped by releases ) +3. GitHub release on the commit with the changelog and package.json modification on the develop branch +4. NPM release ( by default it's disabled, but can be enabled in the configuration file ) + +To configure the authentification, you need to export GITHUB_TOKEN for [GitHub](https://github.com/settings/tokens) + + ``` + export GITHUB_TOKEN=XXX-XXXXXXXXXXXXXXXXXXXXXX + ``` + + To configure npm, you can use the `npm login` command or use a configuration file with a TOKEN : + + ``` + echo "//registry.npmjs.org/:_authToken=YYYYYYYYYYYYYYYYYYYYYYYYYYYYYY" > .npmrc + ``` + +#### Using release-it tool + +There are 3 yarn scripts that can be run to do the release + +##### yarn release-beta + +Automatically calculates and presents 3 beta versions - patch, minor and major for you to choose ( or Other for manual input). + +``` +? Select increment (next version): +❯ prepatch (0.1.1-beta.0) + preminor (0.2.0-beta.0) + premajor (1.0.0-beta.0) + Other, please specify... +``` + +##### yarn release-major-beta + +Same as `yarn release-beta`, but with premajor version pre-selected. + +##### yarn release + +Generic command, does not automatically add the `beta` to version, but you can still manually write it if you choose Other. + +#### Important notes + +> Do not use release-it tool on master branch, the commit on CHANGELOG.md file and the version increase in the package.json file can't be done without a PULL REQUEST. + +> Do not keep Pull Requests from develop to master branches open when you are doing beta releases from the develop branch. As long as a PR to master is open, an automatic script will run on every commit and will update both the version and the changelog to a production-ready state - ( MAJOR.MINOR.PATCH mandatory format for version). + diff --git a/babel.config.js b/babel.config.js new file mode 100644 index 0000000..51bd52b --- /dev/null +++ b/babel.config.js @@ -0,0 +1,17 @@ +module.exports = function (api) { + api.cache(true); + const presets = ['razzle']; + const plugins = [ + [ + 'react-intl', // React Intl extractor, required for the whole i18n infrastructure to work + { + messagesDir: './build/messages/', + }, + ], + ]; + + return { + plugins, + presets, + }; +}; diff --git a/bootstrap b/bootstrap new file mode 100644 index 0000000..8613750 --- /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.config.js b/cypress.config.js new file mode 100644 index 0000000..02c57eb --- /dev/null +++ b/cypress.config.js @@ -0,0 +1,27 @@ +const { defineConfig } = require('cypress'); + +module.exports = defineConfig({ + viewportWidth: 1280, + defaultCommandTimeout: 8888, + chromeWebSecurity: false, + reporter: 'junit', + video: false, + retries: { + runMode: 2, + openMode: 0, + }, + reporterOptions: { + mochaFile: 'cypress/reports/cypress-[hash].xml', + jenkinsMode: true, + toConsole: true, + }, + e2e: { + setupNodeEvents(on, config) { + // e2e testing node events setup code + require('@cypress/code-coverage/task')(on, config); + require('cypress-fail-fast/plugin')(on, config); + return config; + }, + baseUrl: 'http://localhost:3000', + }, +}); diff --git a/cypress/e2e/01-block-basics.cy.js b/cypress/e2e/01-block-basics.cy.js new file mode 100644 index 0000000..089c7b3 --- /dev/null +++ b/cypress/e2e/01-block-basics.cy.js @@ -0,0 +1,29 @@ +import { slateBeforeEach, slateAfterEach } from '../support/e2e'; + +describe('Blocks Tests', () => { + beforeEach(slateBeforeEach); + afterEach(slateAfterEach); + + it('Add Block: Empty', () => { + // Change page title + cy.clearSlateTitle(); + cy.getSlateTitle().type('My Add-on Page'); + + cy.get('.documentFirstHeading').contains('My Add-on Page'); + + cy.getSlate().click(); + + // 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/fixtures/example.json b/cypress/fixtures/example.json new file mode 100644 index 0000000..da18d93 --- /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/support/commands.js b/cypress/support/commands.js new file mode 100644 index 0000000..f269a7f --- /dev/null +++ b/cypress/support/commands.js @@ -0,0 +1,536 @@ +/* eslint no-console: ["error", { allow: ["log"] }] */ + +const SLATE_SELECTOR = '.content-area .slate-editor [contenteditable=true]'; +const SLATE_TITLE_SELECTOR = '.block.inner.title [contenteditable="true"]'; + +// --- 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': 'slate' }, + }, + 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`)); + } + } +); + +// --- Add DX Content-Type ---------------------------------------------------------- +Cypress.Commands.add('addContentType', (name) => { + 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}/@controlpanels/dexterity-types/${name}`, + headers: { + Accept: 'application/json', + }, + auth: auth, + body: { + title: name, + }, + }) + .then(() => console.log(`${name} content-type added.`)); +}); + +// --- Remove DX behavior ---------------------------------------------------------- +Cypress.Commands.add('removeContentType', (name) => { + 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}/@controlpanels/dexterity-types/${name}`, + headers: { + Accept: 'application/json', + }, + auth: auth, + body: {}, + }) + .then(() => console.log(`${name} content-type removed.`)); +}); + +// --- Add DX field ---------------------------------------------------------- +Cypress.Commands.add('addSlateJSONField', (type, name) => { + 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}/@types/${type}`, + headers: { + Accept: 'application/json', + }, + auth: auth, + body: { + id: name, + title: name, + description: 'Slate JSON Field', + factory: 'SlateJSONField', + required: false, + }, + }) + .then(() => console.log(`${name} SlateJSONField field added to ${type}`)); +}); + +// --- Remove DX field ---------------------------------------------------------- +Cypress.Commands.add('removeSlateJSONField', (type, name) => { + 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}/@types/${type}/${name}`, + headers: { + Accept: 'application/json', + }, + auth: auth, + body: {}, + }) + .then(() => + console.log(`${name} SlateJSONField field removed from ${type}`) + ); +}); + +// --- 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`)); +}); + +Cypress.Commands.add('typeInSlate', { prevSubject: true }, (subject, text) => { + return ( + cy + .wrap(subject) + .then((subject) => { + subject[0].dispatchEvent( + new InputEvent('beforeinput', { + inputType: 'insertText', + data: text, + }) + ); + return subject; + }) + // TODO: do this only for Electron-based browser which does not understand instantaneously + // that the user inserted some text in the block + .wait(1000) + ); +}); + +Cypress.Commands.add('lineBreakInSlate', { prevSubject: true }, (subject) => { + return ( + cy + .wrap(subject) + .then((subject) => { + subject[0].dispatchEvent( + new InputEvent('beforeinput', { inputType: 'insertLineBreak' }) + ); + return subject; + }) + // TODO: do this only for Electron-based browser which does not understand instantaneously + // that the block was split + .wait(1000) + ); +}); + +// --- 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); + } + }); + } +); + +Cypress.Commands.add('getSlate', ({ createNewSlate = true } = {}) => { + let slate; + cy.getIfExists( + SLATE_SELECTOR, + () => { + slate = cy.get(SLATE_SELECTOR).last(); + }, + () => { + if (createNewSlate) { + cy.get('.block.inner').last().type('{moveToEnd}{enter}'); + } + slate = cy.get(SLATE_SELECTOR, { timeout: 10000 }).last(); + } + ); + return slate; +}); + +Cypress.Commands.add('clearSlate', (selector) => { + return cy + .get(selector) + .focus() + .click() + .wait(1000) + .type('{selectAll}') + .wait(1000) + .type('{backspace}'); +}); + +Cypress.Commands.add('getSlateTitle', () => { + return cy.get(SLATE_TITLE_SELECTOR, { + timeout: 10000, + }); +}); + +Cypress.Commands.add('clearSlateTitle', () => { + return cy.clearSlate(SLATE_TITLE_SELECTOR); +}); + +Cypress.Commands.add('setSlateSelection', (subject, query, endQuery) => { + cy.get('.slate-editor.selected [contenteditable=true]') + .focus() + .click() + .setSelection(subject, query, endQuery) + .wait(1000); // this wait is needed for the selection change to be detected after +}); + +Cypress.Commands.add('getSlateEditorAndType', (type) => { + cy.getSlate().focus().click().type(type); +}); + +Cypress.Commands.add('setSlateCursor', (subject, query, endQuery) => { + cy.get('.slate-editor.selected [contenteditable=true]') + .focus() + .click() + .setCursor(subject, query, endQuery) + .wait(1000); +}); + +Cypress.Commands.add('clickSlateButton', (button) => { + cy.get(`.slate-inline-toolbar .button-wrapper a[title="${button}"]`, { + timeout: 10000, + }).click({ force: true }); //force click is needed to ensure the button in visible in view. +}); + +Cypress.Commands.add('toolbarSave', () => { + cy.wait(1000); + + // Save + cy.get('#toolbar-save').click(); + cy.waitForResourceToLoad('@navigation'); + cy.waitForResourceToLoad('@breadcrumbs'); + cy.waitForResourceToLoad('@actions'); + cy.waitForResourceToLoad('@types'); + cy.waitForResourceToLoad('my-page'); + cy.url().should('eq', Cypress.config().baseUrl + '/cypress/my-page'); +}); + +// 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'); +}); + +Cypress.Commands.add( + 'getIfExists', + (selector, successAction = () => {}, failAction = () => {}) => { + cy.get('body').then((body) => { + if (body.find(selector).length > 0 && successAction) { + successAction(); + } else if (failAction) { + failAction(); + } + }); + } +); diff --git a/cypress/support/e2e.js b/cypress/support/e2e.js new file mode 100644 index 0000000..b7c02f2 --- /dev/null +++ b/cypress/support/e2e.js @@ -0,0 +1,128 @@ +// *********************************************************** +// 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') + +//Generate code-coverage +import '@cypress/code-coverage/support'; + +// Fail Fast +import "cypress-fail-fast"; + +export const slateBeforeEach = (contentType = 'Document') => { + cy.autologin(); + cy.createContent({ + contentType: 'Document', + contentId: 'cypress', + contentTitle: 'Cypress', + }); + cy.createContent({ + contentType: contentType, + 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'); +}; + +export const slateAfterEach = () => { + cy.autologin(); + cy.removeContent('cypress'); +}; + +export const slateJsonBeforeEach = (contentType = 'slate') => { + cy.autologin(); + cy.addContentType(contentType); + cy.addSlateJSONField(contentType, 'slate'); + slateBeforeEach(contentType); +}; + +export const slateJsonAfterEach = (contentType = 'slate') => { + cy.autologin(); + cy.removeContentType(contentType); + slateAfterEach(); +}; + +export const getSelectedSlateEditor = () => { + return cy.get('.slate-editor.selected [contenteditable=true]').click(); +}; + +export const createSlateBlock = () => { + cy.get('.ui.basic.icon.button.block-add-button').first().click(); + cy.get('.blocks-chooser .title').contains('Text').click(); + cy.get('.ui.basic.icon.button.slate').contains('Text').click(); + return getSelectedSlateEditor(); +}; + +export const getSlateBlockValue = (sb) => { + return sb.invoke('attr', 'data-slate-value').then((str) => { + return typeof str === 'undefined' ? [] : JSON.parse(str); + }); +}; + +export const createSlateBlockWithList = ({ + numbered, + firstItemText, + secondItemText, +}) => { + let s1 = createSlateBlock(); + + s1.typeInSlate(firstItemText + secondItemText); + + // select all contents of slate block + // - this opens hovering toolbar + cy.contains(firstItemText + secondItemText).then((el) => { + selectSlateNodeOfWord(el); + }); + + // TODO: do not hardcode these selectors: + if (numbered) { + // this is the numbered list option in the hovering toolbar + cy.get('.slate-inline-toolbar > :nth-child(9)').click(); + } else { + // this is the bulleted list option in the hovering toolbar + cy.get('.slate-inline-toolbar > :nth-child(10)').click(); + } + + // move the text cursor + const sse = getSelectedSlateEditor(); + sse.type('{leftarrow}'); + for (let i = 0; i < firstItemText.length; ++i) { + sse.type('{rightarrow}'); + } + + // simulate pressing Enter + getSelectedSlateEditor().lineBreakInSlate(); + + return s1; +}; + +export const selectSlateNodeOfWord = (el) => { + return cy.window().then((win) => { + var event = new CustomEvent('Test_SelectWord', { + detail: el[0], + }); + win.document.dispatchEvent(event); + }); +}; diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..5d79f5c --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,32 @@ +version: "3" +services: + backend: + image: eeacms/plone-backend + ports: + - "8080:8080" + environment: + SITE: "Plone" + PROFILES: "eea.kitkat:testing" + + frontend: + build: + context: ./ + dockerfile: ./Dockerfile + args: + ADDON_NAME: "${ADDON_NAME}" + ADDON_PATH: "${ADDON_PATH}" + VOLTO_VERSION: ${VOLTO_VERSION:-16} + ports: + - "3000:3000" + - "3001:3001" + depends_on: + - backend + volumes: + - ./:/app/src/addons/${ADDON_PATH} + environment: + CI: "true" + NODE_ENV: "development" + RAZZLE_JEST_CONFIG: "src/addons/${ADDON_PATH}/jest-addon.config.js" + RAZZLE_INTERNAL_API_PATH: "http://backend:8080/Plone" + RAZZLE_DEV_PROXY_API_PATH: "http://backend:8080/Plone" + HOST: "0.0.0.0" diff --git a/jest-addon.config.js b/jest-addon.config.js new file mode 100644 index 0000000..3c86610 --- /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: { + '\\.(css|less|scss|sass)$': 'identity-obj-proxy', + '@plone/volto/cypress': '/node_modules/@plone/volto/cypress', + '@plone/volto/babel': '/node_modules/@plone/volto/babel', + '@plone/volto/(.*)$': '/node_modules/@plone/volto/src/$1', + '@package/(.*)$': '/src/$1', + '@root/(.*)$': '/src/$1', + '@plone/volto-quanta/(.*)$': '/src/addons/volto-quanta/src/$1', + '@eeacms/(.*?)/(.*)$': '/node_modules/@eeacms/$1/src/$2', + '@plone/volto-slate': + '/node_modules/@plone/volto/packages/volto-slate/src', + '~/(.*)$': '/src/$1', + 'load-volto-addons': + '/node_modules/@plone/volto/jest-addons-loader.js', + }, + transform: { + '^.+\\.js(x)?$': 'babel-jest', + '^.+\\.(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/de/LC_MESSAGES/volto.po b/locales/de/LC_MESSAGES/volto.po new file mode 100644 index 0000000..12b9a47 --- /dev/null +++ b/locales/de/LC_MESSAGES/volto.po @@ -0,0 +1,14 @@ +msgid "" +msgstr "" +"Project-Id-Version: \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: \n" +"PO-Revision-Date: \n" +"Last-Translator: \n" +"Language: \n" +"Language-Team: \n" +"Content-Type: \n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + + diff --git a/locales/en/LC_MESSAGES/volto.po b/locales/en/LC_MESSAGES/volto.po new file mode 100644 index 0000000..12b9a47 --- /dev/null +++ b/locales/en/LC_MESSAGES/volto.po @@ -0,0 +1,14 @@ +msgid "" +msgstr "" +"Project-Id-Version: \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: \n" +"PO-Revision-Date: \n" +"Last-Translator: \n" +"Language: \n" +"Language-Team: \n" +"Content-Type: \n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + + diff --git a/locales/it/LC_MESSAGES/volto.po b/locales/it/LC_MESSAGES/volto.po new file mode 100644 index 0000000..12b9a47 --- /dev/null +++ b/locales/it/LC_MESSAGES/volto.po @@ -0,0 +1,14 @@ +msgid "" +msgstr "" +"Project-Id-Version: \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: \n" +"PO-Revision-Date: \n" +"Last-Translator: \n" +"Language: \n" +"Language-Team: \n" +"Content-Type: \n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + + diff --git a/locales/ro/LC_MESSAGES/volto.po b/locales/ro/LC_MESSAGES/volto.po new file mode 100644 index 0000000..12b9a47 --- /dev/null +++ b/locales/ro/LC_MESSAGES/volto.po @@ -0,0 +1,14 @@ +msgid "" +msgstr "" +"Project-Id-Version: \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: \n" +"PO-Revision-Date: \n" +"Last-Translator: \n" +"Language: \n" +"Language-Team: \n" +"Content-Type: \n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + + diff --git a/locales/volto.pot b/locales/volto.pot new file mode 100644 index 0000000..93c2825 --- /dev/null +++ b/locales/volto.pot @@ -0,0 +1,16 @@ +msgid "" +msgstr "" +"Project-Id-Version: Plone\n" +"POT-Creation-Date: 2023-06-28T10:48:22.678Z\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" + + diff --git a/package.json b/package.json new file mode 100644 index 0000000..f1ad302 --- /dev/null +++ b/package.json @@ -0,0 +1,70 @@ +{ + "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": { + "@plone/scripts": "*", + "@cypress/code-coverage": "^3.10.0", + "cypress-fail-fast": "^5.0.1", + "babel-plugin-transform-class-properties": "^6.24.1", + "husky": "^8.0.3", + "lint-staged": "^14.0.1", + "md5": "^2.3.0" + }, + "lint-staged": { + "src/**/*.{js,jsx,ts,tsx,json}": [ + "make lint-fix", + "make prettier-fix" + ], + "src/**/*.{jsx}": [ + "make i18n" + ], + "theme/**/*.{css,less}": [ + "make stylelint-fix" + ], + "src/**/*.{css,less}": [ + "make stylelint-fix" + ], + "theme/**/*.overrides": [ + "make stylelint-fix" + ], + "src/**/*.overrides": [ + "make stylelint-fix" + ] + }, + "scripts": { + "prepare": "husky install", + "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": "make stylelint", + "stylelint:overrides": "make stylelint-overrides", + "stylelint:fix": "make stylelint-fix", + "prettier": "make prettier", + "prettier:fix": "make prettier-fix", + "lint": "make lint", + "lint:fix": "make lint-fix", + "i18n": "make i18n", + "cypress:run": "make cypress-run", + "cypress:open": "make cypress-open" + } +} diff --git a/package.json.tpl b/package.json.tpl new file mode 100644 index 0000000..cb89fbf --- /dev/null +++ b/package.json.tpl @@ -0,0 +1,70 @@ +{ + "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": { + "@plone/scripts": "*", + "@cypress/code-coverage": "^3.10.0", + "cypress-fail-fast": "^5.0.1", + "babel-plugin-transform-class-properties": "^6.24.1", + "husky": "^8.0.3", + "lint-staged": "^14.0.1", + "md5": "^2.3.0" + }, + "lint-staged": { + "src/**/*.{js,jsx,ts,tsx,json}": [ + "make lint-fix", + "make prettier-fix" + ], + "src/**/*.{jsx}": [ + "make i18n" + ], + "theme/**/*.{css,less}": [ + "make stylelint-fix" + ], + "src/**/*.{css,less}": [ + "make stylelint-fix" + ], + "theme/**/*.overrides": [ + "make stylelint-fix" + ], + "src/**/*.overrides": [ + "make stylelint-fix" + ] + }, + "scripts": { + "prepare": "husky install", + "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": "make stylelint", + "stylelint:overrides": "make stylelint-overrides", + "stylelint:fix": "make stylelint-fix", + "prettier": "make prettier", + "prettier:fix": "make prettier-fix", + "lint": "make lint", + "lint:fix": "make lint-fix", + "i18n": "make i18n", + "cypress:run": "make cypress-run", + "cypress:open": "make cypress-open" + } +} diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..cb042f0 --- /dev/null +++ b/src/index.js @@ -0,0 +1,5 @@ +const applyConfig = (config) => { + return config; +}; + +export default applyConfig;