diff --git a/.github/workflows/acceptance.yml b/.github/workflows/acceptance.yml index f4637cded8..9f6e63162a 100644 --- a/.github/workflows/acceptance.yml +++ b/.github/workflows/acceptance.yml @@ -739,3 +739,412 @@ jobs: with: name: cypress-videos path: packages/seven/cypress/videos + + prefix-core: + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name + runs-on: ubuntu-latest + name: Core Basic Prefixed Site + timeout-minutes: 45 + strategy: + fail-fast: false + matrix: + node-version: [22.x] + steps: + - uses: actions/checkout@v4 + + - name: Set up Node.js environment + uses: ./.github/actions/node_env_setup + with: + node-version: ${{ matrix.node-version }} + + - name: Cypress acceptance tests + uses: cypress-io/github-action@v6 + env: + BABEL_ENV: production + CYPRESS_RETRIES: 2 + # Recommended: pass the GitHub token lets this action correctly + # determine the unique run id necessary to re-run the checks + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + install: false + working-directory: packages/volto + browser: chrome + spec: cypress/tests/core/basic/**/*.js + config: baseUrl=http://localhost/foo + env: prefixPath=/foo + start: | + make ci-acceptance-backend-start + make prefixed-acceptance-frontend-prod-start + make deployment-prefixed-acceptance-web-server-start + wait-on: 'npx wait-on --httpTimeout 20000 http-get://127.0.0.1:55001/plone http://127.0.0.1:3000/foo http://localhost/foo' + + # Upload Cypress screenshots + - uses: actions/upload-artifact@v4 + if: failure() + with: + name: cypress-screenshots + path: packages/volto/cypress/screenshots + # Upload Cypress videos + - uses: actions/upload-artifact@v4 + if: failure() + with: + name: cypress-videos + path: packages/volto/cypress/videos + + prefix-corecontent: + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name + runs-on: ubuntu-latest + name: Core Content Prefixed Site + timeout-minutes: 45 + strategy: + fail-fast: false + matrix: + node-version: [22.x] + steps: + - uses: actions/checkout@v4 + + - name: Set up Node.js environment + uses: ./.github/actions/node_env_setup + with: + node-version: ${{ matrix.node-version }} + + - name: Cypress acceptance tests + uses: cypress-io/github-action@v6 + env: + BABEL_ENV: production + CYPRESS_RETRIES: 2 + # Recommended: pass the GitHub token lets this action correctly + # determine the unique run id necessary to re-run the checks + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + install: false + working-directory: packages/volto + browser: chrome + spec: cypress/tests/core/content/**/*.js + config: baseUrl=http://localhost/foo + env: prefixPath=/foo + start: | + make ci-acceptance-backend-start + make prefixed-acceptance-frontend-prod-start + make deployment-prefixed-acceptance-web-server-start + wait-on: 'npx wait-on --httpTimeout 20000 http-get://127.0.0.1:55001/plone http://127.0.0.1:3000/foo http://localhost/foo' + + # Upload Cypress screenshots + - uses: actions/upload-artifact@v4 + if: failure() + with: + name: cypress-screenshots + path: packages/volto/cypress/screenshots + # Upload Cypress videos + - uses: actions/upload-artifact@v4 + if: failure() + with: + name: cypress-videos + path: packages/volto/cypress/videos + + prefix-corecontrolpanels: + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name + runs-on: ubuntu-latest + name: Core Control Panels Prefixed Site + timeout-minutes: 45 + strategy: + fail-fast: false + matrix: + node-version: [22.x] + steps: + - uses: actions/checkout@v4 + + - name: Set up Node.js environment + uses: ./.github/actions/node_env_setup + with: + node-version: ${{ matrix.node-version }} + + - name: Cypress acceptance tests + uses: cypress-io/github-action@v6 + env: + BABEL_ENV: production + CYPRESS_RETRIES: 2 + # Recommended: pass the GitHub token lets this action correctly + # determine the unique run id necessary to re-run the checks + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + install: false + working-directory: packages/volto + browser: chrome + spec: cypress/tests/core/controlpanels/**/*.js + config: baseUrl=http://localhost/foo + env: prefixPath=/foo + start: | + make ci-acceptance-backend-start + make prefixed-acceptance-frontend-prod-start + make deployment-prefixed-acceptance-web-server-start + wait-on: 'npx wait-on --httpTimeout 20000 http-get://127.0.0.1:55001/plone http://127.0.0.1:3000/foo http://localhost/foo' + + # Upload Cypress screenshots + - uses: actions/upload-artifact@v4 + if: failure() + with: + name: cypress-screenshots + path: packages/volto/cypress/screenshots + # Upload Cypress videos + - uses: actions/upload-artifact@v4 + if: failure() + with: + name: cypress-videos + path: packages/volto/cypress/videos + + prefix-coreblocks: + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name + runs-on: ubuntu-latest + name: Core Blocks Prefixed Site + timeout-minutes: 35 + strategy: + fail-fast: false + matrix: + node-version: [22.x] + steps: + - uses: actions/checkout@v4 + + - name: Set up Node.js environment + uses: ./.github/actions/node_env_setup + with: + node-version: ${{ matrix.node-version }} + + - name: Cypress acceptance tests + uses: cypress-io/github-action@v6 + env: + BABEL_ENV: production + CYPRESS_RETRIES: 2 + # Recommended: pass the GitHub token lets this action correctly + # determine the unique run id necessary to re-run the checks + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + install: false + working-directory: packages/volto + browser: chrome + spec: cypress/tests/core/blocks/*.js + config: baseUrl=http://localhost/foo + env: prefixPath=/foo + start: | + make ci-acceptance-backend-start + make prefixed-acceptance-frontend-prod-start + make deployment-prefixed-acceptance-web-server-start + wait-on: 'npx wait-on --httpTimeout 20000 http-get://127.0.0.1:55001/plone http://127.0.0.1:3000/foo http://localhost/foo' + + # Upload Cypress screenshots + - uses: actions/upload-artifact@v4 + if: failure() + with: + name: cypress-screenshots + path: packages/volto/cypress/screenshots + # Upload Cypress videos + - uses: actions/upload-artifact@v4 + if: failure() + with: + name: cypress-videos + path: packages/volto/cypress/videos + + prefix-coreblockslisting: + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name + runs-on: ubuntu-latest + name: Core Blocks - Listing Prefixed Site + timeout-minutes: 35 + strategy: + fail-fast: false + matrix: + node-version: [22.x] + steps: + - uses: actions/checkout@v4 + + - name: Set up Node.js environment + uses: ./.github/actions/node_env_setup + with: + node-version: ${{ matrix.node-version }} + + - name: Cypress acceptance tests + uses: cypress-io/github-action@v6 + env: + BABEL_ENV: production + CYPRESS_RETRIES: 2 + # Recommended: pass the GitHub token lets this action correctly + # determine the unique run id necessary to re-run the checks + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + install: false + working-directory: packages/volto + browser: chrome + spec: cypress/tests/core/blocks/listing/*.js + config: baseUrl=http://localhost/foo + env: prefixPath=/foo + start: | + make ci-acceptance-backend-start + make prefixed-acceptance-frontend-prod-start + make deployment-prefixed-acceptance-web-server-start + wait-on: 'npx wait-on --httpTimeout 20000 http-get://127.0.0.1:55001/plone http://127.0.0.1:3000/foo http://localhost/foo' + + # Upload Cypress screenshots + - uses: actions/upload-artifact@v4 + if: failure() + with: + name: cypress-screenshots + path: packages/volto/cypress/screenshots + # Upload Cypress videos + - uses: actions/upload-artifact@v4 + if: failure() + with: + name: cypress-videos + path: packages/volto/cypress/videos + + prefix-corevoltoslate: + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name + runs-on: ubuntu-latest + name: Core Volto Slate Prefixed Site + timeout-minutes: 45 + strategy: + fail-fast: false + matrix: + node-version: [22.x] + steps: + - uses: actions/checkout@v4 + + - name: Set up Node.js environment + uses: ./.github/actions/node_env_setup + with: + node-version: ${{ matrix.node-version }} + + - name: Cypress acceptance tests + uses: cypress-io/github-action@v6 + env: + BABEL_ENV: production + CYPRESS_RETRIES: 2 + # Recommended: pass the GitHub token lets this action correctly + # determine the unique run id necessary to re-run the checks + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + install: false + working-directory: packages/volto + browser: chrome + spec: cypress/tests/core/volto-slate/**/*.js + config: baseUrl=http://localhost/foo + env: prefixPath=/foo + start: | + make ci-acceptance-backend-start + make prefixed-acceptance-frontend-prod-start + make deployment-prefixed-acceptance-web-server-start + wait-on: 'npx wait-on --httpTimeout 20000 http-get://127.0.0.1:55001/plone http://127.0.0.1:3000/foo http://localhost/foo' + + # Upload Cypress screenshots + - uses: actions/upload-artifact@v4 + if: failure() + with: + name: cypress-screenshots + path: packages/volto/cypress/screenshots + # Upload Cypress videos + - uses: actions/upload-artifact@v4 + if: failure() + with: + name: cypress-videos + path: packages/volto/cypress/videos + + prefix-multilingual: + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name + name: Multilingual Prefixed Site + runs-on: ubuntu-latest + timeout-minutes: 35 + strategy: + fail-fast: false + matrix: + node-version: [22.x] + steps: + - uses: actions/checkout@v4 + + - name: Set up Node.js environment + uses: ./.github/actions/node_env_setup + with: + node-version: ${{ matrix.node-version }} + + - name: Cypress acceptance tests + uses: cypress-io/github-action@v6 + env: + BABEL_ENV: production + CYPRESS_RETRIES: 2 + # Recommended: pass the GitHub token lets this action correctly + # determine the unique run id necessary to re-run the checks + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + install: false + working-directory: packages/volto + browser: chrome + spec: cypress/tests/multilingual/**/*.js + config: baseUrl=http://localhost/foo + env: prefixPath=/foo + start: | + make deployment-multilingual-acceptance-backend-start + make prefixed-multilingual-acceptance-frontend-prod-start + make deployment-prefixed-acceptance-web-server-start + wait-on: 'npx wait-on --httpTimeout 20000 http-get://127.0.0.1:55001/plone http://127.0.0.1:3000/foo http://localhost/foo' + + # Upload Cypress screenshots + - uses: actions/upload-artifact@v4 + if: failure() + with: + name: cypress-screenshots + path: packages/volto/cypress/screenshots + # Upload Cypress videos + - uses: actions/upload-artifact@v4 + if: failure() + with: + name: cypress-videos + path: packages/volto/cypress/videos + + prefix-workingcopy: + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name + name: Working Copy Prefixed Site + runs-on: ubuntu-latest + timeout-minutes: 35 + strategy: + fail-fast: false + matrix: + node-version: [22.x] + # python-version: [3.7] + steps: + - uses: actions/checkout@v4 + + - name: Set up Node.js environment + uses: ./.github/actions/node_env_setup + with: + node-version: ${{ matrix.node-version }} + + - name: Cypress acceptance tests + uses: cypress-io/github-action@v6 + env: + BABEL_ENV: production + CYPRESS_RETRIES: 2 + # Recommended: pass the GitHub token lets this action correctly + # determine the unique run id necessary to re-run the checks + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + install: false + working-directory: packages/volto + browser: chrome + spec: cypress/tests/workingCopy/**/*.js + config: baseUrl=http://localhost/foo + env: prefixPath=/foo + start: | + make working-copy-acceptance-backend-start + make prefixed-working-copy-acceptance-frontend-prod-start + make deployment-prefixed-acceptance-web-server-start + wait-on: 'npx wait-on --httpTimeout 20000 http-get://127.0.0.1:55001/plone http://127.0.0.1:3000/foo http://localhost/foo' + + # Upload Cypress screenshots + - uses: actions/upload-artifact@v4 + if: failure() + with: + name: cypress-screenshots + path: packages/volto/cypress/screenshots + # Upload Cypress videos + - uses: actions/upload-artifact@v4 + if: failure() + with: + name: cypress-videos + path: packages/volto/cypress/videos diff --git a/Makefile b/Makefile index aaae9399f8..aa761c9442 100644 --- a/Makefile +++ b/Makefile @@ -315,6 +315,42 @@ multilingual-ci-acceptance-test: ## Run Cypress tests in headless mode for CI fo multilingual-ci-acceptance-test-run-all: ## With a single command, run the backend, frontend, and the Cypress tests in headless mode for CI for multilingual tests $(MAKE) -C "./packages/volto/" multilingual-ci-acceptance-test-run-all +######### Prefixed Core Acceptance tests + +.PHONY: prefixed-acceptance-frontend-prod-start +prefixed-acceptance-frontend-prod-start: ## Start the prefixed Core Acceptance Frontend Fixture + $(MAKE) -C "./packages/volto/" prefixed-acceptance-frontend-prod-start + +.PHONY: prefixed-ci-acceptance-test-run-all +prefixed-ci-acceptance-test-run-all: ## Runs prefixed Core Full Acceptance Testing in headless mode + $(MAKE) -C "./packages/volto/" prefixed-ci-acceptance-test-run-all + +.PHONY: prefixed-acceptance-test +prefixed-acceptance-test: ## Start Prefixed Cypress Acceptance Tests + $(MAKE) -C "./packages/volto/" prefixed-acceptance-test + +.PHONY: deployment-prefixed-acceptance-web-server-start +deployment-prefixed-acceptance-web-server-start: ## Start the prefixed webserver + $(MAKE) -C "./packages/volto/" deployment-prefixed-acceptance-web-server-start + +######### Prefixed Working Copy Acceptance tests + +.PHONY: prefixed-working-copy-acceptance-frontend-prod-start +prefixed-working-copy-acceptance-frontend-prod-start: ## Start acceptance frontend in production mode for prefixed working copy tests + $(MAKE) -C "./packages/volto/" prefixed-working-copy-acceptance-frontend-prod-start + +.PHONY: prefixed-working-copy-acceptance-test +prefixed-working-copy-acceptance-test: ## Start Cypress in interactive mode for prefixed working copy tests + $(MAKE) -C "./packages/volto/" prefixed-working-copy-acceptance-test + +.PHONY: prefixed-prefixed-working-copy-ci-acceptance-test +prefixed-working-copy-ci-acceptance-test: ## Run Cypress tests in headless mode for CI for prefixed working copy tests + $(MAKE) -C "./packages/volto/" prefixed-working-copy-ci-acceptance-test + +.PHONY: prefixed-working-copy-ci-acceptance-test-run-all +prefixed-working-copy-ci-acceptance-test-run-all: ## With a single command, run the backend, frontend, and the Cypress tests in headless mode for CI for prefixed working copy tests + $(MAKE) -C "./packages/volto/" prefixed-working-copy-ci-acceptance-test-run-all + ######### Deployment Multilingual Acceptance tests .PHONY: deployment-multilingual-acceptance-backend-start diff --git a/docs/source/deploying/index.md b/docs/source/deploying/index.md index 8555ff5c94..145e03055d 100644 --- a/docs/source/deploying/index.md +++ b/docs/source/deploying/index.md @@ -18,4 +18,5 @@ seamless-mode apache sentry performance +prefixed-root ``` diff --git a/docs/source/deploying/prefixed-root.md b/docs/source/deploying/prefixed-root.md new file mode 100644 index 0000000000..58b1378d82 --- /dev/null +++ b/docs/source/deploying/prefixed-root.md @@ -0,0 +1,58 @@ +--- +myst: + html_meta: + "description": "Prefixed (non-root) deployment for Volto" + "property=og:description": "Prefixed (non-root) deployment for Volto" + "property=og:title": "Prefixed (non-root) deployment" + "keywords": "Volto, deployment" +--- + +# Prefixed (non-root) deployment + +If you're integrating a Volto website within another existing website, you may need to run Volto on a virtual path inside that website instead of the root path. + +The first step is to set an environment variable `RAZZLE_PREFIX_PATH` to the prefixed path of your Volto. +For example, if you want Volto's root to be hosted at `http://example.com/my-prefix`, you need to start Volto with: + +```shell +RAZZLE_PREFIX_PATH=/my-prefix yarn start +``` + +If you need to debug and understand how the requests are rewritten by the Volto SSR, you can add the following environment variables to the Volto start line: + + +```shell +DEBUG_HPM=true DEBUG=superagent RAZZLE_PREFIX_PATH=/my-prefix yarn start +``` + +The prefix location will be used regardless of how you start Volto, whether in development or production mode. +When developing, though, if your backend is something other then `http://localhost:8080`, you'll need to provide your own solution for how to handle things. + +For a production setup, when hosting Volto behind a proxy HTTP server, you can configure your rewrite rules to something like the following, in this case for Apache. + +```apache +RewriteRule ^/level-1/\+\+api\+\+(.*) http://plone:8080/VirtualHostBase/http/example.com:80/Plone/VirtualHostRoot/_vh_level-1$1 [P,L] +RewriteRule ^/level-1(.*) http://volto:3000/level-1$1 [P,L] +``` + +In case you have a deeper prefix path (for example, `/level1/level2`), you can do the following. +Notice the multiple `_vh_` segments in the rewrite rule. + +```apache +RewriteRule ^/level-1/level-2/\+\+api\+\+(.*) http://plone:8080/VirtualHostBase/http/example.com:80/Plone/VirtualHostRoot/_vh_level-1/_vh_level-2$1 [P,L] +RewriteRule ^/level-1/level-2(.*) http://volto:3000/level-1/level-2$1 [P,L] +``` + +And start Volto with the following. + +```shell +RAZZLE_PREFIX_PATH=/level1/level2 RAZZLE_API_PATH=http://example.com/level1/level2 yarn start +``` + +Note, as you'll be integrating Volto with an existing website, you need to configure `config.settings.externalRoutes` so that the router knows which routes it should consider internal. + +Finally, because `RAZZLE_PREFIX_PATH` is also used to configure the location of the static resources in webpack, when you build the static resources bundle, you must use a command such as the following. + +```shell +RAZZLE_PREFIX_PATH=/level1/level2 yarn build +``` diff --git a/packages/volto-slate/news/4290.feature b/packages/volto-slate/news/4290.feature new file mode 100644 index 0000000000..3db4df59eb --- /dev/null +++ b/packages/volto-slate/news/4290.feature @@ -0,0 +1 @@ +added support for prefixPath. @giuliaghisini @cekk @pnicolli @mamico @nileshgulia1 @wesleybl diff --git a/packages/volto-slate/src/editor/plugins/Image/render.jsx b/packages/volto-slate/src/editor/plugins/Image/render.jsx index a3904fcff3..ecb2220cfa 100644 --- a/packages/volto-slate/src/editor/plugins/Image/render.jsx +++ b/packages/volto-slate/src/editor/plugins/Image/render.jsx @@ -1,5 +1,6 @@ import React from 'react'; import { useSelected, useFocused } from 'slate-react'; +import Image from '@plone/volto/components/theme/Image/Image'; export const ImageElement = (props) => { const { attributes, children, element } = props; @@ -16,7 +17,7 @@ export const ImageElement = (props) => { return ( {children} - + ); }; diff --git a/packages/volto/Makefile b/packages/volto/Makefile index d71780c40b..f57ba48eea 100644 --- a/packages/volto/Makefile +++ b/packages/volto/Makefile @@ -272,6 +272,51 @@ working-copy-ci-acceptance-test: ## Run Cypress tests in headless mode for CI fo working-copy-ci-acceptance-test-run-all: ## With a single command, run the backend, frontend, and the Cypress tests in headless mode for CI for working copy tests $(NODEBIN)/start-test "make working-copy-acceptance-backend-start" http-get://127.0.0.1:55001/plone "make working-copy-acceptance-frontend-prod-start" http://127.0.0.1:3000 "make working-copy-ci-acceptance-test" +######### Prefixed Core Acceptance tests + +.PHONY: prefixed-acceptance-frontend-prod-start +prefixed-acceptance-frontend-prod-start: build-deps ## Start the prefixed Core Acceptance Frontend Fixture + RAZZLE_PREFIX_PATH=/foo RAZZLE_API_PATH=http://localhost/foo pnpm build && pnpm start:prod + +.PHONY: prefixed-ci-acceptance-test-run-all +prefixed-ci-acceptance-test-run-all: ## Runs prefixed Core Full Acceptance Testing in headless mode + $(NODEBIN)/start-test "make ci-acceptance-backend-start" http-get://localhost:55001/plone "make prefixed-acceptance-frontend-prod-start" http://localhost:3000 "make ci-acceptance-test" + +.PHONY: prefixed-acceptance-test +prefixed-acceptance-test: ## Start Prefixed Cypress Acceptance Tests + NODE_ENV=production CYPRESS_API=plone $(NODEBIN)/cypress open --config baseUrl='http://localhost/foo' --env prefixPath="/foo" + +.PHONY: deployment-prefixed-acceptance-web-server-start +deployment-prefixed-acceptance-web-server-start: ## Start the prefixed webserver + cd cypress/docker && docker compose -f prefixed.yml up + +######### Prefixed Working Copy Acceptance tests + +.PHONY: prefixed-working-copy-acceptance-frontend-prod-start +prefixed-working-copy-acceptance-frontend-prod-start: build-deps ## Start acceptance frontend in production mode for prefixed working copy tests + RAZZLE_PREFIX_PATH=/foo RAZZLE_API_PATH=http://localhost/foo pnpm build && pnpm start:prod + +.PHONY: prefixed-working-copy-acceptance-test +prefixed-working-copy-acceptance-test: ## Start Cypress in interactive mode for prefixed working copy tests + NODE_ENV=production CYPRESS_API=plone $(NODEBIN)/cypress open --config specPattern='cypress/tests/workingCopy/**/*.{js,jsx,ts,tsx}' --config baseUrl='http://localhost/foo' --env prefixPath='/foo' + +.PHONY: prefixed-working-copy-ci-acceptance-test +prefixed-working-copy-ci-acceptance-test: ## Run Cypress tests in headless mode for CI for prefixed working copy tests + NODE_ENV=production CYPRESS_API=plone $(NODEBIN)/cypress run --config specPattern='cypress/tests/workingCopy/**/*.{js,jsx,ts,tsx}' --config baseUrl='http://localhost/foo' --env prefixPath='/foo' + +.PHONY: prefixed-working-copy-ci-acceptance-test-run-all +prefixed-working-copy-ci-acceptance-test-run-all: ## With a single command, run the backend, frontend, and the Cypress tests in headless mode for CI for prefixed working copy tests + $(NODEBIN)/start-test \ + "make working-copy-acceptance-backend-start & make prefixed-working-copy-acceptance-frontend-prod-start & make deployment-prefixed-acceptance-web-server-start" \ + http://localhost/foo \ + "make prefixed-working-copy-ci-acceptance-test" + +######### Prefixed Multilingual Acceptance tests + +.PHONY: prefixed-multilingual-acceptance-frontend-prod-start +prefixed-multilingual-acceptance-frontend-prod-start: build-deps ## Start the prefixed Core Acceptance Frontend Fixture + ADDONS=@plone/volto-coresandbox:multilingualFixture RAZZLE_PREFIX_PATH=/foo RAZZLE_API_PATH=http://localhost/foo pnpm build && pnpm start:prod + ######### Guillotina Acceptance tests .PHONY: guillotina-acceptance-backend-start diff --git a/packages/volto/cypress/docker/prefixed-rules.yml b/packages/volto/cypress/docker/prefixed-rules.yml new file mode 100644 index 0000000000..072d31ddab --- /dev/null +++ b/packages/volto/cypress/docker/prefixed-rules.yml @@ -0,0 +1,27 @@ +http: + routers: + frontend: + rule: "Host(`localhost`) && PathPrefix(`/foo`)" + service: frontend + backend: + rule: "Host(`localhost`) && PathPrefix(`/foo/++api++`)" + service: backend + middlewares: + - backend + + middlewares: + backend: + replacePathRegex: + regex: "^/foo/\\+\\+api\\+\\+($|/.*)" + replacement: "/VirtualHostBase/http/localhost/plone/++api++/VirtualHostRoot/_vh_foo$1" + + services: + frontend: + loadBalancer: + servers: + - url: "http://host.docker.internal:3000" + backend: + loadBalancer: + servers: + - url: "http://host.docker.internal:55001" + diff --git a/packages/volto/cypress/docker/prefixed.yml b/packages/volto/cypress/docker/prefixed.yml new file mode 100644 index 0000000000..be53becb76 --- /dev/null +++ b/packages/volto/cypress/docker/prefixed.yml @@ -0,0 +1,24 @@ +version: "3.7" + +services: + + proxy: + image: traefik:v2.8 + command: + - "--api.insecure=true" + - "--providers.docker=true" + # - "--providers.docker.exposedbydefault=false" + - "--providers.file=true" + - "--providers.file.filename=/etc/traefik/rules.yml" + - "--entrypoints.web.address=:80" + - "--api.insecure=true" + # - "--accesslog=true" + # - "--log.level=DEBUG" + ports: + - 80:80 + - "8888:8080" + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - ./prefixed-rules.yml:/etc/traefik/rules.yml + extra_hosts: + - host.docker.internal:host-gateway diff --git a/packages/volto/cypress/helpers/index.js b/packages/volto/cypress/helpers/index.js index 243c220d57..57666a5026 100644 --- a/packages/volto/cypress/helpers/index.js +++ b/packages/volto/cypress/helpers/index.js @@ -11,3 +11,36 @@ export function getIfExists( } }); } + +export function getTextNode(el, match) { + const walk = document.createTreeWalker(el, NodeFilter.SHOW_TEXT, null, false); + if (!match) { + return walk.nextNode(); + } + + const nodes = []; + let node; + while ((node = walk.nextNode())) { + if (node.wholeText.includes(match)) { + return node; + } + } +} + +export function setBaseAndExtent(...args) { + const document = args[0].ownerDocument; + document.getSelection().removeAllRanges(); + document.getSelection().setBaseAndExtent(...args); +} + +export function createHtmlPasteEvent(htmlContent) { + return Object.assign( + new Event('paste', { bubbles: true, cancelable: true }), + { + clipboardData: { + getData: () => htmlContent, + types: ['text/html'], + }, + }, + ); +} diff --git a/packages/volto/cypress/support/commands.js b/packages/volto/cypress/support/commands.js index 3e568d7c32..375aa4b95c 100644 --- a/packages/volto/cypress/support/commands.js +++ b/packages/volto/cypress/support/commands.js @@ -1,6 +1,11 @@ +import { + getIfExists, + getTextNode, + setBaseAndExtent, + createHtmlPasteEvent, +} from '../helpers'; /* eslint-disable no-console */ import '@testing-library/cypress/add-commands'; -import { getIfExists } from '../helpers'; import { ploneAuth } from './constants'; const HOSTNAME = Cypress.env('BACKEND_HOST') || '127.0.0.1'; @@ -844,38 +849,14 @@ Cypress.Commands.add('clickSlateButton', (button, timeout = 1000) => { }).click({ force: true }); // force click is needed to ensure the button in visible in view. }); -// 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); -} - -function createHtmlPasteEvent(htmlContent) { - return Object.assign( - new Event('paste', { bubbles: true, cancelable: true }), - { - clipboardData: { - getData: () => htmlContent, - types: ['text/html'], - }, - }, - ); -} +/* add prefixPath from Cypress.env to supplied path + * designed for CI only + * for local, add prefixPath to Cypress.env + */ +Cypress.Commands.add('addBaseUrl', (url) => { + const prefixPath = Cypress.env('prefixPath'); + return prefixPath ? prefixPath + url : url; +}); Cypress.Commands.add('addNewBlock', (blockName, createNewSlate = false) => { let block; diff --git a/packages/volto/cypress/tests/core/basic/actions.js b/packages/volto/cypress/tests/core/basic/actions.js index 58c87c0a3d..c319fb0539 100644 --- a/packages/volto/cypress/tests/core/basic/actions.js +++ b/packages/volto/cypress/tests/core/basic/actions.js @@ -1,4 +1,5 @@ describe('actions Tests', () => { + let prefixPath; beforeEach(() => { cy.autologin(); cy.createContent({ @@ -8,6 +9,7 @@ describe('actions Tests', () => { allow_discussion: true, }); cy.visit('/contents'); + prefixPath = Cypress.env('prefixPath') || ''; }); it('copy', function () { cy.get('tr[aria-label="/my-page-1"]').within(() => { @@ -20,7 +22,7 @@ describe('actions Tests', () => { cy.get('a[class="icon-align-name"]').should( 'have.attr', 'href', - '/copy_of_my-page-1/contents', + `${prefixPath}/copy_of_my-page-1/contents`, ); }); }); @@ -44,7 +46,7 @@ describe('actions Tests', () => { cy.get('a[class="icon-align-name"]').should( 'have.attr', 'href', - '/my-page-1/contents', + `${prefixPath}/my-page-1/contents`, ); }); }); @@ -62,7 +64,7 @@ describe('actions Tests', () => { cy.get('a[class="icon-align-name"]').should( 'have.attr', 'href', - '/my-page-rename/contents', + `${prefixPath}/my-page-rename/contents`, ); }); }); diff --git a/packages/volto/cypress/tests/core/basic/deleteItemModal.js b/packages/volto/cypress/tests/core/basic/deleteItemModal.js index 3a1cbcd929..6a55b94a08 100644 --- a/packages/volto/cypress/tests/core/basic/deleteItemModal.js +++ b/packages/volto/cypress/tests/core/basic/deleteItemModal.js @@ -1,4 +1,5 @@ describe('Modal View for different content types', () => { + let prefixPath; const simpleSlateLink = (target) => { return { '@type': 'slate', @@ -32,6 +33,7 @@ describe('Modal View for different content types', () => { beforeEach(() => { cy.autologin(); cy.visit('/'); + prefixPath = Cypress.env('prefixPath') || ''; }); it('As editor I get a warning on deleting my page when my page is referenced in the richtext', () => { cy.createContent({ @@ -53,7 +55,7 @@ describe('Modal View for different content types', () => { cy.get('[aria-label="/document-linked"] > :nth-child(2)').click(); cy.get('[aria-label="Delete"]').click(); cy.get('.medium > .header').should('be.visible'); - cy.get('li > [href="/document-linked"]'); + cy.get(`li > [href="${prefixPath}/document-linked"]`); }); it('As editor I get a warning on deleting my page when my News-Item is referenced in the richtext', () => { cy.createContent({ @@ -76,7 +78,7 @@ describe('Modal View for different content types', () => { cy.get('[aria-label="/news-item-linked"] > :nth-child(2)').click(); cy.get('[aria-label="Delete"]').click(); cy.get('.medium > .header').should('be.visible'); - cy.get('li > [href="/news-item-linked"]'); + cy.get(`li > [href="${prefixPath}/news-item-linked"]`); }); it('As editor I get a warning on deleting my page when my Event is referenced in the richtext', () => { cy.createContent({ @@ -99,7 +101,7 @@ describe('Modal View for different content types', () => { cy.get('[aria-label="/event-linked"] > :nth-child(2)').click(); cy.get('[aria-label="Delete"]').click(); cy.get('.medium > .header').should('be.visible'); - cy.get('li > [href="/event-linked"]'); + cy.get(`li > [href="${prefixPath}/event-linked"]`); }); it('As editor I get a warning on deleting my page when my File is referenced in the richtext', () => { cy.createContent({ @@ -122,7 +124,7 @@ describe('Modal View for different content types', () => { cy.get('[aria-label="/file-linked"] > :nth-child(2)').click(); cy.get('[aria-label="Delete"]').click(); cy.get('.medium > .header').should('be.visible'); - cy.get('li > [href="/file-linked"]'); + cy.get(`li > [href="${prefixPath}/file-linked"]`); }); it('As editor I get a warning on deleting my page when my Image is referenced in the richtext', () => { cy.createContent({ @@ -145,7 +147,7 @@ describe('Modal View for different content types', () => { cy.get('[aria-label="/image-linked"] > :nth-child(2)').click(); cy.get('[aria-label="Delete"]').click(); cy.get('.medium > .header').should('be.visible'); - cy.get('li > [href="/image-linked"]'); + cy.get(`li > [href="${prefixPath}/image-linked"]`); }); it('As editor I get a warning on deleting my page when my Link is referenced in the richtext', () => { //Test Setup @@ -173,13 +175,15 @@ describe('Modal View for different content types', () => { cy.get('[aria-label="/link-linked"] > :nth-child(2)').click(); cy.get('[aria-label="Delete"]').click(); cy.get('.medium > .header').should('be.visible'); - cy.get('li > [href="/link-linked"]'); + cy.get(`li > [href="${prefixPath}/link-linked"]`); }); }); describe('Test if different forms of Linking content appear in Delete Modal View', () => { + let prefixPath; beforeEach(() => { cy.autologin(); cy.visit('/'); + prefixPath = Cypress.env('prefixPath') || ''; }); it('As editor I get a warning on deleting my page when my Document is referenced in the Teaser Block', () => { cy.createContent({ @@ -220,7 +224,7 @@ describe('Test if different forms of Linking content appear in Delete Modal View cy.get('[aria-label="/document-linked"] > :nth-child(2)').click(); cy.get('[aria-label="Delete"]').click(); cy.get('.medium > .header').should('be.visible'); - cy.get('li > [href="/document-linked"]'); + cy.get(`li > [href="${prefixPath}/document-linked"]`); }); it('As editor I get a warning on deleting my page when my Image is referenced in the Teaser Block', () => { cy.createContent({ @@ -281,10 +285,10 @@ describe('Test if different forms of Linking content appear in Delete Modal View cy.get('[aria-label="Select Image that is linked"]').click(); cy.get('#toolbar-save').click(); cy.visit('/contents'); - cy.get('[aria-label="/document-linked"] > :nth-child(2)').click(); + cy.get(`[aria-label="/document-linked"] > :nth-child(2)`).click(); cy.get('[aria-label="Delete"]').click(); cy.get('.medium > .header').should('be.visible'); - cy.get('li > [href="/document-linked"]'); + cy.get(`li > [href="${prefixPath}/document-linked"]`); }); it('As editor I get a warning on deleting my Document when my Image is referenced via Image Block', () => { cy.createContent({ @@ -311,7 +315,7 @@ describe('Test if different forms of Linking content appear in Delete Modal View cy.get('[aria-label="/image-linked"] > :nth-child(2)').click(); cy.get('[aria-label="Delete"]').click(); cy.get('.medium > .header').should('be.visible'); - cy.get('li > [href="/image-linked"]'); + cy.get(`li > [href="${prefixPath}/image-linked"]`); }); it('As editor I get a warning on deleting my Image when my Image is referenced in the Image Block', () => { cy.createContent({ @@ -349,7 +353,7 @@ describe('Test if different forms of Linking content appear in Delete Modal View cy.get('[aria-label="/document-linked"] > :nth-child(2)').click(); cy.get('[aria-label="Delete"]').click(); cy.get('.medium > .header').should('be.visible'); - cy.get('li > [href="/document-linked"]'); + cy.get(`li > [href="${prefixPath}/document-linked"]`); }); it('As an Editor I get a warning on deleting my document when it is linked somewhere via teaser block inside a grid block', () => { cy.createContent({ @@ -401,6 +405,6 @@ describe('Test if different forms of Linking content appear in Delete Modal View cy.get('[aria-label="/document-linked"] > :nth-child(2)').click(); cy.get('[aria-label="Delete"]').click(); cy.get('.medium > .header').should('be.visible'); - cy.get('li > [href="/document-linked"]'); + cy.get(`li > [href="${prefixPath}/document-linked"]`); }); }); diff --git a/packages/volto/cypress/tests/core/basic/folder-contents.js b/packages/volto/cypress/tests/core/basic/folder-contents.js index 6efb50fe7e..a79da28ce9 100644 --- a/packages/volto/cypress/tests/core/basic/folder-contents.js +++ b/packages/volto/cypress/tests/core/basic/folder-contents.js @@ -1,4 +1,5 @@ describe('Folder Contents Tests', () => { + let prefixPath; beforeEach(() => { cy.intercept('GET', `/**/*?expand*`).as('content'); // given a logged in editor @@ -19,6 +20,7 @@ describe('Folder Contents Tests', () => { }); cy.visit('/my-folder/contents'); cy.wait('@content'); + prefixPath = Cypress.env('prefixPath') || ''; }); it('Renaming via folder contents view', () => { @@ -33,7 +35,7 @@ describe('Folder Contents Tests', () => { cy.get('#content-core table') .contains('Brand new document title') .should('have.attr', 'href') - .and('eq', '/my-folder/brand-new-document-title/contents'); + .and('eq', `${prefixPath}/my-folder/brand-new-document-title/contents`); }); it('Copying the content in the same folder', () => { diff --git a/packages/volto/cypress/tests/core/blocks/block-anchors.js b/packages/volto/cypress/tests/core/blocks/block-anchors.js index 8019c2e9f6..25f86b71cb 100644 --- a/packages/volto/cypress/tests/core/blocks/block-anchors.js +++ b/packages/volto/cypress/tests/core/blocks/block-anchors.js @@ -1,7 +1,11 @@ import { slateBeforeEach } from '../../../support/volto-slate'; describe('Block Tests: Anchors', () => { - beforeEach(slateBeforeEach); + let prefixPath; + beforeEach(() => { + slateBeforeEach(); + prefixPath = Cypress.env('prefixPath') || ''; + }); it('Add Block: add content to TOC', () => { // Change page title @@ -46,9 +50,13 @@ describe('Block Tests: Anchors', () => { cy.contains('Slate Heading Anchors'); cy.get('h2[id="title-1"]').contains('Title 1'); cy.get('h2[id="title-2"]').contains('Title 2'); - cy.get('.table-of-contents a[href="/my-page#title-1"]').click(); + cy.get( + `.table-of-contents a[href="${prefixPath}/my-page#title-1"]`, + ).click(); cy.get('h2[id="title-1"]').scrollIntoView().should('be.visible'); - cy.get('.table-of-contents a[href="/my-page#title-2"]').click(); + cy.get( + `.table-of-contents a[href="${prefixPath}/my-page#title-2"]`, + ).click(); cy.get('h2[id="title-2"]').scrollIntoView().should('be.visible'); }); @@ -100,9 +108,13 @@ describe('Block Tests: Anchors', () => { cy.contains('Slate Heading Anchors'); cy.get('h2[id="title-1"]').contains('Title 1'); cy.get('h2[id="title-2-u-a"]').contains('Title 2 ü à'); - cy.get('.table-of-contents a[href="/my-page#title-1"]').click(); + cy.get( + `.table-of-contents a[href="${prefixPath}/my-page#title-1"]`, + ).click(); cy.get('h2[id="title-1"]').scrollIntoView().should('be.visible'); - cy.get('.table-of-contents a[href="/my-page#title-2-u-a"]').click(); + cy.get( + `.table-of-contents a[href="${prefixPath}/my-page#title-2-u-a"]`, + ).click(); cy.get('h2[id="title-2-u-a"]').scrollIntoView().should('be.visible'); }); }); diff --git a/packages/volto/cypress/tests/core/blocks/blocks-search.js b/packages/volto/cypress/tests/core/blocks/blocks-search.js index 6e355829ef..2972cef119 100644 --- a/packages/volto/cypress/tests/core/blocks/blocks-search.js +++ b/packages/volto/cypress/tests/core/blocks/blocks-search.js @@ -1,5 +1,6 @@ describe('Search Block Tests', () => { var results_number = 3; + let prefixPath; beforeEach(() => { cy.intercept('GET', `/**/*?expand*`).as('content'); cy.intercept('GET', '/**/Document').as('schema'); @@ -34,6 +35,7 @@ describe('Search Block Tests', () => { cy.visit('/'); cy.wait('@content'); + prefixPath = Cypress.env('prefixPath') || ''; }); afterEach(() => { @@ -254,7 +256,7 @@ describe('Search Block Tests', () => { () => cy .get('#page-document .listing-item:first-of-type a') - .should('have.attr', 'href', '/my-event'), + .should('have.attr', 'href', `${prefixPath}/my-event`), () => cy .get('.search-results-count-sort .search-details em') @@ -314,7 +316,7 @@ describe('Search Block Tests', () => { cy.get('#page-document .listing-item:first-of-type a').should( 'have.attr', 'href', - '/my-event', + `${prefixPath}/my-event`, ); cy.get('.search-results-count-sort .search-details em').should( 'contain', @@ -407,7 +409,7 @@ describe('Search Block Tests', () => { cy.get('#page-document .listing-item:first-of-type a').should( 'have.attr', 'href', - '/my-event', + `${prefixPath}/my-event`, ); cy.get('.search-results-count-sort .search-details em').should( 'contain', @@ -455,7 +457,7 @@ describe('Search Block Tests', () => { cy.get('#page-document .listing-item:first-of-type a').should( 'have.attr', 'href', - '/my-event', + `${prefixPath}/my-event`, ); cy.get('.search-results-count-sort .search-details em').should( 'contain', diff --git a/packages/volto/cypress/tests/core/blocks/listing/blocks-listing.js b/packages/volto/cypress/tests/core/blocks/listing/blocks-listing.js index d42791f19d..cd236949ff 100644 --- a/packages/volto/cypress/tests/core/blocks/listing/blocks-listing.js +++ b/packages/volto/cypress/tests/core/blocks/listing/blocks-listing.js @@ -1,4 +1,5 @@ describe('Listing Block Tests', () => { + let prefixPath; beforeEach(() => { cy.intercept('GET', `/**/*?expand*`).as('content'); cy.intercept('GET', '/**/Document').as('schema'); @@ -15,6 +16,7 @@ describe('Listing Block Tests', () => { cy.visit('/'); cy.wait('@content'); + prefixPath = Cypress.env('prefixPath') || ''; }); it('Add Listing block - with no results', () => { @@ -112,7 +114,7 @@ describe('Listing Block Tests', () => { cy.get('#page-document .listing-item:first-of-type a').should( 'have.attr', 'href', - '/my-page/my-page-test', + `${prefixPath}/my-page/my-page-test`, ); }); @@ -292,7 +294,7 @@ describe('Listing Block Tests', () => { cy.get('#page-document .listing-item:first-of-type a').should( 'have.attr', 'href', - '/my-page/my-page-test', + `${prefixPath}/my-page/my-page-test`, ); }); @@ -420,7 +422,7 @@ describe('Listing Block Tests', () => { cy.get('#page-document .listing-item:first-of-type a').should( 'have.attr', 'href', - '/my-page/page-two', + `${prefixPath}/my-page/page-two`, ); }); @@ -474,7 +476,7 @@ describe('Listing Block Tests', () => { cy.get('#page-document .listing-item:first-of-type a').should( 'have.attr', 'href', - '/my-page', + `${prefixPath}/my-page`, ); }); @@ -565,7 +567,7 @@ describe('Listing Block Tests', () => { cy.get('#page-document .listing-item:first-of-type a').should( 'have.attr', 'href', - '/my-page/my-page-test', + `${prefixPath}/my-page/my-page-test`, ); }); @@ -846,7 +848,7 @@ describe('Listing Block Tests', () => { cy.get('#page-document .listing-item:first-of-type a').should( 'have.attr', 'href', - '/my-page/my-folder', + `${prefixPath}/my-page/my-folder`, ); cy.get('.listing-item').should(($els) => { expect($els).to.have.length(2); @@ -984,7 +986,7 @@ describe('Listing Block Tests', () => { cy.get('#page-document .listing-item:first-of-type a').should( 'have.attr', 'href', - '/my-page/my-folder', + `${prefixPath}/my-page/my-folder`, ); cy.isInHTML({ parent: '.listing-item', content: 'My Folder' }); cy.get('.listing-item').should(($els) => { @@ -1100,6 +1102,8 @@ describe('Listing Block Tests', () => { cy.get('.logo').first().should('be.visible').click(); cy.url().should('not.include', '=2'); cy.url().should('not.include', '=3'); + //test back button + cy.wait(1000); cy.navigate('/my-page'); cy.get('.ui.pagination.menu a[value="2"]') @@ -1182,7 +1186,7 @@ describe('Listing Block Tests', () => { cy.get('#page-document .listing-item:first-of-type a').should( 'have.attr', 'href', - '/my-page/my-news-item-test', + `${prefixPath}/my-page/my-news-item-test`, ); }); diff --git a/packages/volto/cypress/tests/core/content/content.js b/packages/volto/cypress/tests/core/content/content.js index 29e910deac..dbe79b0317 100644 --- a/packages/volto/cypress/tests/core/content/content.js +++ b/packages/volto/cypress/tests/core/content/content.js @@ -1,10 +1,12 @@ describe('Add Content Tests', () => { + let prefixPath; beforeEach(() => { cy.intercept('GET', `/**/*?expand*`).as('content'); // give a logged in editor and the site root cy.autologin(); cy.visit('/'); cy.wait('@content'); + prefixPath = Cypress.env('prefixPath') || ''; }); it('As editor I can add a file', function () { @@ -222,7 +224,7 @@ describe('Add Content Tests', () => { // and the link should show up on the link view cy.contains('/link-target'); // and the link redirects to the link target - cy.get('main a[href="/link-target"]').click(); + cy.get(`main a[href="${prefixPath}/link-target"]`).click(); cy.url().should('eq', Cypress.config().baseUrl + '/link-target'); cy.get('main').contains('Link Target'); }); diff --git a/packages/volto/cypress/tests/core/content/createContent.js b/packages/volto/cypress/tests/core/content/createContent.js index d0a83a8f35..2ba87b5b20 100644 --- a/packages/volto/cypress/tests/core/content/createContent.js +++ b/packages/volto/cypress/tests/core/content/createContent.js @@ -1,6 +1,8 @@ describe('createContent Tests', () => { + let prefixPath; beforeEach(() => { cy.autologin(); + prefixPath = Cypress.env('prefixPath') || ''; }); it('Create document', function () { @@ -61,7 +63,7 @@ describe('createContent Tests', () => { cy.get('.view-wrapper a').should( 'have.attr', 'href', - '/my-file/@@download/file', + `${prefixPath}/my-file/@@download/file`, ); // cy.get('.view-wrapper a').click(); }); diff --git a/packages/volto/cypress/tests/core/controlpanels/dexterity-controlpanel-layout.js b/packages/volto/cypress/tests/core/controlpanels/dexterity-controlpanel-layout.js index 0f3ff2596e..c21a1dcd94 100644 --- a/packages/volto/cypress/tests/core/controlpanels/dexterity-controlpanel-layout.js +++ b/packages/volto/cypress/tests/core/controlpanels/dexterity-controlpanel-layout.js @@ -1,4 +1,5 @@ describe('ControlPanel: Dexterity Content-Types Layout', () => { + let prefixPath; beforeEach(() => { cy.intercept('GET', `/**/*?expand*`).as('content'); // given a logged in editor @@ -8,6 +9,7 @@ describe('ControlPanel: Dexterity Content-Types Layout', () => { cy.autologin(); cy.visit('/controlpanel/dexterity-types'); cy.wait('@content'); + prefixPath = Cypress.env('prefixPath') || ''; }); it('Edit Blocks Layout for Book', () => { @@ -16,7 +18,7 @@ describe('ControlPanel: Dexterity Content-Types Layout', () => { cy.get('input[id="field-description"]').type('A book content-type'); cy.get('[title=Save]').click(); - cy.get('a[href="/controlpanel/dexterity-types/book"]').should( + cy.get(`a[href="${prefixPath}/controlpanel/dexterity-types/book"]`).should( 'have.text', 'Book', ); diff --git a/packages/volto/cypress/tests/core/controlpanels/dexterity-controlpanel-schema.js b/packages/volto/cypress/tests/core/controlpanels/dexterity-controlpanel-schema.js index f3f853e4ba..73cc2d85c8 100644 --- a/packages/volto/cypress/tests/core/controlpanels/dexterity-controlpanel-schema.js +++ b/packages/volto/cypress/tests/core/controlpanels/dexterity-controlpanel-schema.js @@ -1,4 +1,5 @@ describe('ControlPanel: Dexterity Content-Types Schema', () => { + let prefixPath; beforeEach(() => { cy.intercept('GET', `/**/*?expand*`).as('content'); // given a logged in editor @@ -8,6 +9,7 @@ describe('ControlPanel: Dexterity Content-Types Schema', () => { cy.autologin(); cy.visit('/controlpanel/dexterity-types'); cy.wait('@content'); + prefixPath = Cypress.env('prefixPath') || ''; }); it('Add Bike content-type with custom schema', () => { @@ -17,7 +19,7 @@ describe('ControlPanel: Dexterity Content-Types Schema', () => { cy.get('input[id="field-description"]').type('Bike content-type'); cy.get('[title=Save]').click(); - cy.get('a[href="/controlpanel/dexterity-types/bike"]').should( + cy.get(`a[href="${prefixPath}/controlpanel/dexterity-types/bike"]`).should( 'have.text', 'Bike', ); diff --git a/packages/volto/cypress/tests/core/controlpanels/dexterity-controlpanel.js b/packages/volto/cypress/tests/core/controlpanels/dexterity-controlpanel.js index 2a3a16e6fd..eb04c57f16 100644 --- a/packages/volto/cypress/tests/core/controlpanels/dexterity-controlpanel.js +++ b/packages/volto/cypress/tests/core/controlpanels/dexterity-controlpanel.js @@ -1,4 +1,5 @@ describe('Folder Contents Tests', () => { + let prefixPath; beforeEach(() => { cy.intercept('GET', `/**/*?expand*`).as('content'); // given a logged in editor @@ -8,10 +9,13 @@ describe('Folder Contents Tests', () => { cy.autologin(); cy.visit('/controlpanel/dexterity-types'); cy.wait('@content'); + prefixPath = Cypress.env('prefixPath') || ''; }); it('Changing name of the Page content type', () => { - cy.get('a[href="/controlpanel/dexterity-types/Document"]').click(); + cy.get( + `a[href="${prefixPath}/controlpanel/dexterity-types/Document"]`, + ).click(); cy.get('input[id="field-title"]').clear().type('Page1{enter}'); cy.get('textarea[id="field-description"]').type( 'This is Page Content Type{enter}', @@ -20,9 +24,8 @@ describe('Folder Contents Tests', () => { cy.get('button[id="toolbar-save"]').click(); cy.visit('/controlpanel/dexterity-types'); - cy.get('a[href="/controlpanel/dexterity-types/Document"]').should( - 'have.text', - 'Page1', - ); + cy.get( + `a[href="${prefixPath}/controlpanel/dexterity-types/Document"]`, + ).should('have.text', 'Page1'); }); }); diff --git a/packages/volto/cypress/tests/minimal/blocks-image.js b/packages/volto/cypress/tests/minimal/blocks-image.js index 4fc23db585..9af203bb19 100644 --- a/packages/volto/cypress/tests/minimal/blocks-image.js +++ b/packages/volto/cypress/tests/minimal/blocks-image.js @@ -3,7 +3,7 @@ describe('Blocks Tests', () => { cy.intercept('GET', `/**/*?expand*`).as('content'); cy.intercept('GET', '/**/Document').as('schema'); cy.intercept('POST', '*').as('saveImage'); - cy.intercept('GET', '/**/image.png/@@images/image-*').as('getImage'); + cy.intercept('GET', '**/image.png/@@images/image-*').as('getImage'); // given a logged in editor and a page in edit mode cy.autologin(); cy.createContent({ @@ -49,54 +49,30 @@ describe('Blocks Tests', () => { }); }); - // OLD ADD IMAGE VIA DRAG AND DROP - // it('Add image via drag and drop', () => { - // const block = 'image'; - - // // Add image Block - // cy.getSlate().click(); - // cy.get('button.block-add-button').click(); - // cy.get('.blocks-chooser .title') - // .contains('media') - // .click(); - // cy.get( - // '.content.active.blocks-list .ui.buttons:first-child button', - // ).click(); - - // const fileName = 'image.png'; - // cy.fixture(fileName).then(fileContent => { - // cy.get(`.ui.block.${block} .dropzone`).upload( - // { - // fileContent, - // fileName, - // mimeType: 'application/png', - // }, - // { subjectType: 'drag-n-drop' }, - // ); - // }); - // }); - - // NEW ADD IMAGE VIA DRAG AND DROP - // it('Add image via drag and drop', () => { - // // when I add an image block via drag and drop - // cy.getSlate().click(); - // cy.get('.ui.basic.icon.button.block-add-button').click(); - // cy.get('.ui.basic.icon.button.image') - // .contains('Image') - // .click(); - // const imagePath = { filePath: 'image.png', mimeType: 'image/png' }; - // cy.get('.ui.block.image .dropzone center img').attachFile(imagePath, { - // subjectType: 'drag-n-drop', - // force: true, - // allowEmpty: true, - // encoding: 'utf8', - // }); - // cy.waitForResourceToLoad('image.png/@@images/image'); - - // cy.get('#toolbar-save').click(); - // cy.wait(5000); - // cy.url().should('eq', Cypress.config().baseUrl + '/my-page'); - // }); + // ADD IMAGE VIA DRAG AND DROP + it('Add image via drag and drop', () => { + // when I add an image block via drag and drop + cy.getSlate().click(); + cy.get('.ui.basic.icon.button.block-add-button').click(); + cy.get('.ui.basic.icon.button.image').contains('Image').click(); + const imagePath = { filePath: 'image.png', mimeType: 'image/png' }; + cy.get('.ui.block.image .center img').attachFile(imagePath, { + subjectType: 'drag-n-drop', + force: true, + allowEmpty: true, + encoding: 'utf8', + }); + cy.waitForResourceToLoad('image.png/@@images/image'); + + cy.get('#toolbar-save').click(); + cy.wait(5000); + cy.url().should('eq', Cypress.config().baseUrl + '/my-page'); + + cy.addBaseUrl('/my-page/image.png/@@images/image').then((value) => + cy.get('.block img').should('have.attr', 'src').and('contains', value), + ); + }); + it('Add image via upload', () => { // when I add an image block via upload cy.getSlate().click(); @@ -114,16 +90,16 @@ describe('Blocks Tests', () => { cy.wait('@getImage'); // then image src must be equal to image name - cy.get('.block img') - .should('have.attr', 'src') - .and('contains', '/my-page/image.png/@@images/image-'); + cy.addBaseUrl('/my-page/image.png/@@images/image').then((value) => + cy.get('.block img').should('have.attr', 'src').and('contains', value), + ); - cy.get('.block img') - .should('be.visible') - .and(($img) => { - // "naturalWidth" and "naturalHeight" are set when the image loads - expect($img[0].naturalWidth).to.be.greaterThan(0); - }); + // cy.get('.block img') + // .should('be.visible') + // .and(($img) => { + // // "naturalWidth" and "naturalHeight" are set when the image loads + // expect($img[0].naturalWidth).to.be.greaterThan(0); + // }); }); it('Create a image block document in edit mode', () => { @@ -143,16 +119,16 @@ describe('Blocks Tests', () => { cy.wait('@saveImage'); cy.wait('@getImage'); - cy.get('.block img') - .should('have.attr', 'src') - .and('contains', '/image.png/@@images/image-'); + cy.addBaseUrl('/image.png/@@images/image').then((value) => + cy.get('.block img').should('have.attr', 'src').and('contains', value), + ); - cy.get('.block img') - .should('be.visible') - .and(($img) => { - // "naturalWidth" and "naturalHeight" are set when the image loads - expect($img[0].naturalWidth).to.be.greaterThan(0); - }); + // cy.get('.block img') + // .should('be.visible') + // .and(($img) => { + // // "naturalWidth" and "naturalHeight" are set when the image loads + // expect($img[0].naturalWidth).to.be.greaterThan(0); + // }); }); it('Create an image block and initially alt attr is empty', () => { diff --git a/packages/volto/cypress/tests/minimal/content.js b/packages/volto/cypress/tests/minimal/content.js index a28a6eeaf3..d2c09ccb39 100644 --- a/packages/volto/cypress/tests/minimal/content.js +++ b/packages/volto/cypress/tests/minimal/content.js @@ -62,6 +62,7 @@ describe('Add Content Tests', () => { it('As editor I can add an image', function () { cy.intercept('POST', '*').as('saveImage'); + cy.intercept('GET', '**/image.png/@@images/image-*').as('getImage'); // when I add an image cy.get('#toolbar-add').click(); cy.get('#toolbar-add-image').click(); @@ -85,16 +86,17 @@ describe('Add Content Tests', () => { cy.get('#toolbar-save').click(); cy.wait('@saveImage'); cy.wait('@content'); + cy.wait('@getImage'); cy.url().should('eq', Cypress.config().baseUrl + '/image.png'); cy.contains('My image'); - cy.get('.view-wrapper img') - .should('be.visible') - .and(($img) => { - // "naturalWidth" and "naturalHeight" are set when the image loads - expect($img[0].naturalWidth).to.be.greaterThan(0); - }); + // cy.get('.view-wrapper img') + // .should('be.visible') + // .and(($img) => { + // // "naturalWidth" and "naturalHeight" are set when the image loads + // expect($img[0].naturalWidth).to.be.greaterThan(0); + // }); }); it('As editor I can add a news item', function () { @@ -161,6 +163,7 @@ describe('Add Content Tests', () => { it('As editor I can add a Link (with an internal link)', function () { cy.intercept('POST', '*').as('saveLink'); + cy.intercept('GET', '/**/my-link').as('getLink'); // Given a Document "Link Target" cy.createContent({ contentType: 'Document', @@ -182,6 +185,7 @@ describe('Add Content Tests', () => { cy.get('#toolbar-save').click(); cy.wait('@saveLink'); + //cy.wait('@getLink'); cy.wait('@content'); cy.url().should('eq', Cypress.config().baseUrl + '/my-link'); @@ -191,7 +195,10 @@ describe('Add Content Tests', () => { // and the link should show up on the link view cy.contains('/link-target'); // and the link redirects to the link target - cy.get('main a[href="/link-target"]').click(); + cy.addBaseUrl('/link-target').then((value) => + cy.get(`main a[href="${value}"]`).click(), + ); + cy.url().should('eq', Cypress.config().baseUrl + '/link-target'); cy.get('main').contains('Link Target'); }); diff --git a/packages/volto/news/4290.feature b/packages/volto/news/4290.feature new file mode 100644 index 0000000000..c470347339 --- /dev/null +++ b/packages/volto/news/4290.feature @@ -0,0 +1 @@ +added prefixPath for site. @giuliaghisini @cekk @pnicolli @mamico @nileshgulia1 @wesleybl diff --git a/packages/volto/razzle.config.js b/packages/volto/razzle.config.js index 5ab88cb132..0342f8f7cd 100644 --- a/packages/volto/razzle.config.js +++ b/packages/volto/razzle.config.js @@ -414,6 +414,12 @@ const defaultModify = ({ ] : []; + const prefixPath = process.env.RAZZLE_PREFIX_PATH || ''; + + if (prefixPath && !dev) { + const publicPath = config.output.publicPath; + config.output.publicPath = `${publicPath}${prefixPath.slice(1)}/`; + } return config; }; diff --git a/packages/volto/src/components/manage/Blocks/Listing/ImageGallery.jsx b/packages/volto/src/components/manage/Blocks/Listing/ImageGallery.jsx index ca18bf100c..b7138cd084 100644 --- a/packages/volto/src/components/manage/Blocks/Listing/ImageGallery.jsx +++ b/packages/volto/src/components/manage/Blocks/Listing/ImageGallery.jsx @@ -1,10 +1,9 @@ -import React from 'react'; import PropTypes from 'prop-types'; import loadable from '@loadable/component'; import 'react-image-gallery/styles/css/image-gallery.css'; import { Button } from 'semantic-ui-react'; import Icon from '@plone/volto/components/theme/Icon/Icon'; -import { flattenToAppURL } from '@plone/volto/helpers/Url/Url'; +import { flattenToAppURL, addPrefixPath } from '@plone/volto/helpers/Url/Url'; import config from '@plone/volto/registry'; import galleryLeftSVG from '@plone/volto/icons/left-key.svg'; @@ -78,13 +77,12 @@ const ImageGalleryTemplate = ({ items }) => { settings.imageObjects.includes(content['@type']) && content.image_field, ); const imagesInfo = renderItems.map((item) => { + const baseUrl = `${addPrefixPath( + flattenToAppURL(item['@id']), + )}/@@images/${item.image_field}`; return { - original: `${flattenToAppURL(item['@id'])}/@@images/${ - item.image_field - }/large`, - thumbnail: `${flattenToAppURL(item['@id'])}/@@images/${ - item.image_field - }/thumb`, + original: `${baseUrl}/large`, + thumbnail: `${baseUrl}/thumb`, }; }); diff --git a/packages/volto/src/components/manage/Controlpanels/Controlpanels.jsx b/packages/volto/src/components/manage/Controlpanels/Controlpanels.jsx index 0602f11d8d..a71f6dcbed 100644 --- a/packages/volto/src/components/manage/Controlpanels/Controlpanels.jsx +++ b/packages/volto/src/components/manage/Controlpanels/Controlpanels.jsx @@ -22,6 +22,15 @@ import Icon from '@plone/volto/components/theme/Icon/Icon'; import Toolbar from '@plone/volto/components/manage/Toolbar/Toolbar'; import VersionOverview from '@plone/volto/components/manage/Controlpanels/VersionOverview'; +import { + getSystemInformation, + listControlpanels, +} from '@plone/volto/actions/controlpanels/controlpanels'; +import { asyncConnect } from '@plone/volto/helpers/AsyncConnect'; +import { compose } from 'redux'; +import { withRouter } from 'react-router'; +import PropTypes from 'prop-types'; + import config from '@plone/volto/registry'; import backSVG from '@plone/volto/icons/back.svg'; @@ -104,15 +113,16 @@ const messages = defineMessages({ /** * Controlpanels container class. */ -export default function Controlpanels({ location }) { +function Controlpanels(props) { + let filteredControlPanels = []; const intl = useIntl(); const [isClient, setIsClient] = useState(false); - const { pathname } = location; const controlpanels = useSelector( (state) => state.controlpanels.controlpanels, ); const controlpanelsRequest = useSelector((state) => state.controlpanels.list); + const pathname = props.location.pathname; const systemInformation = useSelector( (state) => state.controlpanels.systeminformation, ); @@ -139,64 +149,66 @@ export default function Controlpanels({ location }) { : []; const { filterControlPanels } = config.settings; - const filteredControlPanels = map( - concat(filterControlPanels(controlpanels), customcontrolpanels, [ - { - '@id': '/addons', - group: intl.formatMessage(messages.general), - title: intl.formatMessage(messages.addons), - }, - { - '@id': '/database', - group: intl.formatMessage(messages.general), - title: intl.formatMessage(messages.database), - }, - { - '@id': '/rules', - group: intl.formatMessage(messages.content), - title: intl.formatMessage(messages.contentRules), - }, - { - '@id': '/undo', - group: intl.formatMessage(messages.general), - title: intl.formatMessage(messages.undo), - }, - { - '@id': '/aliases', - group: intl.formatMessage(messages.general), - title: intl.formatMessage(messages.urlmanagement), - }, - { - '@id': '/relations', - group: intl.formatMessage(messages.content), - title: intl.formatMessage(messages.relations), - }, - { - '@id': '/moderate-comments', - group: intl.formatMessage(messages.content), - title: intl.formatMessage(messages.moderatecomments), - }, - { - '@id': '/users', - group: intl.formatMessage(messages.usersControlPanelCategory), - title: intl.formatMessage(messages.users), - }, - { - '@id': '/usergroupmembership', - group: intl.formatMessage(messages.usersControlPanelCategory), - title: intl.formatMessage(messages.usergroupmemberbership), - }, - { - '@id': '/groups', - group: intl.formatMessage(messages.usersControlPanelCategory), - title: intl.formatMessage(messages.groups), - }, - ]), - (controlpanel) => ({ - ...controlpanel, - id: last(controlpanel['@id'].split('/')), - }), - ); + if (controlpanels?.length) { + filteredControlPanels = map( + concat(filterControlPanels(controlpanels), customcontrolpanels, [ + { + '@id': '/addons', + group: intl.formatMessage(messages.general), + title: intl.formatMessage(messages.addons), + }, + { + '@id': '/database', + group: intl.formatMessage(messages.general), + title: intl.formatMessage(messages.database), + }, + { + '@id': '/rules', + group: intl.formatMessage(messages.content), + title: intl.formatMessage(messages.contentRules), + }, + { + '@id': '/undo', + group: intl.formatMessage(messages.general), + title: intl.formatMessage(messages.undo), + }, + { + '@id': '/aliases', + group: intl.formatMessage(messages.general), + title: intl.formatMessage(messages.urlmanagement), + }, + { + '@id': '/relations', + group: intl.formatMessage(messages.content), + title: intl.formatMessage(messages.relations), + }, + { + '@id': '/moderate-comments', + group: intl.formatMessage(messages.content), + title: intl.formatMessage(messages.moderatecomments), + }, + { + '@id': '/users', + group: intl.formatMessage(messages.usersControlPanelCategory), + title: intl.formatMessage(messages.users), + }, + { + '@id': '/usergroupmembership', + group: intl.formatMessage(messages.usersControlPanelCategory), + title: intl.formatMessage(messages.usergroupmemberbership), + }, + { + '@id': '/groups', + group: intl.formatMessage(messages.usersControlPanelCategory), + title: intl.formatMessage(messages.groups), + }, + ]), + (controlpanel) => ({ + ...controlpanel, + id: last(controlpanel['@id'].split('/')), + }), + ); + } const groups = map(uniqBy(filteredControlPanels, 'group'), 'group'); const { controlPanelsIcons: icons } = config.settings; @@ -290,3 +302,30 @@ export default function Controlpanels({ location }) { ); } + +/** + * Property types. + * @property {Object} propTypes Property types. + * @static + */ +Controlpanels.propTypes = { + location: PropTypes.shape({ + pathname: PropTypes.string, + }).isRequired, +}; + +export default compose( + asyncConnect([ + { + key: 'controlpanels', + promise: async ({ location, store: { dispatch } }) => + await dispatch(listControlpanels()), + }, + { + key: 'systemInformation', + promise: async ({ location, store: { dispatch } }) => + await dispatch(getSystemInformation()), + }, + ]), + withRouter, +)(Controlpanels); diff --git a/packages/volto/src/components/manage/Toolbar/Toolbar.jsx b/packages/volto/src/components/manage/Toolbar/Toolbar.jsx index 2dd62f5b80..8c37868064 100644 --- a/packages/volto/src/components/manage/Toolbar/Toolbar.jsx +++ b/packages/volto/src/components/manage/Toolbar/Toolbar.jsx @@ -540,7 +540,8 @@ class Toolbar extends Component { ((this.props.content.is_folderish && this.props.types.length > 0) || (config.settings.isMultilingual && - this.props.content['@components']?.translations)) && ( + this.props.content?.['@components'] + ?.translations)) && (