Plone 6. Overriding @plone/mockup upload pattern, practical example

I've noticed this topic coming up here and there from time to time, but I haven't found documentation to guide me through the process of customizing and overriding a pattern from @plone/mockup, specifically the pat-upload pattern.

I'm aware of a direct approach involving forking mockup and plone.staticresources, which allows me to customize any code. However, my goal is to make customizations without modifying the original code.

I have taken a different path, and I'm not sure if it's the right one, although, in principle, it meets my needs.

I am new to webpack and module federation, and I feel that this might not be the proper way of achieving this. I need someone to validate it.

Thanks to the excellent theme by @Esoth, I've gathered the necessary information to create a new pattern and register it with Webpack's Module Federation. Now, the only thing left is to modify the upload pattern to introduce a series of changes:

  • Control of file types in the upload button.
  • Using the "accept" property.
  • Specific control of error messages.
  • Redesign of templates.
  • Modification of single loading to simultaneous loading.

(I have yet to submit a Pull Request with some of these changes, but there is still work to be done.)

That was the point: how to use the same CSS selector .pat-upload but load my pattern instead of the original.

First, I created a webpack project inside a Plone add-on package with the following structure (only main files/folders shown):

.
β”œβ”€β”€ browser
β”‚   β”œβ”€β”€ overrides
β”‚   β”œβ”€β”€ static
β”‚   β”œβ”€β”€ __init__.py
β”‚   └── configure.zcml
β”œβ”€β”€ pat
β”‚   β”œβ”€β”€ upload
β”‚   β”‚   β”œβ”€β”€ templates
β”‚   β”‚   β”‚   β”œβ”€β”€ preview.xml
β”‚   β”‚   β”‚   └── upload.xml
β”‚   β”‚   β”œβ”€β”€ upload.js
β”‚   β”‚   └── upload.scss
β”‚   β”œβ”€β”€ index.js
β”‚   └── patterns.js
β”œβ”€β”€ profiles
β”‚   β”œβ”€β”€ default
β”‚       └── registry
β”‚           └── main.xml
β”œβ”€β”€ views
β”‚   β”œβ”€β”€ __init__.py
β”‚   β”œβ”€β”€ configure.zcml
β”‚   β”œβ”€β”€ test_pat_upload.pt
β”‚   └── test_pat_upload.py
β”œβ”€β”€ configure.zcml
β”œβ”€β”€ i18n.js
β”œβ”€β”€ package.json
β”œβ”€β”€ webpack.config.js
└── widgets.pot


The most important files here are

  • package.json: define project info, dependencies and needed scripts.
  • webpack.config.js: register plugin and adds module federation for the pattern.

package.json

{
  "name": "upload",
  "version": "1.0.0",
  "main": "index.js",
  "author": "rber474",
  "license": "MIT",
  "repository": {
    "type": "git",
    "url": "https://github.com/rber474/once.upload"
  },
  "devDependencies": {
    "@patternslib/dev": "^3.3.5",
    "@plone/mockup": "^5.1.6",
    "babel-loader": "^9.1.2",
    "clean-css-cli": "^5.6.1",
    "npm-run-all": "^4.1.5",
    "sass": "^1.49.11",
    "sass-loader": "^13.2.0",
    "webpack": "^5.89.0",
    "webpack-cli": "^5.1.4"
  },
  "dependencies": {
    "@patternslib/patternslib": "9.9.4",
    "dropzone": "^5.9.3",
    "underscore": "^1.13.6"
  },
  "scripts": {
    "build": "NODE_ENV=production webpack --config webpack.config.js",
    "start": "NODE_ENV=development webpack serve --config webpack.config.js",
    "stats": "NODE_ENV=production webpack --config webpack.config.js --json > stats.json",
    "watch:webpack:plone": "NODE_ENV=development DEPLOYMENT=plone webpack --config webpack.config.js --watch",
    "i18n": "node i18n.js"
  }
}

webpack.config.js

process.traceDeprecation = true;
const mf_config = require("@patternslib/dev/webpack/webpack.mf");
const path = require("path");
const package_json = require("./package.json");
const package_json_mockup = require("@plone/mockup/package.json");
const webpack_config = require("@patternslib/dev/webpack/webpack.config").config;


