Skip to main content

Customize User Interface and Flow-Apps

Embedded Flowable Work frontend

Flowable provides a dedicated JavaScript front end that can either be deployed to any HTML capable server or run embedded directly within the Flowable Server. For details about how to include the embedded frontend in your project, see the related Java Programming Extensions documentation.

Embedded frontend structure

The embedded frontend (flowable-work-frontend) provides an index.html file generated from a Thymeleaf template, which allows for few customizations depending on the runtime resolution of your project properties and of available resources.

In particular, your applications and customizations will be loaded by incorporating two files, custom.css and custom.js, served from the folder located at resources/static/ext. You don't have to provide both, there are empty defaults for them included in the frontend jar. But if you do provide them, the generated index.html will contain a static path to them and append a md5 hash to the file name, so long-term caching works correctly.

In the next chapter, we will describe the setup for building a customized frontend on top of the embedded one.

Frontend-specific properties

Because it is sometimes relevant to configure the frontend slightly differently between your development, testing and production environments, there are a few frontend-specific properties and feature flags which you can define in your application properties.

See Flowable Work properties for the full list.

Setting Up the Project Structure for a customized embedded frontend

The stylesheets can be extended through a custom.css file which will be loaded right after the application styles, in the header of the default index.html.

This chapter assumes you have already created a Java application serving the front end component, included in the flowable-work-frontend jar, and want to extend it via a custom.js file.

This could be done by creating directly a custom.js and writing ES5 code in it.

Instead, assuming you have a small knowledge in modern JavaScript, TypeScript and React, we are instead going to do an application packaged as a UMD module using Rollup.js. We recommend adding the flowable @flowable/work-scripts dev dependency to your frontend project, and all the following things will be handled for you:

  • Modern JavaScript transpilation, Typescript.
  • CSS, SCSS, CSS-IN-JS.
  • File names and output folders.
  • Externals, and global exports.
Reminder

Typescript is not a requirement, this is the preferred language at Flowable. Your application can be built also in plain Javascript. @flowable/work-scripts exposes our internal frontend tooling to allow any custom entry point.

The first step for setting up our frontend project is to create an empty npm or yarn project with a src folder. Then the @flowable/work-scripts and typescript dependencies can be added.

You can use this commands to do it directly through a bash console:

mkdir customize-work-fe/
cd customize-work-fe/
mkdir src/
# in case of using typescript, create a tsconfig.json
echo '{"exclude": ["/node_modules/"], "compilerOptions":{"rootDir": ".","jsx":"react","module":"esNext","moduleResolution":"node","outDir":"./dist","preserveConstEnums":true,"strict":true,"strictFunctionTypes":false,"target":"es5","sourceMap":true,"importHelpers":true,"resolveJsonModule":true,"esModuleInterop":true}}' > tsconfig.json
touch src/index.ts
# init the yarn project
yarn init --yes
yarn add @flowable/work-scripts typescript --dev

This package add a build tool to your project (flowbuild), to use it to build your project please add the following scripts to your package.json:

 {
...
"scripts": {
...
"watch": "flowbuild watch",
"build": "flowbuild build"
},
"devDependencies": {
"@flowable/work-scripts": "^3.11.6",
"typescript": "^4.6.4"
}
...
}

To bundle the frontend you will need to execute the next command:

yarn build

To bundle in watch-mode this means that you can have live-reload of your changes without rebuilding every time, you'll need to execute the next command:

yarn watch

This will generate custom.js and custom.css files in /dist, in addition to copy some static assets like ./src/fonts/**/*.

Copy the generated files from /dist into your Flowable web folder (e.g. /src/main/resources/static/ext) and the new components will be injected into the Work UI.

tip

If you want @flowable/work-scripts can create directly all the generated files to an alternative folder instead to dist/, you can pass the flag -o to any flowbuild command.


For example: flowbuild build -o ../src/main/resources/static/ext

Basic Application Structure

Your application module exports four objects:

  1. A list of applications

  2. The application sorting

  3. A list of additional translations to be merged with existing ones

  4. Other customizations such as form details, form translations, custom header, etc.

