plone / volto

React-based frontend for the Plone Content Management System
https://demo.plone.org/
MIT License
426 stars 576 forks source link

The Big List of Small Volto Rules/Tips #2810

Open tiberiuichim opened 2 years ago

tiberiuichim commented 2 years ago

I think we need a big list of small volto rules. Things that only need one line, just the rule. These are very much context dependent rules, maybe not known, obscure, whatever. Let's try to document them here, maybe? And then we can better organize our docs.

Edit: this turned into a FAQ sort of thing. Oh well...

tiberiuichim commented 2 years ago

A field in a client-side schema can have a default property and it would represent the initial value in the rendered schema.

For block defaults to work properly, you need to pass the onChangeBlock, here's the most important props: <BlockDataForm block={block} schema={schema} onChangeBlock={onChangeBlock} />

tiberiuichim commented 2 years ago

When you write a "blocks in a single block" type of thing, make sure to properly pass selected as false (or true) to the inner blocks to avoid focusing problems. You'll probably also want to set blockHasOwnFocusManagement to true.

tiberiuichim commented 2 years ago

Volto's Pastanaga theme doesn't include all SemanticUI icons because it loads the fonts from the "basic" semantic-ui theme, not the "default" semantic-ui theme. You have to copy those into your theme or set @icon to default in theme.config.

tiberiuichim commented 2 years ago

If you see a console warning from a component and you don't know which is that component, use the React Developer Tools browser extension, the Components tab, as it has quick links at the top to the components with warnings.

tiberiuichim commented 2 years ago

Every time you add a new customized file you have to restart Volto.

tiberiuichim commented 2 years ago

getContent(getBaseUrl(item['@id'])) won't be ok with Volto 14 ++api++ calls. It needs to be getContent(flattenToAppURL(getBaseUrl(item['@id'])))

tiberiuichim commented 2 years ago

Q: Can I use another component library with Volto?

A: Yes! React integration is pretty straight forward, the CSS/styling part is probably less straightforward, but there's no issues there. You could be thinking of using a completely separate set of components + styling for the public site (and your blocks), while Volto uses the semantic-ui-react component for the CMS part (editing interface, block edit wrappers, sidebars, etc).

tiberiuichim commented 2 years ago

The key param to a React component can be used even outside somelist.map() iterations, as a "cache busting" , in case the particular component needs to be recreated, if it keeps some internal state and refuses to respect outside props changing. For example:

<Dropdown defaultValue={x} options={y} />

If x changes, the semantic-ui Dropdown won't acknowledge it, so you can force the recreation of the dropdown with:

<Dropdown key={x} defaultValue={x} options={y} />
sneridagh commented 2 years ago

getContent(getBaseUrl(item['@id'])) won't be ok with Volto 14 ++api++ calls. It needs to be getContent(flattenToAppURL(getBaseUrl(item['@id'])))

No sure about this one, are you sure? the id's should be "clean" if ++api++ is used, if not, the deployment is wrong.

tiberiuichim commented 2 years ago

If you're doing work on the Volto SSR server, you can restart that server by typing rs<enter> (in the terminal where yarn start is running).

tiberiuichim commented 2 years ago

properties vs metadata. Block edit components get passed these two fields, they seem to be identical, but in the case of the "block inside block", the "metadata = content metadata", while "properties = parent block data".

tiberiuichim commented 2 years ago

injectLazyLibs vs loadable(() => import('SomeComponent')). Both should be used! Straight loadable should be used for components, injectLazyLibs should be used for utility code (libraries) or components that are not exported as default exports. injectlazyLibs implements the following pattern: "is the library loaded? if so, render the wrapped component, otherwise don't render anything".

tiberiuichim commented 2 years ago

