From 6a8f3bdec5f42a44796bf975f3f02d2e93262569 Mon Sep 17 00:00:00 2001 From: Mikel Larreategi Date: Thu, 23 Jan 2025 12:56:36 +0100 Subject: [PATCH 1/4] feat: provide an Alternate language content links (#6603) --- packages/volto/news/6602.feature | 1 + .../AlternateHrefLangs/AlternateHrefLangs.jsx | 23 +++ .../AlternateHrefLangs.test.jsx | 135 ++++++++++++++++++ .../volto/src/components/theme/View/View.jsx | 2 + 4 files changed, 161 insertions(+) create mode 100644 packages/volto/news/6602.feature create mode 100644 packages/volto/src/components/theme/AlternateHrefLangs/AlternateHrefLangs.jsx create mode 100644 packages/volto/src/components/theme/AlternateHrefLangs/AlternateHrefLangs.test.jsx diff --git a/packages/volto/news/6602.feature b/packages/volto/news/6602.feature new file mode 100644 index 0000000000..c0d9292115 --- /dev/null +++ b/packages/volto/news/6602.feature @@ -0,0 +1 @@ +Provide language alternate links @erral diff --git a/packages/volto/src/components/theme/AlternateHrefLangs/AlternateHrefLangs.jsx b/packages/volto/src/components/theme/AlternateHrefLangs/AlternateHrefLangs.jsx new file mode 100644 index 0000000000..fc8a14d7c1 --- /dev/null +++ b/packages/volto/src/components/theme/AlternateHrefLangs/AlternateHrefLangs.jsx @@ -0,0 +1,23 @@ +import config from '@plone/volto/registry'; +import Helmet from '@plone/volto/helpers/Helmet/Helmet'; + +const AlternateHrefLangs = (props) => { + const { content } = props; + return ( + + {config.settings.isMultilingual && + content['@components']?.translations?.items?.map((item, key) => { + return ( + + ); + })} + + ); +}; + +export { AlternateHrefLangs }; diff --git a/packages/volto/src/components/theme/AlternateHrefLangs/AlternateHrefLangs.test.jsx b/packages/volto/src/components/theme/AlternateHrefLangs/AlternateHrefLangs.test.jsx new file mode 100644 index 0000000000..7ebb9f0aa0 --- /dev/null +++ b/packages/volto/src/components/theme/AlternateHrefLangs/AlternateHrefLangs.test.jsx @@ -0,0 +1,135 @@ +import React from 'react'; +import Helmet from '@plone/volto/helpers/Helmet/Helmet'; + +import renderer from 'react-test-renderer'; +import configureStore from 'redux-mock-store'; +import { Provider } from 'react-intl-redux'; +import config from '@plone/volto/registry'; + +import { AlternateHrefLangs } from './AlternateHrefLangs'; + +const mockStore = configureStore(); + +describe('AlternateHrefLangs', () => { + beforeEach(() => {}); + it('non multilingual site, renders nothing', () => { + config.settings.isMultilingual = false; + const content = { + '@id': '/', + '@components': {}, + }; + const store = mockStore({ + intl: { + locale: 'en', + messages: {}, + }, + }); + // We need to force the component rendering + // to fill the Helmet + renderer.create( + + + , + ); + + const helmetLinks = Helmet.peek().linkTags; + expect(helmetLinks.length).toBe(0); + }); + it('multilingual site, with some translations', () => { + config.settings.isMultilingual = true; + config.settings.supportedLanguages = ['en', 'es', 'eu']; + + const content = { + '@components': { + translations: { + items: [ + { '@id': '/en', language: 'en' }, + { '@id': '/es', language: 'es' }, + ], + }, + }, + }; + + const store = mockStore({ + intl: { + locale: 'en', + messages: {}, + }, + }); + + // We need to force the component rendering + // to fill the Helmet + renderer.create( + + <> + + + , + ); + const helmetLinks = Helmet.peek().linkTags; + + expect(helmetLinks.length).toBe(2); + + expect(helmetLinks).toContainEqual({ + rel: 'alternate', + href: '/es', + hrefLang: 'es', + }); + expect(helmetLinks).toContainEqual({ + rel: 'alternate', + href: '/en', + hrefLang: 'en', + }); + }); + it('multilingual site, with all available translations', () => { + config.settings.isMultilingual = true; + config.settings.supportedLanguages = ['en', 'es', 'eu']; + const store = mockStore({ + intl: { + locale: 'en', + messages: {}, + }, + }); + + const content = { + '@components': { + translations: { + items: [ + { '@id': '/en', language: 'en' }, + { '@id': '/eu', language: 'eu' }, + { '@id': '/es', language: 'es' }, + ], + }, + }, + }; + + // We need to force the component rendering + // to fill the Helmet + renderer.create( + + + , + ); + + const helmetLinks = Helmet.peek().linkTags; + + // We expect having 3 links + expect(helmetLinks.length).toBe(3); + + expect(helmetLinks).toContainEqual({ + rel: 'alternate', + href: '/eu', + hrefLang: 'eu', + }); + expect(helmetLinks).toContainEqual({ + rel: 'alternate', + href: '/es', + hrefLang: 'es', + }); + expect(helmetLinks).toContainEqual({ + rel: 'alternate', + href: '/en', + hrefLang: 'en', + }); + }); +}); diff --git a/packages/volto/src/components/theme/View/View.jsx b/packages/volto/src/components/theme/View/View.jsx index 902204c869..da8e9a7d82 100644 --- a/packages/volto/src/components/theme/View/View.jsx +++ b/packages/volto/src/components/theme/View/View.jsx @@ -21,6 +21,7 @@ import BodyClass from '@plone/volto/helpers/BodyClass/BodyClass'; import { getBaseUrl, flattenToAppURL } from '@plone/volto/helpers/Url/Url'; import { getLayoutFieldname } from '@plone/volto/helpers/Content/Content'; import { hasApiExpander } from '@plone/volto/helpers/Utils/Utils'; +import { AlternateHrefLangs } from '@plone/volto/components/theme/AlternateHrefLangs/AlternateHrefLangs'; import config from '@plone/volto/registry'; import SlotRenderer from '../SlotRenderer/SlotRenderer'; @@ -234,6 +235,7 @@ class View extends Component { return (
+ {/* Body class if displayName in component is set */} Date: Thu, 23 Jan 2025 12:59:22 +0100 Subject: [PATCH 2/4] Fix lint-staged throwing warnings when a file is checked-in and ignored (#6614) --- .lintstagedrc | 18 ------------------ lint-staged.config.js | 31 +++++++++++++++++++++++++++++++ packages/volto/news/6614.internal | 1 + 3 files changed, 32 insertions(+), 18 deletions(-) delete mode 100644 .lintstagedrc create mode 100644 lint-staged.config.js create mode 100644 packages/volto/news/6614.internal diff --git a/.lintstagedrc b/.lintstagedrc deleted file mode 100644 index e2d1cf3190..0000000000 --- a/.lintstagedrc +++ /dev/null @@ -1,18 +0,0 @@ -{ - "packages/!(volto)/**/*.{js,jsx,ts,tsx}": [ - "pnpm eslint --max-warnings=0 --fix", - "pnpm prettier --single-quote --write" - ], - "packages/volto/**/*.{js,jsx,ts,tsx}": [ - "pnpm --filter @plone/volto lint:husky", - "pnpm --filter @plone/volto prettier:husky" - ], - "packages/volto/src/**/*.{jsx, tsx}": ["pnpm --filter @plone/volto i18n"], - "packages/!(volto)/**/*.{css,less,scss}": ["pnpm stylelint --fix"], - "packages/volto/**/*.{css,less,scss}": [ - "pnpm --filter @plone/volto stylelint --fix" - ], - "packages/volto/**/*.overrides": [ - "pnpm --filter @plone/volto stylelint --fix" - ] -} diff --git a/lint-staged.config.js b/lint-staged.config.js new file mode 100644 index 0000000000..1b1d2ec7ae --- /dev/null +++ b/lint-staged.config.js @@ -0,0 +1,31 @@ +const { ESLint } = require('eslint'); + +const removeIgnoredFiles = async (files) => { + const eslint = new ESLint(); + const ignoredFiles = await Promise.all( + files.map((file) => eslint.isPathIgnored(file)), + ); + const filteredFiles = files.filter((_, i) => !ignoredFiles[i]); + return filteredFiles.join(' '); +}; +module.exports = { + 'packages/!(volto)/**/*.{js,jsx,ts,tsx}': async (files) => { + const filesToLint = await removeIgnoredFiles(files); + return [ + `eslint --max-warnings=0 ${filesToLint}`, + 'pnpm prettier --single-quote --write', + ]; + }, + 'packages/volto/**/*.{js,jsx,ts,tsx}': [ + 'pnpm --filter @plone/volto lint:husky', + 'pnpm --filter @plone/volto prettier:husky', + ], + 'packages/volto/src/**/*.{jsx, tsx}': ['pnpm --filter @plone/volto i18n'], + 'packages/!(volto)/**/*.{css,less,scss}': ['pnpm stylelint --fix'], + 'packages/volto/**/*.{css,less,scss}': [ + 'pnpm --filter @plone/volto stylelint --fix', + ], + 'packages/volto/**/*.overrides': [ + 'pnpm --filter @plone/volto stylelint --fix', + ], +}; diff --git a/packages/volto/news/6614.internal b/packages/volto/news/6614.internal new file mode 100644 index 0000000000..b4614b7da0 --- /dev/null +++ b/packages/volto/news/6614.internal @@ -0,0 +1 @@ +Fix lint-staged throwing warnings when a file is checked-in and ignored. @sneridagh From 1a194df1a26f7a9beffc9e20144ac5698ee7bd01 Mon Sep 17 00:00:00 2001 From: Tisha Soumya Date: Fri, 24 Jan 2025 01:01:05 +0530 Subject: [PATCH 3/4] Add custom checkAccessibility command in cypress (#6611) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Víctor Fernández de Alba --- packages/volto/cypress/support/commands.js | 22 ++++++++++++++++++++++ packages/volto/news/6606.feature | 1 + 2 files changed, 23 insertions(+) create mode 100644 packages/volto/news/6606.feature diff --git a/packages/volto/cypress/support/commands.js b/packages/volto/cypress/support/commands.js index f8ba71f310..5ea83b651e 100644 --- a/packages/volto/cypress/support/commands.js +++ b/packages/volto/cypress/support/commands.js @@ -967,3 +967,25 @@ Cypress.Commands.add('queryCounter', (path, steps, number = 1) => { cy.get('@counterName').its('callCount').should('equal', number); }); + +// Print cypress-axe violations to the terminal +function printAccessibilityViolations(violations) { + cy.task( + 'table', + violations.map(({ id, impact, description, nodes }) => ({ + impact, + description: `${description} (${id})`, + nodes: nodes.length, + })), + ); +} + +Cypress.Commands.add( + 'checkAccessibility', + (subject, { skipFailures = false } = {}) => { + cy.checkA11y(subject, null, printAccessibilityViolations, skipFailures); + }, + { + prevSubject: 'optional', + }, +); diff --git a/packages/volto/news/6606.feature b/packages/volto/news/6606.feature new file mode 100644 index 0000000000..70506d90d7 --- /dev/null +++ b/packages/volto/news/6606.feature @@ -0,0 +1 @@ +feat(cypress):Add custom check Accessibility command @Tishasoumya-02 \ No newline at end of file From 2e893fc021b5087c2957986b3494e749ca6370fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Fern=C3=A1ndez=20de=20Alba?= Date: Thu, 23 Jan 2025 23:21:39 +0100 Subject: [PATCH 4/4] Initial push of the Plone 7 base package (#6599) --- .github/workflows/acceptance.yml | 48 + .github/workflows/changelog.yml | 10 + .github/workflows/cookieplone.yml | 66 + Makefile | 37 + PACKAGES.md | 7 +- package.json | 10 +- packages/blocks/.eslintrc.cjs | 4 +- packages/blocks/RenderBlocks/BlockWrapper.tsx | 4 +- packages/blocks/RenderBlocks/RenderBlocks.tsx | 2 +- packages/blocks/index.ts | 1 + packages/blocks/news/6599.bugfix | 1 + packages/blocks/package.json | 5 + packages/blocks/setupTesting.ts | 3 + packages/blocks/vitest.config.ts | 14 + packages/client/README.md | 2 +- packages/client/news/6599.documentation | 1 + packages/eslintconfig/README.md | 34 + packages/eslintconfig/addons.js | 17 + packages/eslintconfig/package.json | 9 + packages/react-router/.gitignore | 2 + packages/react-router/.npmignore | 6 + packages/react-router/.release-it.json | 28 + packages/react-router/CHANGELOG.md | 59 + packages/react-router/README.md | 188 ++ packages/react-router/news/.gitkeep | 0 packages/react-router/news/6599.bugfix | 2 + packages/react-router/package.json | 77 + packages/react-router/src/index.test.ts | 93 + packages/react-router/src/index.ts | 77 + packages/react-router/towncrier.toml | 33 + packages/react-router/tsconfig.json | 13 + packages/react-router/tsup.config.ts | 9 + packages/seven/.eslintrc.cjs | 88 + packages/seven/.gitignore | 7 + packages/seven/.release-it.json | 26 + packages/seven/CHANGELOG.md | 11 + packages/seven/Makefile | 274 +++ packages/seven/README.md | 33 + packages/seven/app/config.server.ts | 32 + packages/seven/app/config.ts | 11 + packages/seven/app/content.tsx | 56 + packages/seven/app/okroute.tsx | 5 + packages/seven/app/root.tsx | 191 ++ packages/seven/app/routes.ts | 17 + packages/seven/app/utils.ts | 20 + packages/seven/cypress.config.js | 13 + packages/seven/cypress/add-commands.js | 2 + .../seven/cypress/docker/seamless-rules.yml | 27 + packages/seven/cypress/docker/seamless.yml | 24 + packages/seven/cypress/fixtures/broccoli.jpg | Bin 0 -> 102057 bytes packages/seven/cypress/fixtures/example.json | 5 + packages/seven/cypress/fixtures/file.pdf | Bin 0 -> 32772 bytes .../seven/cypress/fixtures/halfdome2022.jpg | Bin 0 -> 319364 bytes packages/seven/cypress/fixtures/image.png | Bin 0 -> 1185 bytes packages/seven/cypress/helpers/index.js | 13 + packages/seven/cypress/support/commands.js | 969 ++++++++++ packages/seven/cypress/support/constants.js | 1 + packages/seven/cypress/support/e2e.js | 53 + packages/seven/cypress/support/guillotina.js | 73 + packages/seven/cypress/support/helpers.js | 1 + .../seven/cypress/support/reset-fixture.js | 43 + .../seven/cypress/support/upgradetests.js | 82 + packages/seven/cypress/support/volto-slate.js | 81 + .../cypress/tests/core/basic/basic.cy.ts | 12 + packages/seven/cypress/tsconfig.json | 8 + packages/seven/news/.gitkeep | 0 packages/seven/news/6599.feature | 1 + packages/seven/package.json | 47 + packages/seven/public/favicon.ico | Bin 0 -> 15086 bytes packages/seven/react-router.config.ts | 7 + packages/seven/registry.config.ts | 4 + packages/seven/towncrier.toml | 33 + packages/seven/tsconfig.json | 33 + packages/seven/vite.config.ts | 31 + packages/slots/.eslintrc.cjs | 10 +- packages/slots/.storybook/preview.ts | 2 +- packages/slots/news/6599.bugfix | 1 + packages/slots/package.json | 4 + packages/theming/.eslintrc.cjs | 4 + packages/theming/news/6599.bugfix | 1 + packages/theming/styles/main.css.map | 0 packages/volto/Makefile | 8 +- packages/volto/news/6599.internal | 1 + pnpm-lock.yaml | 1561 ++++++++++++++--- 84 files changed, 4518 insertions(+), 270 deletions(-) create mode 100644 .github/workflows/cookieplone.yml create mode 100644 packages/blocks/news/6599.bugfix create mode 100644 packages/blocks/setupTesting.ts create mode 100644 packages/blocks/vitest.config.ts create mode 100644 packages/client/news/6599.documentation create mode 100644 packages/eslintconfig/README.md create mode 100644 packages/eslintconfig/addons.js create mode 100644 packages/eslintconfig/package.json create mode 100644 packages/react-router/.gitignore create mode 100644 packages/react-router/.npmignore create mode 100644 packages/react-router/.release-it.json create mode 100644 packages/react-router/CHANGELOG.md create mode 100644 packages/react-router/README.md create mode 100644 packages/react-router/news/.gitkeep create mode 100644 packages/react-router/news/6599.bugfix create mode 100644 packages/react-router/package.json create mode 100644 packages/react-router/src/index.test.ts create mode 100644 packages/react-router/src/index.ts create mode 100644 packages/react-router/towncrier.toml create mode 100644 packages/react-router/tsconfig.json create mode 100644 packages/react-router/tsup.config.ts create mode 100644 packages/seven/.eslintrc.cjs create mode 100644 packages/seven/.gitignore create mode 100644 packages/seven/.release-it.json create mode 100644 packages/seven/CHANGELOG.md create mode 100644 packages/seven/Makefile create mode 100644 packages/seven/README.md create mode 100644 packages/seven/app/config.server.ts create mode 100644 packages/seven/app/config.ts create mode 100644 packages/seven/app/content.tsx create mode 100644 packages/seven/app/okroute.tsx create mode 100644 packages/seven/app/root.tsx create mode 100644 packages/seven/app/routes.ts create mode 100644 packages/seven/app/utils.ts create mode 100644 packages/seven/cypress.config.js create mode 100644 packages/seven/cypress/add-commands.js create mode 100644 packages/seven/cypress/docker/seamless-rules.yml create mode 100644 packages/seven/cypress/docker/seamless.yml create mode 100644 packages/seven/cypress/fixtures/broccoli.jpg create mode 100644 packages/seven/cypress/fixtures/example.json create mode 100644 packages/seven/cypress/fixtures/file.pdf create mode 100644 packages/seven/cypress/fixtures/halfdome2022.jpg create mode 100644 packages/seven/cypress/fixtures/image.png create mode 100644 packages/seven/cypress/helpers/index.js create mode 100644 packages/seven/cypress/support/commands.js create mode 100644 packages/seven/cypress/support/constants.js create mode 100644 packages/seven/cypress/support/e2e.js create mode 100644 packages/seven/cypress/support/guillotina.js create mode 100644 packages/seven/cypress/support/helpers.js create mode 100644 packages/seven/cypress/support/reset-fixture.js create mode 100644 packages/seven/cypress/support/upgradetests.js create mode 100644 packages/seven/cypress/support/volto-slate.js create mode 100644 packages/seven/cypress/tests/core/basic/basic.cy.ts create mode 100644 packages/seven/cypress/tsconfig.json create mode 100644 packages/seven/news/.gitkeep create mode 100644 packages/seven/news/6599.feature create mode 100644 packages/seven/package.json create mode 100644 packages/seven/public/favicon.ico create mode 100644 packages/seven/react-router.config.ts create mode 100644 packages/seven/registry.config.ts create mode 100644 packages/seven/towncrier.toml create mode 100644 packages/seven/tsconfig.json create mode 100644 packages/seven/vite.config.ts create mode 100644 packages/slots/news/6599.bugfix create mode 100644 packages/theming/.eslintrc.cjs create mode 100644 packages/theming/news/6599.bugfix create mode 100644 packages/theming/styles/main.css.map create mode 100644 packages/volto/news/6599.internal diff --git a/.github/workflows/acceptance.yml b/.github/workflows/acceptance.yml index 82480849c7..f4637cded8 100644 --- a/.github/workflows/acceptance.yml +++ b/.github/workflows/acceptance.yml @@ -691,3 +691,51 @@ jobs: with: name: cypress-videos path: packages/volto/cypress/videos + + seven: + 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: Seven + 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/seven + browser: chrome + spec: cypress/tests/core/basic/**/*.cy.ts + start: | + make ci-acceptance-backend-start + make acceptance-frontend-prod-start + wait-on: 'npx wait-on --httpTimeout 20000 http-get://127.0.0.1:55001/plone http://127.0.0.1:3000' + + # Upload Cypress screenshots + - uses: actions/upload-artifact@v4 + if: failure() + with: + name: cypress-screenshots + path: packages/seven/cypress/screenshots + # Upload Cypress videos + - uses: actions/upload-artifact@v4 + if: failure() + with: + name: cypress-videos + path: packages/seven/cypress/videos diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml index b9616ac57d..db5a5e8d93 100644 --- a/.github/workflows/changelog.yml +++ b/.github/workflows/changelog.yml @@ -48,6 +48,8 @@ jobs: - 'packages/slots/**' theming: - 'packages/theming/**' + seven: + - 'packages/seven/**' wrongNews: - added|modified: 'news/**' @@ -150,6 +152,14 @@ jobs: env: BASE_BRANCH: ${{ github.base_ref }} + - name: seven changelog check + if: steps.filter.outputs.seven == 'true' + run: | + git fetch --no-tags origin main + towncrier check --compare-with origin/main --dir packages/seven + env: + BASE_BRANCH: ${{ github.base_ref }} + - name: Wrong location of news changelog check if: steps.filter.outputs.wrongNews == 'true' run: echo "News items should be moved from the repository root to the appropriate package root in `packages/package-name`." && exit 1 diff --git a/.github/workflows/cookieplone.yml b/.github/workflows/cookieplone.yml new file mode 100644 index 0000000000..a9d52a957c --- /dev/null +++ b/.github/workflows/cookieplone.yml @@ -0,0 +1,66 @@ +name: Cookieplone +on: [push, pull_request] + +env: + PYTHON_VERSION: "3.13" + +jobs: + seven: + 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: Seven Cookieplone + timeout-minutes: 45 + strategy: + fail-fast: false + matrix: + node-version: [22.x] + steps: + - uses: actions/checkout@v4 + - run: echo "Current branch is ${GITHUB_REF#refs/heads/}" + - name: Set up Node.js environment + uses: ./.github/actions/node_env_setup + with: + node-version: ${{ matrix.node-version }} + + - name: Generate Cookieplone-based frontend addon + run: | + pipx install cookieplone + COOKIEPLONE_REPOSITORY_TAG=seventemplate pipx run --no-cache cookieplone seven_addon --no-input + + - name: Install generated package + working-directory: seven-add-on + run: | + pnpm dlx mrs-developer missdev --no-config --fetch-https + (cd core && git fetch --depth 1 origin ${GITHUB_REF#refs/heads/}:${GITHUB_REF#refs/heads/} && git checkout ${GITHUB_REF#refs/heads/}) + pnpm i + + - 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: seven-add-on/core/packages/seven + browser: chrome + spec: cypress/tests/core/basic/**/*.cy.ts + start: | + make ci-acceptance-backend-start + make project-acceptance-frontend-prod-start + wait-on: 'npx wait-on --httpTimeout 20000 http-get://127.0.0.1:55001/plone http://127.0.0.1:3000' + + # Upload Cypress screenshots + - uses: actions/upload-artifact@v4 + if: failure() + with: + name: cypress-screenshots + path: packages/seven/cypress/screenshots + # Upload Cypress videos + - uses: actions/upload-artifact@v4 + if: failure() + with: + name: cypress-videos + path: packages/seven/cypress/videos diff --git a/Makefile b/Makefile index 72656906b6..b689fac12c 100644 --- a/Makefile +++ b/Makefile @@ -145,9 +145,24 @@ packages/registry/dist: $(shell find packages/registry/src -type f) packages/components/dist: $(shell find packages/components/src -type f) pnpm build:components +packages/client/dist: $(shell find packages/client/src -type f) + pnpm build:client + +packages/providers/dist: $(shell find packages/providers/src -type f) + pnpm build:providers + +packages/helpers/dist: $(shell find packages/helpers/src -type f) + pnpm build:helpers + +packages/react-router/dist: $(shell find packages/react-router/src -type f) + pnpm build:react-router + .PHONY: build-deps build-deps: packages/registry/dist ## Build dependencies +.PHONY: build-all-deps +build-all-deps: packages/registry/dist packages/components/dist packages/client/dist packages/providers/dist packages/react-router/dist packages/helpers/dist ## Build all dependencies + .PHONY: i18n i18n: ## Converts your po files into json to translate volto frontend $(MAKE) -C "./packages/volto/" i18n @@ -378,6 +393,28 @@ acceptance-server-detached-start: ## Starts test acceptance server main fixture acceptance-server-detached-stop: ## Stop test acceptance server main fixture in detached mode (daemon) docker kill plone-client-acceptance-server +######### Seven acceptance tests + +.PHONY: seven-acceptance-frontend-dev-start +seven-acceptance-frontend-dev-start: ## Start acceptance frontend in development mode for Seven + $(MAKE) -C "./packages/seven/" acceptance-frontend-dev-start + +.PHONY: seven-acceptance-frontend-prod-start +seven-acceptance-frontend-prod-start:: ## Start acceptance frontend in production mode for Seven + $(MAKE) -C "./packages/seven/" acceptance-frontend-prod-start + +.PHONY: seven-acceptance-test +seven-acceptance-test: ## Start Cypress in interactive mode for Seven + $(MAKE) -C "./packages/seven/" acceptance-test + +.PHONY: seven-ci-acceptance-test +seven-ci-acceptance-test: ## Run cypress tests in headless mode for CI for Seven + $(MAKE) -C "./packages/seven/" ci-acceptance-test + +.PHONY: seven-ci-acceptance-test-run-all +seven-ci-acceptance-test-run-all: ## With a single command, start both the acceptance frontend and backend acceptance server, and run Cypress tests in headless mode for Seven + $(MAKE) -C "./packages/seven/" ci-acceptance-test-run-all + # include local overrides if present -include Makefile.local -include ../../../Makefile.local diff --git a/PACKAGES.md b/PACKAGES.md index 5f72ef477c..51de2c3eac 100644 --- a/PACKAGES.md +++ b/PACKAGES.md @@ -15,9 +15,8 @@ and as a development dependency: Plone 6.0.x (Volto 17 and below) does not use any of them. These packages are expected to be used and become part of Plone 7. -Some of them might become part of Plone 6.1.x minor versions. -The packages are divided into three categories or types: +These packages are divided into three categories or types: - core - utilities @@ -53,6 +52,7 @@ The bundle of these packages must work on both CommonJS and ECMAScript Module (E - `@plone/providers` - `@plone/helpers` +- `@plone/react-router` ### Rules @@ -67,6 +67,7 @@ This bundle must work on both CommonJS and ESM environments. - `@plone/blocks` - `@plone/slots` - `@plone/theming` +- `@plone/cmsui` - `@plone/contents` @@ -94,7 +95,7 @@ Some of them are used by the build, and separated in packages for convenience. - `tsconfig` -## Volto add-ons packages +## Volto add-on packages These Volto add-ons are packages used by Volto core. They are always loaded, so they are also called "core packages". diff --git a/package.json b/package.json index 1cf8ebd0e3..756e6d20a0 100644 --- a/package.json +++ b/package.json @@ -5,14 +5,18 @@ "preinstall": "npx only-allow pnpm", "watch": "pnpm --filter @plone/registry --filter @plone/client --filter @plone/components --filter @plone/providers watch", "build:deps": "pnpm --filter @plone/registry build", - "build:all": "pnpm --filter @plone/registry --filter @plone/client --filter @plone/components --filter @plone/providers build", - "build:all:force": "pnpm --filter @plone/registry --filter @plone/client --filter @plone/components --filter @plone/providers build:force", + "build:all": "pnpm --filter @plone/registry --filter @plone/client --filter @plone/components --filter @plone/providers --filter @plone/react-router build", + "build:all:force": "pnpm --filter @plone/registry --filter @plone/client --filter @plone/components --filter @plone/providers --filter @plone/react-router build:force", "build:registry": "pnpm --filter @plone/registry run build", "build:components": "pnpm --filter @plone/components run build", + "build:client": "pnpm --filter @plone/client run build", + "build:providers": "pnpm --filter @plone/providers run build", + "build:helpers": "pnpm --filter @plone/helpers run build", + "build:react-router": "pnpm --filter @plone/react-router run build", "build": "pnpm --filter @plone/volto build", "start": "pnpm --filter @plone/volto start", "start:project": "pnpm --filter plone run start", - "lint": "pnpm build:all && eslint --max-warnings=0 '{apps,packages}/**/*.{js,jsx,ts,tsx}'", + "lint": "make build-all-deps && eslint --max-warnings=0 '{apps,packages}/**/*.{js,jsx,ts,tsx}'", "lint:volto": "pnpm --filter @plone/volto run lint", "test": "pnpm --filter @plone/volto run test", "test:ci": "pnpm --filter @plone/volto run test:ci", diff --git a/packages/blocks/.eslintrc.cjs b/packages/blocks/.eslintrc.cjs index 42cb63d1af..18bae08069 100644 --- a/packages/blocks/.eslintrc.cjs +++ b/packages/blocks/.eslintrc.cjs @@ -1,5 +1,5 @@ /** @type {import('eslint').Linter.Config} */ module.exports = { - extends: ['../../.eslintrc.cjs', 'plugin:react/jsx-runtime'], - ignorePatterns: ['storybook-static', 'dist'], + extends: ['../../.eslintrc.cjs', '../eslintconfig/addons.js'], + ignorePatterns: ['vitest.config.ts'], }; diff --git a/packages/blocks/RenderBlocks/BlockWrapper.tsx b/packages/blocks/RenderBlocks/BlockWrapper.tsx index e94b555d47..d55f8ec8d5 100644 --- a/packages/blocks/RenderBlocks/BlockWrapper.tsx +++ b/packages/blocks/RenderBlocks/BlockWrapper.tsx @@ -11,8 +11,8 @@ const BlockWrapper = (props: BlockWrapperProps) => { const data = content.blocks?.[block]; const category = blocksConfig?.[data['@type']]?.category; // TODO: Bring in the StyleWrapper helpers for calculating styles and classes - const classNames = null; - const style = null; + const classNames = undefined; + const style = undefined; return (
{ const Block = blocksConfig[blockType]?.view || DefaultBlockView; return Block ? ( - + {/* @ts-ignore It's ok to pass the blockData as is */} [!WARNING] +> This package or app is experimental. +> The community offers no support whatsoever for it. +> Breaking changes may occur without notice. diff --git a/packages/eslintconfig/addons.js b/packages/eslintconfig/addons.js new file mode 100644 index 0000000000..cf72e9fa30 --- /dev/null +++ b/packages/eslintconfig/addons.js @@ -0,0 +1,17 @@ +/** @type {import('eslint').Linter.Config} */ +module.exports = { + ignorePatterns: ['storybook-static', 'dist'], + overrides: [ + { + files: ['**/*.{ts,tsx, js, jsx}'], + extends: [ + 'plugin:react/jsx-runtime', // We only want this for non-library code (eg. volto add-ons) + ], + rules: { + 'import/no-unresolved': 1, + 'import/named': 'error', + 'react/jsx-key': [2, { checkFragmentShorthand: true }], + }, + }, + ], +}; diff --git a/packages/eslintconfig/package.json b/packages/eslintconfig/package.json new file mode 100644 index 0000000000..5abc62b4fb --- /dev/null +++ b/packages/eslintconfig/package.json @@ -0,0 +1,9 @@ +{ + "name": "eslintconfig", + "version": "0.0.0", + "private": true, + "license": "MIT", + "publishConfig": { + "access": "public" + } +} diff --git a/packages/react-router/.gitignore b/packages/react-router/.gitignore new file mode 100644 index 0000000000..b947077876 --- /dev/null +++ b/packages/react-router/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +dist/ diff --git a/packages/react-router/.npmignore b/packages/react-router/.npmignore new file mode 100644 index 0000000000..a6d10baa1e --- /dev/null +++ b/packages/react-router/.npmignore @@ -0,0 +1,6 @@ +news +towncrier.toml +.changelog.draft +node_modules/ +.release-it.json +.eslintrc.js diff --git a/packages/react-router/.release-it.json b/packages/react-router/.release-it.json new file mode 100644 index 0000000000..bcb50ec97b --- /dev/null +++ b/packages/react-router/.release-it.json @@ -0,0 +1,28 @@ +{ + "plugins": { + "../scripts/prepublish.js": {} + }, + "hooks": { + "after:bump": [ + "pipx run towncrier build --draft --yes --version ${version} > .changelog.draft && pipx run towncrier build --yes --version ${version}", + "pnpm build:force" + ], + "after:release": "rm .changelog.draft" + }, + "npm": { + "publish": false + }, + "git": { + "changelog": "pipx run towncrier build --draft --yes --version 0.0.0", + "requireUpstream": false, + "requireCleanWorkingDir": false, + "commitMessage": "Release @plone/react-router ${version}", + "tagName": "plone-react-router-${version}", + "tagAnnotation": "Release @plone/react-router ${version}" + }, + "github": { + "release": true, + "releaseName": "@plone/react-router ${version}", + "releaseNotes": "cat .changelog.draft" + } +} diff --git a/packages/react-router/CHANGELOG.md b/packages/react-router/CHANGELOG.md new file mode 100644 index 0000000000..96a8e9b472 --- /dev/null +++ b/packages/react-router/CHANGELOG.md @@ -0,0 +1,59 @@ +# @plone/providers Release Notes + + + + + +## 1.0.0-alpha.6 (2024-11-21) + +### Feature + +- Update RAC to 1.5.0 @sneridagh [#6498](https://github.com/plone/volto/issues/6498) + +## 1.0.0-alpha.5 (2024-11-05) + +### Internal + +- Improve packaging. @sneridagh + +## 1.0.0-alpha.4 (2024-11-05) + +### Internal + +- Bump local `typescript` version. @sneridagh [#6461](https://github.com/plone/volto/issues/6461) +- Replace `parcel` with `tsup`. Better types, better tsconfig. Move to ESM. @sneridagh [#6468](https://github.com/plone/volto/issues/6468) + +## 1.0.0-alpha.3 (2024-10-18) + +## 1.0.0-alpha.2 (2024-10-18) + +### Breaking + +- Improve and group providers. @sneridagh + Breaking: + - The interface of the providers has changed. Please check the new one, and adapt your apps accordingly. [#6069](https://github.com/plone/volto/issues/6069) + +### Internal + +- Update typescript and vitest everywhere @sneridagh [#6407](https://github.com/plone/volto/issues/6407) + +## 1.0.0-alpha.1 (2024-05-23) + +### Internal + +- Cleanup imports in RouterLocation provider @pnicolli [#6029](https://github.com/plone/volto/issues/6029) + +## 1.0.0-alpha.0 (2024-05-13) + +### Feature + +- Initial implementation @sneridagh [#5887](https://github.com/plone/volto/issues/5887) + +### Internal + +- Improvements to the monorepo setup with utilities, especially ESLint. Build cached option to speedup operations. @sneridagh [#5969](https://github.com/plone/volto/issues/5969) +- Saner defaults for building deps, switch default to cached, add `build:force` command @sneridagh [#5980](https://github.com/plone/volto/issues/5980) diff --git a/packages/react-router/README.md b/packages/react-router/README.md new file mode 100644 index 0000000000..75ba6475a9 --- /dev/null +++ b/packages/react-router/README.md @@ -0,0 +1,188 @@ +# `@plone/providers` + +This package contains utility providers for Plone React components. +The main purpose is to provide dependency injection of common required artifacts needed by any app. +These artifacts include: +- Router related +- Plone Client +- URL handling methods + +> [!WARNING] +> This package or app is experimental. +> The community offers no support whatsoever for it. +> Breaking changes may occur without notice. + +## `PloneProvider` + +It provides all the necessary artifacts that an app can need grouped in a single provider. + +```ts +interface PloneProvider { + ploneClient: InstanceType; + queryClient: QueryClient; + useLocation: () => Location | undefined; + useParams: (opts?: any) => Record; + navigate: (path: string) => void; + useHref: (to: string, options?: any) => string; + flattenToAppURL: (path: string | undefined) => string | undefined; +} +``` + +It should be instantiated at the top of your app. +You have to provide the required props depending on the framework and the router used. +This is the example for a Next.js app. +Please refer to the {file}`apps` folder of the Volto repository for more examples of the usage of `PloneProvider` in different React frameworks. + +```tsx +'use client'; +import React from 'react'; +import { + useRouter, + usePathname, + useSearchParams, + useParams, +} from 'next/navigation'; +import { QueryClient } from '@tanstack/react-query'; +import { PloneProvider } from '@plone/providers'; +import PloneClient from '@plone/client'; +import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; +import { flattenToAppURL } from './utils'; +import config from './config'; + +// Custom hook to unify the location object between NextJS and Plone +function useLocation() { + let pathname = usePathname(); + let search = useSearchParams(); + + return { + pathname, + search, + searchStr: '', + hash: (typeof window !== 'undefined' && window.location.hash) || '', + href: (typeof window !== 'undefined' && window.location.href) || '', + }; +} + +const Providers: React.FC<{ + children?: React.ReactNode; +}> = ({ children }) => { + // Creating the clients at the file root level makes the cache shared + // between all requests and means _all_ data gets passed to _all_ users. + // Besides being bad for performance, this also leaks any sensitive data. + // We use this pattern to ensure that every client gets its own clients + const [queryClient] = React.useState( + () => + new QueryClient({ + defaultOptions: { + queries: { + // With SSR, we usually want to set some default staleTime + // above 0 to avoid refetching immediately on the client + staleTime: 60 * 1000, + }, + }, + }), + ); + + const [ploneClient] = React.useState(() => + PloneClient.initialize({ + apiPath: config.settings.apiPath, + }), + ); + + const router = useRouter(); + + return ( + { + router.push(to); + }} + useParams={useParams} + useHref={(to) => flattenToAppURL(to)} + flattenToAppURL={flattenToAppURL} + > + {children} + + + ); +}; + +export default Providers; + +``` + +You can use it anywhere in your app by using the hook `usePloneProvider`. + +```tsx +import { usePloneProvider } from '@plone/providers'; + +const { ploneClient } = usePloneProvider() +``` + +Alternatively, you can use it in any other context property. + +```tsx +const { navigate } = usePloneProvider() +``` + +## `PloneClientProvider` + +`PloneProvider` in a group of other smaller providers. +You can also instantiate and use them as standalone providers. +However, you should do this only if the framework has some limitation on using the bulk `PloneClientProvider`. + +The following snippets show its usage. +First, instantiate the provider. + +```ts +export type PloneClientProviderProps = { + client: InstanceType; + queryClient: QueryClient; + children?: React.ReactNode; +}; +``` + +Second, use its related hook through either of the following examples. + +```tsx +import { usePloneClient } from '@plone/providers'; + +const client = usePloneClient() +``` + +or + +```tsx +const { getContentQuery } = usePloneClient() +``` + +## `AppRouterProvider` + +This provider is included also in `PloneProvider`. +You can also instantiate and use it as a standalone provider. +However, you should do this only if the framework has some limitation on using the bulk `PloneClientProvider`. + +The following code example shows its usage. + +```ts +interface AppRouterProps { + useLocation: () => Location | undefined; + useParams: (opts?: any) => Record; + navigate: (path: string) => void; + useHref?: (to: string, options?: any) => string; + flattenToAppURL: (path: string | undefined) => string | undefined; + children: ReactNode; +} +``` + +The following code sample shows its related hook. + +```tsx +import { useAppRouter } from '@plone/providers'; + +const { useLocation } = useAppRouter() +``` diff --git a/packages/react-router/news/.gitkeep b/packages/react-router/news/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/react-router/news/6599.bugfix b/packages/react-router/news/6599.bugfix new file mode 100644 index 0000000000..383960a097 --- /dev/null +++ b/packages/react-router/news/6599.bugfix @@ -0,0 +1,2 @@ +Initial implementation. +Added `getAddonRoutesConfig` for configuring routes in add-ons. @sneridagh diff --git a/packages/react-router/package.json b/packages/react-router/package.json new file mode 100644 index 0000000000..eaad874428 --- /dev/null +++ b/packages/react-router/package.json @@ -0,0 +1,77 @@ +{ + "name": "@plone/react-router", + "description": "Plone React Router integration package", + "maintainers": [ + { + "name": "Plone Foundation", + "url": "https://plone.org" + } + ], + "funding": "https://github.com/sponsors/plone", + "license": "MIT", + "version": "0.0.0", + "repository": { + "type": "git", + "url": "https://github.com/plone/volto.git" + }, + "bugs": { + "url": "https://github.com/plone/volto/issues" + }, + "homepage": "https://plone.org", + "keywords": [ + "volto", + "plone", + "plone6", + "react", + "helpers" + ], + "publishConfig": { + "access": "public" + }, + "type": "module", + "files": [ + "dist", + "README.md" + ], + "main": "./dist/index.js", + "exports": { + "./package.json": "./package.json", + ".": { + "import": "./dist/index.js", + "default": "./dist/index.cjs" + } + }, + "scripts": { + "build": "tsup", + "build:force": "tsup", + "check:exports": "attw --pack .", + "test": "vitest", + "dry-release": "release-it --dry-run", + "release": "release-it", + "release-major-alpha": "release-it major --preRelease=alpha", + "release-alpha": "release-it --preRelease=alpha" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + }, + "dependencies": {}, + "devDependencies": { + "@arethetypeswrong/cli": "^0.16.4", + "@plone/types": "workspace:*", + "@react-router/dev": "7.1.2", + "@types/node": "22.10.7", + "@types/react": "^18", + "@types/react-dom": "^18", + "release-it": "17.1.1", + "tsconfig": "workspace:*", + "tsup": "^8.3.5", + "typescript": "^5.6.3", + "vitest": "^2.1.3" + } +} diff --git a/packages/react-router/src/index.test.ts b/packages/react-router/src/index.test.ts new file mode 100644 index 0000000000..7f20b57247 --- /dev/null +++ b/packages/react-router/src/index.test.ts @@ -0,0 +1,93 @@ +import { describe, expect, it, afterEach, beforeEach } from 'vitest'; +import { getAddonRoutesConfig } from './index'; +import type { ReactRouterRouteEntry } from '@plone/types'; + +describe('getAddonRoutesConfig', () => { + const addonsInfo = [ + { + name: '@plone/components', + modulePath: '/my/path/to/plone/components', + }, + ]; + + it('route - basic', () => { + const routesConfig: Array = [ + { + type: 'route', + path: '/login', + file: './login.tsx', + }, + ]; + expect(getAddonRoutesConfig(routesConfig, addonsInfo)).toEqual([ + { children: undefined, file: './login.tsx', path: '/login' }, + ]); + }); + + it('route - basic with addon name', () => { + const routesConfig: Array = [ + { + type: 'route', + path: '/login', + file: '@plone/components/login.tsx', + }, + ]; + expect(getAddonRoutesConfig(routesConfig, addonsInfo)).toEqual([ + { + children: undefined, + file: '/my/path/to/plone/components/login.tsx', + path: '/login', + }, + ]); + }); + + it('route - with options', () => { + const routesConfig: Array = [ + { + type: 'route', + path: '/login', + file: './login.tsx', + options: { + id: 'login', + }, + }, + ]; + expect(getAddonRoutesConfig(routesConfig, addonsInfo)).toEqual([ + { + children: undefined, + file: './login.tsx', + path: '/login', + id: 'login', + }, + ]); + }); + + it('route - nested', () => { + const routesConfig: Array = [ + { + type: 'route', + path: '/login', + file: './login.tsx', + children: [ + { + type: 'route', + path: '/login/ok', + file: './login/ok.tsx', + }, + ], + }, + ]; + expect(getAddonRoutesConfig(routesConfig, addonsInfo)).toEqual([ + { + file: './login.tsx', + path: '/login', + children: [ + { + children: undefined, + file: './login/ok.tsx', + path: '/login/ok', + }, + ], + }, + ]); + }); +}); diff --git a/packages/react-router/src/index.ts b/packages/react-router/src/index.ts new file mode 100644 index 0000000000..59788606ce --- /dev/null +++ b/packages/react-router/src/index.ts @@ -0,0 +1,77 @@ +import { route, index, layout, prefix } from '@react-router/dev/routes'; +import type { RouteConfig, RouteConfigEntry } from '@react-router/dev/routes'; +import type { ReactRouterRouteEntry } from '@plone/types'; +import path from 'node:path'; + +export function getAddonRoutesConfig( + routesConfig: Array, + addonsInfo: Array, +): Array { + let resultRoutesConfig: RouteConfig = []; + + for (const routeConfig of routesConfig) { + const containsAddonModule = addonsInfo.find((addon) => + routeConfig.file.includes(addon.name), + ); + if (containsAddonModule) { + routeConfig.file = path.join( + containsAddonModule.modulePath, + routeConfig.file.replace(containsAddonModule.name, ''), + ); + } + switch (routeConfig.type) { + case 'route': + if (routeConfig.options) { + resultRoutesConfig.push( + route(routeConfig.path, routeConfig.file, routeConfig.options), + ); + } else if (routeConfig.children) { + resultRoutesConfig.push( + route( + routeConfig.path, + routeConfig.file, + routeConfig.options || {}, + getAddonRoutesConfig( + routeConfig.children, + addonsInfo, + ) as Array, + ), + ); + } else { + resultRoutesConfig.push(route(routeConfig.path, routeConfig.file)); + } + break; + + case 'index': + resultRoutesConfig.push(index(routeConfig.file, routeConfig.options)); + break; + + case 'layout': + if (routeConfig.options) { + resultRoutesConfig.push( + layout(routeConfig.file, routeConfig.options), + ); + } + if (routeConfig.children) { + resultRoutesConfig.push( + layout( + routeConfig.file, + routeConfig.options || {}, + getAddonRoutesConfig( + routeConfig.children, + addonsInfo, + ) as Array, + ), + ); + } + break; + + case 'prefix': + console.log('prefix not implemented yet'); + break; + default: + break; + } + } + return resultRoutesConfig; +} diff --git a/packages/react-router/towncrier.toml b/packages/react-router/towncrier.toml new file mode 100644 index 0000000000..3ef721f378 --- /dev/null +++ b/packages/react-router/towncrier.toml @@ -0,0 +1,33 @@ +[tool.towncrier] +filename = "CHANGELOG.md" +directory = "news/" +title_format = "## {version} ({project_date})" +underlines = ["", "", ""] +template = "../scripts/templates/towncrier_template.jinja" +start_string = "\n" +issue_format = "[#{issue}](https://github.com/plone/volto/issues/{issue})" + +[[tool.towncrier.type]] +directory = "breaking" +name = "Breaking" +showcontent = true + +[[tool.towncrier.type]] +directory = "feature" +name = "Feature" +showcontent = true + +[[tool.towncrier.type]] +directory = "bugfix" +name = "Bugfix" +showcontent = true + +[[tool.towncrier.type]] +directory = "internal" +name = "Internal" +showcontent = true + +[[tool.towncrier.type]] +directory = "documentation" +name = "Documentation" +showcontent = true diff --git a/packages/react-router/tsconfig.json b/packages/react-router/tsconfig.json new file mode 100644 index 0000000000..d6fbf62fe0 --- /dev/null +++ b/packages/react-router/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "tsconfig/react-library.json", + "include": ["src"], + "exclude": [ + "node_modules", + "build", + "public", + "coverage", + "src/**/*.test.{js,jsx,ts,tsx}", + "src/**/*.spec.{js,jsx,ts,tsx}", + "src/**/*.stories.{js,jsx,ts,tsx}" + ] +} diff --git a/packages/react-router/tsup.config.ts b/packages/react-router/tsup.config.ts new file mode 100644 index 0000000000..82b88a425c --- /dev/null +++ b/packages/react-router/tsup.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'tsup'; + +export default defineConfig({ + entryPoints: ['src/index.ts'], + format: ['cjs', 'esm'], + dts: true, + outDir: 'dist', + clean: true, +}); diff --git a/packages/seven/.eslintrc.cjs b/packages/seven/.eslintrc.cjs new file mode 100644 index 0000000000..21d4f1322c --- /dev/null +++ b/packages/seven/.eslintrc.cjs @@ -0,0 +1,88 @@ +/** + * This is intended to be a basic starting point for linting in your app. + * It relies on recommended configs out of the box for simplicity, but you can + * and should modify this configuration to best suit your team's needs. + */ + +/** @type {import('eslint').Linter.Config} */ +module.exports = { + root: true, + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + ecmaFeatures: { + jsx: true, + }, + }, + env: { + browser: true, + commonjs: true, + es6: true, + }, + + // Base config + extends: ['eslint:recommended'], + + // Ignore Cypress folder + ignorePatterns: [ + 'cypress/', + '.react-router/**/*', + 'tests/registry.config.ts', + ], + + overrides: [ + // React + { + files: ['**/*.{js,jsx,ts,tsx}'], + plugins: ['react', 'jsx-a11y'], + extends: [ + 'plugin:react/recommended', + 'plugin:react/jsx-runtime', + 'plugin:react-hooks/recommended', + 'plugin:jsx-a11y/recommended', + ], + settings: { + react: { + version: 'detect', + }, + 'import/core-modules': ['@plone/registry/addons-loader'], + formComponents: ['Form'], + linkComponents: [ + { name: 'Link', linkAttribute: 'to' }, + { name: 'NavLink', linkAttribute: 'to' }, + ], + }, + }, + + // Typescript + { + files: ['**/*.{ts,tsx}'], + plugins: ['@typescript-eslint', 'import'], + parser: '@typescript-eslint/parser', + settings: { + 'import/internal-regex': '^~/', + 'import/resolver': { + node: { + extensions: ['.ts', '.tsx'], + }, + typescript: { + alwaysTryTypes: true, + }, + }, + }, + extends: [ + 'plugin:@typescript-eslint/recommended', + 'plugin:import/recommended', + 'plugin:import/typescript', + ], + }, + + // Node + { + files: ['.eslintrc.js'], + env: { + node: true, + }, + }, + ], +}; diff --git a/packages/seven/.gitignore b/packages/seven/.gitignore new file mode 100644 index 0000000000..f1eb112b25 --- /dev/null +++ b/packages/seven/.gitignore @@ -0,0 +1,7 @@ +node_modules + +/.cache +/build +.env +.react-router +.registry.loader.js diff --git a/packages/seven/.release-it.json b/packages/seven/.release-it.json new file mode 100644 index 0000000000..d91788c67c --- /dev/null +++ b/packages/seven/.release-it.json @@ -0,0 +1,26 @@ +{ + "hooks": { + "after:bump": [ + "pipx run towncrier build --draft --yes --version ${version} > .changelog.draft", + "pipx run towncrier build --yes --version ${version}" + ], + "after:release": "rm .changelog.draft" + }, + "npm": { + "publish": false + }, + "git": { + "commitArgs": ["--no-verify"], + "changelog": "pipx run towncrier build --draft --yes --version 0.0.0", + "requireUpstream": false, + "requireCleanWorkingDir": false, + "commitMessage": "Release Plone7 ${version}", + "tagName": "plone7-${version}", + "tagAnnotation": "Release Plone7 ${version}" + }, + "github": { + "release": true, + "releaseName": "Plone7 ${version}", + "releaseNotes": "cat .changelog.draft" + } +} diff --git a/packages/seven/CHANGELOG.md b/packages/seven/CHANGELOG.md new file mode 100644 index 0000000000..a90be32e57 --- /dev/null +++ b/packages/seven/CHANGELOG.md @@ -0,0 +1,11 @@ +# Plone 7 Release Notes + + + + + +## 1.0.0 (unreleased) diff --git a/packages/seven/Makefile b/packages/seven/Makefile new file mode 100644 index 0000000000..b012ee8899 --- /dev/null +++ b/packages/seven/Makefile @@ -0,0 +1,274 @@ +# Volto development + +### Defensive settings for make: +# https://tech.davis-hansson.com/p/make/ +SHELL:=bash +.ONESHELL: +.SHELLFLAGS:=-eu -o pipefail -c +.SILENT: +.DELETE_ON_ERROR: +MAKEFLAGS+=--warn-undefined-variables +MAKEFLAGS+=--no-builtin-rules + +# Project settings (read from repo root) +include ../../variables.mk + +# Allow setting the language for backend-docker-start. Default is `en`. +LANGUAGE ?=en + +# Recipe snippets for reuse + +CHECKOUT_BASENAME="$(shell basename $(shell realpath ./))" +CHECKOUT_BRANCH=$(shell git branch --show-current) +CHECKOUT_TMP=../$(CHECKOUT_BASENAME).tmp +CHECKOUT_TMP_ABS="$(shell realpath $(CHECKOUT_TMP))" + +CURRENT_DIR:=$(shell dirname $(realpath $(lastword $(MAKEFILE_LIST)))) + +# We like colors +# From: https://coderwall.com/p/izxssa/colored-makefile-for-golang-projects +RED=`tput setaf 1` +GREEN=`tput setaf 2` +RESET=`tput sgr0` +YELLOW=`tput setaf 3` + + +# Top-level targets + +.PHONY: all +all: help + +# Add the following 'help' target to your Makefile +# and add help text after each target name starting with ' ##' +# to return a pretty list of targets and their descriptions. +.PHONY: help +help: ## This help message + @echo -e "$$(grep -hE '^\S+:.*##' $(MAKEFILE_LIST) | sed -e 's/:.*##\s*/:/' -e 's/^\(.\+\):\(.*\)/\\x1b[36m\1\\x1b[m:\2/' | column -c2 -t -s :)" + +.PHONY: start +start: ## Starts Plone 7 in development mode + pnpm start + +.PHONY: build +build: ## Build a production bundle for distribution + pnpm build + +.PHONY: test +test: ## Run unit tests + pnpm test + +.PHONY: clean +clean: ## Clean development environment + rm -rf node_modules + +##### Build + +.PHONY: cypress-install +cypress-install: ## Install Cypress for acceptance tests + $(NODEBIN)/cypress install + +../registry/dist: $(shell find ../registry/src -type f) + (cd ../../ && pnpm build:registry) + +../components/dist: $(shell find ../components/src -type f) + (cd ../../ && pnpm build:components) + +../client/dist: $(shell find ../client/src -type f) + (cd ../../ && pnpm build:client) + +../providers/dist: $(shell find ../providers/src -type f) + (cd ../../ && pnpm build:providers) + +../helpers/dist: $(shell find ../helpers/src -type f) + (cd ../../ && pnpm build:helpers) + +../react-router/dist: $(shell find ../react-router/src -type f) + (cd ../../ && pnpm build:react-router) + +.PHONY: build-deps +build-deps: ../registry/dist ../components/dist ../client/dist ../providers/dist ../react-router/dist ../helpers/dist ## Build dependencies + +.PHONY: i18n +i18n: ## Extract and compile translations + pnpm i18n + +## Storybook + +.PHONY: storybook-start +storybook-start: ## Start Storybook server on port 6006 + @echo "$(GREEN)==> Start Storybook$(RESET)" + pnpm run storybook + +.PHONY: storybook-build +storybook-build: build-deps ## Build Storybook + pnpm build-storybook -o ../../docs/_build/html/storybook + +##### Release (it runs the one inside) + +.PHONY: release-notes-copy-to-docs +release-notes-copy-to-docs: ## Copy release notes into documentation + cp CHANGELOG.md ../../docs/source/release-notes/index.md + git add ../../docs/source/release-notes/index.md + +##### Docker containers + +.PHONY: backend-docker-start +backend-docker-start: ## Starts a Docker-based backend for development + docker run -it --rm --name=backend -p 8080:8080 -v volto-backend-data:/data -e SITE=Plone -e ADDONS='$(KGS)' -e LANGUAGE='$(LANGUAGE)' $(DOCKER_IMAGE) + +.PHONY: frontend-docker-start +frontend-docker-start: ## Starts a Docker-based frontend for development + docker run -it --rm --name=volto --link backend -p 3000:3000 -e RAZZLE_INTERNAL_API_PATH=http://backend:8080/Plone -e RAZZLE_DEV_PROXY_API_PATH=http://backend:8080/Plone plone/plone-frontend:latest + +##### Acceptance tests (Cypress) +######### Dev mode Acceptance tests + +.PHONY: acceptance-frontend-dev-start +acceptance-frontend-dev-start: ## Start acceptance frontend in development mode + PLONE_API_PATH=http://127.0.0.1:55001/plone pnpm start + +######### Core Acceptance tests + +.PHONY: acceptance-backend-start +acceptance-backend-start: ## Start backend acceptance server + #docker run -it --rm -p 55001:55001 $(DOCKER_IMAGE_ACCEPTANCE) + # Uncomment next line and comment line above to use a custom image with the acceptance server (in case you need to use an specific backend add-on or version) + docker run -it --rm -e ZSERVER_HOST=0.0.0.0 -e ZSERVER_PORT=55001 -p 55001:55001 -e ADDONS='$(KGS) $(TESTING_ADDONS)' -e APPLY_PROFILES=plone.app.contenttypes:plone-content,plone.restapi:default,plone.volto:default-homepage -e CONFIGURE_PACKAGES=plone.app.contenttypes,plone.restapi,plone.volto,plone.volto.cors $(DOCKER_IMAGE) ./bin/robot-server plone.app.robotframework.testing.VOLTO_ROBOT_TESTING + +.PHONY: ci-acceptance-backend-start +ci-acceptance-backend-start: ## Start backend acceptance server in headless mode for CI + docker run -i --rm -p 55001:55001 $(DOCKER_IMAGE_ACCEPTANCE) + # Uncomment next line and comment line above to use a custom image with the acceptance server (in case you need to use an specific backend add-on or version) + # docker run -i --rm -e ZSERVER_HOST=0.0.0.0 -e ZSERVER_PORT=55001 -p 55001:55001 -e ADDONS='$(KGS) $(TESTING_ADDONS)' -e APPLY_PROFILES=plone.app.contenttypes:plone-content,plone.restapi:default,plone.volto:default-homepage -e CONFIGURE_PACKAGES=plone.app.contenttypes,plone.restapi,plone.volto,plone.volto.cors $(DOCKER_IMAGE) ./bin/robot-server plone.app.robotframework.testing.VOLTO_ROBOT_TESTING + +.PHONY: acceptance-frontend-prod-start +acceptance-frontend-prod-start: build-deps ## Start acceptance frontend in production mode + pnpm build && PLONE_API_PATH=http://127.0.0.1:55001/plone pnpm start:prod + +.PHONY: acceptance-test +acceptance-test: ## Start Cypress in interactive mode + NODE_ENV=production CYPRESS_API=plone $(NODEBIN)/cypress open + +.PHONY: ci-acceptance-test +ci-acceptance-test: ## Run cypress tests in headless mode for CI + NODE_ENV=production CYPRESS_API=plone $(NODEBIN)/cypress run --config specPattern='cypress/tests/core/**/*.{ts,tsx}' + +.PHONY: ci-acceptance-test-run-all +ci-acceptance-test-run-all: ## With a single command, start both the acceptance frontend and backend acceptance server, and run Cypress tests in headless mode + $(NODEBIN)/start-test "make ci-acceptance-backend-start" http-get://127.0.0.1:55001/plone "make acceptance-frontend-prod-start" http://127.0.0.1:3000 "make ci-acceptance-test" + +######### Deployment Core Acceptance tests + +.PHONY: deployment-acceptance-frontend-prod-start +deployment-acceptance-frontend-prod-start: build-deps ## Start acceptance frontend in production mode for deployment + pnpm build && pnpm start:prod + +.PHONY: deployment-acceptance-test +deployment-acceptance-test: ## Start Cypress in interactive mode for tests in deployment + NODE_ENV=production CYPRESS_API=plone $(NODEBIN)/cypress open --config baseUrl='http://localhost' + +.PHONY: deployment-acceptance-web-server-start +deployment-acceptance-web-server-start: ## Start the reverse proxy (Traefik) in port 80 for deployment + cd cypress/docker && docker compose -f seamless.yml up + +.PHONY: deployment-ci-acceptance-test-run-all +deployment-ci-acceptance-test-run-all: ## With a single command, run the backend, frontend, and the Cypress tests in headless mode for CI for deployment tests + $(NODEBIN)/start-test "make acceptance-backend-start" http-get://127.0.0.1:55001/plone "make deployment-acceptance-frontend-prod-start" http://127.0.0.1:3000 "make ci-acceptance-test" + +######### Project Acceptance tests + +.PHONY: project-acceptance-frontend-prod-start +project-acceptance-frontend-prod-start: build-deps ## Start acceptance frontend in production mode for project tests + (cd ../../.. && pnpm build && PLONE_API_PATH=http://127.0.0.1:55001/plone pnpm start:prod) + +######### Core Sandbox Acceptance tests + +.PHONY: coresandbox-acceptance-backend-start +coresandbox-acceptance-backend-start: ## Start backend acceptance server for core sandbox tests + docker run -i --rm -p 55001:55001 -e APPLY_PROFILES=plone.app.contenttypes:plone-content,plone.restapi:default,plone.volto:default-homepage,plone.volto:coresandbox -e CONFIGURE_PACKAGES=plone.app.contenttypes,plone.restapi,plone.volto,plone.volto.cors,plone.volto.coresandbox $(DOCKER_IMAGE_ACCEPTANCE) + +.PHONY: coresandbox-acceptance-frontend-prod-start +coresandbox-acceptance-frontend-prod-start: build-deps ## Start acceptance frontend in production mode for core sandbox tests + ADDONS=@plone/volto-coresandbox PLONE_API_PATH=http://127.0.0.1:55001/plone pnpm build && pnpm start:prod + +.PHONY: coresandbox-acceptance-frontend-dev-start +coresandbox-acceptance-frontend-dev-start: build-deps ## Start acceptance frontend in development mode for core sandbox tests + ADDONS=@plone/volto-coresandbox PLONE_API_PATH=http://127.0.0.1:55001/plone pnpm start + +.PHONY: coresandbox-acceptance-test +coresandbox-acceptance-test: ## Start Cypress in interactive mode for core sandbox tests + NODE_ENV=production CYPRESS_API=plone $(NODEBIN)/cypress open --config specPattern='cypress/tests/coresandbox/**/*.{js,jsx,ts,tsx}' + +.PHONY: coresandbox-ci-acceptance-test +coresandbox-ci-acceptance-test: ## Run Cypress tests in headless mode for CI for core sandbox tests + NODE_ENV=production CYPRESS_API=plone $(NODEBIN)/cypress run --config specPattern='cypress/tests/coresandbox/**/*.{js,jsx,ts,tsx}' + +.PHONY: coresandbox-ci-acceptance-test-run-all +coresandbox-ci-acceptance-test-run-all: ## With a single command, run the backend, frontend, and the Cypress tests in headless mode for CI for core sandbox tests + $(NODEBIN)/start-test "make coresandbox-acceptance-backend-start" http-get://127.0.0.1:55001/plone "make coresandbox-acceptance-frontend-prod-start" http://127.0.0.1:3000 "make coresandbox-ci-acceptance-test" + +######### Multilingual Acceptance tests + +.PHONY: multilingual-acceptance-backend-start +multilingual-acceptance-backend-start: ## Start backend acceptance server for multilingual tests + docker run -i --rm -p 55001:55001 -e APPLY_PROFILES=plone.app.contenttypes:plone-content,plone.restapi:default,plone.volto:multilingual $(DOCKER_IMAGE_ACCEPTANCE) + +.PHONY: multilingual-acceptance-frontend-prod-start +multilingual-acceptance-frontend-prod-start: build-deps ## Start acceptance frontend in production mode for multilingual tests + ADDONS=@plone/volto-coresandbox:multilingualFixture PLONE_API_PATH=http://127.0.0.1:55001/plone pnpm build && pnpm start:prod + +.PHONY: multilingual-acceptance-test +multilingual-acceptance-test: ## Start Cypress in interactive mode for multilingual tests + NODE_ENV=production CYPRESS_API=plone $(NODEBIN)/cypress open --config specPattern='cypress/tests/multilingual/**/*.{js,jsx,ts,tsx}' + +.PHONY: multilingual-ci-acceptance-test +multilingual-ci-acceptance-test: ## Run Cypress tests in headless mode for CI for multilingual tests + NODE_ENV=production CYPRESS_API=plone $(NODEBIN)/cypress run --config specPattern='cypress/tests/multilingual/**/*.{js,jsx,ts,tsx}' + +.PHONY: multilingual-ci-acceptance-test-run-all +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 + $(NODEBIN)/start-test "make multilingual-acceptance-backend-start" http-get://127.0.0.1:55001/plone "make multilingual-acceptance-frontend-prod-start" http://127.0.0.1:3000 "make multilingual-ci-acceptance-test" + +######### Deployment Multilingual Acceptance tests + +.PHONY: deployment-multilingual-acceptance-backend-start +deployment-multilingual-acceptance-backend-start: ## Start backend acceptance server for multilingual tests for deployment + docker run -i --rm -p 55001:55001 -e APPLY_PROFILES=plone.app.contenttypes:plone-content,plone.restapi:default,plone.volto:multilingual $(DOCKER_IMAGE_ACCEPTANCE) + +.PHONY: deployment-multilingual-acceptance-frontend-prod-start +deployment-multilingual-acceptance-frontend-prod-start: build-deps ##Start acceptance frontend in production mode for multilingual tests for deployment + ADDONS=@plone/volto-coresandbox:multilingualFixture pnpm build && pnpm start:prod + +.PHONY: deployment-multilingual-acceptance-test +deployment-multilingual-acceptance-test: ## Start Cypress in interactive mode for multilingual tests for deployment + NODE_ENV=production CYPRESS_API=plone $(NODEBIN)/cypress open --config baseUrl='http://localhost',specPattern='cypress/tests/multilingual/**/*.{js,jsx,ts,tsx}' + +.PHONY: deployment-multilingual-ci-acceptance-test +deployment-multilingual-ci-acceptance-test: ## Run Cypress tests in headless mode for CI for multilingual tests for deployment + NODE_ENV=production CYPRESS_API=plone $(NODEBIN)/cypress run --config specPattern='cypress/tests/multilingual/**/*.{js,jsx,ts,tsx}' + +.PHONY: deployment-multilingual-ci-acceptance-test-run-all +deployment-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 for deployment + $(NODEBIN)/start-test "make deployment-multilingual-acceptance-backend-start" http-get://127.0.0.1:55001/plone "make deployment-multilingual-acceptance-frontend-prod-start" http://127.0.0.1:3000 "make deployment-multilingual-ci-acceptance-test" + +######### Working Copy Acceptance tests + +.PHONY: working-copy-acceptance-backend-start +working-copy-acceptance-backend-start: ## Start backend acceptance server for working copy tests + docker run -i --rm -p 55001:55001 -e APPLY_PROFILES=plone.app.contenttypes:plone-content,plone.restapi:default,plone.app.iterate:default,plone.volto:default-homepage $(DOCKER_IMAGE_ACCEPTANCE) + +.PHONY: working-copy-acceptance-frontend-prod-start +working-copy-acceptance-frontend-prod-start: build-deps ## Start acceptance frontend in production mode for working copy tests + ADDONS=@plone/volto-coresandbox:workingCopyFixture PLONE_API_PATH=http://127.0.0.1:55001/plone pnpm build && pnpm start:prod + +.PHONY: working-copy-acceptance-test +working-copy-acceptance-test: ## Start Cypress in interactive mode for working copy tests + NODE_ENV=production CYPRESS_API=plone $(NODEBIN)/cypress open --config specPattern='cypress/tests/workingCopy/**/*.{js,jsx,ts,tsx}' + +.PHONY: working-copy-ci-acceptance-test +working-copy-ci-acceptance-test: ## Run Cypress tests in headless mode for CI for working copy tests + NODE_ENV=production CYPRESS_API=plone $(NODEBIN)/cypress run --config specPattern='cypress/tests/workingCopy/**/*.{js,jsx,ts,tsx}' + +.PHONY: working-copy-ci-acceptance-test-run-all +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" diff --git a/packages/seven/README.md b/packages/seven/README.md new file mode 100644 index 0000000000..9377c8fc62 --- /dev/null +++ b/packages/seven/README.md @@ -0,0 +1,33 @@ +# Plone 7 + +> [!WARNING] +> This package and all the efforts around it are not even in an alpha state and are experimental. +> The community offers no support whatsoever for it. +> Breaking changes may occur without notice. + +This is the initial (and very early) implementation of Plone 7. +After the design and first implementations of all the required pieces (the `@plone/*` libraries) that will compose Plone 7, this package will concentrate all the development during the next years. + +It is based on [React Router](https://reactrouter.com/dev/docs) 7, using the `@plone/*` libraries. + +The name of this package and its folder name in `packages` may also change since it's undecided yet. + +## Releases + +Even in experimental phase, this package will be soft released periodically, under a tag. +This will provide a way to try it out in real development and deploy scenarios. + +## Development + +To start, from the root of the monorepo, issue the following commands. + +```shell +pnpm install +pnpm --filter plone7 run dev +``` + +Then start the Plone backend. + +```shell +make backend-docker-start +``` diff --git a/packages/seven/app/config.server.ts b/packages/seven/app/config.server.ts new file mode 100644 index 0000000000..2d7c951f7d --- /dev/null +++ b/packages/seven/app/config.server.ts @@ -0,0 +1,32 @@ +/** + * This is the server side config entry point + */ +import config from '@plone/registry'; +import ploneClient from '@plone/client'; +import applyAddonConfiguration from '@plone/registry/addons-loader'; + +export default function install() { + applyAddonConfiguration(config); + + config.settings.apiPath = + process.env.PLONE_API_PATH || 'http://localhost:3000'; + config.settings.internalApiPath = + process.env.PLONE_INTERNAL_API_PATH || undefined; + + const cli = ploneClient.initialize({ + apiPath: config.settings.internalApiPath || config.settings.apiPath, + }); + + config.registerUtility({ + name: 'ploneClient', + type: 'client', + method: () => cli, + }); + + console.log('API_PATH is:', config.settings.apiPath); + console.log( + 'INTERNAL_API_PATH is:', + config.settings.internalApiPath || 'not set', + ); + return config; +} diff --git a/packages/seven/app/config.ts b/packages/seven/app/config.ts new file mode 100644 index 0000000000..7f2026e538 --- /dev/null +++ b/packages/seven/app/config.ts @@ -0,0 +1,11 @@ +/** + * This is the client side config entry point + */ +import config from '@plone/registry'; +import applyAddonConfiguration from '@plone/registry/addons-loader'; + +export default function install() { + applyAddonConfiguration(config); + config.settings.apiPath = 'http://localhost:3000'; + return config; +} diff --git a/packages/seven/app/content.tsx b/packages/seven/app/content.tsx new file mode 100644 index 0000000000..8358a163e0 --- /dev/null +++ b/packages/seven/app/content.tsx @@ -0,0 +1,56 @@ +import type { Route } from './+types/content'; +import { data, useLoaderData, useLocation } from 'react-router'; +import PloneClient from '@plone/client'; +import App from '@plone/slots/components/App'; +import config from '@plone/registry'; + +export const meta: Route.MetaFunction = ({ data }) => { + return [ + { title: data?.title }, + { name: 'description', content: data?.description }, + ]; +}; + +const expand = ['navroot', 'breadcrumbs', 'navigation']; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export async function loader({ params, request }: Route.LoaderArgs) { + const ploneClient = config + .getUtility({ + name: 'ploneClient', + type: 'client', + }) + .method(); + + const { getContent } = ploneClient as PloneClient; + + const path = new URL(request.url).pathname; + + if ( + !( + /^https?:\/\//.test(path) || + /^favicon.ico\/\//.test(path) || + /expand/.test(path) || + /\/@@images\//.test(path) || + /\/@@download\//.test(path) || + /^\/assets/.test(path) || + /\.(css|css\.map)$/.test(path) + ) + ) { + try { + return await getContent({ path, expand }); + } catch (error) { + throw data('Content Not Found', { status: 404 }); + } + } else { + console.log('matched path not fetched', path); + throw data('Content Not Found', { status: 404 }); + } +} + +export default function Content() { + const data = useLoaderData(); + const pathname = useLocation().pathname; + + return ; +} diff --git a/packages/seven/app/okroute.tsx b/packages/seven/app/okroute.tsx new file mode 100644 index 0000000000..56472e1bb6 --- /dev/null +++ b/packages/seven/app/okroute.tsx @@ -0,0 +1,5 @@ +export async function loader() { + return new Response(null, { + status: 200, + }); +} diff --git a/packages/seven/app/root.tsx b/packages/seven/app/root.tsx new file mode 100644 index 0000000000..ad5ae56a73 --- /dev/null +++ b/packages/seven/app/root.tsx @@ -0,0 +1,191 @@ +import { useState } from 'react'; +import { + isRouteErrorResponse, + Links, + Meta, + Outlet, + Scripts, + ScrollRestoration, + useHref, + useLocation, + useNavigate as useRRNavigate, + useParams, + useLoaderData, + useRouteLoaderData, +} from 'react-router'; +import type { Route } from './+types/root'; + +import { QueryClient } from '@tanstack/react-query'; +import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; +import PloneClient from '@plone/client'; +import { PloneProvider } from '@plone/providers'; +import { flattenToAppURL } from './utils'; +import config from '@plone/registry'; +import install from './config'; +import installSSR from './config.server'; + +install(); + +import themingMain from '@plone/theming/styles/main.css?url'; +import slotsMain from '@plone/slots/main.css?url'; + +function useNavigate() { + const navigate = useRRNavigate(); + return (to: string) => navigate(flattenToAppURL(to) || ''); +} + +function useHrefLocal(to: string) { + return useHref(flattenToAppURL(to) || ''); +} + +export const meta: Route.MetaFunction = () => [ + { name: 'generator', content: 'Plone 7 - https://plone.org' }, +]; + +export const links: Route.LinksFunction = () => [ + { + rel: 'icon', + href: '/favicon.png', + type: 'image/png', + sizes: 'any', + }, + { + rel: 'icon', + href: '/icon.svg', + type: 'image/svg+xml', + }, + { rel: 'stylesheet', href: themingMain }, + { rel: 'stylesheet', href: slotsMain }, + { rel: 'preconnect', href: 'https://fonts.googleapis.com' }, + { + rel: 'preconnect', + href: 'https://fonts.gstatic.com', + crossOrigin: 'anonymous', + }, + { + rel: 'stylesheet', + href: 'https://fonts.googleapis.com/css2?family=Montserrat:ital,wght@0,100..900;1,100..900&display=swap', + }, +]; + +export async function loader() { + const ssrConfig = installSSR(); + + return { + env: { + PLONE_API_PATH: ssrConfig.settings.apiPath, + PLONE_INTERNAL_API_PATH: ssrConfig.settings.internalApiPath, + }, + }; +} + +export function Layout({ children }: { children: React.ReactNode }) { + const data = useLoaderData(); + const indexLoaderData = useRouteLoaderData('index'); + const contentLoaderData = useRouteLoaderData('content'); + const contentData = indexLoaderData || contentLoaderData; + + return ( + + + + + + + + + +