To do that, create a file in src/index.tsx with the following content:

const applications: ExternalApplication[] = [];
const applicationSorting: { [appId: string]: number } = {};
const translations = {};

export default { applications, applicationSorting, translations };

In the code sample above we are just exposing applications, applicationSorting and translations but as described in the next sections, there are more elements that can be returned by the externals module, see the ExternalConfiguration type from the @flowable/work library for more details:

import type { ExternalConfiguration } from "@flowable/work";

export default {
applications,
applicationSorting,
translations,
detailCustomizations,
headerCustomizations,
customTopNavigationBar,
contentPreview,
customCaseComponents,
additionalData,
formComponents,
formTranslations,
onFormsEvent,
} as ExternalConfiguration;

Typescript and Rollup.js are totally optional and you can use any other language or module bundler as long as it marks the following names as externals:

  • React is available at flowable.React and flowable.ReactDOM.

  • Flowable Forms are available at flowable.Forms.

  • Flowable common components are registered at flowable.Components.

Add a Flow-App using Custom React Components

You can create Flow-Apps directly using Flowable Design, but there are cases where you might want to do more fine-grained customization or add your own components.

Creating the First Application

Let us add a simple application displaying a very basic React component, defined as:

import React from "react";
import classNames from "classnames";

import { ExternalAppComponentProps } from "@flowable/work";

import "./ghostApp.scss";

export type GhostsAppProps = ExternalAppComponentProps & {};

export const GhostsApp = (props: GhostsAppProps) => (
<div
className={classNames("flo-ghostsApp", {
"flo-ghostsApp--engage": props.features.conversations,
})}
>
<div className="flo-ghostsApp__title">Ghosts Application</div>
<div className="flo-ghostsApp__data">{JSON.stringify(props, null, 2)}</div>
</div>
);

Custom Flow-Apps are just regular React components and they follow the same behavior and lifecycle. Render will be only requested when parent props change, as expected.

The sample application dumps all the props received as JSON, with the following information:

export type ExternalAppComponentProps = {
// Current logged-in user
currentUser: CurrentUser;

// Set of configured URLs for the platform services (you can define your own)
endpoints: { [key: string]: string };

// Set of feature toggles enabled/disabled for the current user
features: { [key: string]: boolean };
};

Exposing the Application

To expose the application and make it visible under the left menu bar, you need to define a few properties, such as the name or the icon. Modify the file at src/index.tsx to do that:

const applications: ExternalApplication[] = [
{
applicationId: "ghosts01",
label: "Ghosts!",
labelKey: "ghosts:title",
icon: "skull-crossbones/solid",
component: (props: ExternalAppComponentProps) => <GhostsApp {...props} />,
},
];

Once loaded, your application is available at /ghosts01.

note

The application won't be visible in the menu unless you add the applicationId to the allowedFeatures of your user definition. In this example the allowed feature would be ghosts01.

Rendering a Form Instead of a Full-fledged Component

Instead of defining a component, you can also provide a property named form, that can either receive a form key or the complete form definition:

form: () => "A10_joinParticipantActionForm";

Alternatively, using the form definition:

form: () => ({ rows: [ ... ] })

Make sure also to remove the component property, since it is used instead if defined.

Customizing the Title of the Application

By default, the application title is inherited by the host application. If you want to change the application title for your Flow-App, you can provide a function that is called from the host application:

title: ({ applicationId }: { applicationId: string }) =>
`Ghosts - ${applicationId}`;

Customizing the Icon

We support two icon packs, Font Awesome 5 Pro (FA5) and Feather. The variants for FA5 can be referred by appending /solid, /light or /regular where /regular is the default. Icon names from FA5 have a higher priority than the ones from Feather, for backward compatibility reasons.

Adding Sub-applications

You can also define sub-applications, by filling the sub property:

const applications: ExternalApplication[] = [
{
applicationId: "ghosts01",
label: "Ghosts!",
labelKey: "ghosts:title",
icon: "skull-crossbones/solid",
component: (props: ExternalAppComponentProps) => <GhostsApp {...props} />,
sub: [
{
applicationId: "rockets",
label: "Some Rockets",
labelKey: "pirates:rockets.title",
icon: "rocket/solid",
component: () => <div>Rockets App</div>,
},
],
},
];

