├── .eslintrc
├── packages
├── examples
│ ├── complete-demo
│ │ ├── src
│ │ │ ├── server
│ │ │ │ ├── utils.spec.js
│ │ │ │ ├── fsm
│ │ │ │ │ ├── index.js
│ │ │ │ │ └── fsm.js
│ │ │ │ ├── schema
│ │ │ │ │ ├── index.js
│ │ │ │ │ └── schema.js
│ │ │ │ ├── storage
│ │ │ │ │ ├── index.js
│ │ │ │ │ └── storage.js
│ │ │ │ ├── objectConfig
│ │ │ │ │ ├── index.js
│ │ │ │ │ ├── objectConfig.js
│ │ │ │ │ └── objectConfiguration.json
│ │ │ │ ├── data
│ │ │ │ │ ├── conditions
│ │ │ │ │ │ ├── index.js
│ │ │ │ │ │ └── userHasRoles.js
│ │ │ │ │ └── actions
│ │ │ │ │ │ ├── index.js
│ │ │ │ │ │ ├── updateProcessedBy.js
│ │ │ │ │ │ ├── sendMail.js
│ │ │ │ │ │ └── testAction.js
│ │ │ │ ├── routes
│ │ │ │ │ ├── states.js
│ │ │ │ │ ├── history.js
│ │ │ │ │ ├── sendEvent.js
│ │ │ │ │ ├── availableTransitions.js
│ │ │ │ │ ├── objects.js
│ │ │ │ │ └── editorData.js
│ │ │ │ ├── utils.js
│ │ │ │ └── index.js
│ │ │ ├── client
│ │ │ │ ├── components
│ │ │ │ │ ├── styles.css
│ │ │ │ │ ├── App.react.js
│ │ │ │ │ └── Editor.react.js
│ │ │ │ ├── constants.js
│ │ │ │ ├── customComponentsRegistry
│ │ │ │ │ ├── index.js
│ │ │ │ │ └── FullName.react.js
│ │ │ │ ├── uiGlobalComponents
│ │ │ │ │ ├── uiMessageNotifications
│ │ │ │ │ │ ├── index.js
│ │ │ │ │ │ ├── styles.css
│ │ │ │ │ │ └── Notifications.js
│ │ │ │ │ └── index.js
│ │ │ │ └── index.js
│ │ │ └── common.js
│ │ ├── .gitignore
│ │ ├── .eslintrc
│ │ ├── config
│ │ │ ├── mocha-setup.js
│ │ │ └── webpack.config.js
│ │ └── package.json
│ ├── process-task-manager-ui
│ │ ├── src
│ │ │ ├── client
│ │ │ │ ├── components
│ │ │ │ │ ├── index.js
│ │ │ │ │ └── TaskList.react.js
│ │ │ │ ├── workflow
│ │ │ │ │ ├── objectConfiguration.json
│ │ │ │ │ ├── conditions.js
│ │ │ │ │ ├── actions.js
│ │ │ │ │ └── process-schema.json
│ │ │ │ ├── static
│ │ │ │ │ ├── favicon.ico
│ │ │ │ │ ├── img
│ │ │ │ │ │ └── flags.png
│ │ │ │ │ └── fonts
│ │ │ │ │ │ ├── Lato-Black.woff
│ │ │ │ │ │ ├── Lato-Bold.woff
│ │ │ │ │ │ ├── Lato-Italic.woff
│ │ │ │ │ │ ├── Lato-Light.woff
│ │ │ │ │ │ ├── Lato-Hairline.woff
│ │ │ │ │ │ ├── Lato-Regular.woff
│ │ │ │ │ │ ├── Lato-BlackItalic.woff
│ │ │ │ │ │ ├── Lato-BoldItalic.woff
│ │ │ │ │ │ ├── Lato-LightItalic.woff
│ │ │ │ │ │ ├── Lato-HairlineItalic.woff
│ │ │ │ │ │ ├── glyphicons-halflings-regular.eot
│ │ │ │ │ │ ├── glyphicons-halflings-regular.ttf
│ │ │ │ │ │ ├── glyphicons-halflings-regular.woff
│ │ │ │ │ │ ├── font-awesome
│ │ │ │ │ │ ├── fontawesome-webfont.eot
│ │ │ │ │ │ ├── fontawesome-webfont.ttf
│ │ │ │ │ │ └── fontawesome-webfont.woff
│ │ │ │ │ │ └── glyphicons-halflings-regular.woff2
│ │ │ │ ├── index.js
│ │ │ │ └── index.html
│ │ │ └── server
│ │ │ │ └── index.js
│ │ ├── package.json
│ │ └── webpack.development.config.js
│ └── invoice-console
│ │ ├── machine-schema.json
│ │ ├── package.json
│ │ └── forms.js
├── integration-tests
│ └── history
│ │ ├── .gitignore
│ │ ├── data
│ │ ├── conditions.js
│ │ ├── actions.js
│ │ ├── objectConfiguration.json
│ │ └── schema.json
│ │ └── package.json
├── core
│ ├── src
│ │ ├── .eslintrc
│ │ ├── specs
│ │ │ ├── .eslintrc
│ │ │ ├── Machine.currentState.spec.js
│ │ │ ├── Machine.isInFinalState.spec.js
│ │ │ ├── Machine.is.spec.js
│ │ │ ├── Machine.availableStates.spec.js
│ │ │ ├── Machine.isRunning.spec.js
│ │ │ ├── MachineDefinition.objectAlias.spec.js
│ │ │ ├── MachineDefinition.contructor.spec.js
│ │ │ ├── Machine.availableTransitions.spec.js
│ │ │ ├── Machine.availableAutomaticTransitions.spec.js
│ │ │ ├── MachineDefinition.availableStates.spec.js
│ │ │ ├── ModelDefinition.prepareParams.spec.js
│ │ │ ├── Machine.start.spec.js
│ │ │ ├── Machine.getHistory.spec.js
│ │ │ ├── Machine.can-and-cannot.spec.js
│ │ │ └── Machine.constructor.spec.js
│ │ └── index.js
│ ├── config
│ │ └── mocha-setup.js
│ ├── actionsAndConditions.md
│ └── package.json
├── editor
│ ├── src
│ │ ├── index.js
│ │ ├── components
│ │ │ ├── Actions
│ │ │ │ ├── index.js
│ │ │ │ ├── actionPropTypes.js
│ │ │ │ ├── ActionInvocationEditor.less
│ │ │ │ ├── ActionsTable.less
│ │ │ │ └── utils.js
│ │ │ ├── CodeEditor
│ │ │ │ ├── index.js
│ │ │ │ ├── CodeEditor.react.js
│ │ │ │ └── codemirror-placeholder-mod.js
│ │ │ ├── Guards
│ │ │ │ ├── index.js
│ │ │ │ ├── utils.js
│ │ │ │ ├── guardPropTypes.js
│ │ │ │ └── Guards.less
│ │ │ ├── ParamsEditor
│ │ │ │ ├── index.js
│ │ │ │ ├── components
│ │ │ │ │ ├── PathExpressionInputInjector
│ │ │ │ │ │ ├── index.js
│ │ │ │ │ │ ├── ExpressionInput.react.js
│ │ │ │ │ │ ├── ExpressionSwitcher.react.js
│ │ │ │ │ │ ├── ExpressionEditor.react.js
│ │ │ │ │ │ └── PathExpressionInputInjector.react.js
│ │ │ │ │ ├── ArrayEditor.less
│ │ │ │ │ ├── DateInput.less
│ │ │ │ │ ├── BooleanInput.react.js
│ │ │ │ │ ├── StringInput.react.js
│ │ │ │ │ ├── index.js
│ │ │ │ │ ├── GenericInput.react.js
│ │ │ │ │ ├── EnumInput.react.js
│ │ │ │ │ ├── DateInput.react.js
│ │ │ │ │ ├── IntegerInput.react.js
│ │ │ │ │ ├── DecimalInput.react.js
│ │ │ │ │ ├── MultiSelect.react.js
│ │ │ │ │ └── ArrayEditor.react.js
│ │ │ │ └── ParamsEditor.react.js
│ │ │ ├── StatesTable
│ │ │ │ ├── index.js
│ │ │ │ ├── statePropTypes.js
│ │ │ │ ├── StateEditor.spec.js
│ │ │ │ ├── DeleteStateDialogBody.spec.js
│ │ │ │ ├── DeleteStateDialogBody.react.js
│ │ │ │ └── StatesTable.spec.js
│ │ │ ├── ConfirmDialog
│ │ │ │ └── index.js
│ │ │ ├── WorkflowGraph
│ │ │ │ ├── index.js
│ │ │ │ └── WorkflowGraph.less
│ │ │ ├── TransitionsTable
│ │ │ │ ├── index.js
│ │ │ │ └── TransitionsTable.spec.js
│ │ │ ├── WorkflowEditor
│ │ │ │ ├── index.js
│ │ │ │ ├── styles.less
│ │ │ │ └── customComponents
│ │ │ │ │ └── FullName.react.js
│ │ │ ├── ErrorLabel.react.js
│ │ │ ├── schemaConfigPropTypes.js
│ │ │ ├── Label.react.js
│ │ │ ├── EditorOutput.react.js
│ │ │ ├── Select
│ │ │ │ └── index.js
│ │ │ ├── TopForm.spec.js
│ │ │ ├── utils.spec.js
│ │ │ ├── TopForm.react.js
│ │ │ ├── ErrorLabel.spec.js
│ │ │ ├── TopButtons.react.js
│ │ │ ├── utils.js
│ │ │ └── ObjectInspector.react.js
│ │ └── i18n
│ │ │ └── index.js
│ ├── config
│ │ ├── .eslintrc
│ │ ├── webpack.config.build.js
│ │ ├── mocha-config.js
│ │ ├── webpack.config.showroom.js
│ │ └── webpack.config.common.js
│ ├── scripts
│ │ └── gh-pages
│ │ │ ├── build.sh
│ │ │ └── deploy.sh
│ ├── .gitattributes
│ ├── www
│ │ ├── index.html
│ │ └── index-page.js
│ ├── .gitignore
│ └── README.md
├── history
│ ├── src
│ │ ├── models
│ │ │ ├── .eslintrc
│ │ │ ├── interfaces
│ │ │ │ ├── index.js
│ │ │ │ └── workflowTransitionHistory.js
│ │ │ ├── migrations
│ │ │ │ ├── 20181218111220-create-index-WorkflowTransHist_boid_idx.js
│ │ │ │ ├── 20200320081228-insert-WorkflowTransHist-indexes.js
│ │ │ │ └── 20180123102827-create-workflowTransitionHistory.js
│ │ │ └── definitions
│ │ │ │ ├── index.js
│ │ │ │ └── workflowTransitionHistory.js
│ │ ├── index.spec.js
│ │ └── index.js
│ ├── .eslintrc
│ ├── .npmignore
│ └── package.json
└── task-manager
│ ├── src
│ ├── specs
│ │ ├── .eslintrc
│ │ ├── utils.doUntil.spec.js
│ │ ├── TaskManager.eventSending.spec.js
│ │ └── TaskManager.monitoring.spec.js
│ ├── .eslintrc
│ ├── index.js
│ └── utils.js
│ ├── config
│ ├── mocha-setup.js
│ └── webpack.config.js
│ └── package.json
├── .github
└── issue_template.md
├── .gitattributes
├── .gitignore
├── lerna.json
├── Dockerfile
├── .editorconfig
├── package.json
├── .dockerignore
├── existingFsmLibsReview.md
└── README.md
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "opuscapita"
3 | }
4 |
--------------------------------------------------------------------------------
/packages/examples/complete-demo/src/server/utils.spec.js:
--------------------------------------------------------------------------------
1 | // TODO
2 |
--------------------------------------------------------------------------------
/packages/integration-tests/history/.gitignore:
--------------------------------------------------------------------------------
1 | /data/testdb.sqlite
--------------------------------------------------------------------------------
/packages/core/src/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "node": true
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/packages/core/src/specs/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "mocha": true
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/packages/editor/src/index.js:
--------------------------------------------------------------------------------
1 | module.exports = require('./components/WorkflowEditor');
2 |
--------------------------------------------------------------------------------
/packages/history/src/models/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "mocha": true
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/packages/task-manager/src/specs/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "mocha": true
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/packages/examples/complete-demo/src/client/components/styles.css:
--------------------------------------------------------------------------------
1 | .dropdown-menu a {
2 | font-size: 12px
3 | }
--------------------------------------------------------------------------------
/packages/examples/complete-demo/src/server/fsm/index.js:
--------------------------------------------------------------------------------
1 | import fsm from './fsm';
2 |
3 | export default fsm;
4 |
--------------------------------------------------------------------------------
/packages/task-manager/src/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "node": true,
4 | "es6": true
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/packages/examples/process-task-manager-ui/src/client/components/index.js:
--------------------------------------------------------------------------------
1 | export default from './Container.react';
2 |
--------------------------------------------------------------------------------
/packages/task-manager/src/index.js:
--------------------------------------------------------------------------------
1 | import TaskManager from './TaskManager';
2 |
3 | export {
4 | TaskManager
5 | }
6 |
--------------------------------------------------------------------------------
/packages/examples/complete-demo/src/server/schema/index.js:
--------------------------------------------------------------------------------
1 | import schema from './schema';
2 |
3 | export default schema;
4 |
--------------------------------------------------------------------------------
/packages/examples/process-task-manager-ui/src/client/workflow/objectConfiguration.json:
--------------------------------------------------------------------------------
1 | {
2 | "stateFieldName": "status"
3 | }
--------------------------------------------------------------------------------
/packages/examples/complete-demo/src/server/storage/index.js:
--------------------------------------------------------------------------------
1 | import storage from './storage';
2 |
3 | export default storage;
4 |
--------------------------------------------------------------------------------
/packages/editor/src/components/Actions/index.js:
--------------------------------------------------------------------------------
1 | import ActionsTable from './ActionsTable.react';
2 |
3 | export default ActionsTable;
4 |
--------------------------------------------------------------------------------
/packages/editor/src/components/CodeEditor/index.js:
--------------------------------------------------------------------------------
1 | import CodeEditor from './CodeEditor.react';
2 |
3 | export default CodeEditor;
4 |
--------------------------------------------------------------------------------
/packages/editor/src/components/Guards/index.js:
--------------------------------------------------------------------------------
1 | import GuardsTable from './GuardsTable.react';
2 |
3 | export default GuardsTable;
4 |
--------------------------------------------------------------------------------
/packages/editor/src/components/ParamsEditor/index.js:
--------------------------------------------------------------------------------
1 | import ParamsEditor from './ParamsEditor.react';
2 |
3 | export default ParamsEditor;
4 |
--------------------------------------------------------------------------------
/packages/editor/src/components/StatesTable/index.js:
--------------------------------------------------------------------------------
1 | import StatesTable from './StatesTable.react';
2 |
3 | export default StatesTable;
4 |
--------------------------------------------------------------------------------
/packages/editor/src/components/ConfirmDialog/index.js:
--------------------------------------------------------------------------------
1 | import ConfirmDialog from './ConfirmDialog.react';
2 |
3 | export default ConfirmDialog;
4 |
--------------------------------------------------------------------------------
/packages/editor/src/components/WorkflowGraph/index.js:
--------------------------------------------------------------------------------
1 | import WorkflowGraph from './WorkflowGraph.react';
2 |
3 | export default WorkflowGraph;
4 |
--------------------------------------------------------------------------------
/.github/issue_template.md:
--------------------------------------------------------------------------------
1 |
2 | Meta-Info | Value
3 | -- | --
4 | ExtProjectId | ???
5 | Original Estimation | ???h
6 | Remaining Estimation | ???h
7 |
--------------------------------------------------------------------------------
/packages/examples/complete-demo/.gitignore:
--------------------------------------------------------------------------------
1 | src/server/storage/demo.sqlite
2 | src/server/schema/workflow-schema.json
3 | src/compiled-server
4 | build
--------------------------------------------------------------------------------
/packages/examples/complete-demo/src/client/constants.js:
--------------------------------------------------------------------------------
1 | export const notificationSuccess = 'success';
2 | export const notificationError = 'error';
3 |
--------------------------------------------------------------------------------
/packages/examples/complete-demo/src/server/objectConfig/index.js:
--------------------------------------------------------------------------------
1 | import objectConfig from './objectConfig';
2 |
3 | export default objectConfig;
4 |
--------------------------------------------------------------------------------
/packages/history/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "opuscapita",
3 | "env": {
4 | "node": true,
5 | "es6": true,
6 | "mocha": true
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/packages/editor/src/components/TransitionsTable/index.js:
--------------------------------------------------------------------------------
1 | import TransitionsTable from './TransitionsTable.react';
2 |
3 | export default TransitionsTable;
4 |
--------------------------------------------------------------------------------
/packages/integration-tests/history/data/conditions.js:
--------------------------------------------------------------------------------
1 | const guard1 = () => true
2 |
3 | guard1.paramsSchema = {}
4 |
5 | module.exports = {
6 | guard1
7 | }
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Automatically normalize line endings for all text-based files
2 | # http://git-scm.com/docs/gitattributes#_end_of_line_conversion
3 | * text=lf
4 |
--------------------------------------------------------------------------------
/packages/examples/complete-demo/src/server/data/conditions/index.js:
--------------------------------------------------------------------------------
1 | import userHasRoles from './userHasRoles';
2 |
3 | export default {
4 | userHasRoles
5 | }
6 |
--------------------------------------------------------------------------------
/packages/examples/complete-demo/src/client/customComponentsRegistry/index.js:
--------------------------------------------------------------------------------
1 | import FullName from './FullName.react';
2 |
3 | export default {
4 | fullName: FullName
5 | }
6 |
--------------------------------------------------------------------------------
/packages/core/src/index.js:
--------------------------------------------------------------------------------
1 | import Machine from './Machine';
2 | import MachineDefinition from './MachineDefinition';
3 |
4 | export {
5 | Machine,
6 | MachineDefinition
7 | }
8 |
--------------------------------------------------------------------------------
/packages/examples/complete-demo/src/client/uiGlobalComponents/uiMessageNotifications/index.js:
--------------------------------------------------------------------------------
1 | import Notifications from './Notifications';
2 |
3 | export default Notifications;
4 |
--------------------------------------------------------------------------------
/packages/editor/config/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "opuscapita",
3 | "env": {
4 | "jasmine": true,
5 | "browser": true,
6 | "node": true,
7 | "es6": true
8 | }
9 | }
--------------------------------------------------------------------------------
/packages/examples/complete-demo/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "opuscapita",
3 | "env": {
4 | "jasmine": true,
5 | "browser": true,
6 | "node": true,
7 | "es6": true
8 | }
9 | }
--------------------------------------------------------------------------------
/packages/core/config/mocha-setup.js:
--------------------------------------------------------------------------------
1 | // set node evn
2 | process.env.NODE_ENV = 'test';
3 |
4 | require('babel-register')({
5 | presets: ['es2015', 'stage-0'],
6 | plugins: ['istanbul']
7 | });
8 |
--------------------------------------------------------------------------------
/packages/examples/process-task-manager-ui/src/client/static/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/OpusCapita/fsm-workflow/HEAD/packages/examples/process-task-manager-ui/src/client/static/favicon.ico
--------------------------------------------------------------------------------
/packages/examples/complete-demo/config/mocha-setup.js:
--------------------------------------------------------------------------------
1 | // set node evn
2 | process.env.NODE_ENV = 'test';
3 |
4 | require('babel-register')({
5 | presets: ['env'],
6 | plugins: ['istanbul']
7 | });
8 |
--------------------------------------------------------------------------------
/packages/examples/process-task-manager-ui/src/client/static/img/flags.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/OpusCapita/fsm-workflow/HEAD/packages/examples/process-task-manager-ui/src/client/static/img/flags.png
--------------------------------------------------------------------------------
/packages/examples/complete-demo/src/client/uiGlobalComponents/index.js:
--------------------------------------------------------------------------------
1 | const uiMessageNotifications = require('./uiMessageNotifications').default;
2 |
3 | module.exports = {
4 | uiMessageNotifications
5 | }
6 |
--------------------------------------------------------------------------------
/packages/task-manager/config/mocha-setup.js:
--------------------------------------------------------------------------------
1 | // set node evn
2 | process.env.NODE_ENV = 'test';
3 |
4 | require('babel-register')({
5 | presets: ['es2015', 'stage-0'],
6 | plugins: ['istanbul']
7 | });
8 |
--------------------------------------------------------------------------------
/packages/history/src/models/interfaces/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = sequelize => {
4 | return require('./workflowTransitionHistory.js')(sequelize.model('WorkflowTransitionHistory'));
5 | }
6 |
--------------------------------------------------------------------------------
/packages/examples/process-task-manager-ui/src/client/static/fonts/Lato-Black.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/OpusCapita/fsm-workflow/HEAD/packages/examples/process-task-manager-ui/src/client/static/fonts/Lato-Black.woff
--------------------------------------------------------------------------------
/packages/examples/process-task-manager-ui/src/client/static/fonts/Lato-Bold.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/OpusCapita/fsm-workflow/HEAD/packages/examples/process-task-manager-ui/src/client/static/fonts/Lato-Bold.woff
--------------------------------------------------------------------------------
/packages/examples/process-task-manager-ui/src/client/static/fonts/Lato-Italic.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/OpusCapita/fsm-workflow/HEAD/packages/examples/process-task-manager-ui/src/client/static/fonts/Lato-Italic.woff
--------------------------------------------------------------------------------
/packages/examples/process-task-manager-ui/src/client/static/fonts/Lato-Light.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/OpusCapita/fsm-workflow/HEAD/packages/examples/process-task-manager-ui/src/client/static/fonts/Lato-Light.woff
--------------------------------------------------------------------------------
/packages/editor/src/components/ParamsEditor/components/PathExpressionInputInjector/index.js:
--------------------------------------------------------------------------------
1 | import PathExpressionInputInjector from './PathExpressionInputInjector.react';
2 |
3 | export default PathExpressionInputInjector;
4 |
--------------------------------------------------------------------------------
/packages/examples/process-task-manager-ui/src/client/static/fonts/Lato-Hairline.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/OpusCapita/fsm-workflow/HEAD/packages/examples/process-task-manager-ui/src/client/static/fonts/Lato-Hairline.woff
--------------------------------------------------------------------------------
/packages/examples/process-task-manager-ui/src/client/static/fonts/Lato-Regular.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/OpusCapita/fsm-workflow/HEAD/packages/examples/process-task-manager-ui/src/client/static/fonts/Lato-Regular.woff
--------------------------------------------------------------------------------
/packages/examples/process-task-manager-ui/src/client/static/fonts/Lato-BlackItalic.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/OpusCapita/fsm-workflow/HEAD/packages/examples/process-task-manager-ui/src/client/static/fonts/Lato-BlackItalic.woff
--------------------------------------------------------------------------------
/packages/examples/process-task-manager-ui/src/client/static/fonts/Lato-BoldItalic.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/OpusCapita/fsm-workflow/HEAD/packages/examples/process-task-manager-ui/src/client/static/fonts/Lato-BoldItalic.woff
--------------------------------------------------------------------------------
/packages/examples/process-task-manager-ui/src/client/static/fonts/Lato-LightItalic.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/OpusCapita/fsm-workflow/HEAD/packages/examples/process-task-manager-ui/src/client/static/fonts/Lato-LightItalic.woff
--------------------------------------------------------------------------------
/packages/examples/process-task-manager-ui/src/client/static/fonts/Lato-HairlineItalic.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/OpusCapita/fsm-workflow/HEAD/packages/examples/process-task-manager-ui/src/client/static/fonts/Lato-HairlineItalic.woff
--------------------------------------------------------------------------------
/packages/examples/complete-demo/src/common.js:
--------------------------------------------------------------------------------
1 | // export const objectIdProp = 'invoiceNo';
2 | // export const eventsProp = 'ocfsm_demo__events';
3 |
4 | module.exports = {
5 | objectIdProp: 'invoiceNo',
6 | eventsProp: 'ocfsm_demo__events'
7 | }
8 |
--------------------------------------------------------------------------------
/packages/examples/process-task-manager-ui/src/client/static/fonts/glyphicons-halflings-regular.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/OpusCapita/fsm-workflow/HEAD/packages/examples/process-task-manager-ui/src/client/static/fonts/glyphicons-halflings-regular.eot
--------------------------------------------------------------------------------
/packages/examples/process-task-manager-ui/src/client/static/fonts/glyphicons-halflings-regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/OpusCapita/fsm-workflow/HEAD/packages/examples/process-task-manager-ui/src/client/static/fonts/glyphicons-halflings-regular.ttf
--------------------------------------------------------------------------------
/packages/examples/process-task-manager-ui/src/client/static/fonts/glyphicons-halflings-regular.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/OpusCapita/fsm-workflow/HEAD/packages/examples/process-task-manager-ui/src/client/static/fonts/glyphicons-halflings-regular.woff
--------------------------------------------------------------------------------
/packages/editor/scripts/gh-pages/build.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | rm -rf .gh-pages-tmp &&
4 | node node_modules/@opuscapita/react-showroom-server/src/bin/showroom-scan.js src &&
5 | node node_modules/webpack/bin/webpack.js --config config/webpack.config.showroom.js
--------------------------------------------------------------------------------
/packages/editor/src/components/ParamsEditor/components/ArrayEditor.less:
--------------------------------------------------------------------------------
1 | .oc-fsm-crud-editor--table-array-editor {
2 | width: 100%;
3 | }
4 |
5 | .oc-fsm-crud-editor--table-array-editor td {
6 | vertical-align: top !important;
7 | padding-bottom: 5px;
8 | }
--------------------------------------------------------------------------------
/packages/examples/process-task-manager-ui/src/client/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import Container from './components';
4 |
5 | ReactDOM.render(
6 | ,
7 | document.getElementById("root")
8 | );
9 |
--------------------------------------------------------------------------------
/packages/examples/process-task-manager-ui/src/client/static/fonts/font-awesome/fontawesome-webfont.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/OpusCapita/fsm-workflow/HEAD/packages/examples/process-task-manager-ui/src/client/static/fonts/font-awesome/fontawesome-webfont.eot
--------------------------------------------------------------------------------
/packages/examples/process-task-manager-ui/src/client/static/fonts/font-awesome/fontawesome-webfont.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/OpusCapita/fsm-workflow/HEAD/packages/examples/process-task-manager-ui/src/client/static/fonts/font-awesome/fontawesome-webfont.ttf
--------------------------------------------------------------------------------
/packages/examples/process-task-manager-ui/src/client/static/fonts/glyphicons-halflings-regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/OpusCapita/fsm-workflow/HEAD/packages/examples/process-task-manager-ui/src/client/static/fonts/glyphicons-halflings-regular.woff2
--------------------------------------------------------------------------------
/packages/examples/process-task-manager-ui/src/client/static/fonts/font-awesome/fontawesome-webfont.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/OpusCapita/fsm-workflow/HEAD/packages/examples/process-task-manager-ui/src/client/static/fonts/font-awesome/fontawesome-webfont.woff
--------------------------------------------------------------------------------
/packages/examples/complete-demo/src/client/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import App from './components/App.react';
4 |
5 | export const render = (element, props = {}) => {
6 | ReactDOM.render(, element);
7 | }
8 |
--------------------------------------------------------------------------------
/packages/examples/complete-demo/src/server/data/actions/index.js:
--------------------------------------------------------------------------------
1 | import testAction from './testAction';
2 | import sendMail from './sendMail';
3 | import updateProcessedBy from './updateProcessedBy';
4 |
5 | export default {
6 | testAction,
7 | sendMail,
8 | updateProcessedBy
9 | }
10 |
--------------------------------------------------------------------------------
/packages/editor/src/components/Guards/utils.js:
--------------------------------------------------------------------------------
1 | import { isDef } from '../utils';
2 |
3 | export const removeEmptyParams = ({ params = [], ...rest }) => {
4 | const newParams = params.filter(({ value }) => isDef(value));
5 | return { ...rest, ...(newParams.length && { params: newParams }) }
6 | }
7 |
--------------------------------------------------------------------------------
/packages/editor/src/components/Actions/actionPropTypes.js:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 |
3 | export default PropTypes.shape({
4 | name: PropTypes.string.isRequired,
5 | params: PropTypes.arrayOf(PropTypes.shape({
6 | name: PropTypes.string.isRequired,
7 | value: PropTypes.any
8 | }))
9 | });
10 |
--------------------------------------------------------------------------------
/packages/editor/src/components/WorkflowEditor/index.js:
--------------------------------------------------------------------------------
1 | import WorkflowEditor from './WorkflowEditor.react';
2 | import withExpressionInput from '../ParamsEditor/components/PathExpressionInputInjector';
3 |
4 | export default WorkflowEditor;
5 |
6 | // enable usage with custom components
7 | export { withExpressionInput };
8 |
--------------------------------------------------------------------------------
/packages/editor/.gitattributes:
--------------------------------------------------------------------------------
1 | # Automatically normalize line endings for all text-based files
2 | # http://git-scm.com/docs/gitattributes#_end_of_line_conversion
3 | * text=lf
4 |
5 | # Declare files that will always have LF line endings on checkout.
6 | *.js text eol=lf
7 | *.sh text eol=lf
8 | *.css text eol=lf
9 | *.json text eol=lf
--------------------------------------------------------------------------------
/packages/integration-tests/history/data/actions.js:
--------------------------------------------------------------------------------
1 | const action1 = () => {}
2 |
3 | action1.paramsSchema = {
4 | type: "object",
5 | properties: {
6 | param1: {
7 | type: "integer"
8 | },
9 | param2: {
10 | type: "integer"
11 | }
12 | }
13 | }
14 |
15 | module.exports = {
16 | action1
17 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .*.swp
2 | node_modules
3 | .log
4 | yarn.lock
5 | package-lock.json
6 | *.log
7 | test-results.xml
8 | test-results/report.xml
9 | .idea
10 | .nyc_output
11 | coverage
12 | .gh-pages-tmp
13 |
14 | packages/core/lib
15 | packages/gui/lib
16 | packages/task-manager/lib
17 | packages/business-object-history/.sequelizerc
18 |
--------------------------------------------------------------------------------
/packages/examples/process-task-manager-ui/src/client/workflow/conditions.js:
--------------------------------------------------------------------------------
1 | export default {
2 | isJaneDoe: ({firstName, lastName}) => {
3 | return firstName === 'Jane' && lastName === 'Doe'
4 | },
5 |
6 | isEvenAged: ({age}) => {
7 | return parseInt(age) % 2 === 0;
8 | },
9 |
10 | isTrue: ({}) => {return true}
11 | }
12 |
--------------------------------------------------------------------------------
/packages/examples/complete-demo/src/server/routes/states.js:
--------------------------------------------------------------------------------
1 | import express from 'express'
2 | import schema from '../schema';
3 |
4 | const router = express.Router();
5 |
6 | router.get('/api/states', async(req, res) => {
7 | const { states } = await schema.getSchema();
8 | res.send({ states })
9 | })
10 |
11 | export default router;
12 |
--------------------------------------------------------------------------------
/packages/editor/src/components/ErrorLabel.react.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import Label from './Label.react';
4 |
5 | export default function ErrorLabel({ error }) {
6 | return (
7 |
8 | )
9 | }
10 |
11 | ErrorLabel.propTypes = {
12 | error: PropTypes.string
13 | }
14 |
--------------------------------------------------------------------------------
/packages/editor/src/components/ParamsEditor/components/DateInput.less:
--------------------------------------------------------------------------------
1 | .opuscapita_date-input__picker-container,
2 | .opuscapita_date-input__variants-container {
3 | z-index: 9999 !important;
4 | }
5 |
6 | .opuscapita_date-input input[type="text"], .opuscapita_date-input input[type="text"]:focus {
7 | box-shadow: none !important;
8 | padding: initial;
9 | }
10 |
--------------------------------------------------------------------------------
/packages/editor/src/components/CodeEditor/CodeEditor.react.js:
--------------------------------------------------------------------------------
1 | import CodeMirror from 'kvolkovich-sc-react-codemirror';
2 | import 'codemirror/lib/codemirror.css';
3 | import 'codemirror/theme/eclipse.css';
4 | import 'codemirror/mode/javascript/javascript';
5 | import _ from './codemirror-placeholder-mod'; // eslint-disable-line no-unused-vars
6 |
7 | export default CodeMirror;
8 |
--------------------------------------------------------------------------------
/packages/examples/process-task-manager-ui/src/client/workflow/actions.js:
--------------------------------------------------------------------------------
1 | export default {
2 | logAction: ({ object, from, to, event, context }) => {
3 | context.sendNotification({
4 | notification: {
5 | message: `Object with id: ${object.id} was pushed [${from} -> ${to}] by event: ${event}`,
6 | level: 'info'
7 | }
8 | });
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/lerna.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "3.0.2",
3 | "lerna": "2.8.0",
4 | "packages": [
5 | "packages/core",
6 | "packages/editor",
7 | "packages/examples/complete-demo",
8 | "packages/examples/invoice-console",
9 | "packages/examples/process-task-manager-ui",
10 | "packages/history",
11 | "packages/integration-tests/history",
12 | "packages/task-manager"
13 | ]
14 | }
15 |
--------------------------------------------------------------------------------
/packages/examples/complete-demo/src/server/data/actions/updateProcessedBy.js:
--------------------------------------------------------------------------------
1 | const action = args => {
2 | console.log('action "updateProcessedBy" executed')
3 | }
4 |
5 | action.paramsSchema = {
6 | "type": "object",
7 | "properties": {
8 | processedByFieldName: {
9 | "type": "string"
10 | }
11 | },
12 | "required": ["processedByFieldName"]
13 | }
14 |
15 | export default action;
16 |
--------------------------------------------------------------------------------
/packages/editor/www/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Workflow Editor
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | # Docker context should be ""
2 |
3 | FROM node:8-alpine
4 | LABEL maintainer="OpusCapita"
5 |
6 | ENV HOST 0.0.0.0
7 | ENV PORT 3020
8 |
9 | COPY ./ /demo
10 |
11 | WORKDIR /demo
12 |
13 | RUN npm i --unsafe-perm && \
14 | cd ./packages/examples/complete-demo && \
15 | npm run demo:build
16 |
17 | WORKDIR /demo/packages/examples/complete-demo
18 |
19 | CMD ["npm", "run", "demo:start"]
20 |
21 | EXPOSE $PORT
--------------------------------------------------------------------------------
/packages/editor/src/components/Guards/guardPropTypes.js:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 |
3 | export default PropTypes.oneOfType([
4 | PropTypes.shape({
5 | expression: PropTypes.string.isRequired
6 | }),
7 | PropTypes.shape({
8 | name: PropTypes.string.isRequired,
9 | params: PropTypes.arrayOf(PropTypes.shape({
10 | name: PropTypes.string.isRequired,
11 | value: PropTypes.any
12 | }))
13 | })
14 | ]);
15 |
--------------------------------------------------------------------------------
/packages/examples/process-task-manager-ui/src/client/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Process task manager
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/packages/examples/complete-demo/src/server/objectConfig/objectConfig.js:
--------------------------------------------------------------------------------
1 | import { resolve } from 'path';
2 | import { readJSONFromFile } from '../utils';
3 |
4 | const objectConfigurationPath = resolve(__dirname, './objectConfiguration.json');
5 |
6 | class ObjectConfig {
7 | init = async function() {
8 | this.config = await readJSONFromFile(objectConfigurationPath)
9 | }
10 | getConfig() {
11 | return this.config
12 | }
13 | }
14 |
15 | export default new ObjectConfig();
16 |
--------------------------------------------------------------------------------
/packages/editor/www/index-page.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import Showroom from '@opuscapita/react-showroom-client';
4 |
5 | const element = document.getElementById('main');
6 | const showroom = React.createElement(Showroom, {
7 | loaderOptions: {
8 | componentsInfo: require('.opuscapita-showroom/componentsInfo'),
9 | packagesInfo: require('.opuscapita-showroom/packageInfo')
10 | }
11 | });
12 |
13 | ReactDOM.render(showroom, element);
14 |
--------------------------------------------------------------------------------
/packages/editor/src/i18n/index.js:
--------------------------------------------------------------------------------
1 | import * as en from './en';
2 | import * as de from './de';
3 | import * as fi from './fi';
4 | import * as no from './no';
5 | import * as ru from './ru';
6 | import * as sv from './sv';
7 | import * as da from './da';
8 |
9 | const bundles = { en, de, fi, no, ru, sv, da };
10 |
11 | export default Object.keys(bundles).reduce((acc, lang) => ({
12 | ...acc,
13 | [lang]: {
14 | fsmWorkflowEditor: {
15 | ui: bundles[lang]
16 | }
17 | }
18 | }), {});
19 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig helps developers define and maintain consistent
2 | # coding styles between different editors and IDEs
3 | # editorconfig.org
4 |
5 | root = true
6 |
7 | [*]
8 |
9 | # Change these settings to your own preference
10 | indent_style = space
11 | indent_size = 2
12 |
13 | # We recommend you to keep these unchanged
14 | end_of_line = lf
15 | charset = utf-8
16 | trim_trailing_whitespace = true
17 | insert_final_newline = true
18 |
19 | [*.md]
20 | trim_trailing_whitespace = false
21 |
--------------------------------------------------------------------------------
/packages/editor/src/components/schemaConfigPropTypes.js:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 |
3 | export const releaseGuardsPropTypes = PropTypes.shape({
4 | toState: PropTypes.oneOf(['all', 'single', 'multiple'])
5 | });
6 |
7 | export const stateConfigPropTypes = PropTypes.shape({
8 | availableNames: PropTypes.arrayOf(PropTypes.string),
9 | releaseGuards: releaseGuardsPropTypes
10 | });
11 |
12 | export const schemaConfigPropTypes = PropTypes.shape({
13 | state: stateConfigPropTypes
14 | });
15 |
--------------------------------------------------------------------------------
/packages/history/src/models/migrations/20181218111220-create-index-WorkflowTransHist_boid_idx.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const indexName = 'WorkflowTransHist_boid_idx';
4 |
5 | module.exports = {
6 | up: (queryInterface, DataTypes) => queryInterface.addIndex(
7 | 'WorkflowTransitionHistory',
8 | {
9 | name: indexName,
10 | fields: ['businessObjId']
11 | }
12 | ),
13 | down: (queryInterface, DataTypes) => queryInterface.removeIndex('WorkflowTransitionHistory', indexName)
14 | };
15 |
--------------------------------------------------------------------------------
/packages/examples/complete-demo/src/server/data/conditions/userHasRoles.js:
--------------------------------------------------------------------------------
1 | const condition = args => {
2 | // console.log(`\n\ncondition "userHasRoles" executed with \n${JSON.stringify(args)}\n\n`)
3 | return true
4 | }
5 |
6 | condition.paramsSchema = {
7 | type: "object",
8 | properties: {
9 | restrictedRoles: {
10 | type: "array",
11 | items: {
12 | type: "string",
13 | enum: ["REV", "BOSS"]
14 | }
15 | }
16 | }
17 | }
18 |
19 | export default condition;
20 |
--------------------------------------------------------------------------------
/packages/editor/src/components/ParamsEditor/components/BooleanInput.react.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import Checkbox from 'react-bootstrap/lib/Checkbox';
4 |
5 | export default function BooleanInput({ value, onChange, ...props }) {
6 | return (
7 | onChange(value)}
11 | />
12 | )
13 | }
14 |
15 | BooleanInput.propTypes = {
16 | value: PropTypes.bool,
17 | onChange: PropTypes.func.isRequired
18 | }
19 |
--------------------------------------------------------------------------------
/packages/editor/src/components/StatesTable/statePropTypes.js:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import guardPropTypes from '../Guards/guardPropTypes';
3 |
4 | export const stateReleaseGuardsPropTypes = PropTypes.arrayOf(PropTypes.shape({
5 | to: PropTypes.oneOfType([
6 | PropTypes.string,
7 | PropTypes.arrayOf(PropTypes.string)
8 | ]),
9 | guards: PropTypes.arrayOf(guardPropTypes)
10 | }));
11 |
12 | export default PropTypes.shape({
13 | name: PropTypes.string,
14 | description: PropTypes.string,
15 | release: stateReleaseGuardsPropTypes
16 | });
17 |
--------------------------------------------------------------------------------
/packages/examples/invoice-console/machine-schema.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "invoice approval",
3 | "initialState": "open",
4 | "finalStates": ["approved"],
5 | "transitions": [{
6 | "from": "open",
7 | "event": "registration",
8 | "to": "registered"
9 | }, {
10 | "from": "registered",
11 | "event": "finalBooking",
12 | "to": "transferred"
13 | }, {
14 | "from": "open",
15 | "event": "finalBooking",
16 | "to": "transferred"
17 | }, {
18 | "from": "transferred",
19 | "event": "approve",
20 | "to": "approved"
21 | }]
22 | }
23 |
--------------------------------------------------------------------------------
/packages/examples/invoice-console/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "invoice",
3 | "version": "3.0.2",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "start": "babel-node --presets es2015,stage-0 -- index.js"
8 | },
9 | "private": true,
10 | "author": "Alexey Sergeev ",
11 | "license": "Apache-2.0",
12 | "dependencies": {
13 | "@opuscapita/fsm-workflow-core": "3.0.2",
14 | "babel-cli": "6.24.1",
15 | "babel-preset-es2015": "6.24.1",
16 | "babel-preset-stage-0": "6.24.1",
17 | "blessed": "0.1.81"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/packages/examples/complete-demo/src/client/uiGlobalComponents/uiMessageNotifications/styles.css:
--------------------------------------------------------------------------------
1 | .notification-error {
2 | color: #333;
3 | background-color: #fdf7f7;
4 | border-left: 3px solid #eed3d7;
5 | box-shadow: none;
6 | }
7 |
8 | .notification-error:before {
9 | content: "";
10 | }
11 |
12 | .notification-success {
13 | background-color: #f4f8fa;
14 | color: black;
15 | border-left: 3px solid #bce8f1;
16 | box-shadow: none;
17 | }
18 |
19 | .notification-success:before {
20 | content: "";
21 | }
22 |
23 | .notification {
24 | padding: 15px 0 15px 15px;
25 | }
26 |
27 | .message-error {
28 | color: red;
29 | }
--------------------------------------------------------------------------------
/packages/editor/src/components/Label.react.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import Label from 'react-bootstrap/lib/Label';
4 |
5 | export default function FixedLabel({ message, style, ...rest }) {
6 | return (
7 |
17 | )
18 | }
19 |
20 | FixedLabel.propTypes = {
21 | message: PropTypes.string,
22 | style: PropTypes.objectOf(PropTypes.string)
23 | }
24 |
--------------------------------------------------------------------------------
/packages/core/src/specs/Machine.currentState.spec.js:
--------------------------------------------------------------------------------
1 | import assert from 'assert';
2 | import Machine from '../Machine';
3 | import MachineDefinition from '../MachineDefinition';
4 |
5 | const createMachine = () => {
6 | return new Machine(
7 | {
8 | machineDefinition: new MachineDefinition(),
9 | }
10 | );
11 | }
12 |
13 | describe('machine: currentState', function() {
14 | it('returns correct value', function() {
15 | const state = 'new';
16 | const object = {
17 | [MachineDefinition.getDefaultObjectStateFieldName()]: state
18 | };
19 | assert.equal(createMachine().currentState({ object }), state);
20 | });
21 | });
22 |
--------------------------------------------------------------------------------
/packages/editor/src/components/Guards/Guards.less:
--------------------------------------------------------------------------------
1 | @editor-height: 200px;
2 | @editor-border: 1px solid #ddd;
3 |
4 | .guard-code .CodeMirror {
5 | height: @editor-height;
6 | border: @editor-border;
7 | }
8 | .guard-code .CodeMirror-empty {
9 | color: #777
10 | }
11 |
12 | .output-code .CodeMirror {
13 | height: @editor-height;
14 | border: @editor-border;
15 | background: #f7f7f7;
16 | }
17 |
18 | .example-object .CodeMirror {
19 | height: 440px;
20 | border: @editor-border;
21 | }
22 |
23 | .guards-table {
24 | vertical-align: top;
25 | table-layout: fixed;
26 | }
27 |
28 | .guards-table th {
29 | vertical-align: middle !important
30 | }
31 |
--------------------------------------------------------------------------------
/packages/editor/config/webpack.config.build.js:
--------------------------------------------------------------------------------
1 | const { resolve } = require('path');
2 | const merge = require('webpack-merge');
3 | const common = require('./webpack.config.common');
4 | const nodeExternals = require('webpack-node-externals');
5 |
6 | module.exports = merge(common, {
7 | mode: 'production',
8 | optimization: {
9 | minimize: false
10 | },
11 | entry: [
12 | '../src/index.js'
13 | ],
14 | output: {
15 | path: resolve(__dirname, '../lib'),
16 | filename: 'index.js',
17 | library: `fsm-workflow-editor`,
18 | libraryTarget: 'umd'
19 | },
20 | externals: [
21 | nodeExternals({
22 | modulesFromFile: true
23 | })
24 | ]
25 | });
26 |
--------------------------------------------------------------------------------
/packages/core/src/specs/Machine.isInFinalState.spec.js:
--------------------------------------------------------------------------------
1 | import assert from "assert";
2 | import Machine from "../Machine";
3 | import MachineDefinition from "../MachineDefinition";
4 |
5 | const createMachine = () => {
6 | return new Machine({
7 | machineDefinition: new MachineDefinition({
8 | schema: {
9 | finalStates: ["x", "y", "z"]
10 | }
11 | })
12 | });
13 | };
14 |
15 | describe("machine: isInFinalState", function() {
16 | it("returns correct value", function() {
17 | assert.equal(createMachine().isInFinalState({ object: { status: "x" } }), true);
18 | assert.equal(createMachine().isInFinalState({ object: { status: "a" } }), false);
19 | });
20 | });
21 |
--------------------------------------------------------------------------------
/packages/editor/src/components/Actions/ActionInvocationEditor.less:
--------------------------------------------------------------------------------
1 | @editor-height: 200px;
2 | @editor-border: 1px solid #ddd;
3 |
4 | .oc-fsm-crud-editor--modal-actions-object {
5 | height: 0;
6 | opacity: 0;
7 | transition: all .1s ease-in-out;
8 | }
9 |
10 | .oc-fsm-crud-editor--modal-actions-object.visible-object {
11 | height: @editor-height;
12 | opacity: 1;
13 | transition: all .1s ease-in-out;
14 | }
15 |
16 | .oc-fsm-crud-editor--modal-actions-object .CodeMirror {
17 | height:@editor-height;
18 | border: @editor-border;
19 | }
20 |
21 | .oc-fsm-crud-editor--modal-actions-results .CodeMirror {
22 | height:@editor-height;
23 | border: @editor-border;
24 | background: #f7f7f7;
25 | }
--------------------------------------------------------------------------------
/packages/history/src/index.spec.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const Sequelize = require('sequelize');
4 | const assert = require('assert');
5 |
6 | const makeHistory = require('./');
7 |
8 | describe('history', () => {
9 | let sequelize;
10 |
11 | before(async() => {
12 | sequelize = new Sequelize('sqlite:/:memory:');
13 | await makeHistory(sequelize);
14 | });
15 |
16 | it('should create index for table WorkflowTransitionHistory on field businessObjId', async() => {
17 | const res = await sequelize.getQueryInterface().showIndex('WorkflowTransitionHistory');
18 | assert(res.some(index => index.fields.length === 1 && index.fields[0].attribute === 'businessObjId'));
19 | });
20 | });
21 |
--------------------------------------------------------------------------------
/packages/core/src/specs/Machine.is.spec.js:
--------------------------------------------------------------------------------
1 | import assert from 'assert';
2 | import Machine from '../Machine';
3 | import MachineDefinition from '../MachineDefinition';
4 |
5 | const createMachine = () => {
6 | return new Machine(
7 | {
8 | machineDefinition: new MachineDefinition(),
9 | }
10 | );
11 | }
12 |
13 | describe('machine: is', function() {
14 | it('returns correct value', function() {
15 | const state = 'new';
16 | const object = {
17 | [MachineDefinition.getDefaultObjectStateFieldName()]: state
18 | };
19 | assert.equal(createMachine().is({ object, state }), true);
20 | assert.equal(createMachine().is({ object, state: 'incorrect' }), false);
21 | });
22 | });
23 |
--------------------------------------------------------------------------------
/packages/integration-tests/history/data/objectConfiguration.json:
--------------------------------------------------------------------------------
1 | {
2 | "stateFieldName": "status",
3 | "alias": "testObject",
4 | "schema": {
5 | "type": "object",
6 | "properties": {
7 | "objectId": {
8 | "type": "string"
9 | },
10 | "two": {
11 | "type": "string"
12 | },
13 | "three": {
14 | "type": "integer"
15 | },
16 | "status": {
17 | "type": "string",
18 | "enum": [
19 | "a",
20 | "b",
21 | "c",
22 | "d"
23 | ]
24 | }
25 | }
26 | },
27 | "example": {
28 | "objectId": "1111",
29 | "two": "Some text",
30 | "three": 3,
31 | "status": "a"
32 | }
33 | }
--------------------------------------------------------------------------------
/packages/integration-tests/history/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "fsm-history-integration-tests",
3 | "version": "3.0.2",
4 | "description": "",
5 | "main": "index.js",
6 | "private": true,
7 | "scripts": {
8 | "test": "rimraf data/testdb.sqlite && mocha"
9 | },
10 | "author": "Egor Stambakio ",
11 | "license": "Apache-2.0",
12 | "dependencies": {
13 | "@opuscapita/fsm-workflow-core": "3.0.2",
14 | "@opuscapita/fsm-workflow-history": "3.0.2",
15 | "chai": "4.1.2",
16 | "mocha": "3.5.0",
17 | "mocha-junit-reporter": "1.13.0",
18 | "nyc": "11.1.0",
19 | "rimraf": "2.6.2",
20 | "sequelize": "6.6.5",
21 | "sequelizer": "1.1.4",
22 | "sqlite3": "4.1.1"
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/packages/core/src/specs/Machine.availableStates.spec.js:
--------------------------------------------------------------------------------
1 | import assert from 'assert';
2 | import Machine from '../Machine';
3 |
4 | describe('machine: availableStates', function() {
5 | it('arguments are correctly pased to machineDefinition.findAvailableTransitions', function() {
6 | let getAvailableStatesIsCalled = false;
7 |
8 | const createMachine = () => {
9 | return new Machine(
10 | {
11 | machineDefinition: {
12 | getAvailableStates: () => {
13 | getAvailableStatesIsCalled = true;
14 | }
15 | }
16 | }
17 | );
18 | }
19 |
20 | createMachine().availableStates();
21 |
22 | assert.equal(getAvailableStatesIsCalled, true);
23 | });
24 | });
25 |
--------------------------------------------------------------------------------
/packages/task-manager/src/utils.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Enforces process started with @DoUntil to stop
3 | *
4 | * @param processDecriptor - timer object
5 | */
6 | export function killProcess(processDecriptor) {
7 | clearInterval(processDecriptor);
8 | }
9 |
10 | /**
11 | * Executes @action avery @timeout until @test function returns 'false'
12 | *
13 | * @param action - function to call per iteration
14 | * @param test - true|false guard function
15 | * @param timeout - timeout in millis (1 sec by default)
16 | */
17 | export function doUntil(action, test, timeout = 1000) {
18 | let timer = setInterval(() => {
19 | action();
20 | if (!test()) {
21 | killProcess(timer);
22 | }
23 | }, timeout);
24 |
25 | return timer;
26 | }
27 |
--------------------------------------------------------------------------------
/packages/examples/complete-demo/src/server/routes/history.js:
--------------------------------------------------------------------------------
1 | import express from 'express'
2 | import fsm from '../fsm';
3 |
4 | const router = express.Router();
5 |
6 | router.get('/api/history/:objectId', async(req, res) => {
7 | const { objectId } = req.params;
8 | if (!objectId) {
9 | return res.status(400).send({ error: 'Bad request: objectId not specified' })
10 | }
11 | const historyData = await fsm.history.search({
12 | searchParameters: {
13 | object: {
14 | businessObjId: objectId,
15 | businessObjType: 'invoice'
16 | }
17 | }
18 | });
19 | const history = historyData.
20 | filter(({ event }) => event !== '__START__');
21 | return res.send({ history })
22 | })
23 |
24 | export default router;
25 |
--------------------------------------------------------------------------------
/packages/editor/src/components/Actions/ActionsTable.less:
--------------------------------------------------------------------------------
1 | .oc-fsm-crud-editor--table-actions td {
2 | vertical-align: top !important;
3 | }
4 |
5 | .oc-fsm-crud-editor--table-actions {
6 | table-layout: fixed;
7 | }
8 |
9 | .oc-fsm-crud-editor--table-actions .output-code .CodeMirror {
10 | height: 370px;
11 | }
12 |
13 | .oc-fsm-crud-editor--table-actions .example-object .CodeMirror {
14 | height: 370px;
15 | }
16 |
17 | .oc-fsm-crud-editor--table-actions-parameters {
18 | table-layout: fixed;
19 | width: 100%;
20 | }
21 |
22 | .oc-fsm-crud-editor--table-actions-parameters td {
23 | padding: 8px 5px 8px 0;
24 | overflow-wrap: break-word;
25 | }
26 |
27 | .oc-fsm-crud-editor--table-actions-parameters td.parameter-value {
28 | font-weight: bold;
29 | }
--------------------------------------------------------------------------------
/packages/editor/src/components/Actions/utils.js:
--------------------------------------------------------------------------------
1 | import get from 'lodash/get';
2 |
3 | const evaluateArgs = (actionArgs, commonArgs) => ({
4 | ...actionArgs.reduce((customArgsObject, { name, type, value }) => ({
5 | ...customArgsObject,
6 | [name]: type === 'pathExpression' ? // not implemented in editor yet
7 | get(commonArgs[value.variable], value.path) :
8 | value
9 | }), {}),
10 | ...commonArgs
11 | })
12 |
13 | export const invokeAction = (name, actionArgs, commonArgs) => `Action "${name}" called with parameters:\n` +
14 | JSON.stringify(evaluateArgs(actionArgs, commonArgs), null, 2)
15 |
16 | export const getParamSchema = ({ actions, action, param }) => (
17 | (actions[action].paramsSchema || {}).properties || {}
18 | )[param];
19 |
--------------------------------------------------------------------------------
/packages/editor/src/components/ParamsEditor/components/StringInput.react.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import FormControl from 'react-bootstrap/lib/FormControl';
4 |
5 | export default function StringInput({ value, onChange, ...props }, { i18n }) {
6 | return (
7 | onChange(value)}
13 | />
14 | )
15 | }
16 |
17 | StringInput.propTypes = {
18 | value: PropTypes.string,
19 | onChange: PropTypes.func.isRequired
20 | }
21 |
22 | StringInput.contextTypes = {
23 | i18n: PropTypes.object.isRequired
24 | }
25 |
--------------------------------------------------------------------------------
/packages/editor/src/components/EditorOutput.react.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import WorkflowGraph from './WorkflowGraph';
4 |
5 | export default function EditorOutput({ schema }, { i18n }) {
6 | return (
7 |
8 |
{i18n.getMessage('fsmWorkflowEditor.ui.preview.title')}
9 |
{i18n.getMessage('fsmWorkflowEditor.ui.preview.description')}
10 |
11 |
12 |
13 |
14 | )
15 | }
16 |
17 | EditorOutput.propTypes = {
18 | schema: PropTypes.object.isRequired,
19 | createJsonOutput: PropTypes.func.isRequired
20 | }
21 |
22 | EditorOutput.contextTypes = {
23 | i18n: PropTypes.object.isRequired
24 | }
25 |
--------------------------------------------------------------------------------
/packages/examples/complete-demo/src/server/routes/sendEvent.js:
--------------------------------------------------------------------------------
1 | import express from 'express'
2 | import fsm from '../fsm';
3 | import storage from '../storage';
4 | import { extractObject } from '../utils';
5 |
6 | const router = express.Router();
7 |
8 | router.post('/api/event', async(req, res) => {
9 | const { objectId, event } = req.body;
10 | const object = await storage.getObjectById(objectId);
11 | const { machine } = fsm;
12 | machine.sendEvent({ object: extractObject(object), event, user: 'demouser' }).
13 | then(async(result) => {
14 | await storage.updateObject(result.object)
15 | res.send(result)
16 | }).
17 | catch(err => {
18 | console.log('send event failed')
19 | console.log(err)
20 | res.status(500).send({ error: 'Send event failed!' })
21 | })
22 | })
23 |
24 | export default router;
25 |
--------------------------------------------------------------------------------
/packages/history/src/models/migrations/20200320081228-insert-WorkflowTransHist-indexes.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const TABLE = 'WorkflowTransitionHistory';
3 | const SEARCH_INDEX = 'WorkflowTransitionHistory_boID_boType_to';
4 | const FINISHEDON_INDEX = 'WorkflowTransitionHistory_FinishedOn';
5 |
6 | module.exports = {
7 | up: (queryInterface) => {
8 | queryInterface.addIndex(
9 | TABLE,
10 | {
11 | name: SEARCH_INDEX,
12 | fields: ['businessObjId', 'businessObjType', 'to'],
13 | }
14 | );
15 | queryInterface.addIndex(
16 | TABLE,
17 | {
18 | name: FINISHEDON_INDEX,
19 | fields: ['finishedOn'],
20 | }
21 | );
22 | },
23 | down: (queryInterface) => {
24 | queryInterface.removeIndex(TABLE, FINISHEDON_INDEX);
25 | queryInterface.removeIndex(TABLE, SEARCH_INDEX);
26 | }
27 | };
28 |
--------------------------------------------------------------------------------
/packages/editor/config/mocha-config.js:
--------------------------------------------------------------------------------
1 | // required only by Mocha
2 |
3 | // set node env
4 | process.env.NODE_ENV = 'test';
5 |
6 | const JSDOM = require('jsdom').JSDOM;
7 |
8 | require('babel-register')({
9 | babelrc: false,
10 | presets: [
11 | ["env", {
12 | "targets": {
13 | "browsers": ["last 2 versions", "ie >= 11", "safari >= 7", "Firefox ESR"]
14 | }
15 | }],
16 | "react"
17 | ],
18 | plugins: [
19 | "transform-decorators-legacy",
20 | "transform-class-properties",
21 | "transform-object-rest-spread"
22 | ],
23 | env: {
24 | test: {
25 | plugins: ["istanbul"]
26 | }
27 | }
28 | })
29 |
30 | global.document = new JSDOM('');
31 | global.window = global.document.window;
32 | global.document = window.document;
33 | global.navigator = global.window.navigator;
34 | global.self = global.window;
35 |
--------------------------------------------------------------------------------
/packages/examples/complete-demo/src/server/routes/availableTransitions.js:
--------------------------------------------------------------------------------
1 | import express from 'express'
2 | import fsm from '../fsm';
3 | import storage from '../storage';
4 | import { objectIdProp } from '../../common';
5 | import { extractObject } from '../utils';
6 |
7 | const router = express.Router();
8 |
9 | router.post('/api/transitions', async(req, res) => {
10 | const { objectId } = req.body;
11 | const objects = (await storage.getAllObjects()).map(extractObject);
12 | const object = objects.find(obj => obj[objectIdProp] === objectId);
13 | const { machine } = fsm;
14 | machine.availableTransitions({ object }).
15 | then(result => {
16 | res.send(result)
17 | }).
18 | catch(err => {
19 | console.log('getAvailableTransitions failed')
20 | console.log(err)
21 | res.status(500).send({ error: 'Get transitions failed!' })
22 | })
23 | })
24 |
25 | export default router;
26 |
--------------------------------------------------------------------------------
/packages/editor/src/components/Select/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import ReactSelect from '@opuscapita/react-select';
4 |
5 | const getI18nReactSelectProps = ({ i18n }) => {
6 | const t = (path, args) => i18n.getMessage(`fsmWorkflowEditor.ui.select.${path}`, args);
7 | return ({
8 | searchPromptText: t('typeToSearch'),
9 | promptTextCreator: (option) => t('createOption', { option }),
10 | clearValueText: t('clearValue'),
11 | clearAllText: t('clearAll'),
12 | noResultsText: t('nothingFound'),
13 | loadingPlaceholder: t('loading')
14 | });
15 | }
16 |
17 | export default function Select(props, { i18n }) {
18 | return (
19 |
24 | )
25 | }
26 |
27 | Select.contextTypes = {
28 | i18n: PropTypes.object.isRequired
29 | }
30 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@opuscapita/fsm-workflow",
3 | "private": true,
4 | "license": "Apache-2.0",
5 | "devDependencies": {
6 | "junit-merge": "1.2.3",
7 | "lerna": "2.8.0",
8 | "mkdirp": "0.5.1",
9 | "rimraf": "2.6.2"
10 | },
11 | "scripts": {
12 | "add-dependency": "lerna add",
13 | "bootstrap": "lerna bootstrap",
14 | "clean": "lerna clean --yes",
15 | "diff": "lerna diff",
16 | "lint": "lerna run lint",
17 | "postinstall": "npm run bootstrap",
18 | "start": "lerna run start --parallel",
19 | "updated": "lerna updated || exit 0",
20 | "test": "lerna run test --parallel && rimraf test-results && mkdir test-results && junit-merge -r packages/**/test-results.xml -o test-results/report.xml",
21 | "publish": "lerna publish --exact --skip-git --force-publish=* --repo-version $(jq -r '.version' lerna.json) --yes"
22 | },
23 | "engines": {
24 | "node": ">= 8.x"
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/packages/examples/complete-demo/src/server/data/actions/sendMail.js:
--------------------------------------------------------------------------------
1 | const action = args => {
2 | // console.log(`\n\naction "sendMail" executed with \n${JSON.stringify(args)}\n\n`)
3 | }
4 |
5 | action.paramsSchema = {
6 | "type": "object",
7 | "properties": {
8 | fromAddress: {
9 | "type": "string"
10 | },
11 | greeting: {
12 | "type": "string"
13 | },
14 | sendCopy: {
15 | "type": "boolean"
16 | },
17 | maxRetries: {
18 | "type": "integer"
19 | },
20 | interest: {
21 | "type": "number"
22 | },
23 | language: {
24 | "type": "string",
25 | "enum": ["en", "de", "fi", "ru", "sv", "no"]
26 | },
27 | priority: {
28 | "type": "integer",
29 | "enum": [0, 1, 2, 3, 4, 5]
30 | },
31 | expiryDate: {
32 | "type": "string",
33 | "format": "date"
34 | }
35 | },
36 | "required": ["fromAddress", "greeting"]
37 | }
38 |
39 | export default action;
40 |
--------------------------------------------------------------------------------
/packages/examples/complete-demo/src/server/routes/objects.js:
--------------------------------------------------------------------------------
1 | import express from 'express'
2 | import fsm from '../fsm';
3 | import storage from '../storage';
4 | import { eventsProp } from '../../common'
5 | import { extractObject } from '../utils';
6 |
7 | const router = express.Router();
8 |
9 | router.get('/api/objects', async(req, res) => {
10 | const objects = await storage.getAllObjects();
11 | const businessObjects = objects.map(extractObject);
12 | const { machine } = fsm;
13 | Promise.all(businessObjects.map(object => machine.availableTransitions({ object }))).
14 | then(result => {
15 | res.send(businessObjects.map((object, index) => ({
16 | ...object,
17 | [eventsProp]: result[index].transitions.map(({ event }) => event)
18 | })))
19 | }).
20 | catch(err => {
21 | console.log(err)
22 | res.status(500).send({ error: err.message })
23 | throw err
24 | })
25 | })
26 |
27 | export default router;
28 |
--------------------------------------------------------------------------------
/packages/history/src/models/migrations/20180123102827-create-workflowTransitionHistory.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = {
4 | up: (queryInterface, DataTypes) => queryInterface.createTable('WorkflowTransitionHistory', {
5 | id: { type: DataTypes.INTEGER, allowNull: false, primaryKey: true, autoIncrement: true },
6 | from: { type: DataTypes.STRING, allowNull: false },
7 | to: { type: DataTypes.STRING, allowNull: false },
8 | event: { type: DataTypes.STRING, allowNull: false },
9 | businessObjType: { type: DataTypes.STRING, allowNull: false },
10 | businessObjId: { type: DataTypes.STRING, allowNull: false },
11 | user: { type: DataTypes.STRING, allowNull: false },
12 | workflowName: { type: DataTypes.STRING, allowNull: false },
13 | description: { type: DataTypes.TEXT },
14 | finishedOn: { type: DataTypes.DATE, allowNull: false }
15 | }),
16 | down: (queryInterface, DataTypes) => queryInterface.dropTable('WorkflowTransitionHistory')
17 | };
18 |
--------------------------------------------------------------------------------
/packages/history/src/models/definitions/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const fs = require('fs');
4 | const path = require('path');
5 | const { promisify } = require('util');
6 | const Sequelize = require('sequelize');
7 |
8 | const basename = path.basename(__filename);
9 | const readdir = promisify(fs.readdir);
10 |
11 | /*
12 | * The function import models into sequelize
13 | * and returns updated sequelize.
14 | */
15 | module.exports = async function(sequelize) {
16 | const models = (await readdir(__dirname)).
17 | filter(file => file.indexOf('.') !== 0 && (file !== basename) && (file.slice(-3) === '.js')).
18 | reduce(
19 | (models, file) => {
20 | const model = require(path.join(__dirname, file))(sequelize, Sequelize);
21 |
22 | return Object.assign(models, {
23 | [model.name]: model
24 | });
25 | },
26 | {}
27 | );
28 |
29 | Object.values(models).forEach(model => model.associate && model.associate(models));
30 | return sequelize;
31 | };
32 |
--------------------------------------------------------------------------------
/packages/core/src/specs/Machine.isRunning.spec.js:
--------------------------------------------------------------------------------
1 | import assert from 'assert';
2 | import Machine from '../Machine';
3 |
4 | describe('machine: isRunning', function() {
5 | it('returns correct values', function() {
6 | const machine = new Machine(
7 | {
8 | machineDefinition: {
9 | getAvailableStates: () => ['started', 'finished'],
10 | schema: {
11 | finalStates: ['finished']
12 | },
13 | objectConfiguration: {
14 | stateFieldName: 'status'
15 | }
16 | }
17 | }
18 | );
19 |
20 | // object in undeclared state
21 | let object = { status: 'none' };
22 | assert.equal(machine.isRunning({ object }), false);
23 |
24 | // object in non final state
25 | object = { status: 'started' };
26 | assert.equal(machine.isRunning({ object }), true);
27 |
28 | // object in final state
29 | object = { status: 'finished' };
30 | assert.equal(machine.isRunning({ object }), false);
31 | });
32 | });
33 |
--------------------------------------------------------------------------------
/packages/editor/src/components/ParamsEditor/components/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import StringInput from './StringInput.react';
3 | import BooleanInput from './BooleanInput.react';
4 | import IntegerInput from './IntegerInput.react';
5 | import DecimalInput from './DecimalInput.react';
6 | import EnumInput from './EnumInput.react';
7 | import DateInput from './DateInput.react';
8 |
9 | const components = {
10 | string: StringInput,
11 | boolean: BooleanInput,
12 | integer: IntegerInput,
13 | number: DecimalInput
14 | }
15 |
16 | export default (schema = {}) => {
17 | const { type } = schema;
18 |
19 | let Component = components[type] || components.string;
20 |
21 | if (schema.enum) {
22 | Component = props => (
23 |
28 | )
29 | }
30 |
31 | if (type === 'string' && schema.format === 'date') {
32 | Component = DateInput
33 | }
34 |
35 | return Component
36 | }
37 |
38 |
--------------------------------------------------------------------------------
/packages/history/src/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const Umzug = require('umzug');
4 | const path = require('path');
5 |
6 | const importModels = require('./models/definitions');
7 | const interfaceModels = require('./models/interfaces');
8 |
9 | const MIGRATIONS_DIR = './models/migrations';
10 |
11 | /*
12 | * The function runs pending migrations from MIGRATIONS_DIR,
13 | * (re)imports the package's models into sequelize
14 | * and returns a promise.
15 | * The promise is resolved with { add, search } object.
16 | * The promise is rejected with an error
17 | */
18 | module.exports = sequelize => new Umzug({
19 | storage: 'sequelize',
20 | storageOptions: { sequelize },
21 | migrations: {
22 | params: [
23 | sequelize.getQueryInterface(), // queryInterface
24 | sequelize.constructor // DataTypes
25 | ],
26 | path: path.join(__dirname, MIGRATIONS_DIR),
27 | pattern: /\.js$/
28 | }
29 | }).
30 | up().
31 | then(_ => importModels(sequelize)).
32 | then(_ => interfaceModels(sequelize));
33 |
--------------------------------------------------------------------------------
/packages/examples/complete-demo/src/server/fsm/fsm.js:
--------------------------------------------------------------------------------
1 | import { Machine, MachineDefinition } from '@opuscapita/fsm-workflow-core';
2 | import history from '@opuscapita/fsm-workflow-history';
3 | import actions from '../data/actions';
4 | import conditions from '../data/conditions';
5 | import { objectIdProp } from '../../common';
6 | import schema from '../schema';
7 | import objectConfig from '../objectConfig';
8 |
9 | class FSM {
10 | init = async function(sequelize) {
11 | this.history = await history(sequelize)
12 | }
13 |
14 | get machine() {
15 | return new Machine({
16 | machineDefinition: new MachineDefinition({
17 | schema: schema.getSchema(),
18 | actions,
19 | conditions,
20 | objectConfiguration: objectConfig.getConfig()
21 | }),
22 | convertObjectToReference: object => ({
23 | businessObjType: 'invoice',
24 | businessObjId: object[objectIdProp]
25 | }),
26 | history: this.history
27 | })
28 | }
29 | }
30 |
31 | export default new FSM();
32 |
--------------------------------------------------------------------------------
/packages/examples/complete-demo/src/server/routes/editorData.js:
--------------------------------------------------------------------------------
1 | import express from 'express'
2 | import currentSchema from '../schema';
3 | import objectConfig from '../objectConfig';
4 | import { mapFuncsToParamsSchema } from '../utils';
5 | import actions from '../data/actions';
6 | import conditions from '../data/conditions';
7 |
8 | const router = express.Router();
9 |
10 | router.get('/api/editordata', async(req, res) => {
11 | const schema = currentSchema.getSchema();
12 | const objectConfiguration = objectConfig.getConfig();
13 | res.send({
14 | schema,
15 | actions: mapFuncsToParamsSchema(actions),
16 | conditions: mapFuncsToParamsSchema(conditions),
17 | objectConfiguration
18 | })
19 | })
20 |
21 | router.post('/api/editordata', async(req, res) => {
22 | const { schema } = req.body;
23 | if (schema) {
24 | await currentSchema.setSchema(schema);
25 | res.send({ status: 'OK', schema })
26 | } else {
27 | res.status(400).send({ error: 'Schema in undefined' })
28 | }
29 | })
30 |
31 | export default router;
32 |
--------------------------------------------------------------------------------
/packages/examples/complete-demo/src/server/schema/schema.js:
--------------------------------------------------------------------------------
1 | import fs from 'fs';
2 | import { resolve } from 'path';
3 | import { promisify } from 'util';
4 | import { readJSONFromFile } from '../utils';
5 |
6 | const writeFile = promisify(fs.writeFile);
7 | const schemaFilePath = resolve(__dirname, './workflow-schema.json');
8 | const defaultSchemaFilePath = resolve(__dirname, './default-schema.json');
9 |
10 | class Schema {
11 | init = async function() {
12 | this.schema = await readJSONFromFile(schemaFilePath).
13 | catch(err => {
14 | if (err.code === 'ENOENT') {
15 | // 'schemaFilePath' file does not exist yet -> read default schema
16 | return readJSONFromFile(defaultSchemaFilePath)
17 | }
18 | throw err
19 | })
20 | }
21 |
22 | getSchema() {
23 | return this.schema
24 | }
25 |
26 | setSchema = async function(schema) {
27 | this.schema = schema;
28 | return writeFile(schemaFilePath, JSON.stringify(schema, null, 2))
29 | }
30 | }
31 |
32 | export default new Schema();
33 |
--------------------------------------------------------------------------------
/packages/editor/src/components/ParamsEditor/components/PathExpressionInputInjector/ExpressionInput.react.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { formatArg } from '../../../utils';
4 |
5 | export default class ExpressionInput extends PureComponent {
6 | static propTypes = {
7 | value: PropTypes.any,
8 | onClick: PropTypes.func.isRequired
9 | }
10 |
11 | static contextTypes = {
12 | i18n: PropTypes.object.isRequired
13 | }
14 |
15 | render() {
16 | const { i18n } = this.context;
17 | const { value } = this.props;
18 | return (
19 |
32 | )
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/packages/examples/process-task-manager-ui/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "process-task-manager-ui",
3 | "version": "3.0.2",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "start": "babel-node --presets es2015,stage-0 src/server/index"
8 | },
9 | "private": true,
10 | "author": "Daniel Zhitomirsky ",
11 | "license": "Apache-2.0",
12 | "dependencies": {
13 | "@opuscapita/fsm-workflow-core": "3.0.2",
14 | "@opuscapita/fsm-workflow-task-manager": "3.0.2",
15 | "babel-cli": "7.0.0-alpha.12",
16 | "babel-core": "7.0.0-alpha.12",
17 | "babel-loader": "7.0.0",
18 | "babel-preset-es2015": "7.0.0-alpha.12",
19 | "babel-preset-react": "7.0.0-alpha.12",
20 | "babel-preset-stage-0": "7.0.0-alpha.12",
21 | "babel-register": "7.0.0-alpha.12",
22 | "bluebird": "3.5.0",
23 | "express": "4.15.1",
24 | "react": "15.4.2",
25 | "react-dom": "15.4.2",
26 | "react-notification-system": "0.2.14",
27 | "serve-favicon": "2.4.3",
28 | "webpack": "2.6.1",
29 | "webpack-dev-middleware": "1.10.2"
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/packages/editor/src/components/TopForm.spec.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { expect } from 'chai';
3 | import Enzyme from 'enzyme';
4 | import Adapter from 'enzyme-adapter-react-15';
5 | import { mount } from 'enzyme';
6 | import sinon from 'sinon';
7 | import FormControl from 'react-bootstrap/lib/FormControl';
8 | import TopForm from './TopForm.react';
9 | import { I18nManager } from '@opuscapita/i18n';
10 |
11 | Enzyme.configure({ adapter: new Adapter() });
12 |
13 | describe('', () => {
14 | it('renders top form', () => {
15 | const props = {
16 | name: 'Invoice workflow',
17 | onNameChange: sinon.spy()
18 | };
19 |
20 | const wrapper = mount(, { context: { i18n: new I18nManager() } });
21 | expect(wrapper).to.exist; // eslint-disable-line no-unused-expressions
22 | expect(wrapper.contains(FormControl)).to.be.true; // eslint-disable-line no-unused-expressions
23 | expect(wrapper.find(FormControl).prop('value')).to.equal(props.name);
24 | expect(wrapper.find(FormControl).prop('onChange')).to.equal(props.onNameChange);
25 | });
26 | });
27 |
--------------------------------------------------------------------------------
/packages/editor/config/webpack.config.showroom.js:
--------------------------------------------------------------------------------
1 | const { resolve } = require('path');
2 | const webpack = require('webpack');
3 | const merge = require('webpack-merge');
4 | const common = require('./webpack.config.common');
5 | const HtmlWebpackPlugin = require('html-webpack-plugin');
6 | // const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
7 |
8 | module.exports = merge(common, {
9 | mode: 'development',
10 | plugins: [
11 | new HtmlWebpackPlugin({
12 | template: '../www/index.html',
13 | }),
14 | new webpack.NamedModulesPlugin(),
15 | // new BundleAnalyzerPlugin()
16 | ],
17 | entry: [
18 | '../www/index-page.js'
19 | ],
20 | output: {
21 | path: resolve(__dirname, '../.gh-pages-tmp'),
22 | filename: 'bundle.js'
23 | },
24 | devServer: {
25 | historyApiFallback: true,
26 | // inline: false
27 | },
28 | module: {
29 | rules: [
30 | {
31 | test: /\.md$/,
32 | loader: 'raw-loader'
33 | },
34 | {
35 | test: /\.json$/,
36 | loader: 'json-loader'
37 | }
38 | ]
39 | }
40 | });
41 |
--------------------------------------------------------------------------------
/packages/editor/src/components/WorkflowEditor/styles.less:
--------------------------------------------------------------------------------
1 | .oc-fsm-crud-editor--table td {
2 | vertical-align: middle !important
3 | }
4 |
5 | .oc-fsm-crud-editor--workflow-editor__tab {
6 | height: 480px;
7 | overflow: auto;
8 | border: 1px solid #ddd;
9 | border-top: none;
10 | }
11 |
12 | .oc-fsm-crud-editor--modal {
13 | width: 90%
14 | }
15 |
16 | .oc-fsm-crud-editor--modal-heading.with-padding {
17 | padding: 10px 0 10px
18 | }
19 |
20 | .oc-fsm-crud-editor--modal-heading .output-heading {
21 | display: flex;
22 | align-items: center;
23 | justify-content: space-between;
24 | }
25 |
26 | .oc-fsm-crud-editor--modal-heading .output-heading .right-block {
27 | display: flex;
28 | flex-direction: row;
29 | align-items: center;
30 | min-width: 100px;
31 | justify-content: space-between;
32 | }
33 |
34 | .oc-fsm-crud-editor--title {
35 | display: flex;
36 | align-items: center;
37 | justify-content: space-between;
38 | }
39 |
40 | .oc-fsm-crud-editor--label {
41 | max-width: 100%;
42 | overflow: hidden;
43 | text-overflow: ellipsis;
44 | margin-top: 4px;
45 | display: inline-block;
46 | }
--------------------------------------------------------------------------------
/packages/history/.npmignore:
--------------------------------------------------------------------------------
1 | src/demo
2 | *.spec.js
3 | *~
4 | *.swp
5 | *.kate-swp
6 | *.out
7 | *.iml
8 | *.lock
9 | package-lock.json
10 |
11 | # All the below is taken from
12 | # https://github.com/github/gitignore/blob/master/Node.gitignore
13 | # except for some /node_modules subfolders.
14 |
15 | # Logs
16 | logs
17 | *.log
18 | npm-debug.log*
19 |
20 | # Runtime data
21 | pids
22 | *.pid
23 | *.seed
24 | *.pid.lock
25 |
26 | # Directory for instrumented libs generated by jscoverage/JSCover
27 | lib-cov
28 |
29 | # Coverage directory used by tools like istanbul
30 | coverage
31 |
32 | # nyc test coverage
33 | .nyc_output
34 |
35 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
36 | .grunt
37 |
38 | # node-waf configuration
39 | .lock-wscript
40 |
41 | # Compiled binary addons (http://nodejs.org/api/addons.html)
42 | build/Release
43 |
44 | # Dependency directories
45 | jspm_packages/
46 | /node_modules/*
47 |
48 | # Optional npm cache directory
49 | .npm
50 |
51 | # Optional eslint cache
52 | .eslintcache
53 |
54 | # Optional REPL history
55 | .node_repl_history
56 | .DS_Store
57 |
--------------------------------------------------------------------------------
/packages/examples/process-task-manager-ui/src/server/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const express = require('express');
4 | const fs = require('fs');
5 | const port = 3000;
6 | const host = 'localhost'
7 | const path = require('path');
8 | const webpack = require('webpack');
9 | const compiler = webpack(require('../../webpack.development.config'));
10 |
11 | const app = express();
12 |
13 | let serverOptions = {
14 | watchOptions: {
15 | aggregateTimeout: 300,
16 | poll: true
17 | },
18 | headers: {'Access-Control-Allow-Origin': '*'},
19 | stats: { colors: true },
20 | noInfo: true
21 | };
22 |
23 | app.use(require('webpack-dev-middleware')(compiler, serverOptions));
24 |
25 | app.use(require('serve-favicon')(path.join(__dirname, '../client/static/favicon.ico')));
26 |
27 | app.use(express.static(path.join(__dirname, '../client/static')));
28 | app.get('*', (req, res) => {
29 | res.sendFile(path.join(__dirname, '/../client/index.html'));
30 | });
31 |
32 | app.listen(port, (err) => {
33 | if (err) {
34 | console.log(err);
35 | }
36 | console.log(`The server is running at http://${host}:${port}/`);
37 | });
38 |
--------------------------------------------------------------------------------
/packages/editor/src/components/StatesTable/StateEditor.spec.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { expect } from 'chai';
3 | import Enzyme from 'enzyme';
4 | import Adapter from 'enzyme-adapter-react-15';
5 | import { mount } from 'enzyme';
6 | import StateEditor from './StateEditor.react';
7 | import Modal from 'react-bootstrap/lib/Modal';
8 | import { I18nManager } from '@opuscapita/i18n';
9 |
10 | Enzyme.configure({ adapter: new Adapter() });
11 |
12 | describe('', () => {
13 | it('renders an editor modal', () => {
14 | const props = {
15 | state: {
16 | name: 'one',
17 | isInitial: true,
18 | isFinal: false
19 | },
20 | existingStates: ['one', 'two', 'three'],
21 | onSave: () => {},
22 | onClose: () => {},
23 | };
24 |
25 | const wrapper = mount(, { context: { i18n: new I18nManager() } });
26 | expect(wrapper).to.exist; // eslint-disable-line no-unused-expressions
27 | expect(wrapper.find('modal-header button.close')).to.exist; // eslint-disable-line no-unused-expressions
28 | expect(wrapper.find(Modal).length).to.equal(1);
29 | });
30 | });
31 |
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | Dockerfile
2 | #.dockerignore
3 |
4 | .gitignore
5 | .git/
6 | bower_components/
7 |
8 | *~
9 | *.swp
10 |
11 | db.config.json
12 |
13 | # All the below is taken from
14 | # https://github.com/github/gitignore/blob/master/Node.gitignore
15 |
16 | # Logs
17 | logs
18 | *.log
19 | npm-debug.log*
20 |
21 | # Runtime data
22 | pids
23 | *.pid
24 | *.seed
25 | *.pid.lock
26 |
27 | # Directory for instrumented libs generated by jscoverage/JSCover
28 | lib-cov
29 |
30 | # Coverage directory used by tools like istanbul
31 | coverage
32 |
33 | #Nginx.conf example
34 | nginx.conf.example
35 |
36 | # nyc test coverage
37 | .nyc_output
38 |
39 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
40 | .grunt
41 |
42 | # node-waf configuration
43 | .lock-wscript
44 |
45 | # Compiled binary addons (http://nodejs.org/api/addons.html)
46 | build/Release
47 |
48 | # Dependency directories
49 | # jspm_packages/
50 | **/node_modules
51 |
52 | # Optional npm cache directory
53 | .npm
54 |
55 | # Optional eslint cache
56 | .eslintcache
57 |
58 | # Optional REPL history
59 | .node_repl_history
60 |
61 | #For npm ^5.0.1
62 | package-lock.json
63 |
--------------------------------------------------------------------------------
/packages/core/src/specs/MachineDefinition.objectAlias.spec.js:
--------------------------------------------------------------------------------
1 | import assert from 'assert';
2 | import MachineDefinition from '../MachineDefinition';
3 |
4 | const object = { brand: "tesla" };
5 |
6 | describe('machine definition: object alias', function() {
7 | it('prepareObjectAlias: objectConfiguration is not defined -> empty object is returned', () => {
8 | const machineDefinition = new MachineDefinition();
9 | assert.deepEqual(machineDefinition.prepareObjectAlias(object), {});
10 | });
11 |
12 | it('prepareObjectAlias: objectConfiguration is defined, but alias is not defined -> empty object is returned', () => {
13 | const machineDefinition = new MachineDefinition({
14 | objectConfiguration: {}
15 | });
16 | assert.deepEqual(machineDefinition.prepareObjectAlias(object), {});
17 | });
18 |
19 | it('prepareObjectAlias: objectConfiguration.alias is defined -> {: object} is returned', () => {
20 | const machineDefinition = new MachineDefinition({
21 | objectConfiguration: {
22 | alias: "car"
23 | }
24 | });
25 | assert.deepEqual(machineDefinition.prepareObjectAlias(object), { car: object });
26 | });
27 | });
28 |
--------------------------------------------------------------------------------
/packages/editor/src/components/utils.spec.js:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai';
2 | import { isDef } from './utils';
3 |
4 | describe('utils', () => {
5 | describe('isDef()', () => {
6 | it('returns false for undefined value', () => {
7 | expect(isDef(undefined)).to.be.false; // eslint-disable-line no-unused-expressions
8 | });
9 |
10 | it('returns false for null value', () => {
11 | expect(isDef(null)).to.be.false; // eslint-disable-line no-unused-expressions
12 | });
13 |
14 | it('returns true for anything other than null or undefined', () => {
15 | expect(isDef('')).to.be.true; // eslint-disable-line no-unused-expressions
16 | expect(isDef(0)).to.be.true; // eslint-disable-line no-unused-expressions
17 | expect(isDef('string')).to.be.true; // eslint-disable-line no-unused-expressions
18 | expect(isDef([])).to.be.true; // eslint-disable-line no-unused-expressions
19 | expect(isDef({})).to.be.true; // eslint-disable-line no-unused-expressions
20 | expect(isDef(false)).to.be.true; // eslint-disable-line no-unused-expressions
21 | expect(isDef(true)).to.be.true; // eslint-disable-line no-unused-expressions
22 | });
23 | })
24 | })
25 |
--------------------------------------------------------------------------------
/packages/task-manager/src/specs/utils.doUntil.spec.js:
--------------------------------------------------------------------------------
1 | import assert from 'assert';
2 | import { doUntil, killProcess } from '../utils';
3 |
4 | describe('utils:', () => {
5 | it('changes value until condition is OK', (done) => {
6 | let counter = 0;
7 |
8 | doUntil(
9 | () => {
10 | counter++;
11 | },
12 | () => {
13 | return counter < 5
14 | },
15 | 100
16 | );
17 |
18 | setTimeout(() => {
19 | assert.equal(counter, 5);
20 | done();
21 | }, 600)
22 | });
23 |
24 | it('stops correctly', (done) => {
25 | let counter = 0;
26 | let counterValueAfter300millis;
27 |
28 | // we take a snapshot of counter after some period of execution
29 | // and stop the process
30 | setTimeout(() => {
31 | counterValueAfter300millis = counter;
32 | killProcess(counter);
33 | }, 300);
34 |
35 | // after some longer period we check that the value ws not changed anymore
36 | // if it is true - the process was stopped correctly and the action was no longer executed
37 | setTimeout(() => {
38 | assert.equal(counterValueAfter300millis, counter);
39 | done();
40 | }, 500);
41 | })
42 | });
43 |
--------------------------------------------------------------------------------
/packages/core/src/specs/MachineDefinition.contructor.spec.js:
--------------------------------------------------------------------------------
1 | import assert from 'assert';
2 | import MachineDefinition from '../MachineDefinition';
3 | import bluebird from "bluebird"
4 |
5 | describe('machine definition: constructor', function() {
6 | it('uses correct default object state field name if not specified', () => {
7 | assert.equal(new MachineDefinition().objectConfiguration.stateFieldName,
8 | MachineDefinition.getDefaultObjectStateFieldName());
9 | });
10 |
11 | [null].forEach(promise => {
12 | it(`promise = '${promise}' in constructor is no acceptable`, () => {
13 | assert.throws(() => {
14 | return new MachineDefinition({ promise });
15 | }, Error);
16 | });
17 | });
18 |
19 | // eslint-disable-next-line max-len
20 | it("if library is used outside node (e.g. browser) then 'bluebird' should be used as default Promise implementation", () => {
21 | // store Promise
22 | const { Promise } = global;
23 | try {
24 | global.Promise = undefined;
25 |
26 | assert.equal(new MachineDefinition().promise, bluebird.Promise);
27 | } finally {
28 | // restore promise
29 | global.Promise = Promise;
30 | }
31 | });
32 | });
33 |
--------------------------------------------------------------------------------
/packages/editor/src/components/ParamsEditor/components/GenericInput.react.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import PropTypes from 'prop-types';
3 | import FormGroup from 'react-bootstrap/lib/FormGroup';
4 | import ControlLabel from 'react-bootstrap/lib/ControlLabel';
5 | import withExpressionInput from './PathExpressionInputInjector';
6 |
7 | @withExpressionInput
8 | export default class GenericInput extends PureComponent {
9 | static propTypes = {
10 | value: PropTypes.any,
11 | onChange: PropTypes.func.isRequired,
12 | id: PropTypes.string.isRequired,
13 | label: PropTypes.oneOfType([PropTypes.string, PropTypes.func]).isRequired,
14 | component: PropTypes.func.isRequired
15 | }
16 |
17 | render() {
18 | const { id, label, component: Component, ...props } = this.props;
19 |
20 | let renderLabel = label;
21 |
22 | if (typeof label === 'function') {
23 | const Label = label;
24 | renderLabel = ()
25 | }
26 |
27 | return (
28 |
29 | {renderLabel}
30 |
31 |
32 | )
33 | }
34 | }
35 |
36 |
--------------------------------------------------------------------------------
/packages/editor/src/components/WorkflowGraph/WorkflowGraph.less:
--------------------------------------------------------------------------------
1 | .oc-fsm-crud-editor--workflow-graph {
2 | background-size: 10px 10px;
3 | height: 100%;
4 | }
5 |
6 | .sdfdsfsf {
7 | transform: scale(0.6);
8 | }
9 |
10 | .oc-fsm-crud-editor--workflow-graph text {
11 | fill: #333;
12 | }
13 |
14 | .oc-fsm-crud-editor--workflow-graph .node text {
15 | fill: #fff;
16 | }
17 |
18 | .oc-fsm-crud-editor--workflow-graph__legend {
19 | position: absolute;
20 | bottom: 24px;
21 | right: 40px;
22 | display: flex;
23 | flex-direction: column;
24 | z-index: 10;
25 | }
26 |
27 |
28 | .oc-fsm-crud-editor--workflow-graph__legend-item {
29 | display: inline-flex;
30 | align-items: center;
31 | }
32 |
33 | .oc-fsm-crud-editor--workflow-graph__legend-item-badge {
34 | width: 24px;
35 | height: 16px;
36 | border-radius: 6px;
37 | margin-right: 4px;
38 | }
39 |
40 | .oc-fsm-crud-editor--workflow-graph__legend-item-badge--regular-state {
41 | background: #0277bd;
42 | }
43 |
44 | .oc-fsm-crud-editor--workflow-graph__legend-item-badge--initial-state {
45 | background: #14892c;
46 | }
47 |
48 | .oc-fsm-crud-editor--workflow-graph__legend-item-badge--final-state {
49 | background: #b71c1c;
50 | }
51 |
--------------------------------------------------------------------------------
/packages/core/src/specs/Machine.availableTransitions.spec.js:
--------------------------------------------------------------------------------
1 | import assert from 'assert';
2 | import Machine from '../Machine';
3 |
4 | // maybe it worth to write integration test
5 | describe('machine: availableTransitions', function() {
6 | it('arguments are correctly pased to machineDefinition.getAvailableStates', function() {
7 | // create object
8 | const object = {
9 | status: 'none'
10 | };
11 |
12 | // context
13 | const context = {
14 | sendEmail: () => {}
15 | };
16 |
17 | const createMachine = ({ context = {} }) => {
18 | return new Machine(
19 | {
20 | machineDefinition: {
21 | schema: {},
22 | findAvailableTransitions: (passsedArgument) => {
23 | assert(passsedArgument)
24 | assert.equal(passsedArgument.object, object);
25 | assert.equal(passsedArgument.context, context);
26 | },
27 | objectConfiguration: {
28 | stateFieldName: 'status'
29 | }
30 | },
31 | context: context
32 | }
33 | );
34 | }
35 |
36 | const machine = createMachine({ context });
37 |
38 | return machine.availableTransitions({ object });
39 | });
40 | });
41 |
--------------------------------------------------------------------------------
/packages/editor/src/components/TopForm.react.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import Col from 'react-bootstrap/lib/Col';
4 | import Form from 'react-bootstrap/lib/Form';
5 | import FormGroup from 'react-bootstrap/lib/FormGroup';
6 | import FormControl from 'react-bootstrap/lib/FormControl';
7 | import ControlLabel from 'react-bootstrap/lib/ControlLabel';
8 |
9 | export default function TopForm({ name, onNameChange }, { i18n }) {
10 | return (
11 |
26 | )
27 | }
28 |
29 | TopForm.propTypes = {
30 | name: PropTypes.string.isRequired,
31 | onNameChange: PropTypes.func.isRequired
32 | }
33 |
34 | TopForm.contextTypes = {
35 | i18n: PropTypes.object.isRequired
36 | }
37 |
--------------------------------------------------------------------------------
/packages/editor/src/components/ParamsEditor/components/EnumInput.react.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import FormControl from 'react-bootstrap/lib/FormControl';
4 |
5 | export default function EnumInput({ value, onChange, options, type, ...props }) {
6 | return (
7 | onChange(
12 | value === '' ?
13 | null :
14 | type === 'number' || type === 'integer' ?
15 | Number(value) :
16 | value
17 | )
18 | }
19 | >
20 |
21 | {
22 | options.map((option, i) => (
23 |
24 | ))
25 | }
26 |
27 | )
28 | }
29 |
30 | EnumInput.propTypes = {
31 | value: PropTypes.oneOfType([
32 | PropTypes.string,
33 | PropTypes.number
34 | ]),
35 | onChange: PropTypes.func.isRequired,
36 | options: PropTypes.oneOfType([
37 | PropTypes.arrayOf(PropTypes.string),
38 | PropTypes.arrayOf(PropTypes.number)
39 | ]).isRequired,
40 | type: PropTypes.oneOf(['string', 'number', 'integer'])
41 | }
42 |
--------------------------------------------------------------------------------
/packages/task-manager/config/webpack.config.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const path = require('path');
3 | const webpack = require('webpack');
4 | const NODE_ENV = process.env.NODE_ENV;
5 | const IS_PRODUCTION = NODE_ENV === 'production';
6 |
7 | module.exports = {
8 | entry: path.resolve(__dirname, '../src/index.js'),
9 | context: path.resolve(__dirname),
10 | output: {
11 | publicPath: '/',
12 | path: path.resolve(__dirname, '../lib'),
13 | filename: `index.js`,
14 | library: 'fsm-task-manager',
15 | libraryTarget: 'umd'
16 | },
17 | devtool: IS_PRODUCTION ? false : 'inline-source-map',
18 | watch: IS_PRODUCTION ? false : true,
19 | plugins: [
20 | new webpack.NoEmitOnErrorsPlugin(),
21 | new webpack.DefinePlugin({
22 | 'process.env.NODE_ENV': `"${NODE_ENV}"`
23 | }),
24 | ],
25 | resolve: {
26 | modules: ['node_modules'],
27 | extensions: ['.json', '.js']
28 | },
29 | module: {
30 | rules: [
31 | {
32 | test: /.js?$/,
33 | use: [{
34 | loader: 'babel-loader',
35 | options: {
36 | presets: ['es2015', 'stage-0']
37 | }
38 | }],
39 | include: [
40 | path.resolve(__dirname, '../src')
41 | ]
42 | }
43 | ]
44 | }
45 | };
46 |
--------------------------------------------------------------------------------
/packages/editor/src/components/ParamsEditor/components/DateInput.react.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import padStart from 'lodash/padStart';
4 | import { DateInput as OCDateInput } from '@opuscapita/react-dates';
5 | import { isDef } from '../../utils';
6 | import './DateInput.less';
7 |
8 | /**
9 | * onChange: return string in `full-date` (YYYY-MM-DD) format (https://tools.ietf.org/html/rfc3339#section-5.6) or null
10 | */
11 | export default function DateInput({ value, onChange, ...props }, { i18n }) {
12 | return (
13 | {
18 | if (!date) {
19 | return onChange(null)
20 | }
21 | const year = date.getFullYear();
22 | const month = padStart(`${date.getMonth() + 1}`, 2, '0');
23 | const day = padStart(`${date.getDate()}`, 2, '0');
24 | const str = `${year}-${month}-${day}`;
25 | return onChange(str)
26 | }}
27 | />
28 | )
29 | }
30 |
31 | DateInput.propTypes = {
32 | value: PropTypes.string,
33 | onChange: PropTypes.func.isRequired
34 | }
35 |
36 | DateInput.contextTypes = {
37 | i18n: PropTypes.object.isRequired
38 | }
39 |
--------------------------------------------------------------------------------
/packages/history/src/models/definitions/workflowTransitionHistory.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = (sequelize, DataTypes) => sequelize.define('WorkflowTransitionHistory', {
4 | id: { type: DataTypes.INTEGER, allowNull: false, primaryKey: true, autoIncrement: true },
5 | from: { type: DataTypes.STRING, allowNull: false },
6 | to: { type: DataTypes.STRING, allowNull: false },
7 | event: { type: DataTypes.STRING, allowNull: false },
8 | businessObjType: { type: DataTypes.STRING, allowNull: false },
9 | businessObjId: { type: DataTypes.STRING, allowNull: false },
10 | user: { type: DataTypes.STRING, allowNull: false },
11 | workflowName: { type: DataTypes.STRING, allowNull: false },
12 | description: { type: DataTypes.TEXT }
13 | }, {
14 | // enable timestamps
15 | timestamps: true,
16 |
17 | // don't add the timestamp attribute "updatedAt"
18 | updatedAt: false,
19 |
20 | // rename "createdAt" timestamp attribute
21 | createdAt: 'finishedOn',
22 |
23 | // disable the modification of table names; By default, sequelize will automatically
24 | // transform all passed model names (first parameter of define) into plural.
25 | // if you don't want that, set the following
26 | freezeTableName: true,
27 |
28 | // define the table's name
29 | tableName: 'WorkflowTransitionHistory'
30 | });
31 |
--------------------------------------------------------------------------------
/packages/editor/config/webpack.config.common.js:
--------------------------------------------------------------------------------
1 | const { resolve } = require('path');
2 |
3 | module.exports = {
4 | context: resolve(__dirname, '../src'),
5 | module: {
6 | rules: [
7 | {
8 | test: /.jsx?$/,
9 | loader: 'babel-loader',
10 | options: {
11 | babelrc: false,
12 | presets: [
13 | ["env", {
14 | "targets": {
15 | "browsers": ["last 2 versions", "ie >= 11", "safari >= 7", "Firefox ESR"]
16 | }
17 | }],
18 | "react"
19 | ],
20 | plugins: [
21 | "transform-decorators-legacy",
22 | "transform-class-properties",
23 | "transform-object-rest-spread"
24 | ]
25 | },
26 | exclude: /node_modules/
27 | },
28 | {
29 | test: /\.(css|less)$/,
30 | use: [
31 | { loader: 'style-loader' },
32 | { loader: 'css-loader', options: { importLoaders: 1 } },
33 | { loader: 'less-loader', options: { sourceMap: true } },
34 | {
35 | loader: 'postcss-loader', options: {
36 | plugins: (loader) => [
37 | require('autoprefixer')()
38 | ]
39 | }
40 | }
41 | ]
42 | }
43 | ]
44 | }
45 | };
46 |
--------------------------------------------------------------------------------
/packages/integration-tests/history/data/schema.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "TestWorkflow",
3 | "initialState": "a",
4 | "finalStates": [
5 | "d"
6 | ],
7 | "transitions": [
8 | {
9 | "from": "a",
10 | "to": "b",
11 | "event": "a->b",
12 | "guards": [
13 | {
14 | "name": "guard1"
15 | }
16 | ],
17 | "actions": [
18 | {
19 | "name": "action1",
20 | "params": [
21 | {
22 | "name": "param1",
23 | "value": 1
24 | },
25 | {
26 | "name": "param2",
27 | "value": 2
28 | }
29 | ]
30 | }
31 | ]
32 | },
33 | {
34 | "from": "b",
35 | "to": "c",
36 | "event": "b->c"
37 | },
38 | {
39 | "from": "c",
40 | "to": "d",
41 | "event": "c->d"
42 | },
43 | {
44 | "from": "a",
45 | "to": "d",
46 | "event": "a->d"
47 | },
48 | {
49 | "from": "a",
50 | "to": "c",
51 | "event": "a->c"
52 | }
53 | ],
54 | "states": [
55 | {
56 | "name": "a",
57 | "description": "A"
58 | },
59 | {
60 | "name": "b"
61 | },
62 | {
63 | "name": "c"
64 | },
65 | {
66 | "name": "d"
67 | }
68 | ]
69 | }
70 |
--------------------------------------------------------------------------------
/packages/core/src/specs/Machine.availableAutomaticTransitions.spec.js:
--------------------------------------------------------------------------------
1 | import assert from 'assert';
2 | import Machine from '../Machine';
3 |
4 | // maybe it worth to write integration test
5 | describe('machine: availableAutomaticTransitions', () => {
6 | it('arguments are correctly pased to machineDefinition.findAvailableTransitions', () => {
7 | // create object
8 | const object = {
9 | status: 'none'
10 | };
11 |
12 | // context
13 | const context = {
14 | sendEmail: () => {}
15 | };
16 |
17 | const createMachine = ({ context = {} }) => {
18 | return new Machine(
19 | {
20 | machineDefinition: {
21 | schema: {},
22 | findAvailableTransitions: (passsedArgument) => {
23 | assert(passsedArgument)
24 | assert.equal(passsedArgument.object, object);
25 | assert.equal(passsedArgument.context, context);
26 | assert.equal(passsedArgument.isAutomatic, true);
27 | },
28 | objectConfiguration: {
29 | stateFieldName: 'status'
30 | }
31 | },
32 | context: context
33 | }
34 | );
35 | }
36 |
37 | const machine = createMachine({ context });
38 |
39 | return machine.availableAutomaticTransitions({ object });
40 | });
41 | });
42 |
--------------------------------------------------------------------------------
/packages/editor/src/components/ErrorLabel.spec.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { expect } from 'chai';
3 | import Enzyme from 'enzyme';
4 | import Adapter from 'enzyme-adapter-react-15';
5 | import { mount } from 'enzyme';
6 | import Label from './Label.react';
7 | import ErrorLabel from './ErrorLabel.react';
8 |
9 | Enzyme.configure({ adapter: new Adapter() });
10 |
11 | describe('', () => {
12 | it('renders error label', () => {
13 | const props = {
14 | error: 'Something went wrong'
15 | }
16 | const wrapper = mount();
17 | expect(wrapper).to.exist; // eslint-disable-line no-unused-expressions
18 | expect(wrapper.contains(Label)).to.be.true; // eslint-disable-line no-unused-expressions
19 | expect(wrapper.text().trim()).to.equal(props.error);
20 | expect(wrapper.getDOMNode().style.opacity).to.not.equal('0');
21 | });
22 |
23 | it('hides if error is empty', () => {
24 | const props = {
25 | error: ''
26 | }
27 | const wrapper = mount();
28 | expect(wrapper).to.exist; // eslint-disable-line no-unused-expressions
29 | expect(wrapper.contains(Label)).to.be.true; // eslint-disable-line no-unused-expressions
30 | expect(wrapper.text().trim()).to.equal(props.error);
31 | expect(wrapper.getDOMNode().style.opacity).to.equal('0');
32 | });
33 | });
34 |
--------------------------------------------------------------------------------
/packages/examples/process-task-manager-ui/src/client/components/TaskList.react.js:
--------------------------------------------------------------------------------
1 | import React, {PropTypes} from 'react';
2 |
3 | const TaskList = ({tasks, onSendEvent}) => (
4 |
5 |
6 |
7 |
8 | | ID |
9 | Status |
10 | |
11 |
12 |
13 |
14 | {tasks.map(({object, transitions}) => {
15 | return(
16 |
17 | | {object.id} |
18 | {object.status} |
19 |
20 |
21 | {transitions.map(({ event }) => (
22 |
25 | ))}
26 |
27 | |
28 |
29 | );
30 | })}
31 |
32 |
33 |
34 | );
35 |
36 | TaskList.propTypes = {
37 | tasks: PropTypes.array.isRequired,
38 | onSendEvent: PropTypes.func.isRequired
39 | };
40 |
41 | TaskList.defaultProps = {
42 | tasks: []
43 | };
44 |
45 | export default TaskList;
46 |
--------------------------------------------------------------------------------
/packages/editor/.gitignore:
--------------------------------------------------------------------------------
1 | *~
2 | *.swp
3 | *.kate-swp
4 | *.out
5 | *.iml
6 | *.lock
7 | package-lock.json
8 |
9 | public
10 | .gh-pages-tmp
11 |
12 | /resources/jcatalog/less/jquery-ui-bundle.css
13 | /resources/jcatalog/less/jcatalog-bootstrap-bundle.css
14 | /resources/jcatalog/less/jcatalog-bootstrap-extensions-bundle.css
15 |
16 | /db.config.json
17 | /src/server/data.json.backup
18 |
19 | .idea
20 | /target
21 | /build
22 | /dist
23 | /docs
24 | /esdoc
25 | /lib
26 | /apidoc
27 |
28 | # All the below is taken from
29 | # https://github.com/github/gitignore/blob/master/Node.gitignore
30 | # except for some /node_modules subfolders.
31 |
32 | # Logs
33 | logs
34 | *.log
35 | npm-debug.log*
36 |
37 | # Runtime data
38 | pids
39 | *.pid
40 | *.seed
41 | *.pid.lock
42 |
43 | # Directory for instrumented libs generated by jscoverage/JSCover
44 | lib-cov
45 |
46 | # Coverage directory used by tools like istanbul
47 | coverage
48 |
49 | # nyc test coverage
50 | .nyc_output
51 |
52 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
53 | .grunt
54 |
55 | # node-waf configuration
56 | .lock-wscript
57 |
58 | # Compiled binary addons (http://nodejs.org/api/addons.html)
59 | build/Release
60 |
61 | # Dependency directories
62 | jspm_packages/
63 | /node_modules/*
64 |
65 | # Optional npm cache directory
66 | .npm
67 |
68 | # Optional eslint cache
69 | .eslintcache
70 |
71 | # Optional REPL history
72 | .node_repl_history
73 | .DS_Store
--------------------------------------------------------------------------------
/packages/examples/complete-demo/src/server/utils.js:
--------------------------------------------------------------------------------
1 | import fs from 'fs';
2 | import { promisify } from 'util';
3 | import pick from 'lodash/pick';
4 | import jsfaker from 'json-schema-faker';
5 | import objectConfig from './objectConfig';
6 | import { objectIdProp } from '../common';
7 |
8 | export const generateObjects = ({ objectConfiguration, quantity = 10 }) => {
9 | const businessObjects = [];
10 | for (let i = 0; i < quantity; i++) {
11 | const object = jsfaker(objectConfiguration.schema);
12 | object[objectIdProp] = `${objectConfiguration.alias || 'object'}-${i}`;
13 | businessObjects.push(object);
14 | }
15 | return businessObjects
16 | }
17 |
18 | export const mapFuncsToParamsSchema = funcsObj => Object.keys(funcsObj).reduce((res, name) => ({
19 | ...res,
20 | [name]: {
21 | paramsSchema: funcsObj[name].paramsSchema
22 | }
23 | }), {});
24 |
25 | // take sequelize query output and return clean object
26 | export const extractObject = sequelizeOutput => {
27 | const { dataValues } = sequelizeOutput;
28 | const objectSchema = objectConfig.getConfig().schema;
29 | const objectProps = Object.keys(objectSchema.properties);
30 | return pick(dataValues, objectProps);
31 | }
32 |
33 | const readFile = promisify(fs.readFile);
34 |
35 | export const readJSONFromFile = async function(filePath) {
36 | const data = await readFile(filePath, 'utf8');
37 | let object;
38 | try {
39 | object = JSON.parse(data)
40 | } catch (err) {
41 | throw err
42 | }
43 | return object
44 | }
45 |
--------------------------------------------------------------------------------
/packages/examples/process-task-manager-ui/webpack.development.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const webpack = require('webpack');
3 | const packageVersion = require('./package.json').version;
4 | const fs = require('fs');
5 |
6 | module.exports = {
7 | entry: path.resolve(__dirname, './src/client/index.js'),
8 | context: path.resolve(__dirname),
9 | output: {
10 | publicPath: '/',
11 | path: path.resolve(__dirname, './lib'),
12 | filename: `index.js`,
13 | library: 'demopage',
14 | libraryTarget: 'umd'
15 | },
16 | devtool: 'inline-source-map',
17 | watch: true,
18 | plugins: [
19 | new webpack.NoEmitOnErrorsPlugin(),
20 | ],
21 |
22 | resolveLoader: {
23 | modules: ['node_modules'],
24 | moduleExtensions: ['-loader', '*'],
25 | extensions: ['.js']
26 | },
27 |
28 | resolve: {
29 | modules: [path.join(__dirname, 'node_modules')],
30 | extensions: ['.json', '.js']
31 | },
32 |
33 | module: {
34 | rules: [
35 | {
36 | test: /.js$/,
37 | loader: 'babel-loader',
38 | include: [
39 | path.join(__dirname, 'src'),
40 | path.join(__dirname, 'www'),
41 | fs.realpathSync(path.join(__dirname, './node_modules/@opuscapita/fsm-workflow-core')),
42 | fs.realpathSync(path.join(__dirname, './node_modules/@opuscapita/fsm-workflow-task-manager'))
43 | ],
44 | query: {
45 | presets: ['es2015', 'stage-0', 'react']
46 | }
47 | }
48 | ]
49 | }
50 | };
51 |
--------------------------------------------------------------------------------
/packages/core/src/specs/MachineDefinition.availableStates.spec.js:
--------------------------------------------------------------------------------
1 | import assert from "assert";
2 | import MachineDefinition from "../MachineDefinition";
3 |
4 | const createMachineDefintion = ({ states = [
5 | { name: 'a' },
6 | { name: 'b' },
7 | { name: 'y' },
8 | { name: 'm' },
9 | { name: 'n' },
10 | { name: 'z' },
11 | ], transitions = [
12 | {
13 | from: 'a',
14 | to: 'b'
15 | },
16 | {
17 | from: 'b',
18 | to: 'x'
19 | },
20 | ] } = {}) => {
21 | return new MachineDefinition({
22 | schema: {
23 | initialState: "a",
24 | finalStates: ["x", "y", "z"],
25 | transitions,
26 | states
27 | }
28 | });
29 | };
30 |
31 | describe("machine: getAvailableStates", function() {
32 | it("returns declared states", () =>
33 | assert.deepEqual(createMachineDefintion().getAvailableStates(), ['a', 'b', 'm', 'n', 'x', 'y', 'z'])
34 | );
35 |
36 | it("returns declared states: empty transitions", () =>
37 | assert.deepEqual(
38 | createMachineDefintion({ transitions: [] }).getAvailableStates(),
39 | ['a', 'b', 'm', 'n', 'x', 'y', 'z']
40 | )
41 | );
42 |
43 | it("returns declared states: empty states", () =>
44 | assert.deepEqual(createMachineDefintion({ states: [] }).getAvailableStates(), ['a', 'b', 'x', 'y', 'z'])
45 | );
46 |
47 | it("returns declared states: no states and transitions", () =>
48 | assert.deepEqual(createMachineDefintion({ transitions: [], states: [] }).getAvailableStates(), ['a', 'x', 'y', 'z'])
49 | );
50 | });
51 |
--------------------------------------------------------------------------------
/packages/editor/src/components/ParamsEditor/components/PathExpressionInputInjector/ExpressionSwitcher.react.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | export default class ExpressionSwitcher extends PureComponent {
5 | static propTypes = {
6 | label: PropTypes.string.isRequired,
7 | expression: PropTypes.bool.isRequired,
8 | onClick: PropTypes.func.isRequired
9 | }
10 |
11 | static contextTypes = {
12 | i18n: PropTypes.object.isRequired
13 | }
14 |
15 | componentDidMount() {
16 | // adjust label width
17 | if (this.ref) {
18 | const { parentElement } = this.ref;
19 | if (parentElement && parentElement.nodeName === 'LABEL') {
20 | parentElement.style.width = '100%';
21 | }
22 | }
23 | }
24 |
25 | handleChange = this.props.onClick
26 |
27 | handleRef = el => (this.ref = el);
28 |
29 | render() {
30 | const { i18n } = this.context;
31 | const { label, expression } = this.props;
32 |
33 | return (
34 |
49 | )
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/packages/core/src/specs/ModelDefinition.prepareParams.spec.js:
--------------------------------------------------------------------------------
1 | import assert from 'assert';
2 | import MachineDefinition from '../MachineDefinition';
3 |
4 | describe('path expressions', function() {
5 | const object = {
6 | numberProp: 234234.3454,
7 | stringProp: '%xc%ds%%^%$^&--0$&*$&*'
8 | }
9 |
10 | const params = [
11 | {
12 | name: 'one',
13 | value: 'invoice["numberProp"]',
14 | expression: 'path'
15 | },
16 | {
17 | name: 'two',
18 | value: 'object["stringProp"]',
19 | expression: 'path'
20 | }
21 | ]
22 |
23 | const machineDefinition = new MachineDefinition({
24 | objectConfiguration: {
25 | alias: 'invoice'
26 | }
27 | })
28 |
29 | const implicitParams = {
30 | object,
31 | ...machineDefinition.prepareObjectAlias(object)
32 | }
33 |
34 | it('return evaluated path expressions', () => {
35 | const newParams = machineDefinition.prepareParams({
36 | explicitParams: params,
37 | implicitParams
38 | });
39 | assert.deepEqual({
40 | one: object.numberProp,
41 | two: object.stringProp,
42 | ...implicitParams
43 | }, newParams)
44 | })
45 |
46 | it('throw for invalid path', () => {
47 | try {
48 | const newParams = machineDefinition.prepareParams({
49 | explicitParams: [
50 | {
51 | name: 'three',
52 | expression: 'path',
53 | value: '["numberProp"]' // not prefixed -> error
54 | }
55 | ],
56 | implicitParams
57 | });
58 | assert.fail(`Did not throw for invalid path; returned: ${newParams}`)
59 | } catch (err) {
60 | assert(true)
61 | }
62 | })
63 | });
64 |
--------------------------------------------------------------------------------
/packages/editor/src/components/StatesTable/DeleteStateDialogBody.spec.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { expect } from 'chai';
3 | import Enzyme from 'enzyme';
4 | import Adapter from 'enzyme-adapter-react-15';
5 | import { mount } from 'enzyme';
6 | import DeleteStateDialogBody from './DeleteStateDialogBody.react';
7 | import FormGroup from 'react-bootstrap/lib/FormGroup';
8 | import FormControl from 'react-bootstrap/lib/FormControl';
9 | import Radio from 'react-bootstrap/lib/Radio';
10 | import { I18nManager } from '@opuscapita/i18n';
11 |
12 | Enzyme.configure({ adapter: new Adapter() });
13 |
14 | describe('', () => {
15 | it('renders an modal', () => {
16 | const props = {
17 | i18n: new I18nManager(),
18 | states: [
19 | {
20 | name: 'one',
21 | isInitial: true,
22 | isFinal: false
23 | },
24 | {
25 | name: 'two',
26 | isInitial: false,
27 | isFinal: true
28 | }
29 | ],
30 | stateName: 'one',
31 | onSelect: () => {}
32 | };
33 |
34 | const wrapper = mount();
35 | expect(wrapper).to.exist; // eslint-disable-line no-unused-expressions
36 | expect(wrapper.contains(FormGroup)).to.be.true; // eslint-disable-line no-unused-expressions
37 | expect(wrapper.find(Radio).length).to.equal(2);
38 | expect(wrapper.find(Radio).at(0).prop('checked')).to.be.true; // eslint-disable-line no-unused-expressions
39 | expect(wrapper.find(Radio).at(1).prop('checked')).to.be.false; // eslint-disable-line no-unused-expressions
40 | expect(wrapper.find(Radio).at(1).find(FormControl).prop('value')).to.equal('two');
41 | });
42 | });
43 |
--------------------------------------------------------------------------------
/packages/examples/complete-demo/src/client/uiGlobalComponents/uiMessageNotifications/Notifications.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import { NotificationContainer, NotificationManager } from '@opuscapita/react-notifications';
4 | import '@opuscapita/react-notifications/lib/notifications.css';
5 | import './styles.css';
6 |
7 | const isDef = /* istanbul ignore next */ param => param !== undefined && param !== null;
8 |
9 | const types = {
10 | ERROR: 'error',
11 | WARNING: 'warning',
12 | INFO: 'info',
13 | SUCCESS: 'success'
14 | }
15 |
16 | const defaultTimeout = 3000;
17 |
18 | /* istanbul ignore next */
19 | class Notifications {
20 | constructor() {
21 | this.renderContainer()
22 | }
23 |
24 | renderContainer = _ => {
25 | if (!this.container) {
26 | this.container = document.createElement('div');
27 | document.body.appendChild(this.container);
28 | }
29 |
30 | ReactDOM.render(, this.container)
31 | }
32 |
33 | destroy = _ => {
34 | if (ReactDOM.findDOMNode(this.container)) {
35 | ReactDOM.unmountComponentAtNode(this.container);
36 | }
37 | }
38 |
39 | create = ({ id, type, timeOut = defaultTimeout, message }) => NotificationManager.create({
40 | id: isDef(id) ? id : type,
41 | type,
42 | timeOut,
43 | message
44 | })
45 |
46 | remove = ({ id }) => NotificationManager.remove({ id })
47 |
48 | success = args => this.create({ ...args, type: types.SUCCESS })
49 |
50 | info = args => this.create({ ...args, type: types.INFO })
51 |
52 | warning = args => this.create({ ...args, type: types.WARNING })
53 |
54 | error = args => this.create({ ...args, type: types.ERROR })
55 | }
56 |
57 | export default new Notifications();
58 |
--------------------------------------------------------------------------------
/packages/examples/complete-demo/src/server/data/actions/testAction.js:
--------------------------------------------------------------------------------
1 | const action = args => {
2 | console.log('action "testAction" executed')
3 | }
4 |
5 | action.paramsSchema = {
6 | "type": "object",
7 | "properties": {
8 | nickname: {
9 | "type": "string"
10 | },
11 | fullName: {
12 | "type": "string",
13 | "uiComponent": "fullName"
14 | },
15 | age: {
16 | "type": "integer"
17 | },
18 | bankAccountBalance: {
19 | "type": "number"
20 | },
21 | adult: {
22 | "type": "boolean"
23 | },
24 | favoriteColor: {
25 | "type": "string",
26 | "enum": ['red', 'green', 'blue', 'yellow', 'I\'m achromate']
27 | },
28 | children: {
29 | "type": "integer",
30 | "enum": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
31 | },
32 | confidenceRate: {
33 | "type": "number",
34 | "enum": [15.75, 44.55, 66.7, 99999.9]
35 | },
36 | dateOfBirth: {
37 | "type": "string",
38 | "format": "date"
39 | },
40 | todoList: {
41 | "type": "array",
42 | "items": {
43 | "type": "string"
44 | }
45 | },
46 | nextLotteryNumbers: {
47 | "type": "array",
48 | "items": {
49 | "type": "integer"
50 | }
51 | },
52 | monthlyInterestHistory: {
53 | "type": "array",
54 | "items": {
55 | "type": "number"
56 | }
57 | },
58 | importantDates: {
59 | "type": "array",
60 | "items": {
61 | "type": "string",
62 | "format": "date"
63 | }
64 | },
65 | dinnerMenu: {
66 | "type": "array",
67 | "items": {
68 | "type": "string",
69 | "enum": ['Steak', 'Vegetables', 'Mashrooms', 'Beer']
70 | }
71 | }
72 | }
73 | }
74 |
75 | export default action;
76 |
--------------------------------------------------------------------------------
/packages/examples/complete-demo/src/server/objectConfig/objectConfiguration.json:
--------------------------------------------------------------------------------
1 | {
2 | "stateFieldName": "status",
3 | "alias": "invoice",
4 | "schema": {
5 | "type": "object",
6 | "properties": {
7 | "invoiceNo": {
8 | "type": "string"
9 | },
10 | "customerId": {
11 | "type": "string"
12 | },
13 | "supplierId": {
14 | "type": "string"
15 | },
16 | "netAmount": {
17 | "type": "integer",
18 | "minimum": 0
19 | },
20 | "grossAmount": {
21 | "type": "integer",
22 | "minimum": 0
23 | },
24 | "vatAmount": {
25 | "type": "integer",
26 | "minimum": 0
27 | },
28 | "currencyId": {
29 | "type": "string",
30 | "enum": [
31 | "EUR",
32 | "USD",
33 | "RUB",
34 | "CHF",
35 | "HKD",
36 | "JPY",
37 | "AUD",
38 | "CAD"
39 | ]
40 | },
41 | "status": {
42 | "type": "string",
43 | "enum": [
44 | "inspectionRequired",
45 | "approvalRequired",
46 | "inspClrRequired",
47 | "inspectionRejected",
48 | "approved",
49 | "appClrRequired",
50 | "approvalRejected"
51 | ]
52 | }
53 | },
54 | "required": [
55 | "invoiceNo",
56 | "customerId",
57 | "supplierId",
58 | "netAmount",
59 | "grossAmount",
60 | "vatAmount",
61 | "currencyId",
62 | "status"
63 | ]
64 | },
65 | "example": {
66 | "invoiceNo": "1111",
67 | "customerId": "wefwefewfew",
68 | "supplierId": "33333",
69 | "netAmount": 1000,
70 | "grossAmount": 1200,
71 | "vatAmount": 200,
72 | "currencyId": "EUR",
73 | "status": "inspectionRequired"
74 | }
75 | }
--------------------------------------------------------------------------------
/packages/examples/process-task-manager-ui/src/client/workflow/process-schema.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "process",
3 | "initialState": "started",
4 | "finalStates": [
5 | "finished"
6 | ],
7 | "transitions": [
8 | {
9 | "from": "started",
10 | "event": "register",
11 | "to": "registered",
12 | "actions": [
13 | {
14 | "name": "logAction"
15 | }
16 | ]
17 | },
18 | {
19 | "from": "registered",
20 | "event": "toManualBranch",
21 | "to": "manualStepA",
22 | "actions": [
23 | {
24 | "name": "logAction"
25 | }
26 | ]
27 | },
28 | {
29 | "from": "manualStepA",
30 | "event": "manualEvent1",
31 | "to": "manualStepB",
32 | "actions": [
33 | {
34 | "name": "logAction"
35 | }
36 | ]
37 | },
38 | {
39 | "from": "manualStepB",
40 | "event": "manualFinish",
41 | "to": "finished",
42 | "actions": [
43 | {
44 | "name": "logAction"
45 | }
46 | ]
47 | },
48 | {
49 | "from": "registered",
50 | "event": "toAutoBranch",
51 | "to": "autoStepA",
52 | "actions": [
53 | {
54 | "name": "logAction"
55 | }
56 | ]
57 | },
58 | {
59 | "from": "autoStepA",
60 | "event": "autoEvent1",
61 | "to": "autoStepB",
62 | "actions": [
63 | {
64 | "name": "logAction"
65 | }
66 | ],
67 | "automatic": [
68 | {"name": "isTrue"}
69 | ]
70 | },
71 | {
72 | "from": "autoStepB",
73 | "event": "autoFinish",
74 | "to": "finished",
75 | "actions": [
76 | {
77 | "name": "logAction"
78 | }
79 | ],
80 | "automatic": true
81 | }
82 | ]
83 | }
84 |
--------------------------------------------------------------------------------
/packages/editor/src/components/TopButtons.react.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import PropTypes from 'prop-types';
3 | import Button from 'react-bootstrap/lib/Button';
4 | import Dropdown from 'react-bootstrap/lib/Dropdown';
5 | import MenuItem from 'react-bootstrap/lib/MenuItem';
6 | import FileSaver from 'file-saver';
7 |
8 | export default class TopButtons extends PureComponent {
9 | static propTypes = {
10 | schema: PropTypes.object.isRequired,
11 | onSave: PropTypes.func.isRequired
12 | }
13 |
14 | static contextTypes = {
15 | i18n: PropTypes.object.isRequired
16 | }
17 |
18 | handleDownload = _ => {
19 | const { schema } = this.props;
20 | const blob = new Blob( // eslint-disable-line no-undef
21 | [JSON.stringify(schema, null, 2)],
22 | { type: 'application/json' }
23 | );
24 | FileSaver.saveAs(blob, 'schema.json');
25 | }
26 |
27 | render() {
28 | const { i18n } = this.context;
29 | const { schema, onSave } = this.props;
30 |
31 | return (
32 |
33 |
34 |
43 |
44 |
45 |
50 |
51 |
52 |
53 | )
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/packages/examples/complete-demo/src/server/storage/storage.js:
--------------------------------------------------------------------------------
1 | import { resolve } from 'path';
2 | import Sequelize from 'sequelize';
3 | import Sequelizer from 'sequelizer';
4 | import { objectIdProp } from '../../common';
5 | import objectConfig from '../objectConfig';
6 |
7 | const sqlitePath = resolve(__dirname, './demo.sqlite');
8 |
9 | class Storage {
10 | sequelize = null;
11 | invoiceModel = null;
12 |
13 | init = async function() {
14 | this.sequelize = new Sequelize('mainDB', null, null, {
15 | dialect: "sqlite",
16 | storage: sqlitePath,
17 | define: {
18 | charset: 'utf8',
19 | dialectOptions: {
20 | collate: 'utf8_general_ci'
21 | }
22 | },
23 | sync: { force: true },
24 | logging: null
25 | });
26 |
27 | // create Invoice model from JSON schema
28 | const objectSchema = objectConfig.getConfig().schema;
29 | const definition = Sequelizer.fromJsonSchema(objectSchema, 'InvoiceSchema', {
30 | uniqueFields: [objectIdProp]
31 | });
32 |
33 | this.invoiceModel = this.sequelize.define('invoice', definition);
34 | await this.invoiceModel.drop(); // drop previous table for demo purposes
35 | return this.invoiceModel.sync(); // create a table
36 | }
37 |
38 | getAllObjects = async function() {
39 | return this.invoiceModel.findAll();
40 | }
41 |
42 | getObjectById = async function(id) {
43 | return this.invoiceModel.findOne({ where: { [objectIdProp]: id } });
44 | }
45 |
46 | addObject = async function(object) {
47 | return this.invoiceModel.create(object)
48 | }
49 |
50 | updateObject = async function(object) {
51 | const dbObject = await this.getObjectById(object[objectIdProp]);
52 | if (!dbObject) {
53 | throw new Error('updateObject have not found a host object!')
54 | }
55 | return dbObject.update(object)
56 | }
57 | }
58 |
59 | export default new Storage();
60 |
--------------------------------------------------------------------------------
/packages/examples/complete-demo/config/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
3 |
4 | const plugins = [];
5 |
6 | if (process.env.NODE_ENV === 'production') {
7 | plugins.push(new UglifyJsPlugin())
8 | }
9 |
10 | module.exports = {
11 | context: path.resolve(__dirname, '../'),
12 | plugins,
13 | entry: path.resolve(__dirname, '../src/client/index.js'),
14 | output: {
15 | filename: 'bundle.js',
16 | path: path.resolve(__dirname, '../build'),
17 | publicPath: '/',
18 | library: 'fsmCompleteDemo',
19 | libraryTarget: 'umd',
20 | },
21 | externals: {
22 | react: 'React',
23 | 'react-dom': 'ReactDOM'
24 | },
25 | devServer: {
26 | contentBase: path.resolve(__dirname, '../build'),
27 | host: process.env.HOST || 'localhost'
28 | },
29 | devtool: 'inline-source-map',
30 | module: {
31 | rules: [
32 | {
33 | test: /\.js$/,
34 | include: path.resolve(__dirname, '../src/client'),
35 | use: {
36 | loader: 'babel-loader',
37 | options: {
38 | presets: ['env', 'react'],
39 | plugins: [
40 | require('babel-plugin-transform-class-properties'),
41 | require('babel-plugin-transform-object-rest-spread'),
42 | require('babel-plugin-transform-decorators-legacy').default
43 | ]
44 | }
45 | },
46 | exclude: /node_modules/
47 | },
48 | {
49 | test: /\.css$/,
50 | use: ['style-loader', 'css-loader']
51 | },
52 | {
53 | test: /\.less$/,
54 | use: ['style-loader', 'css-loader', 'less-loader']
55 | },
56 | {
57 | test: /\.(png|woff|woff2|eot|ttf)$/,
58 | loader: ['file-loader']
59 | },
60 | {
61 | test: /\.(svg)(\?[a-z0-9=&.]+)?$/,
62 | use: ['raw-loader']
63 | },
64 | ]
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/packages/examples/complete-demo/src/client/components/App.react.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import { BrowserRouter as Router, Route, withRouter } from 'react-router-dom';
3 | import PropTypes from 'prop-types';
4 | import { I18nManager } from '@opuscapita/i18n';
5 | import { uiMessageNotifications } from '../uiGlobalComponents';
6 | import Menu from './Menu.react';
7 | import HomePage from './HomePage.react';
8 | import Editor from './Editor.react';
9 | import WorkflowHistory from './History.react';
10 | import { notificationSuccess, notificationError } from '../constants';
11 | import './styles.css';
12 |
13 | const url = baseUrl => path => `${baseUrl}/${path}`.replace(/\/{2,}/, '/');
14 |
15 | export default class App extends PureComponent {
16 | static childContextTypes = {
17 | i18n: PropTypes.object.isRequired,
18 | uiMessageNotifications: PropTypes.object.isRequired,
19 | url: PropTypes.func.isRequired
20 | }
21 |
22 | constructor(...args) {
23 | super(...args);
24 | this.i18n = new I18nManager();
25 | }
26 |
27 | getChildContext() {
28 | return {
29 | i18n: this.i18n,
30 | uiMessageNotifications,
31 | url: url(this.props.baseUrl)
32 | }
33 | }
34 |
35 | componentWillUnmount() {
36 | uiMessageNotifications.remove({ id: notificationSuccess })
37 | uiMessageNotifications.remove({ id: notificationError })
38 | }
39 |
40 | render() {
41 | const MyMenu = withRouter(Menu);
42 |
43 | return (
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 | )
53 | }
54 | }
55 |
56 | App.propTypes = {
57 | baseUrl: PropTypes.string
58 | }
59 |
60 | App.defaultProps = {
61 | baseUrl: "/"
62 | }
63 |
--------------------------------------------------------------------------------
/packages/history/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@opuscapita/fsm-workflow-history",
3 | "version": "3.0.2",
4 | "description": "Business-object history for FSM workflow",
5 | "repository": "https://github.com/OpusCapita/fsm/tree/master/packages/history",
6 | "main": "src/index.js",
7 | "files": [
8 | "src"
9 | ],
10 | "scripts": {
11 | "lint": "eslint src",
12 | "test": "rimraf ./.nyc_output ./coverage && nyc --reporter=lcov --reporter=text mocha --reporter=mocha-junit-reporter --recursive \"src/**/*.spec.js\"",
13 | "test-only": "mocha --recursive \"src/**/*.spec.js\""
14 | },
15 | "publishConfig": {
16 | "registry": "https://registry.npmjs.org/",
17 | "access": "public"
18 | },
19 | "author": "",
20 | "license": "Apache-2.0",
21 | "homepage": "https://github.com/OpusCapita/fsm-workflow/tree/master/packages/history#readme",
22 | "keywords": [
23 | "history",
24 | "fsm",
25 | "statemachine",
26 | "state-machine",
27 | "state machine",
28 | "finite-state-machine",
29 | "finite state machine",
30 | "finite",
31 | "machine",
32 | "state",
33 | "transition",
34 | "action",
35 | "state"
36 | ],
37 | "dependencies": {
38 | "umzug": "2.1.0"
39 | },
40 | "peerDependencies": {
41 | "sequelize": "~5 || ~6"
42 | },
43 | "devDependencies": {
44 | "babel-eslint": "7.2.3",
45 | "eslint": "4.14.0",
46 | "eslint-config-opuscapita": "2.0.0",
47 | "eslint-plugin-react": "7.5.1",
48 | "mocha": "5.0.0",
49 | "mocha-junit-reporter": "1.13.0",
50 | "nyc": "11.1.0",
51 | "rimraf": "2.6.1",
52 | "sequelize": "6.6.5",
53 | "sqlite3": "4.1.1"
54 | },
55 | "nyc": {
56 | "per-file": true,
57 | "all": true,
58 | "exclude": [
59 | "**/*.spec.js",
60 | "**/migrations/*.js",
61 | "**/index.js"
62 | ],
63 | "check-coverage": true,
64 | "lines": 100,
65 | "statements": 100,
66 | "functions": 100,
67 | "branches": 100
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/packages/editor/scripts/gh-pages/deploy.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | # ideas used from https://gist.github.com/motemen/8595451
3 |
4 | # abort the script if there is a non-zero error
5 | set -e
6 |
7 | # uncomment for verbose logging
8 | # set -x
9 |
10 | # show where we are on the machine
11 | pwd
12 |
13 | BASEDIR=$(dirname "$0")
14 | SITE_SOURCE="$1"
15 |
16 | if [ ! -d "$SITE_SOURCE" ]
17 | then
18 | echo "Usage: $0 "
19 | exit 1
20 | fi
21 |
22 | # get current git branch name
23 | GIT_BRANCH=`git rev-parse --abbrev-ref HEAD`
24 |
25 | # replace "/", "#", etc. in current git branch name
26 | urlencode() {
27 | node -e "console.log('${*}'.replace('/', '-').replace('#', '-'))"
28 | }
29 |
30 | SAFE_GIT_BRANCH=`urlencode $GIT_BRANCH`
31 | echo "Current branch is $SAFE_GIT_BRANCH"
32 |
33 | # now lets setup a new repo so we can update the gh-pages branch
34 | git config --global user.email "$GH_MAIL" > /dev/null 2>&1
35 | git config --global user.name "$GH_NAME" > /dev/null 2>&1
36 |
37 | # switch into the the gh-pages branch
38 | if git rev-parse --verify origin/gh-pages > /dev/null 2>&1
39 | then
40 | git stash # for during release builds package.json is changed, therefore remove changes before checkout to gh-pages branch
41 | git checkout gh-pages
42 | git pull
43 | else
44 | git checkout --orphan gh-pages
45 | fi
46 |
47 | # delete any old site as we are going to replace it
48 | rm -rf "../../branches/${SAFE_GIT_BRANCH}/editor"
49 | mkdir -p "../../branches/${SAFE_GIT_BRANCH}/editor"
50 |
51 | # copy over or recompile the new site
52 | cp -r ./$SITE_SOURCE/* "../../branches/${SAFE_GIT_BRANCH}/editor"
53 |
54 | # stage any changes and new files
55 | git add -A
56 | # now commit, ignoring branch gh-pages doesn't seem to work, so trying skip
57 | git commit --allow-empty -m "Deploy to GitHub pages [ci skip]"
58 | # and push, but send any output to /dev/null to hide anything sensitive
59 | git push --force --quiet origin gh-pages > /dev/null 2>&1
60 |
61 | echo "Finished Deployment!"
--------------------------------------------------------------------------------
/packages/core/actionsAndConditions.md:
--------------------------------------------------------------------------------
1 | ### Actions and Conditions(guards/automatic)
2 | * JS code as text(expression). E.g.
3 | * condition ```invoice.total > 1000```
4 | * action ```sendEmail({to: 'mr.smith@dot.com'})```
5 |
6 | * Structured function call definition in JSON that refer to externally implemented/defined function and specified arguments:
7 | * condition
8 | ```javascipt
9 | {
10 | name: 'totalCostIsGreaterThan'
11 | arguments: {
12 | value: 1000
13 | }
14 | }
15 | ```
16 | * action
17 | ```javascipt
18 | {
19 | name: 'sendEmail'
20 | arguments: {
21 | to: 'mr.smith@dot.com'
22 | }
23 | }
24 | ```
25 | 3. ...other options?
26 |
27 | Pure JavaScript is easier to define at once, but hard to support. Non developer will be able to write the code but its hard to believe that it would work and as javaScript untyped/dynamically typed language it is hard to constrain people from such error by checking/parsing the code. Code that is not written by developers and could be created on the fly in production is not supportable (ho to apply migration process to something that is not known).
28 |
29 | > Anything that can go wrong will go wrong
30 |
31 | With structured approach it is possible to update code/implantation as soon as you change/update application API in non compatible way. Structural approach provides possibility to build up workflow UI designer that is more ( average) user friendly.
32 | In simple case JS expression/code is much faster to write (no one can stop you from doing this, sure you'll do it):
33 | ```invoice.total > 1000```
34 |
35 | But that does not mean that people would be smart enough to not creates complexity by writing a lot of code:
36 | ```javascipt
37 | if (invoice.items) {
38 | var foundItem = null;
39 | for (var i=0; i < invoice.items.legth; i++){
40 | if (invoice.item[i].amount > 700) {
41 | return true
42 | }
43 | }
44 | }
45 | return false
46 | ```
47 | Instead there could named function with good description, input parameter (+ description and type definition like in React/Flow) and unit tests.
48 |
--------------------------------------------------------------------------------
/packages/task-manager/src/specs/TaskManager.eventSending.spec.js:
--------------------------------------------------------------------------------
1 | import assert from 'assert';
2 | import { Machine, MachineDefinition } from '@opuscapita/fsm-workflow-core';
3 | import TaskManager from '../TaskManager';
4 |
5 | describe('Task manager:event sending', function() {
6 | const machine = new Machine({
7 | machineDefinition: new MachineDefinition({
8 | objectConfiguration: {
9 | "stateFieldName": "status"
10 | },
11 | schema: {
12 | "name": "test",
13 | "initialState": "init",
14 | "finalStates": [
15 | "finished"
16 | ],
17 | "transitions": [
18 | {
19 | "from": "init",
20 | "event": "finish",
21 | "to": "finished",
22 | }
23 | ]
24 | }
25 | }),
26 | convertObjectToReference: (o) => {
27 | return {
28 | // ...we don't need it here
29 | }
30 | }
31 | });
32 |
33 | let object = {};
34 |
35 | beforeEach(() => {
36 | object[MachineDefinition.getDefaultObjectStateFieldName()] = '';
37 | });
38 |
39 | const search = ({ searchParams }) => {
40 | return new Promise((resolve, reject) => {
41 | setTimeout(() => (resolve([object])), 100);
42 | })
43 | };
44 |
45 | const update = (newValue) => {
46 | return new Promise((resolve, reject) => {
47 | setTimeout(() => {
48 | object = { ...newValue };
49 | resolve({ object: newValue });
50 | }, 100);
51 | })
52 | };
53 |
54 |
55 | it('test starting & saving after it', (done) => {
56 | const tm = new TaskManager({ machine, search, update });
57 | tm.start({ object }).then((result) => {
58 | assert.equal(object.status, 'init');
59 | done();
60 | });
61 | });
62 |
63 | it('test event sending & saving', (done) => {
64 | const tm = new TaskManager({ machine, search, update });
65 | machine.start({ object }).then((startedTask) => {
66 | tm.sendEvent({ object, event: 'finish' }).then((result) => {
67 | assert.equal(object.status, 'finished');
68 | done();
69 | })
70 | });
71 | });
72 | });
73 |
--------------------------------------------------------------------------------
/packages/editor/src/components/ParamsEditor/components/IntegerInput.react.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import PropTypes from 'prop-types';
3 | import FormControl from 'react-bootstrap/lib/FormControl';
4 | import ErrorLabel from '../../ErrorLabel.react';
5 | import { valueOrNull } from '../../utils';
6 |
7 | export default class IntegerInput extends PureComponent {
8 | static propTypes = {
9 | value: PropTypes.number,
10 | onChange: PropTypes.func
11 | }
12 |
13 | static contextTypes = {
14 | i18n: PropTypes.object
15 | }
16 |
17 | constructor(...args) {
18 | super(...args);
19 | const { i18n } = this.context;
20 | const { value } = this.props;
21 | this.state = {
22 | value: i18n.formatNumber(valueOrNull(value)) || ''
23 | }
24 | }
25 |
26 | handleChange = ({ target: { value } }) => {
27 | const { i18n } = this.context;
28 |
29 | let error;
30 |
31 | try {
32 | const result = i18n.parseNumber(value || null);
33 | this.props.onChange(result)
34 | } catch (err) {
35 | error = i18n.getMessage('fsmWorkflowEditor.ui.paramsEditor.integerInput.inValid')
36 | } finally {
37 | this.setState({ value, error })
38 | }
39 | }
40 |
41 | handleBlur = _ => {
42 | const { i18n } = this.context;
43 | const { value } = this.state;
44 |
45 | try {
46 | const parsed = i18n.parseNumber(value || null);
47 | this.setState({ value: i18n.formatNumber(valueOrNull(parsed)) || '' })
48 | } catch (err) {
49 | this.setState({ error: i18n.getMessage('fsmWorkflowEditor.ui.paramsEditor.integerInput.inValid') })
50 | }
51 | }
52 |
53 | render() {
54 | const { value, error } = this.state;
55 |
56 | return (
57 |
58 |
64 |
65 |
66 | )
67 | }
68 | }
69 |
70 | IntegerInput.propTypes = {
71 | value: PropTypes.number
72 | }
73 |
74 | IntegerInput.contextTypes = {
75 | i18n: PropTypes.object
76 | }
77 |
--------------------------------------------------------------------------------
/packages/history/src/models/interfaces/workflowTransitionHistory.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | /*
4 | * The function return Model.create(...) promise
5 | * which resolved value is a row instance.
6 | */
7 | const add = model => fields => model.create(fields);
8 |
9 | /*
10 | * The function returns Model.findAll(...) promise
11 | * which resolved value is an array of objects.
12 | */
13 | const search = model => {
14 | const Op = (
15 | model.sequelize &&
16 | model.sequelize.constructor &&
17 | model.sequelize.constructor.Op
18 | ) || {
19 | gt: '$gt',
20 | gte: '$gte',
21 | lt: '$lt',
22 | lte: '$lte'
23 | };
24 | const { gt, gte, lt, lte } = Op
25 | return (
26 | {
27 | object: { businessObjId, businessObjType } = {},
28 | user,
29 | workflowName,
30 | finishedOn = {}
31 | } = {},
32 | {
33 | max = 25,
34 | offset = 0
35 | } = {},
36 | {
37 | by = 'finishedOn',
38 | order = 'desc'
39 | } = {}
40 | ) => {
41 | return model.
42 | findAll({
43 | where: {
44 | ...(businessObjId && { businessObjId }),
45 | ...(businessObjType && { businessObjType }),
46 | ...(user && { user }),
47 | ...(workflowName && { workflowName }),
48 | ...(Object.keys(finishedOn).length && {
49 | finishedOn: {
50 | ...(finishedOn.gt && { [gt]: finishedOn.gt }),
51 | ...(finishedOn.gte && { [gte]: finishedOn.gte }),
52 | ...(finishedOn.lt && { [lt]: finishedOn.lt }),
53 | ...(finishedOn.lte && { [lte]: finishedOn.lte })
54 | }
55 | })
56 | },
57 | raw: true,
58 | order: [[by, order.toUpperCase()]],
59 | limit: max,
60 | offset
61 | })
62 | }
63 | };
64 |
65 | /*
66 | * The function return Model.delete(...) promise
67 | * which resolved value is number of removed records.
68 | */
69 | const del = model => (where = null) => {
70 | return model.destroy({ where });
71 | };
72 |
73 | module.exports = model => ({
74 | add: add(model),
75 | search: search(model),
76 | delete: del(model)
77 | });
78 |
--------------------------------------------------------------------------------
/packages/editor/src/components/ParamsEditor/components/PathExpressionInputInjector/ExpressionEditor.react.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import PropTypes from 'prop-types';
3 | import Modal from 'react-bootstrap/lib/Modal';
4 | import Button from 'react-bootstrap/lib/Button';
5 | import ObjectInspector from '../../../ObjectInspector.react';
6 | import { unifyPath } from '../../../utils';
7 |
8 | export default class ExpressionEditor extends PureComponent {
9 | static propTypes = {
10 | onClose: PropTypes.func.isRequired,
11 | onSelect: PropTypes.func.isRequired,
12 | currentPath: PropTypes.string
13 | }
14 |
15 | static contextTypes = {
16 | i18n: PropTypes.object.isRequired,
17 | objectConfiguration: PropTypes.object.isRequired
18 | }
19 |
20 | handleClick = ({ path }) => {
21 | const { objectConfiguration: { alias = 'object' } } = this.context;
22 | this.props.onSelect(`${alias}${unifyPath(path)}`)
23 | }
24 |
25 | render() {
26 | const {
27 | objectConfiguration: {
28 | alias = 'object',
29 | example
30 | },
31 | i18n
32 | } = this.context;
33 |
34 | const { currentPath } = this.props;
35 |
36 | return (
37 |
43 |
44 |
45 | {i18n.getMessage('fsmWorkflowEditor.ui.paramsEditor.selectProperty', { businessObject: alias })}
46 |
47 |
48 |
49 |
56 |
57 |
58 |
61 |
62 |
63 | )
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/packages/task-manager/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@opuscapita/fsm-workflow-task-manager",
3 | "version": "3.0.2",
4 | "description": "Task manager for FSM workflow",
5 | "main": "./lib/index.js",
6 | "files": [
7 | "lib"
8 | ],
9 | "scripts": {
10 | "build": "cross-env NODE_ENV=production webpack --config config/webpack.config.js",
11 | "lint": "eslint src",
12 | "test-only": "mocha src/**/*.spec.js --require config/mocha-setup.js",
13 | "test": "rimraf ./.nyc_output ./coverage && cross-env nyc mocha --require config/mocha-setup.js --recursive \"src/**/*.spec.js\"",
14 | "prepare": "npm run build",
15 | "prepack": "npm run build"
16 | },
17 | "repository": "https://github.com/OpusCapita/fsm/tree/master/packages/task-manager",
18 | "publishConfig": {
19 | "registry": "https://registry.npmjs.org/",
20 | "access": "public"
21 | },
22 | "author": "Daniel Zhitomirsky ",
23 | "license": "Apache-2.0",
24 | "bugs": {
25 | "url": "https://github.com/OpusCapita/fsm/issues"
26 | },
27 | "homepage": "https://github.com/OpusCapita/fsm/tree/master/task-manager#readme",
28 | "dependencies": {
29 | "bluebird": "3.5.0"
30 | },
31 | "devDependencies": {
32 | "@opuscapita/fsm-workflow-core": "3.0.2",
33 | "@opuscapita/npm-scripts": "2.0.0",
34 | "babel-core": "6.26.0",
35 | "babel-eslint": "7.2.3",
36 | "babel-loader": "7.1.2",
37 | "babel-plugin-istanbul": "4.1.4",
38 | "babel-preset-es2015": "6.24.1",
39 | "babel-preset-react": "6.24.1",
40 | "babel-preset-stage-0": "6.24.1",
41 | "babel-register": "6.26.0",
42 | "cross-env": "5.0.5",
43 | "eslint": "3.19.0",
44 | "eslint-config-opuscapita": "1.0.9",
45 | "eslint-plugin-react": "6.10.3",
46 | "mocha": "3.5.0",
47 | "mocha-junit-reporter": "1.13.0",
48 | "nyc": "11.1.0",
49 | "rimraf": "2.6.1",
50 | "webpack": "3.5.5"
51 | },
52 | "nyc": {
53 | "sourceMap": false,
54 | "instrument": false,
55 | "exclude": [
56 | "**/*.spec.js"
57 | ],
58 | "check-coverage": true,
59 | "lines": 70,
60 | "statements": 70,
61 | "functions": 70,
62 | "branches": 60
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/packages/core/src/specs/Machine.start.spec.js:
--------------------------------------------------------------------------------
1 | import assert from 'assert';
2 | import Machine from '../Machine';
3 | import MachineDefinition from '../MachineDefinition';
4 | import bluebird from 'bluebird';
5 |
6 | const convertObjectToReference = (object) => {
7 | return {
8 | businessObjId: 'tesla',
9 | businessObjType: 'car'
10 | }
11 | };
12 |
13 | const createMachine = ({ history } = {}) => {
14 | return new Machine(
15 | {
16 | machineDefinition: new MachineDefinition({
17 | schema: {
18 | name: 'verification',
19 | initialState: 'started'
20 | }
21 | }),
22 | history,
23 | convertObjectToReference
24 | }
25 | );
26 | }
27 |
28 | describe('machine: start', function() {
29 | it('returns promise', function() {
30 | const result = createMachine().start({
31 | object: {
32 | status: 'none'
33 | }
34 | });
35 | assert(result && typeof result.then === 'function')
36 | });
37 |
38 | it('sets correctly initial state', function() {
39 | const result = createMachine().start({
40 | object: {
41 | status: 'none'
42 | }
43 | });
44 | return result.then(({ object }) => {
45 | // console.log(`object '${JSON.stringify(object)}'`)
46 | assert.equal(object.status, 'started');
47 | })
48 | });
49 |
50 | it('creates correct history record', function() {
51 | let historyRecordUnderTest = null;
52 | const history = {
53 | add(passedData) {
54 | historyRecordUnderTest = passedData;
55 | return bluebird.Promise.resolve(historyRecordUnderTest);
56 | }
57 | };
58 | const machine = createMachine({ history });
59 | const object = {
60 | status: 'none'
61 | };
62 | const user = 'johnny';
63 | const description = 'getoff!';
64 | return machine.start({ object, user, description }).then(({ object }) => {
65 | assert.deepEqual(historyRecordUnderTest, {
66 | from: 'NULL',
67 | to: object.status,
68 | event: '__START__',
69 | ...convertObjectToReference(object),
70 | workflowName: machine.machineDefinition.schema.name,
71 | user,
72 | description
73 | });
74 | });
75 | });
76 | });
77 |
--------------------------------------------------------------------------------
/packages/examples/complete-demo/src/client/components/Editor.react.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import PropTypes from 'prop-types';
3 | import superagent from 'superagent';
4 | import WorkflowEditor from '@opuscapita/fsm-workflow-editor';
5 | import componentsRegistry from '../customComponentsRegistry';
6 | import { notificationSuccess, notificationError } from '../constants';
7 |
8 | export default class Editor extends PureComponent {
9 | static contextTypes = {
10 | uiMessageNotifications: PropTypes.object.isRequired,
11 | url: PropTypes.func.isRequired
12 | }
13 |
14 | state = {}
15 |
16 | componentDidMount() {
17 | this.getWorkflowJSON()
18 | }
19 |
20 | getWorkflowJSON = _ => {
21 | const { uiMessageNotifications, url } = this.context;
22 | const self = this;
23 | superagent.
24 | get(url('/api/editordata')).
25 | then(res => {
26 | self.setState({ ...res.body })
27 | }).
28 | catch(err => {
29 | console.log(err);
30 | uiMessageNotifications.error({
31 | id: notificationError,
32 | message: 'Failed to load editor data: ' + err.message
33 | });
34 | })
35 | }
36 |
37 | handleSave = data => {
38 | console.log({ data });
39 | const { uiMessageNotifications, url } = this.context;
40 | superagent.
41 | post(url('/api/editordata')).
42 | send(data).
43 | then(res => {
44 | uiMessageNotifications.success({
45 | id: notificationSuccess,
46 | message: 'Workflow schema saved successfully'
47 | });
48 | }).
49 | catch(err => {
50 | console.log(err);
51 | uiMessageNotifications.error({
52 | id: notificationError,
53 | message: 'Save failed: ' + err.message
54 | });
55 | })
56 | }
57 |
58 | render() {
59 | const { schema, actions, conditions, objectConfiguration } = this.state;
60 |
61 | const props = {
62 | workflow: {
63 | schema,
64 | actions,
65 | conditions,
66 | objectConfiguration
67 | },
68 | componentsRegistry,
69 | onSave: this.handleSave
70 | }
71 |
72 | return schema ? () : null
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/packages/editor/src/components/utils.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | export const isDef = v => v !== undefined && v !== null;
5 |
6 | export const valueOrNull = value => isDef(value) ? value : null;
7 |
8 | export const omitIfEmpty = propName => obj => Object.keys(obj).reduce((acc, key) => ({
9 | ...acc,
10 | ...((key !== propName || isDef(obj[propName])) && { [key]: obj[key] })
11 | }), {})
12 |
13 | /**
14 | * Return i18n message for id or id itself in case if message is not present in i18n
15 | * @param {string} field - any of 'actions', 'conditions', 'states'
16 | */
17 | export const getLabel = i18n => field => value => {
18 | const key = `fsmWorkflowEditor.${field}.${value}.label`;
19 | const message = i18n.getMessage(key);
20 | return message === key ? value : message;
21 | }
22 |
23 | export const unifyPath = path => path.split('.').slice(1).map(s => `[${JSON.stringify(s)}]`).join('');
24 |
25 | export const formatArg = ({ i18n, schema = {}, value, expression }) => {
26 | if (expression) {
27 | return (
28 |
29 |
30 | {i18n.getMessage('fsmWorkflowEditor.ui.paramsEditor.expression')}
31 |
32 | {`\u2000${value}`}
33 |
34 | )
35 | }
36 |
37 | const { type, format } = schema;
38 | const componentType = type === 'string' && format === 'date' ? 'date' : type;
39 |
40 | switch (componentType) {
41 | case 'number':
42 | return i18n.formatDecimalNumber(value);
43 | case 'integer':
44 | return i18n.formatNumber(value);
45 | case 'boolean':
46 | return isDef(value) ? String(value) : value;
47 | case 'date':
48 | return isDef(value) ? i18n.formatDate(new Date(value)) : value;
49 | case 'array':
50 | return (
51 |
52 | {
53 | (value || []).map((v, i) => (
54 | - {formatArg({ i18n, schema: schema.items, value: v })}
55 | ))
56 | }
57 |
58 | );
59 | default:
60 | return value
61 | }
62 | }
63 |
64 | formatArg.propTypes = {
65 | i18n: PropTypes.object.isRequired,
66 | schema: PropTypes.object,
67 | value: PropTypes.any,
68 | expression: PropTypes.string
69 | }
70 |
--------------------------------------------------------------------------------
/packages/editor/README.md:
--------------------------------------------------------------------------------
1 | # Usage
2 |
3 | [Editor demo (showroom)](https://opuscapita.github.io/fsm-workflow/branches/master/editor/?currentComponentName=WorkflowEditor&maxContainerWidth=100%25) - see example schema in props
4 |
5 | [Actions & conditions paramsSchema definition and usage](https://github.com/OpusCapita/fsm-workflow/blob/master/packages/editor/src/components/Actions/Readme.md)
6 |
7 | ## i18n
8 |
9 | UI labels for `states`, `conditions`, `actions` and `params` can be translated.
10 |
11 | To add translations `register` an object of the following structure within [i18nManager](https://github.com/OpusCapita/i18n) in context of your app:
12 |
13 | ```
14 | de: { // locale
15 | fsmWorkflowEditor: {
16 | actions: { // here you define translations for actions
17 | testAction: { // action name like in workflow.actions
18 | label: 'Test Action', // this text is a UI label for this action
19 | params: {
20 | nickname: { // param name in this action's schema
21 | label: 'Nickname' // UI label for this param
22 | },
23 | fullName: {
24 | label: 'Full Name'
25 | }
26 | }
27 | },
28 | sendMail: {
29 | label: 'Send Email',
30 | params: {
31 | fromAddress: {
32 | label: "Sender' address"
33 | }
34 | }
35 | },
36 | ...
37 | },
38 | conditions: { // like in workflow.conditions
39 | userHasRoles: {
40 | label: 'User Has Roles',
41 | params: {
42 | restrictedRoles: {
43 | label: 'Only these roles are allowed'
44 | }
45 | }
46 | },
47 | ...
48 | },
49 | states: {
50 | approved: {
51 | label: 'Approved'
52 | },
53 | inspectionRequired: {
54 | label: "Inspection Required"
55 | },
56 | ...
57 | }
58 | }
59 | },
60 | fi: {
61 | ...same structure
62 | }
63 | ```
64 |
65 | Plain objects are also ok:
66 |
67 | ```
68 | de: {
69 | 'fsmWorkflowEditor.states.approved.label': 'Approved',
70 | 'fsmWorkflowEditor.actions.testAction.label': 'Test action',
71 | 'fsmWorkflowEditor.actions.testAction.params.nickname.label': 'Nickname',
72 | ...
73 | }
74 | ```
--------------------------------------------------------------------------------
/packages/editor/src/components/ParamsEditor/components/DecimalInput.react.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import PropTypes from 'prop-types';
3 | import FormControl from 'react-bootstrap/lib/FormControl';
4 | import ErrorLabel from '../../ErrorLabel.react';
5 | import { valueOrNull } from '../../utils';
6 |
7 | export default class DecimalInput extends PureComponent {
8 | static propTypes = {
9 | value: PropTypes.number,
10 | onChange: PropTypes.func
11 | }
12 |
13 | static contextTypes = {
14 | i18n: PropTypes.object
15 | }
16 |
17 | constructor(...args) {
18 | super(...args);
19 | const { i18n } = this.context;
20 | const { value } = this.props;
21 | this.state = {
22 | value: i18n.formatDecimalNumber(valueOrNull(value)) || ''
23 | }
24 | }
25 |
26 | handleChange = ({ target: { value } }) => {
27 | const { i18n } = this.context;
28 |
29 | let error;
30 |
31 | try {
32 | const result = i18n.parseDecimalNumber(value || null);
33 | // chop long tail after decimal separator
34 | this.props.onChange(i18n.parseDecimalNumber(i18n.formatDecimalNumber(valueOrNull(result)) || null))
35 | } catch (err) {
36 | error = i18n.getMessage('fsmWorkflowEditor.ui.paramsEditor.decimalInput.inValid')
37 | } finally {
38 | this.setState({ value, error })
39 | }
40 | }
41 |
42 | handleBlur = _ => {
43 | const { i18n } = this.context;
44 | const { value } = this.state;
45 |
46 | try {
47 | const parsed = i18n.parseDecimalNumber(value || null);
48 | const formatted = i18n.formatDecimalNumber(valueOrNull(parsed));
49 | this.setState({ value: formatted || '' })
50 | } catch (err) {
51 | this.setState({ error: i18n.getMessage('fsmWorkflowEditor.ui.paramsEditor.decimalInput.inValid') })
52 | }
53 | }
54 |
55 | render() {
56 | const { value, error } = this.state;
57 |
58 | return (
59 |
60 |
66 |
67 |
68 | )
69 | }
70 | }
71 |
72 | DecimalInput.propTypes = {
73 | value: PropTypes.number
74 | }
75 |
76 | DecimalInput.contextTypes = {
77 | i18n: PropTypes.object
78 | }
79 |
--------------------------------------------------------------------------------
/packages/editor/src/components/CodeEditor/codemirror-placeholder-mod.js:
--------------------------------------------------------------------------------
1 | import CodeMirror from 'codemirror';
2 |
3 | // CodeMirror, copyright (c) by Marijn Haverbeke and others
4 | // Distributed under an MIT license: http://codemirror.net/LICENSE
5 |
6 | /* eslint-disable */
7 | (function(mod) {
8 | mod(CodeMirror);
9 | }(function(CodeMirror) {
10 | CodeMirror.defineOption("placeholder", "", function(cm, val, old) {
11 | const prev = old && old != CodeMirror.Init;
12 | if (val && !prev) {
13 | cm.on("blur", onBlur);
14 | cm.on("change", onChange);
15 | cm.on("focus", onFocus);
16 | cm.on("swapDoc", onChange);
17 | onChange(cm);
18 | } else if (!val && prev) {
19 | cm.off("blur", onBlur);
20 | cm.off("change", onChange);
21 | cm.off("swapDoc", onChange);
22 | clearPlaceholder(cm);
23 | const wrapper = cm.getWrapperElement();
24 | wrapper.className = wrapper.className.replace(" CodeMirror-empty", "");
25 | }
26 |
27 | if (val && !cm.hasFocus()) onBlur(cm);
28 | });
29 |
30 | function clearPlaceholder(cm) {
31 | if (cm.state.placeholder) {
32 | cm.state.placeholder.parentNode.removeChild(cm.state.placeholder);
33 | cm.state.placeholder = null;
34 | }
35 | }
36 | function setPlaceholder(cm) {
37 | clearPlaceholder(cm);
38 | const elt = cm.state.placeholder = document.createElement("pre");
39 | elt.style.cssText = "height: 0; overflow: visible";
40 | elt.style.direction = cm.getOption("direction");
41 | elt.className = "CodeMirror-placeholder";
42 | var placeHolder = cm.getOption("placeholder")
43 | if (typeof placeHolder == "string") placeHolder = document.createTextNode(placeHolder)
44 | elt.appendChild(placeHolder)
45 | cm.display.lineSpace.insertBefore(elt, cm.display.lineSpace.firstChild);
46 | }
47 |
48 | function onBlur(cm) {
49 | if (isEmpty(cm)) setPlaceholder(cm);
50 | }
51 |
52 | function onFocus(cm) {
53 | clearPlaceholder(cm);
54 | }
55 |
56 | function onChange(cm) {
57 | const wrapper = cm.getWrapperElement(), empty = isEmpty(cm);
58 | wrapper.className = wrapper.className.replace(" CodeMirror-empty", "") + (empty ? " CodeMirror-empty" : "");
59 | }
60 |
61 | function isEmpty(cm) {
62 | return (cm.lineCount() === 1) && (cm.getLine(0) === "");
63 | }
64 | }));
65 | /* eslint-enable */
66 |
--------------------------------------------------------------------------------
/packages/core/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@opuscapita/fsm-workflow-core",
3 | "version": "3.0.2",
4 | "description": "FSM workflow (for Node.js)",
5 | "main": "lib/index.js",
6 | "files": [
7 | "lib"
8 | ],
9 | "scripts": {
10 | "lint": "eslint src",
11 | "lint-fix": "eslint src --fix",
12 | "test-only": "mocha src/**/*.spec.js --require config/mocha-setup.js",
13 | "test": "rimraf ./.nyc_output ./coverage && cross-env nyc mocha --reporter=mocha-junit-reporter --require config/mocha-setup.js --recursive \"src/**/*.spec.js\"",
14 | "build": "rimraf lib && babel --copy-files --no-babelrc --presets es2015,stage-0 --ignore *.spec.js src --out-dir lib",
15 | "prepare": "npm run build",
16 | "prepack": "npm run build"
17 | },
18 | "repository": "https://github.com/OpusCapita/fsm-workflow/tree/master/packages/core",
19 | "publishConfig": {
20 | "registry": "https://registry.npmjs.org/",
21 | "access": "public"
22 | },
23 | "author": "Alexey Sergeev ",
24 | "license": "Apache-2.0",
25 | "bugs": {
26 | "url": "https://github.com/OpusCapita/fsm-workflow/issues"
27 | },
28 | "homepage": "https://github.com/OpusCapita/fsm-workflow/tree/master/packages/core#readme",
29 | "keywords": [
30 | "fsm",
31 | "statemachine",
32 | "state-machine",
33 | "state machine",
34 | "finite-state-machine",
35 | "finite state machine",
36 | "finite",
37 | "machine",
38 | "state",
39 | "transition",
40 | "action",
41 | "state"
42 | ],
43 | "dependencies": {
44 | "bluebird": "3.5.0"
45 | },
46 | "devDependencies": {
47 | "babel-cli": "6.26.0",
48 | "babel-core": "6.25.0",
49 | "babel-eslint": "8.1.2",
50 | "babel-plugin-istanbul": "4.1.4",
51 | "babel-preset-es2015": "6.24.1",
52 | "babel-preset-stage-0": "6.24.1",
53 | "babel-register": "6.26.0",
54 | "cross-env": "5.0.5",
55 | "eslint": "4.14.0",
56 | "eslint-config-opuscapita": "2.0.1",
57 | "eslint-plugin-react": "7.5.1",
58 | "mocha": "3.5.0",
59 | "mocha-junit-reporter": "1.13.0",
60 | "nyc": "11.1.0",
61 | "rimraf": "2.6.1"
62 | },
63 | "nyc": {
64 | "sourceMap": false,
65 | "instrument": false,
66 | "exclude": [
67 | "**/*.spec.js"
68 | ],
69 | "check-coverage": true,
70 | "lines": 100,
71 | "statements": 100,
72 | "functions": 100,
73 | "branches": 100
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/packages/editor/src/components/ParamsEditor/components/MultiSelect.react.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import PropTypes from 'prop-types';
3 | import FormGroup from 'react-bootstrap/lib/FormGroup';
4 | import ControlLabel from 'react-bootstrap/lib/ControlLabel';
5 | import { formatArg } from '../../utils';
6 | import withExpressionInput from './PathExpressionInputInjector';
7 | import Select from '../../Select';
8 |
9 | const value2rs = ({ i18n, schema, value }) => ({
10 | label: formatArg({ i18n, schema, value }),
11 | value
12 | })
13 |
14 | const rs2value = ({ value }) => value;
15 |
16 | @withExpressionInput
17 | export default class MultiSelect extends PureComponent {
18 | static propTypes = {
19 | value: PropTypes.oneOfType([
20 | PropTypes.array,
21 | PropTypes.string
22 | ]),
23 | onChange: PropTypes.func.isRequired,
24 | id: PropTypes.string.isRequired,
25 | label: PropTypes.oneOfType([PropTypes.string, PropTypes.func]).isRequired,
26 | schema: PropTypes.shape({
27 | items: PropTypes.shape({
28 | enum: PropTypes.array.isRequired
29 | })
30 | }).isRequired,
31 | component: PropTypes.func
32 | }
33 |
34 | static contextTypes = {
35 | i18n: PropTypes.object.isRequired
36 | }
37 |
38 | value2rs = value => value2rs({
39 | i18n: this.context.i18n,
40 | schema: this.props.schema.items,
41 | value
42 | })
43 |
44 | handleChange = value => this.props.onChange(value.map(rs2value))
45 |
46 | render() {
47 | const { id, label, value, schema, component: Component } = this.props;
48 | const rs2value = (Array.isArray(value) ? value : []).map(this.value2rs);
49 | const options = schema.items.enum.map(this.value2rs);
50 |
51 | let renderLabel = label;
52 |
53 | if (typeof label === 'function') {
54 | const Label = label;
55 | renderLabel = ()
56 | }
57 |
58 | return (
59 |
60 | {renderLabel}
61 | {
62 | Component ?
63 | (
64 |
65 | ) :
66 | (
67 |
75 | )
76 | }
77 |
78 | )
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/packages/editor/src/components/TransitionsTable/TransitionsTable.spec.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { expect } from 'chai';
3 | import Enzyme from 'enzyme';
4 | import Adapter from 'enzyme-adapter-react-15';
5 | import { mount } from 'enzyme';
6 | import TransitionsTable from './TransitionsTable.react';
7 | import { I18nManager } from '@opuscapita/i18n';
8 | import messages from '../../i18n';
9 | import { getLabel } from '../utils';
10 |
11 | const i18n = new I18nManager();
12 | i18n.register('asdaa', messages);
13 |
14 | Enzyme.configure({ adapter: new Adapter() });
15 |
16 | describe('', () => {
17 | it('renders a table of states', () => {
18 | const props = {
19 | transitions: [
20 | {
21 | "from": "inspectionRequired",
22 | "to": "approvalRequired",
23 | "event": "automatic-inspect"
24 | },
25 | {
26 | "from": "inspectionRequired",
27 | "to": "inspClrRequired",
28 | "event": "sendToClarification"
29 | }
30 | ],
31 | states: [
32 | "inspectionRequired",
33 | "approvalRequired",
34 | "inspClrRequired"
35 | ],
36 | getStateLabel: getLabel(i18n)('states'),
37 | onEditTransition: () => {},
38 | onDeleteTransition: () => {},
39 | onSaveGuards: () => {},
40 | onSaveActions: () => {},
41 | objectConfiguration: {}
42 | };
43 |
44 | const wrapper = mount(, { context: { i18n } });
45 | expect(wrapper).to.exist; // eslint-disable-line no-unused-expressions
46 | // check table header
47 | const header = wrapper.find('thead tr').at(0).find('th');
48 | expect(header.length).to.equal(4);
49 | expect(header.at(0).text().trim()).to.equal(i18n.getMessage('fsmWorkflowEditor.ui.transitions.event.label'));
50 | expect(header.at(1).text().trim()).to.equal(i18n.getMessage('fsmWorkflowEditor.ui.transitions.from.label'));
51 | expect(header.at(2).text().trim()).to.equal(i18n.getMessage('fsmWorkflowEditor.ui.transitions.to.label'));
52 | expect(header.at(3).find('button').text().trim()).
53 | to.equal(i18n.getMessage('fsmWorkflowEditor.ui.buttons.add.label'));
54 |
55 | const rows = wrapper.find('tbody tr');
56 | const firstRow = rows.at(0).find('td');
57 | expect(firstRow.at(0).text().trim()).to.equal(props.transitions[0].event);
58 | expect(firstRow.at(1).text().trim()).to.equal(props.getStateLabel(props.transitions[0].from))
59 | expect(firstRow.at(2).text().trim()).to.equal(props.getStateLabel(props.transitions[0].to))
60 | });
61 | });
62 |
--------------------------------------------------------------------------------
/packages/core/src/specs/Machine.getHistory.spec.js:
--------------------------------------------------------------------------------
1 | import assert from 'assert';
2 | import Machine from '../Machine';
3 | import MachineDefinition from '../MachineDefinition';
4 | import bluebird from 'bluebird';
5 |
6 | const convertObjectToReference = (object) => {
7 | return {
8 | businessObjId: 'Usain Bolt',
9 | businessObjType: 'sprinter'
10 | }
11 | };
12 |
13 | const workflowName = 'sprint';
14 | const createMachine = ({ history } = {}) => {
15 | return new Machine(
16 | {
17 | machineDefinition: new MachineDefinition({
18 | schema: {
19 | name: workflowName
20 | }
21 | }),
22 | history,
23 | convertObjectToReference
24 | }
25 | );
26 | }
27 |
28 | describe('machine: getHistory', function() {
29 | it('history.search is called with correct parameters and returns provided results', function() {
30 | const object = {
31 | nick: 'flash'
32 | };
33 | const foundHistoryRecord = {
34 | from: 'start',
35 | to: 'finish',
36 | event: 'gong-blow',
37 | finihedOn: Date(),
38 | user: 'start gate official',
39 | description: "stadium sreams: 'run baby, run!'",
40 | ...convertObjectToReference(object),
41 | workflowName
42 | };
43 | let passedSearchParameters, passedPaging, passedSorting = null;
44 | const history = {
45 | search(searchParameters, paging, sorting) {
46 | passedSearchParameters = searchParameters;
47 | passedPaging = paging;
48 | passedSorting = sorting;
49 | return bluebird.Promise.resolve([foundHistoryRecord]);
50 | }
51 | };
52 | const searchParameters = {
53 | object,
54 | user: 'start gate official',
55 | finishedOn: null
56 | };
57 | const paging = {
58 | max: 30,
59 | offset: 99
60 | };
61 | const sorting = {
62 | by: 'finishedOn',
63 | order: 'desc'
64 | };
65 | const machine = createMachine({ history });
66 | return machine.getHistory(searchParameters, paging, sorting).then((results) => {
67 | // check passed argiments
68 | assert.deepEqual(passedSearchParameters, {
69 | ...searchParameters,
70 | object: convertObjectToReference(searchParameters.object),
71 | workflowName
72 | });
73 | assert.deepEqual(passedPaging, paging);
74 | assert.deepEqual(passedSorting, sorting);
75 | // // check results
76 | assert.equal(results.length, 1);
77 | const { object, ...otherHistoryProperties } = results[0];
78 | assert.deepEqual(results[0], {
79 | object: convertObjectToReference(object),
80 | ...otherHistoryProperties,
81 | });
82 | });
83 | });
84 | });
85 |
--------------------------------------------------------------------------------
/packages/task-manager/src/specs/TaskManager.monitoring.spec.js:
--------------------------------------------------------------------------------
1 | import assert from 'assert';
2 | import { Machine, MachineDefinition } from '@opuscapita/fsm-workflow-core';
3 | import TaskManager from '../TaskManager';
4 |
5 | describe('Task manager:monitoring', function() {
6 | const machine = new Machine({
7 | machineDefinition: new MachineDefinition({
8 | objectConfiguration: {
9 | "stateFieldName": "status"
10 | },
11 | schema: {
12 | "name": "test",
13 | "initialState": "init",
14 | "finalStates": [
15 | "finished"
16 | ],
17 | "transitions": [
18 | {
19 | "from": "init",
20 | "event": "autoFinish",
21 | "to": "finished",
22 | "automatic": true
23 | }
24 | ]
25 | }
26 | }),
27 | convertObjectToReference: (o) => {
28 | return {
29 | // ...we don't need it here
30 | }
31 | }
32 | });
33 |
34 | let object = {};
35 |
36 | beforeEach(() => {
37 | object[MachineDefinition.getDefaultObjectStateFieldName()] = '';
38 | });
39 |
40 | const search = ({ searchParams }) => {
41 | return new Promise((resolve, reject) => {
42 | setTimeout(() => (resolve([object])), 100);
43 | })
44 | };
45 |
46 | const update = (newValue) => {
47 | return new Promise((resolve, reject) => {
48 | setTimeout(() => {
49 | object = { ...newValue };
50 | resolve();
51 | }, 100);
52 | })
53 | };
54 |
55 |
56 | it('automatic execution & monitoring test', (done) => {
57 | const tm = new TaskManager({ machine, search, update });
58 | tm.run();
59 |
60 | setTimeout(() => {
61 | assert.equal(machine.isInFinalState({ object }), true);
62 | tm.stop();
63 | done();
64 | }, 1500);
65 | });
66 |
67 |
68 | it('task list obtaining', (done) => {
69 | const tm = new TaskManager({ machine, search, update });
70 | tm.list({ searchParams: { param1: 1, param2: 'string value' } }).then((tasks) => {
71 | assert.equal(tasks.length, 1);
72 | assert.equal(tasks[0].status, '');
73 | done();
74 | })
75 | });
76 |
77 | it('test stop & getting the statistics', (done) => {
78 | const tm = new TaskManager({ machine, search, update });
79 | tm.run(200);
80 |
81 | setTimeout(() => {
82 | assert.equal(tm.stop(), true);
83 | let processExecutionInfo = Array.from(tm.processCache.values())[0];
84 | assert.equal(processExecutionInfo.name, machine.machineDefinition.schema.name);
85 | assert.equal(
86 | processExecutionInfo.started.getTime() < processExecutionInfo.finished.getTime(),
87 | true
88 | );
89 |
90 | done();
91 | }, 1000);
92 | })
93 | });
94 |
--------------------------------------------------------------------------------
/packages/core/src/specs/Machine.can-and-cannot.spec.js:
--------------------------------------------------------------------------------
1 | import assert from "assert";
2 | import Machine from "../Machine";
3 | import MachineDefinition from "../MachineDefinition";
4 |
5 | const createMachine = ({ states } = {}) => {
6 | return new Machine({
7 | machineDefinition: new MachineDefinition({
8 | schema: {
9 | transitions: [
10 | {
11 | from: "opened",
12 | event: "close",
13 | to: "closed"
14 | },
15 | {
16 | from: "closed",
17 | event: "open",
18 | to: "opened"
19 | }
20 | ],
21 | ...(states ? { states } : {})
22 | }
23 | })
24 | });
25 | };
26 |
27 | describe("machine: can", function() {
28 | it("returns true", function() {
29 | return createMachine().can({ object: { status: "opened" }, event: "close" }).then(result => {
30 | assert.equal(result, true);
31 | });
32 | });
33 |
34 | it("returns false", function() {
35 | return createMachine().can({ object: { status: "opened" }, event: "open" }).then(result => {
36 | assert.equal(result, false);
37 | });
38 | });
39 |
40 | it("returns false is release guard denies transition", function() {
41 | return createMachine({
42 | states: [
43 | {
44 | name: 'opened',
45 | release: [
46 | {
47 | guards: [
48 | {
49 | expression: 'object.enabled'
50 | }
51 | ]
52 | }
53 | ]
54 | }
55 | ]
56 | }).can({ object: { status: "opened", enabled: false }, event: "close" }).then(result => {
57 | assert.equal(result, false);
58 | });
59 | });
60 |
61 | it("returns true is release guard aloows transition", function() {
62 | return createMachine({
63 | states: [
64 | {
65 | name: 'opened',
66 | release: [
67 | {
68 | guards: [
69 | {
70 | expression: 'object.enabled'
71 | }
72 | ]
73 | }
74 | ]
75 | }
76 | ]
77 | }).can({ object: { status: "opened", enabled: true }, event: "close" }).then(result => {
78 | assert.equal(result, true);
79 | });
80 | });
81 | });
82 |
83 | describe("machine: cannot", function() {
84 | it("returns true", function() {
85 | return createMachine().cannot({ object: { status: "opened" }, event: "open" }).then(result => {
86 | assert.equal(result, true);
87 | });
88 | });
89 |
90 | it("returns false", function() {
91 | return createMachine().cannot({ object: { status: "opened" }, event: "close" }).then(result => {
92 | assert.equal(result, false);
93 | });
94 | });
95 | });
96 |
--------------------------------------------------------------------------------
/packages/editor/src/components/ObjectInspector.react.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import PropTypes from 'prop-types';
3 | // polyfills needed for 'react-inspector'
4 | import 'core-js/fn/array/from';
5 | import 'core-js/es6/symbol';
6 | import {
7 | ObjectInspector,
8 | ObjectName,
9 | ObjectValue,
10 | chromeLight
11 | } from 'react-inspector';
12 | import { unifyPath } from './utils';
13 |
14 | export default class ExampleObjectInspector extends PureComponent {
15 | static propTypes = {
16 | name: PropTypes.string,
17 | object: PropTypes.object,
18 | onClickPropName: PropTypes.func,
19 | showValues: PropTypes.bool,
20 | currentPath: PropTypes.string
21 | }
22 |
23 | static defaultProps = {
24 | object: {},
25 | showValues: true
26 | }
27 |
28 | handleClickPropName = propData => _ => this.props.onClickPropName(propData);
29 |
30 | exampleObjectNodeRenderer = ({ depth, name, data, isNonenumerable, path }) => {
31 | const { onClickPropName, showValues, currentPath } = this.props;
32 |
33 | return depth === 0 ?
34 | (
35 |
36 |
37 |
38 | ) :
39 | (
40 |
41 |
49 |
60 |
61 | {
62 | showValues && (
63 |
64 | )
65 | }
66 |
67 | )
68 | }
69 |
70 | render() {
71 | const { name, object } = this.props;
72 |
73 | return (
74 |
89 | )
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/packages/examples/complete-demo/src/client/customComponentsRegistry/FullName.react.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import PropTypes from 'prop-types';
3 | import FormGroup from 'react-bootstrap/lib/FormGroup';
4 | import ControlLabel from 'react-bootstrap/lib/ControlLabel';
5 | import FormControl from 'react-bootstrap/lib/FormControl';
6 | import { withExpressionInput } from '@opuscapita/fsm-workflow-editor';
7 |
8 | const splitFullName = str => {
9 | const s = (str || '').replace(/ +(?= )/g, '');
10 | const spaceIndex = s.indexOf(' ');
11 | return [s.slice(0, spaceIndex), s.slice(spaceIndex + 1)];
12 | }
13 |
14 | @withExpressionInput
15 | export default class FullName extends PureComponent {
16 | static propTypes = {
17 | label: PropTypes.oneOfType([PropTypes.string, PropTypes.func]).isRequired,
18 | value: PropTypes.string,
19 | onChange: PropTypes.func.isRequired,
20 | component: PropTypes.func
21 | }
22 |
23 | constructor(...args) {
24 | super(...args);
25 | const [first, last] = splitFullName(this.props.value);
26 | this.state = { first, last }
27 | }
28 |
29 | componentWillReceiveProps({ value }) {
30 | const [first, last] = splitFullName(value);
31 | this.setState(prevState => ({
32 | ...(first !== prevState.first && { first }),
33 | ...(last !== prevState.last && { last })
34 | }))
35 | }
36 |
37 | handleChange = _ => {
38 | const { first, last } = this.state;
39 | this.props.onChange(first + ' ' + last)
40 | }
41 |
42 | handleChangeFirstName = ({ target: { value } }) => this.setState({
43 | first: value.trim()
44 | }, this.handleChange)
45 |
46 | handleChangeLastName = ({ target: { value } }) => this.setState({
47 | last: value
48 | }, this.handleChange)
49 |
50 | render() {
51 | const { label, component: Component, value } = this.props;
52 | const { first, last } = this.state;
53 |
54 | // TODO maybe move this logic to HOC; it doesn't belong here
55 | let renderLabel = label;
56 |
57 | if (typeof label === 'function') {
58 | const Label = label;
59 | renderLabel = ()
60 | }
61 |
62 | return (
63 |
64 | {renderLabel}
65 | {
66 | Component ?
67 | (
68 |
69 | ) :
70 | (
71 |
72 |
78 |
84 |
85 | )
86 | }
87 |
88 | )
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/packages/editor/src/components/WorkflowEditor/customComponents/FullName.react.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import PropTypes from 'prop-types';
3 | import FormGroup from 'react-bootstrap/lib/FormGroup';
4 | import ControlLabel from 'react-bootstrap/lib/ControlLabel';
5 | import FormControl from 'react-bootstrap/lib/FormControl';
6 | import withExpressionInput from '../../ParamsEditor/components/PathExpressionInputInjector';
7 |
8 | const splitFullName = str => {
9 | const s = (str || '').replace(/ +(?= )/g, '');
10 | const spaceIndex = s.indexOf(' ');
11 | return [s.slice(0, spaceIndex), s.slice(spaceIndex + 1)];
12 | }
13 |
14 | @withExpressionInput
15 | export default class FullName extends PureComponent {
16 | static propTypes = {
17 | label: PropTypes.oneOfType([PropTypes.string, PropTypes.func]).isRequired,
18 | value: PropTypes.string,
19 | onChange: PropTypes.func.isRequired,
20 | component: PropTypes.func
21 | }
22 |
23 | constructor(...args) {
24 | super(...args);
25 | const [first, last] = splitFullName(this.props.value);
26 | this.state = { first, last }
27 | }
28 |
29 | componentWillReceiveProps({ value }) {
30 | const [first, last] = splitFullName(value);
31 | this.setState(prevState => ({
32 | ...(first !== prevState.first && { first }),
33 | ...(last !== prevState.last && { last })
34 | }))
35 | }
36 |
37 | handleChange = _ => {
38 | const { first, last } = this.state;
39 | this.props.onChange(first + ' ' + last)
40 | }
41 |
42 | handleChangeFirstName = ({ target: { value } }) => this.setState({
43 | first: value.trim()
44 | }, this.handleChange)
45 |
46 | handleChangeLastName = ({ target: { value } }) => this.setState({
47 | last: value
48 | }, this.handleChange)
49 |
50 | render() {
51 | const { label, component: Component, value } = this.props;
52 | const { first, last } = this.state;
53 |
54 | // TODO maybe move this logic to HOC; it doesn't belong here
55 | let renderLabel = label;
56 |
57 | if (typeof label === 'function') {
58 | const Label = label;
59 | renderLabel = ()
60 | }
61 |
62 | return (
63 |
64 | {renderLabel}
65 | {
66 | Component ?
67 | (
68 |
69 | ) :
70 | (
71 |
72 |
78 |
84 |
85 | )
86 | }
87 |
88 | )
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/packages/core/src/specs/Machine.constructor.spec.js:
--------------------------------------------------------------------------------
1 | import assert from 'assert';
2 | import Machine from '../Machine';
3 | import MachineDefinition from '../MachineDefinition';
4 | import bluebird from "bluebird"
5 |
6 | const createMachineCorrectly = ({ promise, history } = {}) => {
7 | return new Machine(
8 | {
9 | machineDefinition: new MachineDefinition(),
10 | promise,
11 | history
12 | }
13 | );
14 | }
15 |
16 | describe('machine: constructor', function() {
17 | it('should throw Error as machineDefinitionProvider is not defined', function() {
18 | assert.throws(() => {
19 | return new Machine()
20 | }, Error);
21 | });
22 |
23 | it('should throw Error as machine name is not defined', function() {
24 | assert.throws(() => {
25 | return new Machine({ machineDefinitionProvider: {} });
26 | }, Error);
27 | });
28 |
29 | it('should throw Error as machine definition is not found', function() {
30 | assert.throws(() => {
31 | return new Machine(
32 | {
33 | machineDefinitionProvider: {
34 | definitions: {
35 | }
36 | },
37 | name: 'testmachine'
38 | }
39 | );
40 | }, Error);
41 | });
42 |
43 | it('should not throw Error as Worklfow should be correctly created', function() {
44 | assert.doesNotThrow(createMachineCorrectly);
45 | });
46 |
47 | it('check default machine promise', function() {
48 | const w = createMachineCorrectly();
49 | assert.equal(w.promise, Machine.defaultPromise());
50 | });
51 |
52 | it('check passed machine promise', function() {
53 | const promise = 'MegaPromise';
54 | const w = createMachineCorrectly({ promise });
55 | assert.equal(w.promise, promise);
56 | });
57 |
58 | [null].forEach(promise => {
59 | it(`promise = '${promise}' in constructor is no acceptable`, () => {
60 | assert.throws(() => {
61 | createMachineCorrectly({ promise });
62 | }, Error);
63 | });
64 | });
65 |
66 | // eslint-disable-next-line max-len
67 | it("if library is used outside node (e.g. browser) then 'bluebird' should be used as default Promise implementation", () => {
68 | // store Promise
69 | const { Promise } = global;
70 | try {
71 | global.Promise = undefined;
72 |
73 | assert.equal(createMachineCorrectly().promise, bluebird.Promise);
74 | } finally {
75 | // restore promise
76 | global.Promise = Promise;
77 | }
78 | });
79 |
80 | // default history and passed history
81 | it('check default machine history', function() {
82 | // default history
83 | let w = createMachineCorrectly();
84 | assert.notEqual(w.history, null);
85 | assert.notEqual(w.history, undefined);
86 | // passed history
87 | const history = {};
88 | w = createMachineCorrectly({ history });
89 | assert.equal(w.history, history);
90 | });
91 |
92 | it('convertObjectToReference default implementation thows exception', () => {
93 | assert.throws(() => {
94 | createMachineCorrectly().convertObjectToReference();
95 | }, /.*convertObjectToReference.*is.*not.*defined.*/);
96 | });
97 | });
98 |
--------------------------------------------------------------------------------
/packages/editor/src/components/StatesTable/DeleteStateDialogBody.react.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import PropTypes from 'prop-types';
3 | import statePropTypes from './statePropTypes';
4 | import FormGroup from 'react-bootstrap/lib/FormGroup';
5 | import Radio from 'react-bootstrap/lib/Radio';
6 | import FormControl from 'react-bootstrap/lib/FormControl';
7 | import { getLabel } from '../utils';
8 |
9 | // TODO maybe optimize communication between components to make it less coupled
10 | export default class DeleteStateDialogBody extends PureComponent {
11 | static propTypes = {
12 | i18n: PropTypes.object.isRequired,
13 | states: PropTypes.arrayOf(statePropTypes).isRequired,
14 | stateName: PropTypes.string.isRequired,
15 | onSelect: PropTypes.func.isRequired
16 | }
17 |
18 | constructor(...args) {
19 | super(...args);
20 |
21 | const { states, stateName } = this.props;
22 |
23 | this.state = {
24 | alternativeState: (states.filter(({ name }) => stateName !== name)[0] || {}).name,
25 | selectedOptionIndex: 0
26 | }
27 | }
28 |
29 | handleSelectState = ({ target: { value } }) => this.setState({
30 | alternativeState: value
31 | }, _ => this.props.onSelect({ index: this.state.selectedOptionIndex, alternative: value }))
32 |
33 | handleSelectOption = index => _ => this.setState({
34 | selectedOptionIndex: index
35 | }, _ => this.props.onSelect({ index, alternative: this.state.alternativeState }))
36 |
37 | render() {
38 | const { states, stateName, i18n } = this.props;
39 | const { alternativeState, selectedOptionIndex } = this.state;
40 | const otherStates = states.filter(({ name }) => stateName !== name);
41 | const getStateName = getLabel(i18n)('states');
42 |
43 | return (
44 |
45 |
46 | {i18n.getMessage(
47 | 'fsmWorkflowEditor.ui.states.deleteDialog.message.description',
48 | { stateName: getStateName(stateName) }
49 | )}
50 |
51 |
52 |
57 | {i18n.getMessage('fsmWorkflowEditor.ui.states.deleteDialog.message.delete')}
58 |
59 |
64 | {i18n.getMessage(
65 | 'fsmWorkflowEditor.ui.states.deleteDialog.message.swap',
66 | { stateName: getStateName(stateName) }
67 | )}
68 |
74 | {
75 | otherStates.map(({ name }, i) => (
76 |
77 | ))
78 | }
79 |
80 |
81 |
82 |
83 | )
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/packages/editor/src/components/ParamsEditor/components/ArrayEditor.react.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import PropTypes from 'prop-types';
3 | import './ArrayEditor.less';
4 | import withExpressionInput from './PathExpressionInputInjector';
5 |
6 | @withExpressionInput
7 | export default class ArrayEditor extends PureComponent {
8 | static propTypes = {
9 | label: PropTypes.oneOfType([PropTypes.string, PropTypes.func]).isRequired,
10 | value: PropTypes.oneOfType([
11 | PropTypes.arrayOf(PropTypes.any),
12 | PropTypes.string
13 | ]),
14 | itemComponent: PropTypes.func,
15 | onChange: PropTypes.func.isRequired,
16 | component: PropTypes.func
17 | }
18 |
19 | handleAdd = _ => this.props.onChange((this.props.value || []).concat(null))
20 |
21 | handleChange = index => newValue => this.props.onChange(
22 | [...this.props.value.slice(0, index), newValue, ...this.props.value.slice(index + 1)]
23 | )
24 |
25 | handleDelete = index => _ => this.props.onChange(
26 | [...this.props.value.slice(0, index), ...this.props.value.slice(index + 1)]
27 | )
28 |
29 | render() {
30 | const { label, value, itemComponent: ItemComponent, component: CustomComponent } = this.props;
31 |
32 | let renderLabel = label;
33 |
34 | if (typeof label === 'function') {
35 | const Label = label;
36 | renderLabel = ()
37 | }
38 |
39 | return (
40 |
41 |
42 |
43 |
44 | |
45 |
46 | |
47 | {
48 | !CustomComponent && (
49 |
50 |
55 | |
56 | )
57 | }
58 |
59 |
60 |
61 | {
62 | CustomComponent ?
63 | (
64 |
65 | |
66 |
67 | |
68 |
69 | ) :
70 | (
71 | (Array.isArray(value) ? value : []).map((v, i, arr) => (
72 |
73 | |
74 |
75 | |
76 |
77 |
82 | |
83 |
84 | ))
85 | )
86 | }
87 |
88 |
89 |
90 | )
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/packages/editor/src/components/ParamsEditor/components/PathExpressionInputInjector/PathExpressionInputInjector.react.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import PropTypes from 'prop-types';
3 | import ExpressionSwitcher from './ExpressionSwitcher.react';
4 | import ExpressionInput from './ExpressionInput.react';
5 | import ExpressionEditor from './ExpressionEditor.react';
6 |
7 | // TODO warn about mismatch in object/param schemas
8 |
9 | export default WrappedComponent => WrappedComponent &&
10 | class WithPathExpressionInput extends PureComponent {
11 | static propTypes = {
12 | label: PropTypes.string.isRequired,
13 | onChange: PropTypes.func.isRequired,
14 | param: PropTypes.shape({
15 | value: PropTypes.any,
16 | expression: PropTypes.string
17 | })
18 | }
19 |
20 | state = {
21 | expression: !!this.props.param.expression,
22 | showDialog: false,
23 | isSwitching: false
24 | }
25 |
26 | // waiting for null value to come back from outside after switching back from expression to regular value
27 | componentWillReceiveProps({ param: { value } }) {
28 | const { expression, isSwitching } = this.state;
29 | if (!expression && isSwitching) {
30 | this.setState({ isSwitching: false })
31 | }
32 | }
33 |
34 | handleChange = value => this.props.onChange({
35 | value,
36 | ...(this.state.expression && { expression: 'path' })
37 | })
38 |
39 | handleSelectProp = path => {
40 | this.handleChange(path)
41 | this.handleClose({ selected: true })
42 | }
43 |
44 | handleToggleMode = _ => this.setState(prevState => ({
45 | expression: !prevState.expression,
46 | ...(prevState.expression ? { isSwitching: true } : { showDialog: true })
47 | }), _ => !this.state.expression && this.handleChange(null))
48 |
49 | handleEdit = _ => this.setState({ showDialog: true })
50 |
51 | handleClose = ({ selected } = {}) => this.setState(prevState => ({
52 | showDialog: false,
53 | ...(!selected && !this.props.param.value && { expression: false })
54 | }))
55 |
56 | render() {
57 | const { label, param, ...props } = this.props;
58 | const { expression, showDialog, isSwitching } = this.state;
59 |
60 | const newProps = {
61 | ...props,
62 | value: param.value,
63 | label: _ => (
64 |
69 | ),
70 | onChange: this.handleChange
71 | }
72 |
73 | if (expression || isSwitching) {
74 | newProps.component = _ => (
75 |
79 | )
80 | }
81 |
82 | return (
83 |
84 |
85 | {
86 | showDialog && (
87 |
92 | )
93 | }
94 |
95 | )
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/packages/examples/complete-demo/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@opuscapita/fsm-workflow-demo",
3 | "version": "3.0.2",
4 | "main": "index.js",
5 | "license": "MIT",
6 | "private": true,
7 | "scripts": {
8 | "lint": "rimraf src/compiled-server && eslint src",
9 | "test-only": "mocha src/**/*.spec.js --require config/mocha-setup.js",
10 | "test": "rimraf ./.nyc_output ./coverage && cross-env nyc mocha --reporter=mocha-junit-reporter --require config/mocha-setup.js --recursive \"src/**/*.spec.js\"",
11 | "add-dependency": "cd ../../../ && npm run add-dependency -- --scope=@opuscapita/fsm-workflow-demo",
12 | "start": "rimraf src/compiled-server && babel src/server -s -D -d src/compiled-server && cross-env PORT=3020 NODE_ENV=development node src/compiled-server/index.js",
13 | "demo:build": "rimraf build && cross-env NODE_ENV=production webpack --config config/webpack.config.js && rimraf src/compiled-server && babel src/server -s -D -d src/compiled-server",
14 | "demo:start": "cross-env NODE_ENV=production node src/compiled-server/index.js"
15 | },
16 | "dependencies": {
17 | "@opuscapita/fsm-workflow-core": "3.0.2",
18 | "@opuscapita/fsm-workflow-editor": "3.0.2",
19 | "@opuscapita/fsm-workflow-history": "3.0.2",
20 | "@opuscapita/i18n": "1.1.1",
21 | "@opuscapita/react-navigation": "0.1.2",
22 | "@opuscapita/react-notifications": "1.4.3",
23 | "body-parser": "1.18.2",
24 | "express": "4.16.2",
25 | "json-schema-faker": "0.4.7",
26 | "lodash": "4.17.5",
27 | "morgan": "1.9.0",
28 | "prop-types": "15.6.0",
29 | "react-bootstrap": "0.31.5",
30 | "react-router-dom": "4.2.2",
31 | "sequelize": "6.6.5",
32 | "sequelizer": "1.1.4",
33 | "sqlite3": "4.1.1",
34 | "superagent": "3.8.2"
35 | },
36 | "devDependencies": {
37 | "babel-cli": "6.26.0",
38 | "babel-eslint": "8.2.1",
39 | "babel-loader": "7.1.2",
40 | "babel-plugin-istanbul": "4.1.4",
41 | "babel-plugin-transform-class-properties": "6.24.1",
42 | "babel-plugin-transform-decorators-legacy": "1.3.4",
43 | "babel-plugin-transform-object-rest-spread": "6.26.0",
44 | "babel-preset-env": "1.6.1",
45 | "babel-preset-react": "6.24.1",
46 | "copy-webpack-plugin": "4.5.2",
47 | "cross-env": "5.1.3",
48 | "css-loader": "0.28.9",
49 | "eslint": "4.18.0",
50 | "eslint-config-opuscapita": "2.0.0",
51 | "eslint-plugin-react": "7.6.1",
52 | "file-loader": "1.1.8",
53 | "html-webpack-plugin": "3.2.0",
54 | "less-loader": "4.0.5",
55 | "mocha": "3.5.0",
56 | "mocha-junit-reporter": "1.13.0",
57 | "nyc": "11.1.0",
58 | "raw-loader": "0.5.1",
59 | "react": "15.6.2",
60 | "react-dom": "15.6.2",
61 | "rimraf": "2.6.2",
62 | "style-loader": "0.20.1",
63 | "uglifyjs-webpack-plugin": "1.2.0",
64 | "webpack": "3.4.1",
65 | "webpack-dev-middleware": "2.0.5"
66 | },
67 | "peerDependencies": {
68 | "react": "^15.6.2 || ^16.2.0",
69 | "react-dom": "^15.6.2 || ^16.2.0"
70 | },
71 | "babel": {
72 | "presets": [
73 | [
74 | "env",
75 | {
76 | "targets": {
77 | "node": "8"
78 | }
79 | }
80 | ]
81 | ],
82 | "plugins": [
83 | "transform-object-rest-spread",
84 | "transform-class-properties"
85 | ]
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/packages/examples/invoice-console/forms.js:
--------------------------------------------------------------------------------
1 | const blessed = require("blessed");
2 |
3 | const eventForm = ({
4 | screen,
5 | formProperties = {},
6 | availableEvents = [],
7 | onSubmit = () => {}
8 | }) => {
9 | const form = blessed.form({
10 | label: 'Action Form',
11 | mouse: true,
12 | keys: true,
13 | vi: true,
14 | left: 0,
15 | top: 0,
16 | width: "100%",
17 | scrollable: true,
18 | scrollbar: {
19 | ch: " "
20 | },
21 | border: 'line',
22 | ...formProperties
23 | });
24 |
25 | form.on("submit", function(data) {
26 | availableEvents.find(event => {
27 | if (data[event]) {
28 | onSubmit({ event });
29 | return;
30 | }
31 | });
32 | });
33 |
34 | const radioset = blessed.radioset({
35 | parent: form,
36 | shrink: true
37 | })
38 |
39 | let top = 0;
40 | const options = [];
41 | for (let i = 0; i < availableEvents.length; i++) {
42 | options.push(blessed.radiobutton({
43 | parent: radioset,
44 | mouse: true,
45 | checked: (i === 0),
46 | keys: true,
47 | shrink: true,
48 | top: top++,
49 | shrink: true,
50 | name: availableEvents[i],
51 | content: availableEvents[i]
52 | }));
53 | }
54 |
55 | const submit = blessed.button({
56 | parent: form,
57 | mouse: true,
58 | keys: true,
59 | top: top++,
60 | shrink: true,
61 | name: "submit",
62 | content: "Send Event",
63 | padding: {
64 | left: 1,
65 | right: 1
66 | },
67 | style: {
68 | bg: "white",
69 | fg: "black",
70 | focus: {
71 | bg: "blue"
72 | }
73 | }
74 | });
75 |
76 | submit.on("press", function() {
77 | form.submit();
78 | });
79 |
80 | if (options.length > 0) {
81 | options[0].focus();
82 | }
83 | screen.render();
84 |
85 | return form;
86 | };
87 |
88 | const startForm = ({
89 | screen,
90 | formProperties = {},
91 | submitLabel = 'Start',
92 | onSubmit = () => {},
93 | }) => {
94 | // console.log(`arguments[0]: '${JSON.stringify(arguments[0])}'`);
95 | const form = blessed.form({
96 | label: 'Action Form',
97 | mouse: true,
98 | keys: true,
99 | vi: true,
100 | left: 0,
101 | top: 0,
102 | width: "100%",
103 | scrollable: true,
104 | scrollbar: {
105 | ch: " "
106 | },
107 | border: 'line',
108 | ...formProperties
109 | });
110 |
111 | form.on("submit", onSubmit);
112 |
113 | const submit = blessed.button({
114 | parent: form,
115 | mouse: true,
116 | keys: true,
117 | // top: 10,
118 | shrink: true,
119 | name: "submit",
120 | content: submitLabel,
121 | padding: {
122 | left: 1,
123 | right: 1
124 | },
125 | style: {
126 | bg: "white",
127 | fg: "black",
128 | focus: {
129 | bg: "blue"
130 | }
131 | }
132 | });
133 |
134 | submit.on("press", function() {
135 | form.submit();
136 | });
137 |
138 | submit.focus();
139 | screen.render();
140 |
141 | return form;
142 | };
143 |
144 | const restartForm = (input) => {
145 | return startForm({...input, submitLabel:'Start'});
146 | }
147 |
148 | export default {
149 | startForm,
150 | eventForm,
151 | restartForm,
152 | };
153 |
--------------------------------------------------------------------------------
/packages/editor/src/components/StatesTable/StatesTable.spec.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { expect } from 'chai';
3 | import Enzyme from 'enzyme';
4 | import Adapter from 'enzyme-adapter-react-15';
5 | import { mount } from 'enzyme';
6 | import StatesTable from './StatesTable.react';
7 | import { I18nManager } from '@opuscapita/i18n';
8 | import messages from '../../i18n';
9 |
10 | const i18n = new I18nManager();
11 | i18n.register('asddass', messages);
12 |
13 | Enzyme.configure({ adapter: new Adapter() });
14 |
15 | describe('', () => {
16 | it('renders a table of states', () => {
17 | const props = {
18 | states: [
19 | { name: 'one' },
20 | { name: 'two' }
21 | ],
22 | initialState: 'one',
23 | finalStates: ['two'],
24 | onDelete: () => {},
25 | onEdit: () => {},
26 | statesInTransitions: []
27 | };
28 |
29 | const wrapper = mount(, { context: { i18n } });
30 | expect(wrapper).to.exist; // eslint-disable-line no-unused-expressions
31 |
32 | const header = wrapper.find('thead tr').at(0).find('th');
33 | expect(header.length).to.equal(5);
34 | expect(header.at(0).text().trim()).to.equal(i18n.getMessage('fsmWorkflowEditor.ui.states.name.label'));
35 | expect(header.at(1).text().trim()).to.equal(i18n.getMessage('fsmWorkflowEditor.ui.states.description.label'));
36 | expect(header.at(2).text().trim()).to.equal(i18n.getMessage('fsmWorkflowEditor.ui.states.initial.label'));
37 | expect(header.at(3).text().trim()).to.equal(i18n.getMessage('fsmWorkflowEditor.ui.states.final.label'));
38 | expect(header.at(4).find('button').text().trim()).
39 | to.equal(i18n.getMessage('fsmWorkflowEditor.ui.buttons.add.label'));
40 |
41 | expect(wrapper.find('tbody tr')).to.have.length(props.states.length);
42 |
43 | const firstRow = wrapper.find('tbody tr').at(0).find('td');
44 | expect(firstRow.at(0).text()).to.equal(props.states[0].name);
45 | expect(firstRow.at(2).childAt(0).exists()).to.be.true; // eslint-disable-line no-unused-expressions
46 | expect(firstRow.at(3).childAt(0).exists()).to.be.false; // eslint-disable-line no-unused-expressions
47 | expect(firstRow.at(4).find('button').at(0).text().trim()).
48 | to.equal(i18n.getMessage('fsmWorkflowEditor.ui.buttons.edit.label'));
49 | expect(firstRow.at(4).find('button').at(1).text().trim()).
50 | to.equal(i18n.getMessage('fsmWorkflowEditor.ui.guards.label'));
51 | expect(firstRow.at(4).find('button').at(2).text().trim()).
52 | to.equal(i18n.getMessage('fsmWorkflowEditor.ui.buttons.delete.label'));
53 |
54 | const secondRow = wrapper.find('tbody tr').at(1).find('td');
55 | expect(secondRow.at(0).text()).to.equal(props.states[1].name);
56 | expect(secondRow.at(2).childAt(0).exists()).to.be.false; // eslint-disable-line no-unused-expressions
57 | expect(secondRow.at(3).childAt(0).exists()).to.be.true; // eslint-disable-line no-unused-expressions
58 | expect(secondRow.at(4).find('button').at(0).text().trim()).
59 | to.equal(i18n.getMessage('fsmWorkflowEditor.ui.buttons.edit.label'));
60 | expect(secondRow.at(4).find('button').at(1).text().trim()).
61 | to.equal(i18n.getMessage('fsmWorkflowEditor.ui.guards.label'));
62 | expect(secondRow.at(4).find('button').at(2).text().trim()).
63 | to.equal(i18n.getMessage('fsmWorkflowEditor.ui.buttons.delete.label'));
64 | });
65 | });
66 |
--------------------------------------------------------------------------------
/existingFsmLibsReview.md:
--------------------------------------------------------------------------------
1 | #### http://machina-js.org
2 | It looks strange
3 | - states are dynamic, calculated inside function
4 | - is able to set up timer and timeout based automatic event sending via "on enter " for specific state
5 |
6 | #### http://statejs.org
7 | - one of ideas is to define object states in each state object could behave differently
8 |
9 | #### https://github.com/fschaefer/Stately.js
10 | - action dynamically calculates and returns target state/node (or nothing to prevent transition)
11 | - transition could be static, e.g. no action: {from, event, to}, to is a state (String) instead of action (function)
12 |
13 | #### https://github.com/apla/dataflo.ws
14 |
15 | Is not FSM. Looks more like mvc where controller actions are defined as series of tasks. Each task is actually one JS statement that is function call which result could be assigned to variable.
16 | "project documentation sucks"
17 |
18 | #### https://github.com/jakesgordon/javascript-state-machine
19 |
20 | FSM: event, from, to
21 | +
22 | callbacks : onbefore, onafter, onleave, onenter
23 | each callback is a function(event, from, to, )
24 |
25 | guards could be emulated via onbefore and onleave: if false is returned event is canceled
26 |
27 | actions needs to be defined in callbacks, for example in onleave
28 |
29 | #### https://github.com/vzaccaria/fluent-fsm
30 | transitions are defined using regular expressions
31 | documentation is rather poor
32 |
33 | #### https://github.com/neochrome/simple-fsm
34 | defines states and machine behavior (methods/functions) in each state
35 |
36 | fsm.SOMESTATE();
37 | fsm.action1(); // action1 is available in "SOMESTATE"
38 |
39 | #### http://ignitejs.com/getting_started/introduction.html
40 | is not available, still sources without documentation is here: https://github.com/ignitejs/ignite
41 |
42 | #### noblemachine (https://github.com/noblesamurai/noblemachine)
43 | linear queue/state machine - no graph
44 |
45 | #### Automata (https://github.com/hyperandroid/Automata)
46 | supports automatic transitions - via defined timeout (!)
47 | supports guards
48 | supports subStates(?)
49 |
50 | theoretically looks ok, but small amount of tests
51 |
52 | #### https://github.com/stephenhandley/states
53 | no events, just state e.g. object.state('some state') -> means set up specific state. in addition onEnter and onExit handlers could be defined
54 |
55 | #### https://github.com/mtabini/chine
56 | "schema" (graph) is not a plain JS object; basically each transition is a function, each state and event declaration is function call: name('event'), incoming('from state'), outcoming('to state')
57 |
58 | #### https://github.com/dolphin278/fsm (https://github.com/dolphin278/graph)
59 | transition is an edge
60 | very primitive
61 | callback for each successful transition could be defined
62 |
63 | #### https://github.com/ichernev/node-state
64 |
65 |
66 | #### https://github.com/vstirbu/fsm-as-promised
67 | Heavy influenced by https://github.com/jakesgordon/javascript-state-machine
68 |
69 | Based on Promise-s.
70 |
71 | Improvements/extensions:
72 | - function/callback argument is one object that has named properties 'name' (event), 'from', 'to', 'args' (passed when firing event)
73 | - more callbacks: onleave, onenter, onentered - specific states/event agnostic
74 |
75 | looks good, a lot of tests
76 |
77 | #### http://getcontenttools.com/api/fsm
78 |
79 | only simple transitions and callbacks
80 |
--------------------------------------------------------------------------------
/packages/editor/src/components/ParamsEditor/ParamsEditor.react.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import PropTypes from 'prop-types';
3 | import Row from 'react-bootstrap/lib/Row';
4 | import Col from 'react-bootstrap/lib/Col';
5 | import getParamComponent from './components';
6 | import GenericInput from './components/GenericInput.react';
7 | import ArrayEditor from './components/ArrayEditor.react';
8 | import MultiSelect from './components/MultiSelect.react';
9 |
10 | export default class ParamsEditor extends PureComponent {
11 | static propTypes = {
12 | paramsSchema: PropTypes.shape({
13 | properties: PropTypes.objectOf.isRequired
14 | }),
15 | params: PropTypes.object,
16 | componentsRegistry: PropTypes.objectOf(PropTypes.func),
17 | onChangeParam: PropTypes.func.isRequired,
18 | getLabel: PropTypes.func.isRequired
19 | }
20 |
21 | getParam = name => (this.props.params || {})[name] || {};
22 |
23 | render() {
24 | const { onChangeParam, componentsRegistry, getLabel } = this.props;
25 | const { properties: params } = this.props.paramsSchema;
26 |
27 | const inputs = Object.keys(params).map((name, i) => {
28 | const param = this.getParam(name);
29 | const paramSchema = params[name];
30 | const type = (paramSchema || {}).type;
31 | const customComponentName = (paramSchema || {}).uiComponent;
32 | const CustomComponent = (componentsRegistry || {})[customComponentName];
33 | const handleChange = onChangeParam(name);
34 |
35 | // TODO maybe unify components API / create a common wrapper to abstract param/component props logic
36 | return type === 'array' ?
37 | ((paramSchema || {}).items || {}).enum ?
38 | (
39 |
47 | ) :
48 | (
49 |
56 | ) :
57 | // not an array
58 | CustomComponent ?
59 | (
60 |
65 | ) :
66 | (
67 |
75 | )
76 | });
77 |
78 | const grid = [];
79 |
80 | if (inputs && inputs.length) {
81 | for (let rowIndex = 0; rowIndex < Math.ceil(inputs.length); rowIndex++) {
82 | const cols = []
83 | for (let colIndex = 0; colIndex < 2; colIndex++) {
84 | cols.push(
85 |
86 | {inputs[rowIndex * 2 + colIndex]}
87 |
88 | )
89 | }
90 | const row = ({cols}
);
91 | grid.push(row);
92 | }
93 | }
94 |
95 | return ({grid}
)
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/packages/examples/complete-demo/src/server/index.js:
--------------------------------------------------------------------------------
1 | import { resolve } from 'path';
2 | import express from 'express';
3 | import { Server } from 'http';
4 | import morgan from 'morgan';
5 | import webpack from 'webpack';
6 | import webpackDevMiddleware from 'webpack-dev-middleware';
7 | import webpackConfig from '../../config/webpack.config';
8 | import bodyParser from 'body-parser';
9 | import objectRoutes from './routes/objects';
10 | import sendEventRoute from './routes/sendEvent';
11 | import transitionsRoute from './routes/availableTransitions';
12 | import editorDataRoute from './routes/editorData';
13 | import statesRoute from './routes/states';
14 | import historyRoute from './routes/history';
15 | import storage from './storage';
16 | import fsm from './fsm';
17 | import schema from './schema';
18 | import objectConfig from './objectConfig';
19 | import { generateObjects } from './utils';
20 |
21 | const host = process.env.HOST || 'localhost';
22 | const port = process.env.PORT || 3020;
23 |
24 | const app = express();
25 | const server = Server(app);
26 |
27 | app.use(bodyParser.json());
28 | app.use(morgan('tiny', { immediate: true }));
29 |
30 | app.use(objectRoutes);
31 | app.use(sendEventRoute);
32 | app.use(transitionsRoute);
33 | app.use(editorDataRoute);
34 | app.use(statesRoute);
35 | app.use(historyRoute);
36 |
37 | const compiler = webpack(webpackConfig);
38 |
39 | if (process.env.NODE_ENV === 'development') {
40 | app.use(webpackDevMiddleware(compiler, {
41 | noInfo: true,
42 | publicPath: webpackConfig.output.publicPath
43 | }))
44 | } else {
45 | app.get('/bundle.js', (req, res) => {
46 | res.sendFile(resolve(__dirname, '../../build/bundle.js'));
47 | })
48 | }
49 |
50 | app.get('*', function(req, res) {
51 | res.setHeader('Content-Type', 'text/html');
52 | res.send(`
53 |
54 |
55 |
56 |
57 | Workflow Demo App
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
74 |
75 | `
76 | );
77 | });
78 |
79 | // initialize data and start server
80 |
81 | (async function() {
82 | await schema.init();
83 | await objectConfig.init();
84 | await storage.init();
85 | await fsm.init(storage.sequelize);
86 |
87 | // generate invoices based on schema
88 | const objectConfiguration = objectConfig.getConfig();
89 | const invoices = generateObjects({ objectConfiguration })
90 | const { machine } = fsm;
91 | return Promise.all(invoices.map(invoice => {
92 | machine.start({ object: invoice, user: 'demouser' });
93 | return storage.addObject(invoice)
94 | }))
95 | }()).
96 | then(_ => {
97 | server.listen(port, host, _ => console.log(`Server is listening on http://${host}:${port}`));
98 | }).
99 | catch(err => console.log('INIT FAILED', err));
100 |
101 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## FSM Workflow (for Node.js)
2 |
3 | [](https://circleci.com/gh/OpusCapita/fsm-workflow)
4 | 
5 |
6 | ### Demo
7 |
8 | - [Complete demo app](https://demo.core.dev.opuscapita.com/fsm-workflow/master)
9 | - [Worflow editor demo (showroom)](https://opuscapita.github.io/fsm-workflow/branches/master/editor/?currentComponentName=WorkflowEditor&maxContainerWidth=100%25)
10 |
11 | ### Introduction
12 | [Finite State Machine](https://en.wikipedia.org/wiki/Finite-state_machine) workflow is implemented in JS using [Promise](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Promise)-s.
13 |
14 | - state is stored in the business object related to workflow(machine), not in an extra
15 | workflow generic object. Multiple workflows could be defined for one business object,
16 | it means that for each workflow own state field should be used
17 | - one state per workflow execution (no parallelism)
18 | - actions are executed in the transition, not in the node/state
19 | - no event sending inside the workflow itself (in action)
20 | - no variables in state workflow: all variables/data need to be stored in
21 | the business objects
22 | - events: visible/available in UI as action buttons for the user
23 | - workflow definition stored as JSON
24 | - guard support (transition/event availability is defined via
25 | condition/expression/function = guard)
26 | - hierarchical states are not supported
27 |
28 | #### Notes
29 |
30 | The following things will be implemented later as extensions/helpers (separate sibling library) or in specific application:
31 | - automatic transitions
32 | - task list is based on domain object
33 | - graphical editor
34 | - logging
35 | - analysis
36 |
37 | **P.S.** basic ideas on how FSM API looks like are taken from [fsm-as-promised](https://github.com/vstirbu/fsm-as-promised)
38 |
39 | ### FSM (Core)
40 |
41 | FSM core could be found here [here](packages/core/README.md)
42 |
43 | ### Task/Work Management
44 |
45 | Task management is implemented as additional library. You can find more detailed info [here](packages/task-manager/README.md).
46 |
47 | ### Workflow Transition History
48 |
49 | Workflow Transition History is implemented as separate library. You can find more information [here](packages/history/README.md).
50 |
51 | ### FAQ for developers
52 | **How do I set a version for all packages?**
53 | Define it as `version` value in `lerna.json` file. There's no need to rewrite version in packages, release process will handle it itself.
54 |
55 | ### References
56 |
57 | [Existing FSM libs review](existingFsmLibsReview.md)
58 |
59 | ### Contributors
60 |
61 | | [
](https://github.com/kvolkovich-sc) | [**Alexey Sergeev**](https://github.com/asergeev-sc) |
62 | | :---: | :---: |
63 | | [
](https://github.com/kvolkovich-sc) | [**Kirill Volkovich**](https://github.com/kvolkovich-sc) |
64 | | [
](https://github.com/dzhitomirsky-sc) | [**Daniel Zhitomirsky**](https://github.com/dzhitomirsky-sc) |
65 | | [
](https://github.com/estambakio-sc) | [**Egor Stambakio**](https://github.com/estambakio-sc) |
66 |
67 |
68 | Contributing are welcome. We need YOU! :metal:
69 |
--------------------------------------------------------------------------------