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.
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.
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:
A list of applications
The application sorting
A list of additional translations to be merged with existing ones
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
andflowable.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
.
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:
When the URL is
#/ghosts01
, theGhostApp
component is loaded.When the URL is
#/ghosts01/rockets
, a simplediv
is rendered.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'
},
}
};
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".
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.
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):
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
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
.
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.
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.
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",
],
};