Notice that when there are sub-applications, the first one loads by default unless the base URL is defined:

  1. When the URL is #/ghosts01, the GhostApp component is loaded.

  2. When the URL is #/ghosts01/rockets, a simple div is rendered.

  3. When you transition from other applications to the Ghosts one, it loads the first application at #/ghosts01/rockets.

We recommend setting the parent application component to be the same as the first application.

Changing Application Sorting

The applicationSorting property allows for the definition of custom application sorting. You can also override existing sort order in Work or Engage. For instance, if you want to place your recently created Ghosts application as the first one, followed by contacts and putting Engage always at the end:

const applicationSorting = {
pirates: 1,
contacts: 3,
conversations: 800,
};

By default there is a gap of 10 units for existing applications, with the following values:

conversations: 20,
work: 30,
tasks: 40,
contacts: 70,
reports: 80,
templateManagement: 100

Translations

Adding Translations to a Flow-App

Applications support defining a labelKey that is used to retrieve the label displayed from the translation bundles. You can get different translations by modifying the translations property:

const applications: ExternalApplication[] = [
{
applicationId: 'eggplants',
labelKey: 'eggplants:title',
...
}
];

const translations = {
'en-US': {
eggplants: {
title: 'Eggplants'
}
},
'en-UK': {
eggplants: {
title: 'Aubergine'
}
},
'es-ES': {
eggplants: {
title: 'Berenjenas'
},
}
};
note

In Flowable, a translation and a labelKey can be associated with a specific prefix mention below.

For example, in Flowable Work, the prefix "work" is added before the labelKey and translation item to denote their association with this system.

const applications: ExternalApplication[] = [
{
applicationId: 'streamlineflow',
labelKey: 'work:streamlineflow.title',
...
}
];

const translations = {
'en-US': {
work: {
streamlineflow: {
title: 'StreamlineFlow'
}
},
...
},
};

Translation prefixes

  • common
  • compliance
  • contacts
  • documents
  • engage
  • features
  • inspect
  • metrics
  • native
  • reports
  • templates
  • themes
  • work
  • workspaces

Adding Form Engine Translations

By providing the formTranslations object to the external customisations, you can easily add your own Forms translations.

    //index.ts
...

const translations = {
...
};

const formTranslations = {
en: {
dtable: {
actionShowFilters: 'Show filters',
actionClearFilters: 'Clear filters'
},
validation: {
isRequired: 'Field must not be empty',
minLength: 'Field length must be longer or equal to {{extraSettings.minLength}}',
maxLength: 'Field length must be smaller or equal to {{extraSettings.maxLength}}',
minValue: 'Field must be a number greater than or equal to {{extraSettings.min}}'
}
}
};

...

export default {
...
translations,
formTranslations,
...
};

This will create form translations for most common language variations (e.g. 'en_us', 'en_uk', 'en_gb', etc.)

If you want to override one particular variation you can do it by adding it to the formTranslations object.


...

const formTranslations = {
en: {
dtable: {
actionShowFilters: 'Show filters',
actionClearFilters: 'Clear filters'
},
validation: {
isRequired: 'Field must not be empty',
minLength: 'Field length must be longer or equal to {{extraSettings.minLength}}',
maxLength: 'Field length must be smaller or equal to {{extraSettings.maxLength}}',
minValue: 'Field must be a number greater than or equal to {{extraSettings.min}}'
}
},
'en_gb': {
validation: {
isRequired: 'This field is mandatory'
}
}
};

...

In this example the value validation.isRequired will be "Field must not be empty" for every language variation except in en_gb case, that will be overwritten by "This field is mandatory".


note

All variation keys are treated as lowercase underscored strings. E.g: en-GB or en-gb keys will be parsed automatically to en_gb.

Important: If more than one variation key is provided for the same language variation, the effective translations will be set as the key that is lowest in the formTranslations object.

You can find the complete list of values that can be overwritten here


