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.