You can try a custom version of Volto in a Volto project using yalc. Inside Volto, run yalc publish, inside your Volto project run: yalc add @plone/volto --no-pure. Note that this technique can be used not only for Volto, but for third-party packages as well (if you want to try a package that hasn't been published yet, or to do custom development on it).

tiberiuichim commented 2 years ago

When manually writing a schema to generate a block settings form, be aware that you can add any properties in the form field definition, according to what the FormFieldWrapper or the wrapped widget component receive. For example, you could pass wrapped: false to render a widget in its simplest form, with no widget decorations (no grid, no label, no description, etc).

tiberiuichim commented 2 years ago

Why does Volto use Bearer token-based authentication instead of an authentication cookie? Cookies can't cross domains and, given the proper cors permissions, the json endpoints api path can be set to any website.

kreafox commented 2 years ago

Example to overwrite existing cypress commands to add a delay between commands:

// cypress/support/commands.js

const COMMAND_DELAY = 1000;
for (const command of [
  'visit',
  'click',
  'type',
  'clear',
  'contains',
]) {
  Cypress.Commands.overwrite(command, (originalFn, ...args) => {
    return new Promise((resolve) => {
      setTimeout(() => {
        resolve(originalFn(...args));
      }, COMMAND_DELAY);
    });
  });
}

More information about custom commands and overwriting existing commands: https://on.cypress.io/custom-commands

tiberiuichim commented 2 years ago

How to test a Volto feature branch with a Volto project:

In you local clone of plone/volto clone, checkout the desired feature branch. Then run

yalc publish

Yalc is a global utility, similar to mrs-developer. You have to install it with:

npm install -g yalc

Then, in your Volto project, you can now run:

yalc add @plone/volto --no-pure 

This "installs" the "locally published copy of @plone/volto" in your project. It makes a hard copy of it and it changes package.json, so beware not to push that in your sourcecode repo. Now you can test Volto with your project. Once you're done, you can remove the local copy of volto with

yalc retreat

Notice that the workflow is pretty cumbersome if you want to do development of Volto, as you have to always do the dance of publish+add on every change in your original Volto clone.

tiberiuichim commented 2 years ago

You can debug backend calls done by the SSR nodejs server with DEBUG=superagent yarn start.

tiberiuichim commented 2 years ago

How to test a Volto feature branch with a Volto project, part 2 (unconfirmed):

In mrs-developer.json add your volto branch:

{
  "volto": {
    "url": "git@github.com:plone/volto.git",
    "https": "https://github.com/plone/volto.git",
    "package": "@plone/volto",
    "branch": "slots_quanta_split_relative_path",
    "develop": true,
    "path": ""
  }
}

In the generated jsconfig.json, make sure that the 'src' is not a part of the volto path:

{
    "compilerOptions": {
        "paths": {
            "@plone/volto": [
                "addons/volto"
            ],
...
tiberiuichim commented 1 year ago

When you shadow a Volto module, always check if there's any relative imports such as: https://github.com/plone/volto/blob/caeaaacb724e4846f519cccafca4b984f38ee4d2/src/components/theme/View/SummaryView.jsx#L11

In this case, you need to rewrite the imports, for example:

import { PreviewImage } from '@plone/volto/components';
tiberiuichim commented 1 year ago

If you need to login and switch between cypress dedicated / regular volto, the Volto Login chrome extension is handy: https://github.com/collective/plone6-autologin-extension

tiberiuichim commented 1 year ago

Volto supports multiple customization paths in each addon, you can specify them in the package.json:customizationPaths. It's an array of strings (paths) relative to the root of the package.

tiberiuichim commented 1 year ago

How to debug jest tests:

tiberiuichim commented 1 year ago

Did you know you can use JSX in schema texts? For example:

  properties: {
    use_live_data: {
      type: 'boolean',
      title: 'Use live data',
      default: true,
    },
    hover_format_xy: {
      type: 'string',
      title: 'Hover format',
      placeholder: '',
      description: (
        <>
          See{' '}
          <a
            target="_blank"
            rel="noopener noreferrer"
            href="https://github.com/d3/d3-3.x-api-reference/blob/master/Formatting.md#d3_format"
          >
            D3 format documentation
          </a>
        </>
      ),
    }
tiberiuichim commented 1 year ago

For route-based views, you may want to tweak the breadcrumbs. Here's a way to do this from the useEffect of the route view component (example from a live project, adjust to your needs):

  React.useEffect(() => {
    const handler = async () => {
      if (item) {
        const action = {
          type: 'GET_BREADCRUMBS_SUCCESS',
          result: {
            items: [
              {
                title: 'Datahub',
                '@id': '/en/datahub',
              },
              {
                title: rawTitle,
                '@id': `/en/datahub/view/${docid}`,
              },
            ],
          },
        };
        await dispatch({ type: 'GET_BREADCRUMBS_PENDING' }); // satisfy content load protection
        await dispatch(action);
      }
    };

    handler();
  }, [item, dispatch, docid, rawTitle]);

Edit: this no longer works in Volto 17.

tiberiuichim commented 1 year ago

If the Plone site is not named "Plone" (for example, I have a Plone site that's at http://localhost:8080/cca), I have to start volto with:

env RAZZLE_API_PATH=http://localhost:8080/cca yarn start

# or for production check:
yarn build
env RAZZLE_API_PATH=http://localhost:8080/cca yarn start:prod

To avoid always having to add the env var, I can add a .env.development file inside the Volto project root with the env vars inside it:

RAZZLE_API_PATH=http://localhost:8080/cca
tiberiuichim commented 1 year ago

You can debug devproxy issues withDEBUG_HPM=1

tiberiuichim commented 1 year ago

If you get 404 from the endpoint JSON calls and everything looks fine, you may have an old version of plone.rest, which implements the ++api++ traverser.

nileshgulia1 commented 1 year ago

we can use <UniversalLink/> to render all types of links in a typical volto project.

tiberiuichim commented 1 year ago

If the Plone site is not named "Plone" (for example, I have a Plone site that's at http://localhost:8080/cca), I have to start volto with:

env RAZZLE_API_PATH=http://localhost:8080/cca

To avoid always having to add the env var, I can add a .env.development file inside the Volto project root with the env vars inside it:

RAZZLE_API_PATH=http://localhost:8080/cca

Actually I think this is not really good, as it's not using "seamless mode". RAZZLE_DEV_PROXY_API_PATH should be used.

tiberiuichim commented 1 year ago

You can enable HotReloading for a node_modules library by adding something like this in your razzle.config.js (or razzle.extend.js)

  if (config.devServer) {
    config.devServer.watchOptions.ignored = /node_modules\/(?!(@plone\/volto|@elastic))/g;
  }
tiberiuichim commented 1 year ago

If you have to tweak a schema for a block, it must be done in a schema enhancer. Don't do it in the block edit component, as that makes it difficult to do (future) data validations and block default values.

tiberiuichim commented 1 year ago

This may not be obvious, but if you have code like:

const { facetOptions } = React.useState(getFacetOptions());

getFacetOptions() will be called on every rendering of our component. It's obvious, of course, if you would consider the code as the equivalent:

const { facetOptions } = irelevantFunction(getFacetOptions());
tiberiuichim commented 1 year ago

If you're building docker images, make sure not to have the *.yml in .dockerignore, as the .yarnrc.yml, with its setup needs to be there!

defaultSemverRangePrefix: ""

nodeLinker: node-modules

yarnPath: .yarn/releases/yarn-3.2.3.cjs

If you don't do this, then yarn will use a different strategy, which is incompatible with razzle (it won't find webpack)

Solves errors such as:

Error: razzle tried to access webpack (a peer dependency) but it isn't provided by your application; this makes the require call ambiguous and unsound.
tiberiuichim commented 1 year ago

Here's my procedure for upgrading a Volto project to latest volto (major upgrades):

tiberiuichim commented 1 year ago

If you need to ship an addon with a static file that you have to reference from code, you can use the static file loader. For example:

import dummyPath from './myfile.dummy';

function DummyLink() {
    return <a href={dummyPath}>Dummy</a>
}

The webpack file loader is configured as a default fallback loader, so if you want to reference JS or JSON files, make sure to rename them with a different extension. The result of importing the static file is a path generated by webpack.

tiberiuichim commented 1 year ago

How to change the default language for a website to something other then English, if you have created your Plone site as default English:

If you don't do this, all redirections for new visitors go to the language of the root content item, which is English.

tiberiuichim commented 1 year ago

A question often asked: can I work on X ticket in Volto? The answer is always, yes, but here's more context for it:

One of the core principles in Plone (Volto's backend, and by extension also Volto) is that our software is a do-ocracy. Things will get done only if we start working on them. So, really, there's no task in Volto that's off-limits. There are, though, limits to the things that can be approached, realistically, by new contributors. There's back history, context, understanding of the overall vision, etc, that all come with experience.

Also, most important to read: https://6.dev-docs.plone.org/volto/developer-guidelines/contributing.html

tiberiuichim commented 1 year ago

How to add an emergency user with the new plone-backend docker image. With a running plone-backend container, where the service is called plone, run:

docker compose exec plone ./docker-entrypoint.sh bash
bin/addzopeuser -c etc/zeo.conf myuser mypass

Another method:

docker compose exec plone ./docker-entrypoint.sh console

Inside the debug console, run:

>>> app.acl_users._doAddUser('myuser', 'mypassword', ['Manager'], [])
<PropertiedUser 'myuser'>
>>> import transaction; transaction.commit()

Exit the console with ctrl+d. Make sure you have a return value for _doAddUser, otherwise the user is not created because it exists.

tiberiuichim commented 1 year ago

How to use plone-backend for development purposes. The biggest problem is that it's slow to start when you have DEVELOP addons.

So, to develop an addon with that image, you can rewrite the plone service command to something like (docker-compose.yml):

command: bash
tty: true

Now start a shell to the container with:

docker compose exec plone bash
./docker-entrypoint.sh bash
bin/runwsgi -v etc/zope.ini config_file=zeo.conf

By running the docker-entrypoint.sh bash command, it will start a subshell that has all the required environment variables setup. Now starting the zope wsgi server is very fast, as it doesn't have to do all the pip install commands.

For relstorage,

docker compose exec backend bash
./docker-entrypoint.sh bash
bin/zconsole debug etc/relstorage.conf
tiberiuichim commented 1 year ago

If you need to provide a facet for a custom "field" for the search block, follow this semi-guide: https://github.com/plone/volto/issues/3020#issuecomment-1385542090

tiberiuichim commented 1 year ago

Using nvm helps your Volto development. If your environment doesn't let you install global packages with npm install -g mrs-developer unless you use sudo, it's a sign you're doing something wrong. You should really use nvm or another node version manager.

davisagli commented 1 year ago

Getting settings directly from the config in @plone/volto/registry is convenient, but it makes it hard to override them for a specific context. It's better to make it a prop for your component, and then fall back to getting a default from the config.

tiberiuichim commented 1 year ago

The default html block in Volto is "restricted", by having its HTML content processed by Plone's safe html transform.

You can create an "escape" html block, that's unrestricted, by simply doing something like:

  config.blocks.blocksConfig.plainHtml = {
    ...config.blocks.blocksConfig.html,
    id: 'plainHtml',
  };
  config.blocks.blocksConfig.html.restricted = true;

This is because the block transformer in plone.restapi is restricted to a block with id html.

FarooqAlaulddin commented 1 year ago

API class does not support array parameters see #4349 for details. One work around that worked for me is to serialize/deserialize your object before/after the API. For example, if you passed the following object to API.get:

 {
   searchTerm: 'power',
   page: 1,
   perPage: 10,
   advancedFields: [
    {name: 'field1', value:'val for field 1'},
    {name: 'field2', value:'val for field 2'}
    {name: 'field3', value:'val for field 3'}
   ]
 }

By default there is no support by the API class to handle array parameters advancedFields. But since Plone support [] notation. For my case the following two functions solved my issue:

const serialize = (obj, prefix) => {
    var str = [],
        p;
    for (p in obj) {
        if (obj.hasOwnProperty(p)) {
            var k = prefix ? prefix + "[" + p + "]" : p,
                v = obj[p];
            str.push((v !== null && typeof v === "object") ?
                serialize(v, k) :
                encodeURIComponent(k) + "=" + encodeURIComponent(v));
        }
    }
    return str.join("&");
}

// Incase needed.
const deserialize = (str) => {
    var obj = {};
    var pairs = str.replace("?", "").split("&");
    for (var i = 0; i < pairs.length; i++) {
        var pair = pairs[i];
        var indexOfEqual = pair.indexOf("=");
        var key = decodeURIComponent(pair.substring(0, indexOfEqual));
        var value = decodeURIComponent(pair.substring(indexOfEqual + 1));

        if (!isNaN(value) && value !== "") {
            value = Number(value);
        } else if (value === 'true') {
          value = true;
        } else if (value === 'false') {
          value = false;
        }

        var keys = key.split("[").map((k) => k.replace("]", ""));
        var lastKey = keys.pop();
        var currentObj = obj;
        for (var j = 0; j < keys.length; j++) {
            var innerKey = keys[j];
            if (!currentObj[innerKey]) {
                currentObj[innerKey] = {};
            }
            currentObj = currentObj[innerKey];
        }
        currentObj[lastKey] = value;
    }
    return obj;
}

Both tested for 2 levels.

tiberiuichim commented 1 year ago

If one of your dependencies is shipped as an mjs module, you'll have to get Babel to compile, before it can be included in the Volto bundle. You can do this from any addon with a razzle.extend.js (of course, this can be done from a razzle.config.js as well, but code has to be adjusted):

const path = require('path');
const makeLoaderFinder = require('razzle-dev-utils/makeLoaderFinder');

const modify = (config, { target, dev }, webpack) => {
  const medusaPath = path.dirname(
    require.resolve('@medusajs/medusa-js'),
  );
  const babelLoaderFinder = makeLoaderFinder('babel-loader');
  const babelLoader = config.module.rules.find(babelLoaderFinder);
  const { include } = babelLoader;
  include.push(medusaPath);

  return config;
};

module.exports = {
  plugins: (plugs) => plugs,
  modify,
};
tiberiuichim commented 1 year ago

traefik configuration:

  routers:
    frontend:
      rule: "Host(`localhost`)"
      service: frontend
    backend:
      rule: "Host(`localhost`) && PathPrefix(`/++api++`)"
      service: backend
      middlewares:
        - backend

  middlewares:
    backend:
      replacePathRegex:
        regex: "^/\\+\\+api\\+\\+($|/.*)"
        replacement: "/VirtualHostBase/http/localhost/plone/++api++/VirtualHostRoot$1"

  services:
    frontend:
      loadBalancer:
        servers:
          - url: "http://host.docker.internal:3000"
    backend:
      loadBalancer:
        servers:
          - url: "http://host.docker.internal:55001"

Also https://github.com/plone/volto/blob/33962f130f25a92760848c07ab59bcd13a2ef37d/docker-compose.yml#L14

tiberiuichim commented 1 year ago

When using Apache as frontend proxy, I had to set to allow seamless mode to work properly. Maybe relevant, my frontend (Volto) was running in its own Docker container. See docs https://httpd.apache.org/docs/2.2/mod/mod_proxy.html#proxypreservehost

ProxyPreserveHost On
tiberiuichim commented 1 year ago

To use traefik as a backend for development, you need to add in your local .env.development:

RAZZLE_DEV_PROXY_API_PATH=http://plone.docker.localhost:8090/Plone
RAZZLE_DEV_PROXY_INSECURE=true
tiberiuichim commented 6 months ago

When you shadow a Volto file, you can minimize the impact by doing something like this (where we shadow the helpers/Blocks/Blocks.js module:

import * as original from '@plone/volto-original/helpers/Blocks/Blocks';

original.X = X; // you can rewrite functions from the original module, or add new ones

module.exports = original;