const formTranslations = {
...
'en_gb': {
validation: {
isRequired: 'This field is mandatory'
}
},
// this second variation will be parsed to 'en_gb'
// and set as the effective translation.
'en-GB': {
validation: {
isRequired: 'This field must be completed'
}
}
};

...


Customizing Tabs for Details Forms

The details forms for processes, tasks, and cases are customized by exposing a property named detailCustomizations when you define a custom Flow-App. Please refer to this document to learn how can you define custom Flow-Apps using regular React components.

We start by defining a function that gets the element loaded and the detail form type and returns a promise with the tab customizations:

import { CaseInstance, DetailCustomizations, DetailCustomizationType, ProcessInstance, Task } from '@flowable/work';

export const detailCustomizations = (
element: CaseInstance | ProcessInstance | Task,
type: DetailCustomizationType
): Promise<DetailCustomizations> => {
if (type === "case") {
return caseDetailCustomizations(element as CaseInstance);
} else if (type === "process") {
return processDetailCustomization(element as ProcessInstance);
} else if (type === "task") {
return taskDetailCustomizations(element as Task);
} else if (type === 'casePage') {
return casePageDetailCustomizations(element as CaseInstance);
}
return Promise.resolve(emptyDetailCustomizations);
};

That function does no modifications to existing tabs, but we use it as starting point for the following sections. Notice that depending on the detail form type, we are using a different function. For instance, let us create a function to customize task details tabs:

const taskDetailCustomizations = (element: Task): Promise<DetailCustomizations> => {
return Promise.resolve({
externalTabs: {
...
},
tabOverrides: {
...
}
});
};

Notice that the details customization should return an object that has two entries externalTabs, to add a new tab to the detail pages, and tabOverrides to modify the properties of existing tabs.

The details customization function should be exposed from your custom.js module:

export default {
applications,
applicationSorting,
translations,
detailCustomizations,
};

How to Add a Tab

It is as simple as adding the tab definition to the externalTabs property:

const taskDetailCustomizations = (
element: Task
): Promise<DetailCustomizations> => {
return Promise.resolve({
externalTabs: {
ghosts: {
label: "Ghosts",
icon: "skull",
component: () => <div>Ghosts</div>,
},
},
});
};

Tab definitions support the following properties:

export type ExternalTab = {
tabId?: string;
label: string;
icon: string;
order?: number;
hidden?: boolean;
component?: (props?: unknown) => JSX.Element;
componentDefaultProps?: { [key: string]: unknown };
form?: () => {
form: FormLayout | string;
onOutcomePressed?: OnOutcomePressed;
};
};

Conditionally Rendering Tab Contents

In some cases, you might want to check some properties before loading or displaying the tab. Remember the function receives an element parameter with the details for the task, process or case being displayed.

In the following example we change the tab label when the process is completed:

const processDetailCustomization = (element: ProcessInstance): Promise<DetailCustomizations> => {
const processIsComplete = element && !!element.endTime;
return Promise.resolve({
externalTabs: {
orders: {
label: processIsComplete ? "Current Orders" : "Completed Orders",
icon: "shopping-cart",
order: 1,
component: () => <div className="flo-ordersTab">Orders</div>,
},
},
tabOverrides: {},
});
};

How to Change the Icon or Label of an Existing Tab

If one wants to change the Work Form tab, altering the default order, label and icon it can be done by providing the new values:

tabOverrides: {
workForm: {
label: 'Stranger Things',
icon: 'ghost',
order: 25
}
}

The properties you defined in the tabOverrides overwrite existing ones before loading the tab.

For instance, if you want to hide a the sub-items tab, add a hidden: true to the definition:

{
subItems: {
hidden: true;
}
}

Show folders in Documents Tab

By default the "Folders" switcher in the Documents tab is disabled. To change this default behaviour you need to define an overwrite in the componentDefaultProps section of tabOverrides as shown below.

{
externalTabs: {
...
},
tabOverrides: {
documents: {
label: 'Pre-configured Documents',
componentDefaultProps: {
showFolders: true
}
},
...
}
}

