diff --git a/client/karma.config.js b/client/karma.config.js index 5d487044f..7a486f88d 100644 --- a/client/karma.config.js +++ b/client/karma.config.js @@ -98,7 +98,7 @@ module.exports = function(karma) { use: 'react-svg-loader' }, { - test: /\.(css|bpmn|cmmn|dmn|less|xml|png|svg|form)$/, + test: /\.(css|bpmn|cmmn|dmn|less|xml|png|svg|form|rpa)$/, type: 'asset/source' } ] diff --git a/client/package.json b/client/package.json index a91a5d6ec..ea95dd191 100644 --- a/client/package.json +++ b/client/package.json @@ -19,6 +19,8 @@ "@camunda/form-playground": "^0.19.1", "@camunda/improved-canvas": "^1.7.5", "@camunda/linting": "^3.30.0", + "@camunda/rpa-integration": "0.0.1-alpha.0", + "@carbon/icons-react": "^11.53.0", "@codemirror/commands": "^6.6.2", "@codemirror/lang-json": "^6.0.1", "@codemirror/lang-xml": "^6.1.0", @@ -55,11 +57,13 @@ "formik": "2.0.4", "ids": "^1.0.0", "inherits-browser": "^0.1.0", + "install": "^0.13.0", "min-dash": "^4.1.1", "min-dom": "^4.2.1", "mitt": "^3.0.0", "mixpanel-browser": "^2.55.1", "modeler-moddle": "^0.2.0", + "npm": "^10.9.2", "p-defer": "^4.0.1", "p-series": "^3.0.0", "react": "^16.14.0", diff --git a/client/src/app/TabsProvider.js b/client/src/app/TabsProvider.js index bb60d28d3..aeef3aefe 100644 --- a/client/src/app/TabsProvider.js +++ b/client/src/app/TabsProvider.js @@ -18,6 +18,8 @@ import { import replaceIds from '@bpmn-io/replace-ids'; +import { Bot } from '@carbon/icons-react'; + import { Linter as BpmnLinter } from '@camunda/linting'; import { FormLinter } from '@camunda/form-linting/lib/FormLinter'; @@ -28,6 +30,7 @@ import dmnDiagram from './tabs/dmn/diagram.dmn'; import cloudDmnDiagram from './tabs/cloud-dmn/diagram.dmn'; import form from './tabs/form/initial.form'; import cloudForm from './tabs/form/initial-cloud.form'; +import rpaScript from './tabs/rpa/initial.rpa'; import { ENGINES @@ -483,6 +486,43 @@ export default class TabsProvider { getLinter() { return formLinter; } + }, + 'rpa': { + name: 'RPA', + encoding: 'utf8', + exports: {}, + extensions: [ 'rpa' ], + canOpen(file) { + return file.name.endsWith('.rpa'); + }, + getComponent(options) { + return import('./tabs/rpa'); + }, + getIcon() { + return Bot; + }, + getInitialContents() { + return rpaScript; + }, + getInitialFilename(suffix) { + return `script_${suffix}.rpa`; + }, + getHelpMenu() { + return []; + }, + getNewFileMenu() { + return [ { + label: 'RPA script', + group: 'Camunda 8', + action: 'create-diagram', + options: { + type: 'rpa' + } + } ]; + }, + getLinter() { + return null; + } } }; diff --git a/client/src/app/tabs/rpa/DeployButton.js b/client/src/app/tabs/rpa/DeployButton.js new file mode 100644 index 000000000..63b7648bd --- /dev/null +++ b/client/src/app/tabs/rpa/DeployButton.js @@ -0,0 +1,76 @@ +/** + * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH + * under one or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information regarding copyright + * ownership. + * + * Camunda licenses this file to you under the MIT; you may not use this file + * except in compliance with the MIT License. + */ + +import React, { useEffect, useRef, useState } from 'react'; + +import DeployIcon from 'icons/Deploy.svg'; + + +import { Overlay } from '../../../shared/ui'; +import { Fill } from '../../slot-fill'; +import classNames from 'classnames'; + +export default function DeployButton(props) { + const editor = props.editor || {}; + + const eventBus = editor.eventBus; + + const [ isOpen, setIsOpen ] = useState(false); + const buttonRef = useRef(); + + const onClose = () => { + setIsOpen(false); + }; + + useEffect(() => { + const cb = () => { + setIsOpen(true); + }; + + eventBus?.on('dialog.run.open', cb); + + return () => { + eventBus?.off('dialog.run.open', cb); + }; + }, [ eventBus ]); + + return <> + { + + + + } + { isOpen && + +
+ {/* TODO: Add deploy dialog */} +

Coming Soon

+
+
+ } + ; +} \ No newline at end of file diff --git a/client/src/app/tabs/rpa/RPAEditor.js b/client/src/app/tabs/rpa/RPAEditor.js new file mode 100644 index 000000000..1b43068d1 --- /dev/null +++ b/client/src/app/tabs/rpa/RPAEditor.js @@ -0,0 +1,383 @@ +/** + * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH + * under one or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information regarding copyright + * ownership. + * + * Camunda licenses this file to you under the MIT; you may not use this file + * except in compliance with the MIT License. + */ + +import React from 'react'; + +// import { +// WithCache, +// WithCachedState, +// CachedComponent +// } from 'camunda-modeler-plugin-helpers/components'; + +const { + WithCache, + WithCachedState, + CachedComponent +} = window.components; + + +// import Monaco from './Monaco'; + +import * as css from './RPAEditor.less'; + +// import { getRobotEditMenu } from './getRobotEditMenu'; + +import { + isString +} from 'min-dash'; + +import { RPAEditor as RPACodeEditor, DebugInfo } from '@camunda/rpa-integration'; +import PropertiesPanelContainer from '../../resizable-container/PropertiesPanelContainer'; +import { getRPAEditMenu } from './getRobotEditMenu'; +import { Fill } from '../../slot-fill'; +import RunButton from './RunButton'; +import StatusButton from './StatusButton'; +import { Loader } from '../../primitives'; +import DeployButton from './DeployButton'; + +export class RPAEditor extends CachedComponent { + + constructor(props) { + super(props); + + this.state = { + loading: true + }; + + this.modelerRef = React.createRef(); + this.propertiesPanelRef = React.createRef(); + + this.handleLayoutChange = this.handleLayoutChange.bind(this); + } + + + handleListeners(editor, deregister) { + const method = deregister ? 'off' : 'on'; + + editor.eventBus[method]('config.updated', this.saveConfig); + + editor.eventBus[method]('model.changed', this.handleChanged); + } + + + saveConfig = (config) => { + this.props.setConfig('rpa', { runtimeConfig: config }); + this.setCached({ + lastRuntimeConfig: config + }); + }; + + + componentDidMount() { + + const { + editorContainer, propertiesContainer, editor + } = this.getCached(); + + this.modelerRef.current.appendChild(editorContainer); + this.propertiesPanelRef.current.appendChild(propertiesContainer); + + if (!editor) { + + // Create editor if not present + this.createEditor().then(() => { + this.handleChanged(); + }); + } else { + + // or reimport if config changed + this.checkImport(); + this.setState({ + loading: false + }); + } + } + + async createEditor() { + this.setState({ + loading: true + }); + + const rpaConfig = await this.props.getConfig('rpa', {}); + + const { + editorContainer, + propertiesContainer, + editor: cachedEditor + } = this.getCached(); + + if (cachedEditor) { + cachedEditor.destroy(); + } + + const editor = RPACodeEditor({ + container: editorContainer, + propertiesPanel: { + container: propertiesContainer + }, + runtimeConfig: rpaConfig.runtimeConfig, + value: this.props.xml + }); + + this.setCached({ + editor, + lastXML: this.props.xml, + lastRuntimeConfig: rpaConfig.runtimeConfig + }); + + this.setState({ + loading: false + }); + + this.handleListeners(editor); + + return editor; + } + + componentWillUnmount() { + const { + editorContainer, + propertiesContainer, + editor + } = this.getCached(); + + editorContainer.remove(); + propertiesContainer.remove(); + + this.handleListeners(editor, true); + } + + componentDidUpdate(prevProps) { + if (isXMLChange(prevProps.xml, this.props.xml)) { + this.checkImport(); + } + + if (isChachedStateChange(prevProps, this.props)) { + this.handleChanged(); + } + } + + triggerAction(action) { + const { + editor + } = this.getCached(); + + const { + editor: monaco + } = editor; + + if (action === 'undo') { + monaco.trigger('menu', 'undo'); + } + + if (action === 'redo') { + monaco.trigger('menu', 'redo'); + } + + if (action === 'find') { + monaco.getAction('actions.find').run(); + } + + if (action === 'findNext') { + monaco.getAction('editor.action.nextMatchFindAction').run(); + } + + if (action === 'findPrev') { + monaco.getAction('editor.action.previousMatchFindAction').run(); + } + + if (action === 'replace') { + monaco.getAction('editor.action.startFindReplaceAction').run(); + } + } + + async checkImport() { + const { xml } = this.props; + + const { + lastXML, + lastRuntimeConfig + } = this.getCached(); + + if (isXMLChange(lastXML, xml)) { + this.createEditor(); + return; + } + + + const rpaConfig = await this.props.getConfig('rpa', {}); + if (rpaConfig.runtimeConfig !== lastRuntimeConfig) { + this.createEditor(); + return; + } + } + + isDirty() { + + const { + editor, + lastXML + } = this.getCached(); + + return isXMLChange(editor.getValue(), lastXML); + } + + handleChanged = () => { + const { + onChanged + } = this.props; + + const { + editor: monaco + } = this.getCached(); + + const undoState = { + redo: monaco.editor.getModel().canRedo(), + undo: monaco.editor.getModel().canUndo() + }; + + const editMenu = getRPAEditMenu(undoState); + + const dirty = this.isDirty(); + + const newState = { + canExport: false, + dirty, + save: true, + ...undoState + }; + + // ensure backwards compatibility + // https://github.com/camunda/camunda-modeler/commit/78357e3ed9e6e0255ac8225fbdf451a90457e8bf#diff-bd5be70c4e5eadf1a316c16085a72f0fL17 + newState.editable = true; + newState.searchable = true; + + const windowMenu = []; + + if (typeof onChanged === 'function') { + onChanged({ + ...newState, + editMenu, + windowMenu + }); + } + + this.setState({ + ...newState + }); + }; + + handleLayoutChange(newLayout) { + const { + onLayoutChanged + } = this.props; + + if (onLayoutChanged) { + onLayoutChanged(newLayout); + } + } + + getXML() { + const { editor } = this.getCached(); + + const xml = editor.getValue(); + + this.setCached({ + lastXML: xml + }); + + return xml; + } + + render() { + + const { editor } = this.getCached(); + + const loading = this.state.loading; + + return ( + <> +