module.exports = () => {
    let config = {
        entry: {
            "bundle.min": path.resolve(__dirname, "pat/index"),
        },
        ...
    };

    config = webpack_config({
        config: config,
        package_json: package_json,
    });
    config.output.path = path.resolve(__dirname, "browser/static");
    config.output.clean = true;

    config.module.rules.push({
        test: /\.svg$/i,
        type: 'asset/resource',
        exclude: path.join(__dirname, "node_modules/bootstrap-icons/icons/"),
    });

    config.plugins.push(
        mf_config({
            name: "upload",
            filename: "remote.min.js",
            remote_entry: config.entry["bundle.min"],
            dependencies: {
                ...package_json_mockup.dependencies,
                ...package_json.dependencies,
            },
            shared: {
                bootstrap: {
                    singleton: true,
                    requiredVersion: package_json.dependencies["bootstrap"],
                    eager: true,
                },
                jquery: {
                    singleton: true,
                    requiredVersion: "3.7.1",
                    eager: true,
                },
                "bootstrap-icons": {
                    singleton: true,
                    requiredVersion: package_json_mockup.dependencies["boostrap-icons"],
                    eager: true,
                }
            },
        })
    );

    if (process.env.NODE_ENV === "development") {
        config.devServer.port = "8011";
        config.devServer.static.directory = __dirname;
    }

    return config;
};

If you are creating a new pattern then name should be unique as Module Federation doesn't allow repeated ids. Here I am giving the same name as the pattern I am trying to override.

Under the pat folder, there are two files: index.js and patterns.js, based on the same structure in
@plone/mockup.

index.js

// Webpack entry point for module federation.

import "@patternslib/patternslib/webpack/module_federation";

// Webpack entry point for module federation.

import("./patterns.js");

pattern.js

// Core
import registry from "@patternslib/patternslib/src/core/registry";

import "./upload/upload";

// Import pattern styles in JavaScript
window.__patternslib_import_styles = true;

registry.init();

and finally, the basic file content for upload.js:

import _ from "underscore";
import _t from "@plone/mockup/src/core/i18n-wrapper";
import Base from "@patternslib/patternslib/src/core/base";
import utils from "@plone/mockup/src/core/utils";


let Dropzone;

// Extender el patrΓ³n "upload" de @plone/mockup
export default Base.extend({
    name: "upload",
    trigger: ".pat-upload",
    parser: "mockup",

    ...
});

Note I am extending the Base pattern instead of the Upload. This solution doesn't work if I try to extend the latest. But if I change the pattern name and MF name and then I can extend upload from @plone/mockup/src/pat/upload/upload.

After building the project, add the bundle.min.js as a plone.bundle in profiles/default/registry/main.xml:

<?xml version="1.0"?>
<registry
    xmlns:i18n="http://xml.zope.org/namespaces/i18n"
    i18n:domain="once.upload">

<records interface="Products.CMFPlone.interfaces.IBundleRegistry"
    prefix="plone.bundles/once.upload">
    <value key="depends" />
    <value key="load_async">False</value>
    <value key="load_defer">False</value>
    <value key="enabled">True</value>
    <value key="jscompilation">++plone++once.upload/bundle.min.js</value>
    <value key="csscompilation" />
  </records>

</registry>

To test it, I have create a browser view:

# -*- coding: utf-8 -*-

# from once.upload import _
from Products.Five.browser import BrowserView
from zope.interface import implementer
from zope.interface import Interface

import json
import logging


logger = logging.getLogger(__name__)


class ITestPatUpload(Interface):
    """Marker Interface for ITestPatUpload"""


@implementer(ITestPatUpload)
class TestPatUpload(BrowserView):

    def __call__(self):
        # Implement your own actions:
        return self.index()

    def upload(self):
        """"""
        logger.info("procesando fichero")
        response = self.request.response
        response.setHeader("Content-type", "application/json")
        response.setStatus(200)
        return json.dumps({"message": "Prueba de mensaje", "error": ""})

    def json_settings(self):
        """Configure dropzone"""

        settings = {
            "url": "custom-upload",
            "acceptedFiles": ".pdf, .docx, .jpeg, .jpg",
            "maxFiles": 5,
            "maxFilesize": 12,
            "createImageThumbnails": False,
            "autoProcessQueue": False,
            "uploadMultiple": False,
        }
        return json.dumps(settings)
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:metal="http://xml.zope.org/namespaces/metal"
      xmlns:tal="http://xml.zope.org/namespaces/tal"
      xmlns:i18n="http://xml.zope.org/namespaces/i18n"
      i18n:domain="once.upload"
      metal:use-macro="context/main_template/macros/master">
<body>

  <metal:custom_title fill-slot="content-title">
    <h1 tal:replace="structure context/@@title" />
  </metal:custom_title>

  <metal:custom_description fill-slot="content-description">
    <p tal:replace="structure context/@@description" />
  </metal:custom_description>

  <metal:content-core fill-slot="content-core">
  <metal:block define-macro="content-core">

    <h2>Main content</h2>
    <div class="pat-upload" data-pat-upload=""
      tal:define="settings view/json_settings;"
      tal:attributes="data-pat-upload settings"
    />

  </metal:block>
  </metal:content-core>