Tab Identifiers Reference

For each one of the detail pages for process, cases, and tasks this is the list of identifiers used for customizations:

  • Case details

    • Open tasks: task

    • Work form: workForm

    • Sub-items: subItems

    • Documents attached: documents

    • History: history

  • Process details

    • Work form: workForm

    • Sub-items: subItems

    • Documents attached: documents

    • History: history

  • Task details

    • Form: taskForm

    • Involved people: people

    • Sub Tasks: subTasks

    • Documents attached: documents

    • History: history

Customizing Headers for Cases, Processes, Tasks and Case Pages

The headers for Cases, Processes, Tasks and Case Pages can be customized by exposing a property named headerCustomizations.

The object headerCustomizations could look like this:

import { FlowableHeaderSize } from '@flowable/work';

const headerCustomizations = {
tasks: {
['humanTask1']: (props: { task?; actions?; stages?; payload?: Payload }) => {
return (
<div>
<div>my custom TASK header for humanTask1</div>
<div>{props.task?.name}</div>
</div>
);
},
['task-key-2']: FlowableHeaderSize.Medium
},
casePages: {
['myCustomView']: (props: { caseInstance; actions; stages; payload }) => {
return (
<div>
my custom header <br /> actions:{JSON.stringify(props.actions)}
<br /> stages: {JSON.stringify(props.stages)}
<br /> case: {JSON.stringify(props.caseInstance)}
</div>
);
},
global: FlowableHeaderSize.Medium
},
cases: {
global: (props: { actions?; stages?; payload?: Payload }) => <div>my custom header</div>,
['casePageModel']: (props: { caseInstance?; actions?; stages?; payload?: Payload }) => {
return (
<div>
<div>my custom CASE header for casePageModel changed</div>
<div>{props.caseInstance?.name}</div>
</div>
);
}
['casePageModel2']: FlowableHeaderSize.Large
},
processes: {
global: FlowableHeaderSize.Small
}
};

There are four main sections: cases, processes, tasks and casePages. For each of those we have the option to define a custom header at two levels:

  • definition key: will be applied only for that case/process/task definition
  • global: will be applied to all other case/process/task

For setting a custom header we have two options: passing a react component or a size of the header (sm, md, xlg).

In the case of the react component, Flowable Work will pass as parameters the current task/case, related actions, stages and payload.

const headerCustomizations = {
tasks: {
["humanTask1"]: (props: {
task?;
actions?;
stages?;
payload?: Payload;
}) => {
return (
<div>
<div>my custom TASK header for humanTask1</div>
<div>{props.task?.name}</div>
</div>
);
},
["task-key-2"]: FlowableHeaderSize.Medium,
},
};

The header customization function should be exposed from your custom.js module:

export default { headerCustomizations };

Customizing the application header

Similar to the rest of customizations, the top navigation bar or application header can be replaced by a custom react component and exposed as customTopNavigationBar. The only requirement for the component is to accept children as prop, this way we can easily inject the user profile component. In order to show the top navigation bar, the feature flag topNavigationBar should be enabled.

Custom Component

import React from "react";

import "./customTopNavigationBar.scss";

type CustomTopNavigationBarProps = {
children?: React.ReactNode;
};

export const CustomTopNavigationBar = (props: CustomTopNavigationBarProps) => {
const { children } = props;
return (
<div className="custom-top-navigation-bar">
<div>
<img src="https://robohash.org/customnav" />
</div>
<div>External Custom Navigation Top Bar</div>
{children}
</div>
);
};

index.tsx

import { CustomTopNavigationBar as customTopNavigationBar } from './customizations/CustomTopNavigationBar/CustomTopNavigationBar';

...

export default {
...
customTopNavigationBar
};

Content Library Extensions (Document Preview)

Similar to adding extensions to the Work UI, the document preview screen has been improved to accept flexible extensions based on the vertical tabs. By adding custom vertical tabs you can override / hide default tabs or add new ones.

Expanded

The Document Preview is now fully customizable, driven by an object of custom tabs, you can create your own ones or override / hide the existing ones. The id of the tab will be based on the name you give to the object property, this is important because this id will be used for the routing.

