diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..69f3d2e --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,58 @@ +name: Release as OCI images + +on: + push: + branches: + - main +jobs: + tests: + uses: ./.github/workflows/ci.yaml + + publish-image: + name: Publish OCI image to GitHub Container Registry + runs-on: ubuntu-latest + strategy: + matrix: + include: + - image: ghcr.io/bluedynamics/mximages-plone/mx-plone-frontend + dockerfile: frontend/Dockerfile + - image: ghcr.io/bluedynamics/mximages-plone/mx-plone-backend + dockerfile: backend/Dockerfile + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + - name: Docker meta + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ matrix.image }} + tags: | + type=ref,event=branch + type=ref,event=pr + type=pep440,pattern={{version}} + type=pep440,pattern={{major}}.{{minor}}.{{patch}} + type=pep440,pattern={{major}}.{{minor}} + type=pep440,pattern={{major}} + type=sha + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Build and push Backend + uses: docker/build-push-action@v5 + with: + context: "{{defaultContext}}:container" + file: ${{ matrix.dockerfile }} + platforms: linux/amd64,linux/arm64 + build-args: | + PACKAGE_VERSION=${{ steps.meta.outputs.version }} + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + diff --git a/backend/.gitignore b/backend/.gitignore index d7e1419..2140e26 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -19,7 +19,7 @@ node_modules/ # venv related pyvenv.cfg -.venv +.venv/ # mxdev .mxmake diff --git a/backend/Dockerfile b/backend/Dockerfile index 6131b40..4e92a91 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -1,10 +1,10 @@ # syntax=docker/dockerfile:1 FROM python:3.12-slim-bookworm -LABEL maintainer="GeoSphere Austria " \ - org.label-schema.name="kvoltoweb-backend" \ - org.label-schema.description="kvoltoweb backend image." \ - org.label-schema.vendor="GeoSphere Austria" +LABEL maintainer="Jens Klein " \ + org.label-schema.name="mx-plone-image-backend" \ + org.label-schema.description="BlueDynamics MX Plone Image Backend" \ + org.label-schema.vendor="BlueDynamics Alliance" # Python optimization for OCI images: no bytecode writing ENV PYTHONDONTWRITEBYTECODE 1 diff --git a/backend/pyproject.toml b/backend/pyproject.toml deleted file mode 100644 index 9c0e1b8..0000000 --- a/backend/pyproject.toml +++ /dev/null @@ -1,205 +0,0 @@ -[project] -name = "kvoltoweb.backend" -version = "1.0.0a1.dev0" -description = "GeoSphere Austria Volto website feat. Kubernetes." -readme = "README.md" -license = "gpl-2.0-only" -requires-python = ">=3.12" -authors = [ - { name = "GeoSphere Austria", email = "kontakt@geosphere.at" }, -] -keywords = [ - "CMS", - "Plone", - "Python", -] -classifiers = [ - "Development Status :: 3 - Alpha", - "Environment :: Web Environment", - "Framework :: Plone", - "Framework :: Plone :: 6.1", - "Framework :: Plone :: Addon", - "Framework :: Plone :: Distribution", - "License :: OSI Approved :: GNU General Public License v2 (GPLv2)", - "Operating System :: OS Independent", - "Programming Language :: Python", - "Programming Language :: Python :: 3.12", -] -dependencies = [ - "plone.app.caching", - "plone.app.multilingual", - "plone.app.upgrade", - "plone.volto", - "plone.distribution>=2.0.0b1", - "collective.volto.formsupport[honeypot]", -] - -[project.optional-dependencies] -test = [ - "plone.app.testing", - "plone.restapi[test]", - "pytest", - "pytest-cov", - "pytest-plone>=0.5.0", -] -search=[ - "collective.elastic.plone[redis,opensearch]", -] -production = [ - "relstorage[postgresql]", - "geosphere.kvoltoweb[search]", -] - -[project.urls] -Homepage = "https://gitlab.geosphere.at" -Documentation = "https://gitlab.geosphere.at/app/homepage/kvoltoweb/-/wikis/home" -Tracker = "https://gitlab.geosphere.at/app/homepage/kvoltoweb/-/issues" - -[build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" - -[tool.hatch.build.targets.sdist] -include = [ - "/src", - "/tests" -] -[tool.hatch.build.targets.wheel] -packages = [ - "/src/kvoltoweb", -] - -# Generated from: -# https://github.com/plone/meta/tree/main/config/default -# See the inline comments on how to expand/tweak this configuration file -[tool.towncrier] -directory = "news/" -filename = "CHANGES.md" -start_string = "\n" -title_format = "## {version} ({project_date})" -template = "news/.changelog_template.jinja" -underlines = ["", "", ""] - -[[tool.towncrier.type]] -directory = "breaking" -name = "Breaking changes:" -showcontent = true - -[[tool.towncrier.type]] -directory = "feature" -name = "New features:" -showcontent = true - -[[tool.towncrier.type]] -directory = "bugfix" -name = "Bug fixes:" -showcontent = true - -[[tool.towncrier.type]] -directory = "internal" -name = "Internal:" -showcontent = true - -[[tool.towncrier.type]] -directory = "documentation" -name = "Documentation:" -showcontent = true - -[[tool.towncrier.type]] -directory = "tests" -name = "Tests" -showcontent = true - -## -# Add extra configuration options in .meta.toml: -# [pyproject] -# towncrier_extra_lines = """ -# extra_configuration -# """ -## - -[tool.isort] -profile = "plone" - -## -# Add extra configuration options in .meta.toml: -# [pyproject] -# isort_extra_lines = """ -# extra_configuration -# """ -## - -[tool.black] -target-version = ["py311"] - -## -# Add extra configuration options in .meta.toml: -# [pyproject] -# black_extra_lines = """ -# extra_configuration -# """ -## - -[tool.codespell] -ignore-words-list = "discreet,vew" -skip = "*.po,*.min.js" -## -# Add extra configuration options in .meta.toml: -# [pyproject] -# codespell_ignores = "foo,bar" -# codespell_skip = "*.po,*.map,package-lock.json" -## - -[tool.dependencychecker] -Zope = [ - # Zope own provided namespaces - 'App', 'OFS', 'Products.Five', 'Products.OFSP', 'Products.PageTemplates', - 'Products.SiteAccess', 'Shared', 'Testing', 'ZPublisher', 'ZTUtils', - 'Zope2', 'webdav', 'zmi', - # ExtensionClass own provided namespaces - 'ExtensionClass', 'ComputedAttribute', 'MethodObject', - # Zope dependencies - 'AccessControl', 'Acquisition', 'AuthEncoding', 'beautifulsoup4', 'BTrees', - 'cffi', 'Chameleon', 'DateTime', 'DocumentTemplate', - 'MultiMapping', 'multipart', 'PasteDeploy', 'Persistence', 'persistent', - 'pycparser', 'python-gettext', 'pytz', 'RestrictedPython', 'roman', - 'soupsieve', 'transaction', 'waitress', 'WebOb', 'WebTest', 'WSGIProxy2', - 'z3c.pt', 'zc.lockfile', 'ZConfig', 'zExceptions', 'ZODB', 'zodbpickle', - 'zope.annotation', 'zope.browser', 'zope.browsermenu', 'zope.browserpage', - 'zope.browserresource', 'zope.cachedescriptors', 'zope.component', - 'zope.configuration', 'zope.container', 'zope.contentprovider', - 'zope.contenttype', 'zope.datetime', 'zope.deferredimport', - 'zope.deprecation', 'zope.dottedname', 'zope.event', 'zope.exceptions', - 'zope.filerepresentation', 'zope.globalrequest', 'zope.hookable', - 'zope.i18n', 'zope.i18nmessageid', 'zope.interface', 'zope.lifecycleevent', - 'zope.location', 'zope.pagetemplate', 'zope.processlifetime', 'zope.proxy', - 'zope.ptresource', 'zope.publisher', 'zope.schema', 'zope.security', - 'zope.sequencesort', 'zope.site', 'zope.size', 'zope.structuredtext', - 'zope.tal', 'zope.tales', 'zope.testbrowser', 'zope.testing', - 'zope.traversing', 'zope.viewlet' -] -'Products.CMFCore' = [ - 'docutils', 'five.localsitemanager', 'Missing', 'Products.BTreeFolder2', - 'Products.GenericSetup', 'Products.MailHost', 'Products.PythonScripts', - 'Products.StandardCacheManagers', 'Products.ZCatalog', 'Record', - 'zope.sendmail', 'Zope' -] -'plone.base' = [ - 'plone.batching', 'plone.registry', 'plone.schema','plone.z3cform', - 'Products.CMFCore', 'Products.CMFDynamicViewFTI', -] -python-dateutil = ['dateutil'] -pytest-plone = ['pytest', 'plone.testing', 'plone.app.testing'] -ignore-packages = ['plone.app.iterate', 'plone.app.upgrade', 'plone.volto', 'zestreleaser.towncrier', 'zest.releaser', 'pytest-cov'] - - -[tool.check-manifest] -ignore = [ - ".editorconfig", - ".flake8", - ".meta.toml", - "dependabot.yml", - "mx.ini", - "tox.ini", - -] diff --git a/backend/tox.ini b/backend/tox.ini deleted file mode 100644 index aee4db9..0000000 --- a/backend/tox.ini +++ /dev/null @@ -1,141 +0,0 @@ -# Generated from: -# https://github.com/plone/meta/tree/master/config/default -# See the inline comments on how to expand/tweak this configuration file -[tox] -# We need 4.4.0 for constrain_package_deps. -min_version = 4.4.0 -envlist = - lint - test - dependencies - - -## -# Add extra configuration options in .meta.toml: -# [tox] -# envlist_lines = """ -# my_other_environment -# """ -# config_lines = """ -# my_extra_top_level_tox_configuration_lines -# """ -## - -[testenv] -skip_install = true -allowlist_externals = - echo - false -# Make sure typos like `tox -e formaat` are caught instead of silently doing nothing. -# See https://github.com/tox-dev/tox/issues/2858. -commands = - echo "Unrecognized environment name {envname}" - false - -[testenv:init] -description = Prepare environment -skip_install = true -deps = - mxdev -commands = - mxdev -c mx.ini - echo "Initial setup for mxdev" - - -[testenv:dependencies-graph] -description = generate a graph out of the dependencies of the package -skip_install = false -allowlist_externals = - sh -setenv = - PIP_CONSTRAINT=constraints-mxdev.txt -deps = - pipdeptree==2.5.1 - graphviz # optional dependency of pipdeptree -commands = - sh -c 'pipdeptree --exclude setuptools,wheel,pipdeptree,zope.interface,zope.component --graph-output svg > dependencies.svg' - -[testenv:test] -description = run the distribution tests -use_develop = true -skip_install = false -constrain_package_deps = true -set_env = - ROBOT_BROWSER=headlesschrome - -deps = - -r requirements-mxdev.txt - -commands = - pytest --disable-warnings {posargs} {toxinidir}/tests -extras = - test - - -[testenv:coverage] -description = get a test coverage report -use_develop = true -skip_install = false -constrain_package_deps = true -set_env = - ROBOT_BROWSER=headlesschrome - -## -# Specify extra test environment variables in .meta.toml: -# [tox] -# test_environment_variables = """ -# PIP_EXTRA_INDEX_URL=https://my-pypi.my-server.com/ -# """ -# -# Set constrain_package_deps .meta.toml: -# [tox] -# constrain_package_deps = "false" -## -deps = - coverage - -c constraints.txt - -commands = - coverage run --source plone.distribution -m pytest {posargs} --disable-warnings {toxinidir}/tests - coverage report -m --format markdown - coverage xml -extras = - test - - -[testenv:circular] -description = ensure there are no cyclic dependencies -use_develop = true -skip_install = false -set_env = - -## -# Specify extra test environment variables in .meta.toml: -# [tox] -# test_environment_variables = """ -# PIP_EXTRA_INDEX_URL=https://my-pypi.my-server.com/ -# """ -## -allowlist_externals = - sh -deps = - pipdeptree - pipforester - -c https://dist.plone.org/release/6.0-dev/constraints.txt - -commands = - # Generate the full dependency tree - sh -c 'pipdeptree -j > forest.json' - # Generate a DOT graph with the circular dependencies, if any - pipforester -i forest.json -o forest.dot --cycles - # Report if there are any circular dependencies, i.e. error if there are any - pipforester -i forest.json --check-cycles -o /dev/null - - -## -# Add extra configuration options in .meta.toml: -# [tox] -# extra_lines = """ -# _your own configuration lines_ -# """ -## diff --git a/frontend/.eslintrc.js b/frontend/.eslintrc.js new file mode 100644 index 0000000..e9b8316 --- /dev/null +++ b/frontend/.eslintrc.js @@ -0,0 +1,30 @@ +const fs = require('fs'); +const projectRootPath = __dirname; + +let coreLocation; +if (fs.existsSync(`${projectRootPath}/core`)) + coreLocation = `${projectRootPath}/core`; +else if (fs.existsSync(`${projectRootPath}/../../core`)) + coreLocation = `${projectRootPath}/../../core`; + +module.exports = { + extends: `${coreLocation}/packages/volto/.eslintrc`, + rules: { + 'import/no-unresolved': 1, + }, + settings: { + 'import/resolver': { + alias: { + map: [ + ['@plone/volto', `${coreLocation}/packages/volto/src`], + [ + '@plone/volto-slate', + `${coreLocation}/core/packages/volto-slate/src`, + ], + ['@plone/registry', `${coreLocation}/packages/registry/src`], + ], + extensions: ['.js', '.jsx', '.ts', '.tsx', '.json'], + }, + }, + }, +}; diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..d8a17e0 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,13 @@ +.*project +.settings/ +.vscode +*~ +acceptance/cypress/videos/ +acceptance/node_modules +.storybook-build +build +core +node_modules +addons +results +pnpm-lock.yaml diff --git a/frontend/.npmignore b/frontend/.npmignore new file mode 100644 index 0000000..eb6fa94 --- /dev/null +++ b/frontend/.npmignore @@ -0,0 +1,16 @@ +.vscode/ +.history +logs +*.log +npm-debug.log* +.DS_Store +*.swp +yarn-error.log + +node_modules +dockerfiles +acceptance +build +dist +yarn.lock +.storybook diff --git a/frontend/.npmrc b/frontend/.npmrc new file mode 100644 index 0000000..71c6843 --- /dev/null +++ b/frontend/.npmrc @@ -0,0 +1,6 @@ +public-hoist-pattern[]=*eslint* +public-hoist-pattern[]=*prettier* +public-hoist-pattern[]=*stylelint* +public-hoist-pattern[]=*cypress* +public-hoist-pattern[]=*process* +public-hoist-pattern[]=*parcel* diff --git a/frontend/.pre-commit-config.yaml b/frontend/.pre-commit-config.yaml new file mode 100644 index 0000000..e09f255 --- /dev/null +++ b/frontend/.pre-commit-config.yaml @@ -0,0 +1,7 @@ +repos: + - repo: https://github.com/pre-commit/mirrors-eslint + rev: 'v9.2.0' + hooks: + - id: eslint + files: '^packages/.*/src/.*/*.(js,jsx,ts,tsx)$' + types: [file] diff --git a/frontend/.prettierignore b/frontend/.prettierignore new file mode 100644 index 0000000..16f05c7 --- /dev/null +++ b/frontend/.prettierignore @@ -0,0 +1,3 @@ +.storybook +CHANGELOG.md +README.md diff --git a/frontend/.prettierrc b/frontend/.prettierrc new file mode 100644 index 0000000..c56f626 --- /dev/null +++ b/frontend/.prettierrc @@ -0,0 +1,12 @@ +{ + "trailingComma": "all", + "singleQuote": true, + "overrides": [ + { + "files": "*.overrides", + "options": { + "parser": "less" + } + } + ] + } diff --git a/frontend/.storybook/main.js b/frontend/.storybook/main.js new file mode 100644 index 0000000..1b15d3b --- /dev/null +++ b/frontend/.storybook/main.js @@ -0,0 +1,188 @@ +const webpack = require('webpack'); +const fs = require('fs'); +const path = require('path'); + +const projectRootPath = path.resolve('.'); +const lessPlugin = require('@plone/volto/webpack-plugins/webpack-less-plugin'); +const scssPlugin = require('razzle-plugin-scss'); + +const createConfig = require('razzle/config/createConfigAsync.js'); +const razzleConfig = require(path.join(projectRootPath, 'razzle.config.js')); + +const SVGLOADER = { + test: /icons\/.*\.svg$/, + use: [ + { + loader: 'svg-loader', + }, + { + loader: 'svgo-loader', + options: { + plugins: [ + { + name: 'preset-default', + params: { + overrides: { + convertPathData: false, + removeViewBox: false, + }, + }, + }, + 'removeTitle', + 'removeUselessStrokeAndFill', + ], + }, + }, + ], +}; + +const defaultRazzleOptions = { + verbose: false, + debug: {}, + buildType: 'iso', + cssPrefix: 'static/css', + jsPrefix: 'static/js', + enableSourceMaps: true, + enableReactRefresh: true, + enableTargetBabelrc: false, + enableBabelCache: true, + forceRuntimeEnvVars: [], + mediaPrefix: 'static/media', + staticCssInDev: false, + emitOnErrors: false, + disableWebpackbar: false, + browserslist: [ + '>1%', + 'last 4 versions', + 'Firefox ESR', + 'not ie 11', + 'not dead', + ], +}; + +module.exports = { + stories: [ + '../packages/**/*.mdx', + '../packages/**/*.stories.@(js|jsx|ts|tsx)', + ], + addons: [ + '@storybook/addon-links', + '@storybook/addon-essentials', + '@storybook/addon-webpack5-compiler-babel', + ], + framework: { + name: '@storybook/react-webpack5', + options: { builder: { useSWC: true } }, + }, + typescript: { + check: false, + checkOptions: {}, + reactDocgen: 'react-docgen-typescript', + reactDocgenTypescriptOptions: { + compilerOptions: { + allowSyntheticDefaultImports: false, + esModuleInterop: false, + }, + propFilter: () => true, + }, + }, + webpackFinal: async (config, { configType }) => { + // `configType` has a value of 'DEVELOPMENT' or 'PRODUCTION' + // You can change the configuration based on that. + // 'PRODUCTION' is used when building the static version of storybook. + + // Make whatever fine-grained changes you need + let baseConfig; + baseConfig = await createConfig( + 'web', + 'dev', + { + // clearConsole: false, + modifyWebpackConfig: razzleConfig.modifyWebpackConfig, + plugins: razzleConfig.plugins, + }, + webpack, + false, + undefined, + [], + defaultRazzleOptions, + ); + const AddonConfigurationRegistry = require('@plone/registry/src/addon-registry'); + + const registry = new AddonConfigurationRegistry(projectRootPath); + + config = lessPlugin({ registry }).modifyWebpackConfig({ + env: { target: 'web', dev: 'dev' }, + webpackConfig: config, + webpackObject: webpack, + options: {}, + }); + + config = scssPlugin.modifyWebpackConfig({ + env: { target: 'web', dev: 'dev' }, + webpackConfig: config, + webpackObject: webpack, + options: { razzleOptions: {} }, + }); + + // Put the SVG loader on top and prevent the asset/resource rule + // from processing the app's SVGs + config.module.rules.unshift(SVGLOADER); + const fileLoaderRule = config.module.rules.find((rule) => + rule.test.test('.svg'), + ); + fileLoaderRule.exclude = /icons\/.*\.svg$/; + + config.plugins.unshift( + new webpack.DefinePlugin({ + __DEVELOPMENT__: true, + __CLIENT__: true, + __SERVER__: false, + }), + ); + + const resultConfig = { + ...config, + resolve: { + ...config.resolve, + alias: { ...config.resolve.alias, ...baseConfig.resolve.alias }, + fallback: { ...config.resolve.fallback, zlib: false }, + }, + }; + + // Add-ons have to be loaded with babel + const addonPaths = registry + .getAddons() + .map((addon) => fs.realpathSync(addon.modulePath)); + + resultConfig.module.rules[13].exclude = (input) => + // exclude every input from node_modules except from @plone/volto + /node_modules\/(?!(@plone\/volto)\/)/.test(input) && + // Storybook default exclusions + /storybook-config-entry\.js$/.test(input) && + /storybook-stories\.js$/.test(input) && + // If input is in an addon, DON'T exclude it + !addonPaths.some((p) => input.includes(p)); + + resultConfig.module.rules[13].include = [ + /preview\.jsx/, + ...resultConfig.module.rules[13].include, + ...addonPaths, + ]; + + const addonExtenders = registry.getAddonExtenders().map((m) => require(m)); + + const extendedConfig = addonExtenders.reduce( + (acc, extender) => + extender.modify(acc, { target: 'web', dev: 'dev' }, config), + resultConfig, + ); + + // Note: we don't actually support razzle plugins, which are also a feature + // of the razzle.extend.js addons file. Those features are probably + // provided in a different manner by Storybook plugins (for example scss + // loaders). + + return extendedConfig; + }, +}; diff --git a/frontend/.storybook/preview.jsx b/frontend/.storybook/preview.jsx new file mode 100644 index 0000000..1d2ac84 --- /dev/null +++ b/frontend/.storybook/preview.jsx @@ -0,0 +1,26 @@ +import '@plone/volto/config'; // This is the bootstrap for the global config - client side +import React from 'react'; +import { StaticRouter } from 'react-router-dom'; +import { IntlProvider } from 'react-intl'; +import enMessages from '@root/../locales/en.json'; + +import '@root/theme'; + +export const parameters = { + controls: { + matchers: { + color: /(background|color)$/i, + date: /Date$/, + }, + }, +}; + +export const decorators = [ + (Story) => ( + + + + + + ), +]; diff --git a/frontend/.stylelintrc b/frontend/.stylelintrc new file mode 100644 index 0000000..09d4a27 --- /dev/null +++ b/frontend/.stylelintrc @@ -0,0 +1,32 @@ +{ + "extends": [ + "stylelint-config-idiomatic-order" + ], + "plugins": [ + "stylelint-prettier" + ], + "overrides": [ + { + "files": [ + "**/*.less" + ], + "customSyntax": "postcss-less" + }, + { + "files": [ + "**/*.overrides" + ], + "customSyntax": "postcss-less" + }, + { + "files": [ + "**/*.scss" + ], + "customSyntax": "postcss-scss" + } + ], + "rules": { + "prettier/prettier": true, + "order/properties-alphabetical-order": null + } +} diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..cc958ac --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,49 @@ +FROM node:20-slim + +LABEL maintainer="Jens Klein " \ + org.label-schema.name="mx-plone-image-frontend" \ + org.label-schema.description="BlueDynamics MX Plone Image Frontend" \ + org.label-schema.vendor="BlueDynamics Alliance" + +ARG CI_JOB_TOKEN + +# prepare and cleanup +RUN \ + useradd --system -m -d /app -U -u 500 plone &&\ + apt-get update &&\ + buildDeps="busybox wget git ca-certificates make" &&\ + apt-get install -y --no-install-recommends $buildDeps &&\ + busybox --install -s &&\ + apt-get -y clean &&\ + corepack enable + +USER plone + +# Add local code +COPY --chown=plone:plone ./frontend /app +WORKDIR /app + +# Install and build application +RUN \ + corepack install &&\ + pnpm dlx mrs-developer missdev --hard --no-config --fetch-https &&\ + pnpm install &&\ + pnpm build &&\ + echo "Y" | pnpm prune --prod --no-optional &&\ + rm -rf .cache .local/share/pnpm/store &&\ + find . -name .git -exec rm -rfv {} + + +# Expose default Express port +EXPOSE 3000 + +# Set healthcheck to port 3000 +HEALTHCHECK --interval=10s --timeout=5s --start-period=30s CMD [ -n "$LISTEN_PORT" ] || LISTEN_PORT=3000 ; wget -q http://127.0.0.1:"$LISTEN_PORT" -O - || exit 1 + +# disables health check if uncommented +# HEALTHCHECK --interval=10s --timeout=5s --start-period=60s CMD exit 0 + +# Entrypoint would be pnpm +ENTRYPOINT [ "pnpm" ] + +# And the image will run in production mode +CMD ["start:prod"] \ No newline at end of file diff --git a/frontend/Dockerfile.dockerignore b/frontend/Dockerfile.dockerignore new file mode 100644 index 0000000..5d943e7 --- /dev/null +++ b/frontend/Dockerfile.dockerignore @@ -0,0 +1,10 @@ +frontend/.storybook +frontend/*.log +frontend/**/node_modules +frontend/addons +frontend/build +frontend/cache +frontend/core +frontend/cypress +frontend/node_modules +Makefile diff --git a/frontend/Makefile b/frontend/Makefile new file mode 100644 index 0000000..64e61de --- /dev/null +++ b/frontend/Makefile @@ -0,0 +1,125 @@ +### Defensive settings for make: +# https://tech.davis-hansson.com/p/make/ +SHELL:=bash +.ONESHELL: +# for Makefile debugging purposes add -x to the .SHELLFLAGS +.SHELLFLAGS:=-eu -o pipefail -O inherit_errexit -c +.SILENT: +.DELETE_ON_ERROR: +MAKEFLAGS+=--warn-undefined-variables +MAKEFLAGS+=--no-builtin-rules + +CURRENT_DIR:=$(shell dirname $(realpath $(lastword $(MAKEFILE_LIST)))) + +# Recipe snippets for reuse + +# 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` + +PLONE_VERSION=6 +DOCKER_IMAGE=plone/server-dev:${PLONE_VERSION} +DOCKER_IMAGE_ACCEPTANCE=plone/server-acceptance:${PLONE_VERSION} + +ADDON_NAME='myproject-frontend' +IMAGE_NAME=mximages-plone-frontend-local +IMAGE_TAG?=latest +VOLTO_VERSION = $(shell cat ./mrs.developer.json | python -c "import sys, json; print(json.load(sys.stdin)['core']['tag'])") + +.PHONY: help +help: ## Show this help + @echo -e "$$(grep -hE '^\S+:.*##' $(MAKEFILE_LIST) | sed -e 's/:.*##\s*/:/' -e 's/^\(.\+\):\(.*\)/\\x1b[36m\1\\x1b[m:\2/' | column -c2 -t -s :)" + +# Dev Helpers + +.PHONY: install +install: ## Installs the add-on in a development environment + pnpm dlx mrs-developer missdev --no-config --fetch-https + pnpm i + +.PHONY: start +start: ## Starts Volto, allowing reloading of the add-on during development + pnpm start + +.PHONY: build +build: ## Build a production bundle for distribution of the project with the add-on + pnpm build + +.PHONY: i18n +i18n: ## Sync i18n + pnpm --filter $(ADDON_NAME) i18n + +.PHONY: ci-i18n +ci-i18n: ## Check if i18n is not synced + pnpm --filter $(ADDON_NAME) i18n && git diff -G'^[^\"POT]' --exit-code + +.PHONY: format +format: ## Format codebase + pnpm lint:fix + pnpm prettier:fix + pnpm stylelint:fix + +.PHONY: lint +lint: ## Lint, or catch and remove problems, in code base + pnpm lint + pnpm prettier + pnpm stylelint --allow-empty-input + +.PHONY: test +test: ## Run unit tests + pnpm test + +.PHONY: ci-test +ci-test: ## Run unit tests in CI + CI=1 RAZZLE_JEST_CONFIG=$(CURRENT_DIR)/jest-addon.config.js pnpm --filter @plone/volto test -- --passWithNoTests + +.PHONY: backend-docker-start +backend-docker-start: ## Starts a Docker-based backend for development + @echo "$(GREEN)==> Start Docker-based Plone Backend$(RESET)" + docker run -it --rm --name=backend -p 8080:8080 -e SITE=Plone $(DOCKER_IMAGE) + +## 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 Storybook + @echo "$(GREEN)==> Build Storybook$(RESET)" + mkdir -p $(CURRENT_DIR)/.storybook-build + pnpm run build-storybook -o $(CURRENT_DIR)/.storybook-build + +## Acceptance +.PHONY: acceptance-frontend-dev-start +acceptance-frontend-dev-start: ## Start acceptance frontend in development mode + RAZZLE_API_PATH=http://127.0.0.1:55001/plone pnpm start + +.PHONY: acceptance-frontend-prod-start +acceptance-frontend-prod-start: ## Start acceptance frontend in production mode + RAZZLE_API_PATH=http://127.0.0.1:55001/plone pnpm build && pnpm start:prod + +.PHONY: acceptance-backend-start +acceptance-backend-start: ## Start backend acceptance server + docker run -it --rm -p 55001:55001 $(DOCKER_IMAGE_ACCEPTANCE) + +.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) + +.PHONY: acceptance-test +acceptance-test: ## Start Cypress in interactive mode + pnpm --filter @plone/volto exec cypress open --config-file $(CURRENT_DIR)/cypress.config.js --config specPattern=$(CURRENT_DIR)'/cypress/tests/**/*.{js,jsx,ts,tsx}' + +.PHONY: ci-acceptance-test +ci-acceptance-test: ## Run cypress tests in headless mode for CI + pnpm --filter @plone/volto exec cypress run --config-file $(CURRENT_DIR)/cypress.config.js --config specPattern=$(CURRENT_DIR)'/cypress/tests/**/*.{js,jsx,ts,tsx}' + +.PHONY: build-image +build-image: ## Build Docker Image + @cd .. + @DOCKER_BUILDKIT=1 docker build . -t $(IMAGE_NAME):$(IMAGE_TAG) -f frontend/Dockerfile --build-arg VOLTO_VERSION=$(VOLTO_VERSION) --progress=plain + @cd - diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..57e7500 --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,157 @@ +# myproject (myproject-frontend) + +A project using Plone 6. + +## Features + + + +## Installation + + +Add `myproject-frontend` to your `package.json`: + +```json +"dependencies": { + "myproject-frontend": "*" +} +``` + +Add `myproject-frontend` to your `volto.config.js`: + +```javascript +const addons = ['myproject-frontend']; +``` + +If this package provides a Volto theme, and you want to activate it, then add the following to your `volto.config.js`: + +```javascript +const theme = 'myproject-frontend'; +``` + +## Test installation + +Visit http://localhost:3000/ in a browser, login, and check the awesome new features. + + +## Development + +The development of this add-on is done in isolation using a new approach using pnpm workspaces and latest `mrs-developer` and other Volto core improvements. +For this reason, it only works with pnpm and Volto 18 (currently in alpha). + + +### Pre-requisites + +- [Node.js](https://6.docs.plone.org/install/create-project.html#node-js) +- [Make](https://6.docs.plone.org/install/create-project.html#make) +- [Docker](https://6.docs.plone.org/install/create-project.html#docker) + + +### Make convenience commands + +Run `make help` to list the available commands. + +```text +help Show this help +install Installs the add-on in a development environment +start Starts Volto, allowing reloading of the add-on during development +build Build a production bundle for distribution of the project with the add-on +i18n Sync i18n +ci-i18n Check if i18n is not synced +format Format codebase +lint Lint, or catch and remove problems, in code base +test Run unit tests +ci-test Run unit tests in CI +backend-docker-start Starts a Docker-based backend for development +storybook-start Start Storybook server on port 6006 +storybook-build Build Storybook +acceptance-frontend-dev-start Start acceptance frontend in development mode +acceptance-frontend-prod-start Start acceptance frontend in production mode +acceptance-test Start Cypress in interactive mode +ci-acceptance-test Run cypress tests in headless mode for CI +build-image Build Docker Image +``` + +### Development environment set up + +Install package requirements. + +```shell +make install +``` + +### Start developing + +Start the backend. + +```shell +make backend-start-docker +``` + +In a separate terminal session, start the frontend. + +```shell +pnpm start +``` + +### Lint code + +Run ESlint, Prettier, and Stylelint in analyze mode. + +```shell +make lint +``` + +### Format code + +Run ESlint, Prettier, and Stylelint in fix mode. + +```shell +make format +``` + +### i18n + +Extract the i18n messages to locales. + +```shell +make i18n +``` + +### Unit tests + +Run unit tests. + +```shell +make test +``` + +### Run Cypress tests + +Run each of these steps in separate terminal sessions. + +In the first session, start the frontend in development mode. + +```shell +make start-test-acceptance-frontend-dev +``` + +In the second session, start the backend acceptance server. + +```shell +make start-test-acceptance-server +``` + +In the third session, start the Cypress interactive test runner. + +```shell +make test-acceptance +``` + +## License + +The project is licensed under the MIT license. + +## Credits and Acknowledgements 🙏 + +Crafted with care by **This was generated by [cookiecutter-volto](https://github.com/plone/cookiecutter-volto/frontend_addon) on 2024-05-30 12:01:51**. A special thanks to all contributors and supporters! diff --git a/frontend/cypress.config.js b/frontend/cypress.config.js new file mode 100644 index 0000000..dba4b58 --- /dev/null +++ b/frontend/cypress.config.js @@ -0,0 +1,13 @@ +const { defineConfig } = require('cypress'); + +module.exports = defineConfig({ + viewportWidth: 1280, + viewportHeight: 1280, + retries: { + runMode: 3, + }, + e2e: { + baseUrl: 'http://localhost:3000', + specPattern: 'cypress/tests/**/*.cy.{js,jsx,ts,tsx}', + }, +}); diff --git a/frontend/cypress/.gitkeep b/frontend/cypress/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/frontend/cypress/support/commands.js b/frontend/cypress/support/commands.js new file mode 100644 index 0000000..6a44064 --- /dev/null +++ b/frontend/cypress/support/commands.js @@ -0,0 +1 @@ +import '@plone/volto/cypress/add-commands'; diff --git a/frontend/cypress/support/e2e.js b/frontend/cypress/support/e2e.js new file mode 100644 index 0000000..4ff23d6 --- /dev/null +++ b/frontend/cypress/support/e2e.js @@ -0,0 +1,15 @@ +import 'cypress-axe'; +import 'cypress-file-upload'; +import './commands'; +import 'cypress-axe'; +import { setup, teardown } from '@plone/volto/cypress/support/reset-fixture'; + +beforeEach(function () { + cy.log('Setting up API fixture'); + setup(); +}); + +afterEach(function () { + cy.log('Tearing down API fixture'); + teardown(); +}); diff --git a/frontend/cypress/tests/.gitkeep b/frontend/cypress/tests/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/frontend/cypress/tests/example.cy.js b/frontend/cypress/tests/example.cy.js new file mode 100644 index 0000000..f3a1fad --- /dev/null +++ b/frontend/cypress/tests/example.cy.js @@ -0,0 +1,20 @@ +context('Example Acceptance Tests', () => { + describe('Visit a page', () => { + beforeEach(() => { + // Given a logged in editor + cy.viewport('macbook-16'); + cy.createContent({ + contentType: 'Document', + contentId: 'document', + contentTitle: 'Test document', + }); + cy.autologin(); + }); + + it('As editor I can add edit a Page', function () { + cy.visit('/document'); + cy.navigate('/document/edit'); + cy.get('#toolbar-save').click(); + }); + }); +}); diff --git a/frontend/jest-addon.config.js b/frontend/jest-addon.config.js new file mode 100644 index 0000000..984c267 --- /dev/null +++ b/frontend/jest-addon.config.js @@ -0,0 +1,17 @@ +module.exports = { + roots: ['../../../packages'], + testMatch: ['/../../../../**/?(*.)+(spec|test).[jt]s?(x)'], + collectCoverageFrom: [ + 'src/addons/**/src/**/*.{js,jsx,ts,tsx}', + '!src/**/*.d.ts', + ], + transformIgnorePatterns: ['node_modules/(?!(volto-slate|@plone/volto)/)'], + coverageThreshold: { + global: { + branches: 5, + functions: 5, + lines: 5, + statements: 5, + }, + }, +}; diff --git a/frontend/mrs.developer.json b/frontend/mrs.developer.json new file mode 100644 index 0000000..e3e6446 --- /dev/null +++ b/frontend/mrs.developer.json @@ -0,0 +1,9 @@ +{ + "core": { + "output": "./", + "package": "@plone/volto", + "url": "git@github.com:plone/volto.git", + "https": "https://github.com/plone/volto.git", + "tag": "18.0.0-alpha.41" + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..ddfa2b9 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,43 @@ +{ + "name": "mximage-frontend-dev", + "version": "1.0.0-alpha.0", + "description": "A new project using Plone 6.", + "author": "BlueDynmaics Alliance", + "homepage": "https://github.com/bluedynamics/mximages-plone/tree/main/frontend", + "license": "MIT", + "keywords": [ + "volto-addon", + "volto", + "plone", + "react" + ], + "scripts": { + "preinstall": "npx only-allow pnpm", + "start": "pnpm build:deps && VOLTOCONFIG=$(pwd)/volto.config.js pnpm --filter @plone/volto start", + "start:prod": "pnpm --filter @plone/volto start:prod", + "build": "pnpm build:deps && VOLTOCONFIG=$(pwd)/volto.config.js pnpm --filter @plone/volto build", + "build:deps": "pnpm --filter @plone/registry --filter @plone/components build", + "i18n": "pnpm --filter myproject-frontend i18n", + "test": "RAZZLE_JEST_CONFIG=$(pwd)/jest-addon.config.js pnpm --filter @plone/volto test -- --passWithNoTests", + "lint": "eslint --max-warnings=0 'packages/**/src/**/*.{js,jsx,ts,tsx}'", + "lint:fix": "eslint --fix 'packages/**/src/**/*.{js,jsx,ts,tsx}'", + "prettier": "prettier --check 'packages/**/src/**/*.{js,jsx,ts,tsx}'", + "prettier:fix": "prettier --write 'packages/**/src/**/*.{js,jsx,ts,tsx}' ", + "stylelint": "stylelint 'packages/**/src/**/*.{css,scss,less}' --allow-empty-input", + "stylelint:fix": "stylelint 'packages/**/src/**/*.{css,scss,less}' --fix --allow-empty-input", + "dry-release": "pnpm --filter myproject-frontend dry-release", + "release": "pnpm --filter myproject-frontend release", + "release-major-alpha": "pnpm --filter myproject-frontend release-major-alpha", + "release-alpha": "pnpm --filter myproject-frontend release-alpha", + "storybook": "pnpm build:deps && VOLTOCONFIG=$(pwd)/volto.config.js pnpm --filter @plone/volto storybook dev -p 6006 -c $(pwd)/.storybook", + "build-storybook": "pnpm build:deps && VOLTOCONFIG=$(pwd)/volto.config.js pnpm --filter @plone/volto build-storybook -c $(pwd)/.storybook" + }, + "dependencies": { + "@plone/registry": "workspace:*", + "@plone/volto": "workspace:*" + }, + "devDependencies": { + "mrs-developer": "^2.2.0" + }, + "packageManager": "pnpm@9.4.0" +} diff --git a/frontend/pnpm-workspace.yaml b/frontend/pnpm-workspace.yaml new file mode 100644 index 0000000..f9c0485 --- /dev/null +++ b/frontend/pnpm-workspace.yaml @@ -0,0 +1,4 @@ +packages: + # all packages in direct subdirs of packages/ + - 'core/packages/*' + - 'packages/*' diff --git a/frontend/public/android-chrome-192x192.png b/frontend/public/android-chrome-192x192.png new file mode 100644 index 0000000..5a31bad Binary files /dev/null and b/frontend/public/android-chrome-192x192.png differ diff --git a/frontend/public/android-chrome-512x512.png b/frontend/public/android-chrome-512x512.png new file mode 100644 index 0000000..b522d84 Binary files /dev/null and b/frontend/public/android-chrome-512x512.png differ diff --git a/frontend/public/apple-touch-icon.png b/frontend/public/apple-touch-icon.png new file mode 100644 index 0000000..f0a5f8b Binary files /dev/null and b/frontend/public/apple-touch-icon.png differ diff --git a/frontend/public/configuration.png b/frontend/public/configuration.png new file mode 100644 index 0000000..91513c7 Binary files /dev/null and b/frontend/public/configuration.png differ diff --git a/frontend/public/elasticsearch_nested_field.png b/frontend/public/elasticsearch_nested_field.png new file mode 100644 index 0000000..3a2568c Binary files /dev/null and b/frontend/public/elasticsearch_nested_field.png differ diff --git a/frontend/public/favicon-16x16.png b/frontend/public/favicon-16x16.png new file mode 100644 index 0000000..bf6cf15 Binary files /dev/null and b/frontend/public/favicon-16x16.png differ diff --git a/frontend/public/favicon-32x32.png b/frontend/public/favicon-32x32.png new file mode 100644 index 0000000..0411131 Binary files /dev/null and b/frontend/public/favicon-32x32.png differ diff --git a/frontend/public/favicon.ico b/frontend/public/favicon.ico new file mode 100644 index 0000000..b9c0d0c Binary files /dev/null and b/frontend/public/favicon.ico differ diff --git a/frontend/public/icon.svg b/frontend/public/icon.svg new file mode 100644 index 0000000..72640f0 --- /dev/null +++ b/frontend/public/icon.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/frontend/public/index.html.spa b/frontend/public/index.html.spa new file mode 100644 index 0000000..27e655d --- /dev/null +++ b/frontend/public/index.html.spa @@ -0,0 +1,34 @@ + + + + + + + + + React App + + + + + +
+