</body>
</html>

and these are the results:

original upload screenshots

my custom upload screenshots

Sorry for this long post.

2 Likes

I have no solution, but I'm looking for exactly the same customization options :+1:t4:

All the complete code for this example is in this WIP repo

1 Like

Thank you for sharing your experience!

Unfortunately there is not very much documentation for @plone/mockup left in Plone 6 docs (but there's an upcoming PR Integrate Mockup's documentation by thet Β· Pull Request #1548 Β· plone/documentation Β· GitHub ) ... I know that this topic was covered in the Plone 5 trainings and I've found these parts here https://github.com/plone/training/blob/2022/docs/javascript/exercises/8.md ... I gave this a try in my latest Plone 6 integration package and this was my experience:

  1. My integration package was created with plonecli
  2. There's a template called mockup_pattern which I ran like this:
$ cd src/<my_integration_package>
$ plonecli add mockup_pattern
...
--> Pattern name: upload

this creates all the webpack/MF config related files for you and generates basically what you have outlined above. (You can find the templates here https://github.com/plone/bobtemplates.plone/tree/main/bobtemplates/plone/mockup_pattern)

webpack.config.js

process.traceDeprecation = true;
const mf_config = require("@patternslib/dev/webpack/webpack.mf");
const package_json = require("./package.json");
const package_json_mockup = require("@plone/mockup/package.json");
const package_json_patternslib = require("@patternslib/patternslib/package.json");
const path = require("path");
const webpack_config = require("@patternslib/dev/webpack/webpack.config").config;

module.exports = () => {
    let config = {
        entry: {
            "<integration-package>.min": path.resolve(__dirname, "resources/index.js"),
        },
    };

    config = webpack_config({
        config: config,
        package_json: package_json,
    });
    config.output.path = path.resolve(__dirname, "src/<integration-package-path>/browser/static/bundles");

    config.plugins.push(
        mf_config({
            name: "<integration-package>",
            filename: "<integration-package>-remote.min.js",
            remote_entry: config.entry["<integration-package>.min"],
            dependencies: {
                ...package_json_patternslib.dependencies,
                ...package_json_mockup.dependencies,
                ...package_json.dependencies,
            },
        })
    );

    if (process.env.NODE_ENV === "development") {
        config.devServer.port = "8011";
        config.devServer.static.directory = path.resolve(__dirname, "./resources/");
    }

    // console.log(JSON.stringify(config, null, 4));

    return config;
};

NOTE: for the MF entry name you can choos what you want as long as its unique.

resources/index.js

// Webpack entry point for module federation.
// This import needs to be kept with brackets.
import("./bundle");

resources/bundle.js

import registry from "@patternslib/patternslib/src/core/registry";

import "./pat-upload/upload";

registry.init();

And our pat-upload pattern code with the overriding of the existing patternn

resources/pat-upload/upload.js

import $ from "jquery";
import registry from "@patternslib/patternslib/src/core/registry";
import Upload from "@plone/mockup/src/pat/upload/upload";

// delete default registered pattern:
delete registry.patterns.upload;
delete $.fn.patUpload;

class Pattern extends Upload {
    static name = "upload";
    static trigger = ".pat-upload";
    static parser = "mockup";

    async init() {
        let self = this;
        self.options = {
            ...self.options,
            maxFiles: 1,
            maxFilesize: 10,
        };
        Upload.prototype.init.call(self);
    }
};


// Register Pattern class in the global pattern registry and make it usable there.
registry.register(Pattern);

// Export Pattern as default export.
// You can import it as ``import AnyName from "./energieinstitut";``
export default Pattern;
// Export BasePattern as named export.
// You can import it as ``import { Pattern } from "./energieinstitut";``
export { Pattern };

As you can see that we are deleting the "default" registered pattern from the registry before re-registering our pattern code.
Make sure you override the options before calling Upload.prototype.init.call().

Maybe this helps to simplify your code above.

But there's one caveat: as I've tried this out, it worked with the upload pattern called from markup with class="pat-upload" but it didn't work when the pattern gets directly imported as module, like pat-structure does (aka folder_contents) ... maybe @thet has some more insights on this as he is one of the main Patternslib developer nowadays ...

Extra tip
when working with patterns and mockup I find the debug log on the console sometimes useful. This can be enabled by adding ?loglevel=DEBUG to your browser URL.

3 Likes

Thank you for this helpful post!
The key has to be:

// delete default registered pattern:
delete registry.patterns.upload;
delete $.fn.patUpload;

I have also noticed this caveat: structure/js/viewsApp.js is importing UploadView which uses upload pattern class. I have been trying to override this import with an alias. But no results yet.

Again, thanks for your help.

Rafa

2 Likes