The extensions module accepts now another element: contentPreview: ContentLibraryExtensions:

export type ContentLibraryExtensions = {
tabs: { [tabId: string]: Omit<ContentLibraryTab, "tabId"> };
};

export type ContentLibraryTabButton = {
label: string;
onClick: (content: Content, reloadContent?: () => void) => void;
cssMofifier?: string;
icon?: string;
primaryButton?: boolean;
};

export type ContentLibraryTab = {
tabId: string;
tabContentRender: (props: { content: Content }) => JSX.Element;
contentPreviewRender?: (props: { content: Content }) => JSX.Element;
icon?: (() => JSX.Element) | string;
expanded?: boolean;
hideDownloadbutton?: boolean;
hideTab?: boolean;
onTabClick?: (content: Content, reloadContent?: () => void) => void;
headerButtons?: ContentLibraryTabButton[];
};

There is only one mandatory property for a tab: the ​tabContentRender. You don't need to specify the ​tabId ​because it will be created for you automatically. The most basic tab you could create on your own could look like:

export const contentPreview: ContentLibraryExtensions = {
tabs: {
myTab: {
tabContentRender: () => <span>hello world</span>,
},
},
};

In the next sample, we are creating a couple of new tabs unicorn and settings, and we are overriding the info tab.

export const contentPreview: ContentLibraryExtensions = {
tabs: {
unicorn: {
tabContentRender: () => <img src="https://robohash.org/unicorn" />,
icon: "unicorn",
contentPreviewRender: () => (
<img src="https://robohash.org/unicorn_preview" />
),
onTabClick: (content, reload) => {
alert(`this is content ${content.id}`);
reload();
},
headerButtons: [
{
label: "foo",
icon: "star",
onClick: (content, reload) => {
alert(`this is content ${content.id}`);
reload();
},
},
{
label: "bar",
icon: "heart",
onClick: (content, reload) => {
alert(`this is content ${content.id}`);
reload();
},
},
],
},
settings: {
tabContentRender: () => <img src="https://robohash.org/settings" />,
icon: "cogs",
expanded: false,
onTabClick: (content, reload) => {
alert(`this is content ${content.id}`);
reload();
},
headerButtons: [
{
label: "foo",
icon: "star",
onClick: (content, reload) => {
alert(`this is content ${content.id}`);
reload();
},
},
{
label: "bar",
icon: "heart",
onClick: (content, reload) => {
alert(`this is content ${content.id}`);
reload();
},
},
],
},
info: {
icon: () => (
<img src="https://robohash.org/flowable" width="50" height="50" />
),
tabContentRender: () => null,
},
},
};

This will render something like the next (of course tabContentRender and contentPreviewRender can be as complex as your component is):

Expanded

note

tabContentRender is mandatory at the moment, this has some limitations, because we cannot inherit the default tabContentRender so it will be improved in future releases.

In the last sample, the tab info has been replaced to show a custom icon and no tab content at all (for icons you can provide your own, or use a Flowable one passing the string id of the icon)

Tab outcomes

The selected tab uses: a custom tab render, a custom content preview render and it provides a couple of extra buttons that you can see on the top bar. The tab click handler is provided with the content object and a callback function to refresh the document preview component. The buttons will receive on the onClick event the same parameters as the tab click handler.

(content: Content, reloadContent?: () => void) => void
note

If you want to remove the Download file button, you can simply specify this to your tab with hideDownloadButton and it won't be render, this property is also not global, but tab based

Http Client Custom Configuration

It is possible to customize the configuration of the http client used by the frontend application using the httpClientCustomConfiguration extension point that expose the axios instance.

To add a custom configuration to the http client you can assign a function to the httpClientCustomConfiguration extension point in the custom.js file and use interceptors to intercept and/or mutate outgoing requests or incoming responses.

In the example below two interceptors are added to the axios instance, the first adds a custom header to all the outgoing requests, the second intercepts all the incoming responses and in case of a 401 error will redirect to an external IDM:

window.flowable.httpClientCustomConfiguration = function (io) {
//add custom headers
io.interceptors.request.use(function (config) {
config.headers = {
...config.headers,
"X-Requested-With": "XMLHttpRequest",
};
return config;
});

//errorHandler
io.interceptors.response.use(
function (r) {
return r;
},
function errorHandler(err) {
if (statusCodeIs(err, 401)) {
window.location.href = "<external IDM>";
}
}
);
};

function statusCodeIs(error, code) {
return (
(error && error.response && error.response.status === code) ||
(error && error.status === code) ||
false
);
}

Flowbuild custom configuration

Under the hood @flowable/work-scripts uses @flowable/flowbuild package, this means that you can override the initial configuration of @flowable/work-scripts in multiple ways.

Using CLI arguments

You can pass any accepted argument that @flowable/flowbuild with Rollup accepts.

For example, you can pass a custom entrypoint:

 {
...
"scripts": {
...
"build": "flowbuild build --entry=./src/custom/entry.jsx"
},
"dependencies": {
"@flowable/work-scripts": "^3.11.1"
}
...
}

By default, we setup some defaults like name, output, umdName, format and entry.

tip

To know which arguments you can overwrite, check the --help usage on the command line:

yarn flowbuild build --help
# or watch
yarn flowbuild watch --help

Creating a config file

If you need to modify more deeply your configuration, like adding a plugin you can create a flowbuild.config.js file in the root of your project and override the default one.

flowbuild.config.js
const { workCustomization } = require("@flowable/flowbuild/lib/templates");

/** @type {import('@flowable/flowbuild').FlowbuildConfig} */
module.exports = {
rollup(config, outputNum) {
const workCustomizationDfltConfig = workCustomization.rollup(
config,
outputNum
);
workCustomizationDfltConfig.plugins.push(svg());
return workCustomizationDfltConfig;
},
};

In this code snippet we're creating a Flowbuild configuration file, where we're extending the default configuration that comes with the Work Customization Template. It's mandatory to return always the modified config.

And then override the default configuration:

{
"scripts": {
"build": "flowbuild build --config=./flowbuild.config.js"
}
}

Internally it's already using the --config argument that's why we overwrite this configuration.

note

We don't use directly @flowable/flowbuild instead of @flowable/work-scripts because @flowable/work-scripts package is preconfigured under the hood to output the correct files (custom.css,custom.js), and output with the UMD format.

Does something similar to this:

flowbuild build -n custom --strictName --umdName flowable.externals -f umd -o dist --outputCss=dist/custom.css --rollup -c ${workScriptsConfigTemplate} --clean

Using an alternative tool

If for some reason @flowable/work-scripts dependency cannot be used, the most important configuration files that need to be present are the following Typescript configuration:

{
"compilerOptions": {
"jsx": "react",
"module": "esNext",
"moduleResolution": "node",
"outDir": "./dist",
"preserveConstEnums": true,
"strict": true,
"strictFunctionTypes": false,
"target": "es5",
"sourceMap": true,
"importHelpers": true,
"resolveJsonModule": true,
"esModuleInterop": true
}
}

and the following rollup.config.js configuration:

export default {
input: "src/index.tsx",
output: {
name: "flowable.externals",
file: "./dist/custom.js",
format: "umd",
sourcemap: true,
globals: {
react: "flowable.React",
"react-dom": "flowable.ReactDOM",
"react-router": "flowable.ReactRouter",
"@flowable/forms": "flowable.Forms",
"@flowable/work": "flowable.Components",
},
},
plugins: [
json(),
sass({
output: "./dist/custom.css",
runtime: node_sass,
options: { outputStyle: production ? "compressed" : "expanded" },
}),
copy({
targets: [{ src: "./src/fonts/", dest: "./dist" }],
}),
resolve(),
commonjs(),
typescript(),
production && uglify(),
sourceMaps(),
copy({
targets: [{ src: "dist/*", dest: "../src/main/resources/static/ext" }],
}),
],
external: [
"react",
"react-dom",
"react-router",
"@flowable/forms",
"@flowable/work",
"@flowable/work-scripts",
],
};