diff --git a/bpmn-js-spiffworkflow/README.md b/bpmn-js-spiffworkflow/README.md index cecac093..c3de64f9 100644 --- a/bpmn-js-spiffworkflow/README.md +++ b/bpmn-js-spiffworkflow/README.md @@ -13,9 +13,6 @@ This extension creates a BPMN editor with all the capabilities of [BPMN.js](http * Ability to insert BPMN's Data Input and Data Output Objects. * A SpiffWorkflow centric Properties Panel for specifying scripts to run before and after a task, and for defining documentation, and Mark-up content for displaying in user and manual tasks. Among other things. -# Future Plans -* We look forward to integrating a real time Python execution environment for live script development. - # Data Input and Data Output Element This extension will allow you to drag BPMN Data Input and Data Output elements onto the diagram and give them appropriate labels. This will generate valid BPMN elements in the underlying XML file - connecting them to the IO Specification of the process, as shown below: ```xml @@ -68,5 +65,38 @@ To build the example into the `public` folder execute npm run all ``` +# Integration Points +You can use the EventBus to communicate with this UI, sending and receiving messages to change +the behavior of the editor making it easier for your end users. There are many examples of +this in the app.js file. +Below is a table of all the events that are sent and accepted: + +| Event Name | Description | Fired or Acceped | Parameters | Description | +| ------------------------------ | ---------------------------------------------------------------------------- | ---------------- |----------------------| ------------------------------------------------------------------------ | +| spiff.service\_tasks.requested | Request a list of available services that can be called from a service task. | Fired | \- | | +| spiff.service\_tasks.returned | Provides a list of services. | Recieved | serviceTaskOperators | ex: \[{id:'Chuck Facts', parameters\[{id:'category', type:'string'}\]}\] | +| spiff.script.edit | Request to edit a python script in some sort of facy editor. | Fired | scriptType | one of: script, preScript, postScript | +| | | | value | The actual python script | +| | | | element | The element that needs updating | +| | | | eventBus | Used by receiver to fire back an event | +| spiff.script.update | Update a python script to a new value. | Recieved | scriptType | one of: script, preScript, postScript | +| | | | value | The updated script | +| | | | element | The element that needs updating | +| spiff.markdown.edit | Request to edit markdown in a fancy editor. | Fired | element | The element that needs updating | +| | | | value | The current markdown content | +| spiff.markdown.update | Update Markdown content for a paticular elements 'instructions'. | Recieved | element | The element that needs updating | +| | | | value | Tne updated Markdown content | +| spiff.callactivity.edit | Requst to edit a call activity by process id. | Fired | processId | The Process the users wants to edit | +| spiff.file.edit | request to edit a file, but file name. | Fired | value | The file name the user wants to edit | +| spiff.dmn.edit | request to edit a dmn by process id. | Fired | value | The DMN id the user wants to edit | +| spiff.json\_files.requested | request a list of local json files. | Fired | optionType | The type of options required ('json' or 'dmn') | +| spff.dmn\_files.requested | request of list of local dmn files. | | | | +| spiff.json\_files.returned | Return a list of available json files | Recieved | options | \[{lable:'My Label', value:'1'}\] | +| spff.dmn\_files.returned | Return a list of available dmn files. | Recieved | options | \[{lable:'My Label', value:'1'}\] | + + + + + ## License MIT diff --git a/bpmn-js-spiffworkflow/app/app.js b/bpmn-js-spiffworkflow/app/app.js index eb0c5bdf..00eac02e 100644 --- a/bpmn-js-spiffworkflow/app/app.js +++ b/bpmn-js-spiffworkflow/app/app.js @@ -3,7 +3,7 @@ import { BpmnPropertiesPanelModule, BpmnPropertiesProviderModule, } from 'bpmn-js-properties-panel'; -import diagramXML from '../test/spec/bpmn/user_form.bpmn'; +import diagramXML from '../test/spec/bpmn/diagram.bpmn'; import spiffworkflow from './spiffworkflow'; import setupFileOperations from './fileOperations'; @@ -89,32 +89,37 @@ const myCodeMirror = CodeMirror(document.getElementById('code_editor'), { const saveCodeBtn = document.getElementById('saveCode'); let launchCodeEvent = null; -bpmnModeler.on('script.editor.launch', (newEvent) => { +bpmnModeler.on('spiff.script.edit', (newEvent) => { launchCodeEvent = newEvent; myCodeMirror.setValue(launchCodeEvent.script); - setTimeout(function() { + setTimeout(function () { myCodeMirror.refresh(); - },1); // We have to wait a moment before calling refresh. + }, 1); // We have to wait a moment before calling refresh. document.getElementById('code_overlay').style.display = 'block'; document.getElementById('code_editor').focus(); }); saveCodeBtn.addEventListener('click', (_event) => { const { scriptType, element } = launchCodeEvent; - launchCodeEvent.eventBus.fire('script.editor.update', { element, scriptType, script: myCodeMirror.getValue()} ) + launchCodeEvent.eventBus.fire('spiff.script.update', { + element, + scriptType, + script: myCodeMirror.getValue(), + }); document.getElementById('code_overlay').style.display = 'none'; }); - /** * Like Python Script Editing, it can be nice to edit your Markdown in a * good editor as well. */ -var simplemde = new SimpleMDE({ element: document.getElementById("markdown_textarea") }); +const simplemde = new SimpleMDE({ + element: document.getElementById('markdown_textarea'), +}); let launchMarkdownEvent = null; -bpmnModeler.on('markdown.editor.launch', (newEvent) => { +bpmnModeler.on('spiff.markdown.edit', (newEvent) => { launchMarkdownEvent = newEvent; - simplemde.value(launchMarkdownEvent.markdown); + simplemde.value(launchMarkdownEvent.value); document.getElementById('markdown_overlay').style.display = 'block'; document.getElementById('markdown_editor').focus(); }); @@ -122,10 +127,60 @@ bpmnModeler.on('markdown.editor.launch', (newEvent) => { const saveMarkdownBtn = document.getElementById('saveMarkdown'); saveMarkdownBtn.addEventListener('click', (_event) => { const { element } = launchMarkdownEvent; - launchMarkdownEvent.eventBus.fire('markdown.editor.update', { element, markdown:simplemde.value() }); + launchMarkdownEvent.eventBus.fire('spiff.markdown.update', { + element, + value: simplemde.value(), + }); document.getElementById('markdown_overlay').style.display = 'none'; }); +/** + * Also can be good to launch an editor for a call activity, or file + * Not implemented here but imagine opening up a new browser tab + * and showing a different process or completly different file editor. + */ +bpmnModeler.on('spiff.callactivity.edit', (newEvent) => { + console.log( + 'Open new window with editor for call activity: ', + newEvent.processId + ); +}); + +/** + * Also can be good to launch an editor for a call activity, or DMN + * Not implemented here but imagine opening up a new browser tab + * and showing a different process. + */ +bpmnModeler.on('spiff.file.edit', (newEvent) => { + console.log('Open new window to edit file: ', newEvent.value); +}); +bpmnModeler.on('spiff.dmn.edit', (newEvent) => { + console.log('Open new window to edit DMN table: ', newEvent.value); +}); + +/** + * Also handy to get a list of available files that can be used in a given + * context, say json files for a form, or a DMN file for a BusinessRuleTask + */ +bpmnModeler.on('spiff.json_files.requested', (event) => { + event.eventBus.fire('spiff.json_files.returned', { + options: [ + { label: 'pizza_form.json', value: 'pizza_form.json' }, + { label: 'credit_card_form.json', value: 'credit_card_form.json' }, + ], + }); +}); + +bpmnModeler.on('spiff.dmn_files.requested', (event) => { + event.eventBus.fire('spiff.dmn_files.returned', { + options: [ + { label: 'Pizza Special Prices', value: 'pizza_prices' }, + { label: 'Topping Prices', value: 'topping_prices' }, + { label: 'Test Decision', value: 'test_decision' }, + ], + }); +}); + // This handles the download and upload buttons - it isn't specific to // the BPMN modeler or these extensions, just a quick way to allow you to // create and save files, so keeping it outside the example. diff --git a/bpmn-js-spiffworkflow/app/spiffworkflow/callActivity/propertiesPanel/CallActivityPropertiesProvider.js b/bpmn-js-spiffworkflow/app/spiffworkflow/callActivity/propertiesPanel/CallActivityPropertiesProvider.js index cf847d6a..24bb56f4 100644 --- a/bpmn-js-spiffworkflow/app/spiffworkflow/callActivity/propertiesPanel/CallActivityPropertiesProvider.js +++ b/bpmn-js-spiffworkflow/app/spiffworkflow/callActivity/propertiesPanel/CallActivityPropertiesProvider.js @@ -1,5 +1,5 @@ import { is } from 'bpmn-js/lib/util/ModelUtil'; -import { TextFieldEntry } from '@bpmn-io/properties-panel'; +import { HeaderButton, TextFieldEntry } from '@bpmn-io/properties-panel'; import { useService } from 'bpmn-js-properties-panel'; const LOW_PRIORITY = 500; @@ -45,21 +45,36 @@ function createCalledElementGroup(element, translate, moddle, commandStack) { commandStack, translate, }, + /* Commented out until such time as we can effectively calculate the list of available processes by process id */ + /* + { + id: `called_element_launch_button`, + element, + component: LaunchEditorButton, + moddle, + commandStack, + translate, + }, + */ ], }; } +function getCalledElementValue(element) { + const { calledElement } = element.businessObject; + if (calledElement) { + return calledElement; + } + return ''; +} + function CalledElementTextField(props) { const { element } = props; const { translate } = props; const debounce = useService('debounceInput'); const getValue = () => { - const { calledElement } = element.businessObject; - if (calledElement) { - return calledElement; - } - return ''; + return getCalledElementValue(element); }; const setValue = (value) => { @@ -75,3 +90,20 @@ function CalledElementTextField(props) { debounce, }); } + +function LaunchEditorButton(props) { + const { element } = props; + const eventBus = useService('eventBus'); + return HeaderButton({ + id: 'spiffworkflow-open-call-activity-button', + class: 'spiffworkflow-properties-panel-button', + onClick: () => { + const processId = getCalledElementValue(element); + eventBus.fire('spiff.callactivity.edit', { + element, + processId, + }); + }, + children: 'Launch Editor', + }); +} diff --git a/bpmn-js-spiffworkflow/app/spiffworkflow/extensions/extensionHelpers.js b/bpmn-js-spiffworkflow/app/spiffworkflow/extensions/extensionHelpers.js new file mode 100644 index 00000000..1a6cbf1a --- /dev/null +++ b/bpmn-js-spiffworkflow/app/spiffworkflow/extensions/extensionHelpers.js @@ -0,0 +1,117 @@ +const SPIFF_PARENT_PROP = 'spiffworkflow:properties'; +const SPIFF_PROP = 'spiffworkflow:property'; +const PREFIX = 'spiffworkflow:'; + +/** + * + * Spiff Extensions can show up in two distinct ways. The useProperties toggles between them + * + * 1. They might be a top level extension, such as a buisness rule, for example: + * + * + * my_id + * + * + * 2. Or the extension value may exist in a name/value pair inside a Spiffworkflow Properties extension. You would + * do this if you wanted to hide the values from the SpiffWorkflow enginge completely, and pass these values + * through unaltered to your actual application. For Example: + * + * + * + * + * + * + * + * + */ + + +/** + * Returns the string value of the spiff extension with the given name on the provided element. "" + * @param useProperties if set to true, will look inside extensions/spiffworkflow:properties otherwise, just + * looks for a spiffworkflow:[name] and returns that value inside of it. + * @param element + * @param name + */ +export function getExtensionValue(element, name) { + + const useProperties = !name.startsWith(PREFIX); + let extension; + if (useProperties) { + extension = getExtensionProperty(element, name); + } else { + extension = getExtension(element, name); + } + if (extension) { + return extension.value; + } + return ''; +} + +export function setExtensionValue(element, name, value, moddle, commandStack) { + + const useProperties = !name.startsWith(PREFIX) + const { businessObject } = element; + + // Assure we have extensions + let extensions = businessObject.extensionElements; + if (!extensions) { + extensions = moddle.create('bpmn:ExtensionElements'); + } + + if (useProperties) { + let properties = getExtension(element, SPIFF_PARENT_PROP); + let property = getExtensionProperty(element, name); + if (!properties) { + properties = moddle.create(SPIFF_PARENT_PROP); + extensions.get('values').push(properties); + } + if (!property) { + property = moddle.create(SPIFF_PROP); + properties.get('properties').push(property); + } + property.value = value; + property.name = name; + } else { + let extension = getExtension(element, name); + if (!extension) { + extension = moddle.create(name); + extensions.get('values').push(extension) + } + extension.value = value; + } + + commandStack.execute('element.updateModdleProperties', { + element, + moddleElement: businessObject, + properties: { + extensionElements: extensions, + }, + }); +} + +function getExtension(element, name) { + const bizObj = element.businessObject; + if (!bizObj.extensionElements) { + return null; + } + const extensionElements = bizObj.extensionElements.get('values'); + return extensionElements.filter(function (extensionElement) { + if (extensionElement.$instanceOf(name)) { + return true; + } + })[0]; +} + + +function getExtensionProperty(element, name) { + const parentElement = getExtension(element, SPIFF_PARENT_PROP); + if (parentElement) { + return parentElement.get('properties').filter(function (propertyElement) { + return ( + propertyElement.$instanceOf(SPIFF_PROP) && propertyElement.name === name + ); + })[0]; + } + return null; +} diff --git a/bpmn-js-spiffworkflow/app/spiffworkflow/extensions/propertiesPanel/ExtensionsPropertiesProvider.js b/bpmn-js-spiffworkflow/app/spiffworkflow/extensions/propertiesPanel/ExtensionsPropertiesProvider.js index f53eba17..1955dd93 100644 --- a/bpmn-js-spiffworkflow/app/spiffworkflow/extensions/propertiesPanel/ExtensionsPropertiesProvider.js +++ b/bpmn-js-spiffworkflow/app/spiffworkflow/extensions/propertiesPanel/ExtensionsPropertiesProvider.js @@ -1,13 +1,13 @@ import { ListGroup } from '@bpmn-io/properties-panel'; import { is, isAny } from 'bpmn-js/lib/util/ModelUtil'; import scriptGroup, { SCRIPT_TYPE } from './SpiffScriptGroup'; -import { SpiffExtensionCalledDecision } from './SpiffExtensionCalledDecision'; -import { SpiffExtensionTextInput } from './SpiffExtensionTextInput'; -import instructionsGroup from './SpiffExtensionInstructionsForEndUser'; import { ServiceTaskParameterArray, ServiceTaskOperatorSelect, ServiceTaskResultTextInput, } from './SpiffExtensionServiceProperties'; +import {OPTION_TYPE, SpiffExtensionSelect} from './SpiffExtensionSelect'; +import {SpiffExtensionLaunchButton} from './SpiffExtensionLaunchButton'; +import {SpiffExtensionTextArea} from './SpiffExtensionTextArea'; const LOW_PRIORITY = 500; @@ -140,30 +140,51 @@ function createUserGroup(element, translate, moddle, commandStack) { element, moddle, commandStack, - component: SpiffExtensionTextInput, - label: translate('JSON Schema Filename'), - description: translate('RJSF Json Data Structure Filename'), + component: SpiffExtensionSelect, + optionType: OPTION_TYPE.json_files, name: 'formJsonSchemaFilename', + label: translate('JSON Schema Filename'), + description: translate('Form Description (RSJF)'), + }, + { + component: SpiffExtensionLaunchButton, + element, + name: 'formJsonSchemaFilename', + label: translate('Launch Editor'), + event: 'spiff.file.edit', + description: translate('Edit the form description'), }, { element, moddle, commandStack, - component: SpiffExtensionTextInput, + component: SpiffExtensionSelect, + optionType: OPTION_TYPE.json_files, label: translate('UI Schema Filename'), - description: translate('RJSF User Interface Filename'), + event: 'spiff.file.edit', + description: translate('Rules for displaying the form. (RSJF Schema)'), name: 'formUiSchemaFilename', }, + { + component: SpiffExtensionLaunchButton, + element, + name: 'formUiSchemaFilename', + label: translate('Launch Editor'), + event: 'spiff.file.edit', + description: translate('Edit the form schema'), + }, ], }; } /** - * Create a group on the main panel with a text box (for choosing the dmn to connect) + * Select and launch for Business Rules + * * @param element * @param translate * @param moddle - * @returns entries + * @param commandStack + * @returns {{entries: [{moddle, component: ((function(*): preact.VNode)|*), name: string, description, label, commandStack, element},{component: ((function(*): preact.VNode)|*), name: string, description, label, event: string, element}], id: string, label}} */ function createBusinessRuleGroup(element, translate, moddle, commandStack) { return { @@ -174,9 +195,19 @@ function createBusinessRuleGroup(element, translate, moddle, commandStack) { element, moddle, commandStack, - component: SpiffExtensionCalledDecision, - label: translate('Decision Id'), - description: translate('Id of the decision'), + component: SpiffExtensionSelect, + optionType: OPTION_TYPE.dmn_files, + name: 'spiffworkflow:calledDecisionId', + label: translate('Select Decision Table'), + description: translate('Select a decision table from the list'), + }, + { + element, + component: SpiffExtensionLaunchButton, + name: 'spiffworkflow:calledDecisionId', + label: translate('Launch Editor'), + event: 'spiff.dmn.edit', + description: translate('Modify the Decision Table'), }, ], }; @@ -199,14 +230,26 @@ function createUserInstructionsGroup ( id: 'instructions', label: translate('Instructions'), entries: [ - ...instructionsGroup({ + { element, moddle, commandStack, - translate, + component: SpiffExtensionTextArea, + name: 'spiffworkflow:instructionsForEndUser', label: 'Instructions', description: 'The instructions to display when completing this task.', - }), + }, + { + element, + moddle, + commandStack, + component: SpiffExtensionLaunchButton, + name: 'spiffworkflow:instructionsForEndUser', + label: translate('Launch Editor'), + event: 'spiff.markdown.edit', + listenEvent: 'spiff.markdown.update', + description: translate('Edit the form schema'), + } ], }; } diff --git a/bpmn-js-spiffworkflow/app/spiffworkflow/extensions/propertiesPanel/SpiffExtensionCalledDecision.js b/bpmn-js-spiffworkflow/app/spiffworkflow/extensions/propertiesPanel/SpiffExtensionCalledDecision.js deleted file mode 100644 index 7154fbc7..00000000 --- a/bpmn-js-spiffworkflow/app/spiffworkflow/extensions/propertiesPanel/SpiffExtensionCalledDecision.js +++ /dev/null @@ -1,81 +0,0 @@ -import { useService } from 'bpmn-js-properties-panel'; -import { TextFieldEntry } from '@bpmn-io/properties-panel'; - -const SPIFF_PROP = 'spiffworkflow:calledDecisionId'; - -/** - * A generic properties' editor for text input. - * Allows you to provide additional SpiffWorkflow extension properties. Just - * uses whatever name is provide on the property, and adds or updates it as - * needed. - * - * - - - my_id - - - * - * @returns {string|null|*} - */ -export function SpiffExtensionCalledDecision(props) { - const { element } = props; - const { commandStack } = props; - const { moddle } = props; - const { label } = props; - const { description } = props; - const debounce = useService('debounceInput'); - - const getPropertyObject = () => { - const bizObj = element.businessObject; - if (!bizObj.extensionElements) { - return null; - } - return bizObj.extensionElements.get('values').filter(function (e) { - return e.$instanceOf(SPIFF_PROP); - })[0]; - }; - - const getValue = () => { - const property = getPropertyObject(); - if (property) { - return property.calledDecisionId; - } - return ''; - }; - - const setValue = (value) => { - let property = getPropertyObject(); - const { businessObject } = element; - let extensions = businessObject.extensionElements; - - if (!property) { - property = moddle.create(SPIFF_PROP); - if (!extensions) { - extensions = moddle.create('bpmn:ExtensionElements'); - } - extensions.get('values').push(property); - } - property.calledDecisionId = value; - - commandStack.execute('element.updateModdleProperties', { - element, - moddleElement: businessObject, - properties: { - extensionElements: extensions, - }, - }); - }; - - return ( - - ); -} diff --git a/bpmn-js-spiffworkflow/app/spiffworkflow/extensions/propertiesPanel/SpiffExtensionInstructionsForEndUser.js b/bpmn-js-spiffworkflow/app/spiffworkflow/extensions/propertiesPanel/SpiffExtensionInstructionsForEndUser.js deleted file mode 100644 index b4cd5400..00000000 --- a/bpmn-js-spiffworkflow/app/spiffworkflow/extensions/propertiesPanel/SpiffExtensionInstructionsForEndUser.js +++ /dev/null @@ -1,136 +0,0 @@ -import { useService } from 'bpmn-js-properties-panel'; -import { - HeaderButton, - isTextFieldEntryEdited, - TextAreaEntry, -} from '@bpmn-io/properties-panel'; - -const SPIFF_PROP = 'spiffworkflow:instructionsForEndUser'; - -/** - * A generic properties' editor for text input. - * Allows you to provide additional SpiffWorkflow extension properties. Just - * uses whatever name is provide on the property, and adds or updates it as - * needed. - * - * - * - * @returns {string|null|*} - */ -function SpiffExtensionInstructionsForEndUser(props) { - const { element, commandStack, moddle, label, description } = props; - const debounce = useService('debounceInput'); - - const getValue = () => { - return getPropertyValue(element); - }; - - const setValue = (value) => { - setProperty(commandStack, moddle, element, value); - }; - - return TextAreaEntry({ - id: 'extension_instruction_for_end_user', - element, - description, - label, - getValue, - setValue, - debounce, - }); -} - -function getPropertyObject(element) { - const bizObj = element.businessObject; - if (!bizObj.extensionElements) { - return null; - } - return bizObj.extensionElements.get('values').filter(function (e) { - return e.$instanceOf(SPIFF_PROP); - })[0]; -} - -function getPropertyValue(element) { - const property = getPropertyObject(element); - if (property) { - return property.instructionsForEndUser; - } - return ''; -} - -function setProperty(commandStack, moddle, element, value) { - let property = getPropertyObject(element); - const { businessObject } = element; - let extensions = businessObject.extensionElements; - - if (!property) { - property = moddle.create(SPIFF_PROP); - if (!extensions) { - extensions = moddle.create('bpmn:ExtensionElements'); - } - extensions.get('values').push(property); - } - property.instructionsForEndUser = value; - - commandStack.execute('element.updateModdleProperties', { - element, - moddleElement: businessObject, - properties: { - extensionElements: extensions, - }, - }); -} - -function LaunchMarkdownEditorButton(props) { - const { element, moddle, commandStack } = props; - const eventBus = useService('eventBus'); - return HeaderButton({ - className: 'spiffworkflow-properties-panel-button', - onClick: () => { - const markdown = getPropertyValue(element); - eventBus.fire('markdown.editor.launch', { - element, - markdown, - eventBus, - }); - // Listen for a response, to update the script. - eventBus.once('markdown.editor.update', (event) => { - console.log("Markdown update!!!") - setProperty(commandStack, moddle, event.element, event.markdown); - }); - }, - children: 'Launch Editor', - }); -} - -/** - * Generates a text box and button for editing markdown. - * @param element The element that should get the markdown. - * @param moddle For updating the underlying xml document when needed. - * @returns {[{component: (function(*)), isEdited: *, id: string, element},{component: (function(*)), isEdited: *, id: string, element}]} - */ -export default function getEntries(props) { - const { element, moddle, label, description, translate, commandStack } = - props; - - return [ - { - id: `edit_markdown`, - element, - component: SpiffExtensionInstructionsForEndUser, - isEdited: isTextFieldEntryEdited, - moddle, - commandStack, - label, - description, - }, - { - id: `launchMarkdownEditor`, - element, - component: LaunchMarkdownEditorButton, - isEdited: isTextFieldEntryEdited, - moddle, - commandStack, - }, - ]; -} diff --git a/bpmn-js-spiffworkflow/app/spiffworkflow/extensions/propertiesPanel/SpiffExtensionLaunchButton.js b/bpmn-js-spiffworkflow/app/spiffworkflow/extensions/propertiesPanel/SpiffExtensionLaunchButton.js new file mode 100644 index 00000000..332c9084 --- /dev/null +++ b/bpmn-js-spiffworkflow/app/spiffworkflow/extensions/propertiesPanel/SpiffExtensionLaunchButton.js @@ -0,0 +1,39 @@ +import { HeaderButton } from '@bpmn-io/properties-panel'; +import { useService } from 'bpmn-js-properties-panel'; +import {getExtensionValue, setExtensionValue} from '../extensionHelpers'; + +/** + * Sends a notification to the host application saying the user + * would like to edit something. Hosting application can then + * update the value and send it back. + */ +export function SpiffExtensionLaunchButton(props) { + const { element, name, event, listenEvent } = props; + const eventBus = useService('eventBus'); + return HeaderButton({ + className: 'spiffworkflow-properties-panel-button', + id: `launch_editor_button_${name}`, + onClick: () => { + const value = getExtensionValue(element, name); + eventBus.fire(event, { + value, + eventBus, + listenEvent, + }); + + // Listen for a respose if the listenEvent is provided, and + // set the value to the response + // Optional additional arguments if we should listen for a reponse. + if (listenEvent) { + const { commandStack, moddle } = props; + // Listen for a response, to update the script. + eventBus.once(listenEvent, (response) => { + console.log("Calling Update!") + setExtensionValue(element, name, response.value, moddle, commandStack); + }); + } + + }, + children: 'Launch Editor', + }); +} diff --git a/bpmn-js-spiffworkflow/app/spiffworkflow/extensions/propertiesPanel/SpiffExtensionSelect.js b/bpmn-js-spiffworkflow/app/spiffworkflow/extensions/propertiesPanel/SpiffExtensionSelect.js new file mode 100644 index 00000000..edbc80a2 --- /dev/null +++ b/bpmn-js-spiffworkflow/app/spiffworkflow/extensions/propertiesPanel/SpiffExtensionSelect.js @@ -0,0 +1,90 @@ +import { SelectEntry } from '@bpmn-io/properties-panel'; +import { useService } from 'bpmn-js-properties-panel'; +import { + getExtensionValue, + setExtensionValue, +} from '../extensionHelpers'; + +const spiffExtensionOptions = {}; + +export const OPTION_TYPE = { + json_files: 'json_files', + dmn_files: 'dmn_files', +}; + +/** + * Allow selecting an option from a list of available options, and setting + * the name and value of a SpiffWorkflow Property to the one selected in the + * dropdown list. + * The list of options must be provided by the containing library - by responding + * to a request passed to the eventBus. + * When needed, the event "spiff.${optionType}.requested" will be fired. + * The response should be sent to "spiff.${optionType}.returned". The response + * event should include an 'options' attribute that is list of labels and values: + * [ { label: 'Product Prices DMN', value: 'Process_16xfaqc' } ] + */ +export function SpiffExtensionSelect(props) { + const { element } = props; + const { commandStack } = props; + const { moddle } = props; + const { label, description } = props; + + const { name } = props; + const { optionType } = props; + + const debounce = useService('debounceInput'); + const eventBus = useService('eventBus'); + + const getValue = () => { + return getExtensionValue(element, name); + }; + + const setValue = (value) => { + console.log(`Set Value called with ${ value}`); + setExtensionValue(element, name, value, moddle, commandStack); + }; + + if ( + !(optionType in spiffExtensionOptions) || + spiffExtensionOptions[optionType].length === 0 + ) { + spiffExtensionOptions[optionType] = []; + requestOptions(eventBus, element, commandStack, optionType); + } + const getOptions = () => { + const optionList = []; + if (optionType in spiffExtensionOptions) { + spiffExtensionOptions[optionType].forEach((opt) => { + optionList.push({ + label: opt.label, + value: opt.value, + }); + }); + } + return optionList; + }; + + return SelectEntry({ + id: `extension_${name}`, + element, + label, + description, + getValue, + setValue, + getOptions, + debounce, + }); +} + +function requestOptions(eventBus, element, commandStack, optionType) { + // Little backwards, but you want to assure you are ready to catch, before you throw + // or you risk a race condition. + eventBus.once(`spiff.${optionType}.returned`, (event) => { + spiffExtensionOptions[optionType] = event.options; + commandStack.execute('element.updateProperties', { + element, + properties: {}, + }); + }); + eventBus.fire(`spiff.${optionType}.requested`, { eventBus }); +} diff --git a/bpmn-js-spiffworkflow/app/spiffworkflow/extensions/propertiesPanel/SpiffExtensionTextArea.js b/bpmn-js-spiffworkflow/app/spiffworkflow/extensions/propertiesPanel/SpiffExtensionTextArea.js new file mode 100644 index 00000000..bc54d263 --- /dev/null +++ b/bpmn-js-spiffworkflow/app/spiffworkflow/extensions/propertiesPanel/SpiffExtensionTextArea.js @@ -0,0 +1,35 @@ +import {useService } from 'bpmn-js-properties-panel'; +import {TextAreaEntry, TextFieldEntry} from '@bpmn-io/properties-panel'; +import { + getExtensionValue, setExtensionValue +} from '../extensionHelpers'; + + +/** + * A generic properties' editor for text area. + */ +export function SpiffExtensionTextArea(props) { + const element = props.element; + const commandStack = props.commandStack, moddle = props.moddle; + const name = props.name, label = props.label, description = props.description; + const debounce = useService('debounceInput'); + + const getValue = () => { + return getExtensionValue(element, name) + } + + const setValue = value => { + setExtensionValue(element, name, value, moddle, commandStack) + }; + + return ; + +} diff --git a/bpmn-js-spiffworkflow/app/spiffworkflow/extensions/propertiesPanel/SpiffExtensionTextInput.js b/bpmn-js-spiffworkflow/app/spiffworkflow/extensions/propertiesPanel/SpiffExtensionTextInput.js index c08b80fd..a72ad461 100644 --- a/bpmn-js-spiffworkflow/app/spiffworkflow/extensions/propertiesPanel/SpiffExtensionTextInput.js +++ b/bpmn-js-spiffworkflow/app/spiffworkflow/extensions/propertiesPanel/SpiffExtensionTextInput.js @@ -1,8 +1,9 @@ import {useService } from 'bpmn-js-properties-panel'; import { TextFieldEntry } from '@bpmn-io/properties-panel'; +import { + getExtensionValue, setExtensionValue +} from '../extensionHelpers'; -const SPIFF_PARENT_PROP = "spiffworkflow:properties" -const SPIFF_PROP = "spiffworkflow:property" /** * A generic properties' editor for text input. @@ -25,65 +26,12 @@ export function SpiffExtensionTextInput(props) { const name = props.name, label = props.label, description = props.description; const debounce = useService('debounceInput'); - const getPropertiesObject = () => { - const bizObj = element.businessObject; - if (!bizObj.extensionElements) { - return null; - } else { - const extensionElements = bizObj.extensionElements.get("values"); - return extensionElements.filter(function (extensionElement) { - if (extensionElement.$instanceOf(SPIFF_PARENT_PROP)) { - return extensionElement; - } - })[0]; - } - } - - const getPropertyObject = () => { - const parentElement = getPropertiesObject(); - if (parentElement) { - return parentElement.get("properties").filter(function (propertyElement) { - return propertyElement.$instanceOf(SPIFF_PROP) && propertyElement.name === name; - })[0]; - } - return null; - } - const getValue = () => { - const property = getPropertyObject() - if (property) { - return property.value; - } - return "" + return getExtensionValue(element, name) } const setValue = value => { - let properties = getPropertiesObject() - let property = getPropertyObject() - let businessObject = element.businessObject; - let extensions = businessObject.extensionElements; - - if (!extensions) { - extensions = moddle.create('bpmn:ExtensionElements'); - } - if (!properties) { - properties = moddle.create(SPIFF_PARENT_PROP); - extensions.get('values').push(properties); - } - if (!property) { - property = moddle.create(SPIFF_PROP); - properties.get('properties').push(property); - } - property.value = value; - property.name = name; - - commandStack.execute('element.updateModdleProperties', { - element, - moddleElement: businessObject, - properties: { - "extensionElements": extensions - } - }); + setExtensionValue(element, name, value, moddle, commandStack) }; return { const script = getScriptString(element, type); - eventBus.fire('script.editor.launch', { + eventBus.fire('spiff.script.edit', { element, scriptType: type, script, eventBus, }); // Listen for a response, to update the script. - eventBus.once('script.editor.update', (event) => { + eventBus.once('spiff.script.update', (event) => { updateScript( commandStack, moddle, @@ -113,7 +113,7 @@ function updateScript(commandStack, moddle, element, scriptType, newValue) { if (!extensionElements) { extensionElements = moddle.create('bpmn:ExtensionElements'); } - scriptObj.script = newValue; + scriptObj.value = newValue; extensionElements.get('values').push(scriptObj); commandStack.execute('element.updateModdleProperties', { element, @@ -124,18 +124,23 @@ function updateScript(commandStack, moddle, element, scriptType, newValue) { }); } } else { + let newProps = { value: newValue }; + if (scriptType === SCRIPT_TYPE.bpmn) { + newProps = { script: newValue }; + } commandStack.execute('element.updateModdleProperties', { element, moddleElement: scriptObj, - properties: { - script: newValue, - }, + properties: newProps, }); } } function getScriptString(element, scriptType) { const scriptObj = getScriptObject(element, scriptType); + if (scriptObj && scriptObj.value) { + return scriptObj.value; + } if (scriptObj && scriptObj.script) { return scriptObj.script; } diff --git a/bpmn-js-spiffworkflow/app/spiffworkflow/messages/propertiesPanel/MessagePayload.js b/bpmn-js-spiffworkflow/app/spiffworkflow/messages/propertiesPanel/MessagePayload.js index 3ba4d6df..cfa82b84 100644 --- a/bpmn-js-spiffworkflow/app/spiffworkflow/messages/propertiesPanel/MessagePayload.js +++ b/bpmn-js-spiffworkflow/app/spiffworkflow/messages/propertiesPanel/MessagePayload.js @@ -28,7 +28,7 @@ export function MessagePayload(props) { const getValue = () => { const messagePayloadObject = getMessagePayloadObject(); if (messagePayloadObject) { - return messagePayloadObject.messagePayload; + return messagePayloadObject.value; } return ''; }; @@ -46,7 +46,7 @@ export function MessagePayload(props) { } messageElement.extensionElements.get('values').push(messagePayloadObject); } - messagePayloadObject.messagePayload = value; + messagePayloadObject.value = value; }; return ( diff --git a/bpmn-js-spiffworkflow/app/spiffworkflow/messages/propertiesPanel/MessageVariable.js b/bpmn-js-spiffworkflow/app/spiffworkflow/messages/propertiesPanel/MessageVariable.js index 8a0353f7..b50c3373 100644 --- a/bpmn-js-spiffworkflow/app/spiffworkflow/messages/propertiesPanel/MessageVariable.js +++ b/bpmn-js-spiffworkflow/app/spiffworkflow/messages/propertiesPanel/MessageVariable.js @@ -28,7 +28,7 @@ export function MessageVariable(props) { const getValue = () => { const messageVariableObject = getMessageVariableObject(); if (messageVariableObject) { - return messageVariableObject.messageVariable; + return messageVariableObject.value; } return ''; }; @@ -48,7 +48,7 @@ export function MessageVariable(props) { .get('values') .push(messageVariableObject); } - messageVariableObject.messageVariable = value; + messageVariableObject.value = value; }; return ( diff --git a/bpmn-js-spiffworkflow/app/spiffworkflow/moddle/spiffworkflow.json b/bpmn-js-spiffworkflow/app/spiffworkflow/moddle/spiffworkflow.json index 90cb3352..764c31c0 100644 --- a/bpmn-js-spiffworkflow/app/spiffworkflow/moddle/spiffworkflow.json +++ b/bpmn-js-spiffworkflow/app/spiffworkflow/moddle/spiffworkflow.json @@ -9,7 +9,7 @@ "superClass": [ "Element" ], "properties": [ { - "name": "script", + "name": "value", "isBody": true, "type": "String" } @@ -20,7 +20,7 @@ "superClass": [ "Element" ], "properties": [ { - "name": "script", + "name": "value", "isBody": true, "type": "String" } @@ -31,7 +31,7 @@ "superClass": [ "Element" ], "properties": [ { - "name": "messagePayload", + "name": "value", "isBody": true, "type": "String" } @@ -42,7 +42,7 @@ "superClass": [ "Element" ], "properties": [ { - "name": "messageVariable", + "name": "value", "isBody": true, "type": "String" } @@ -53,7 +53,7 @@ "superClass": [ "Element" ], "properties": [ { - "name": "calledDecisionId", + "name": "value", "isBody": true, "type": "String" } @@ -64,7 +64,7 @@ "superClass": [ "Element" ], "properties": [ { - "name": "instructionsForEndUser", + "name": "value", "isBody": true, "type": "String" } diff --git a/bpmn-js-spiffworkflow/test/spec/BusinessRulePropsSpec.js b/bpmn-js-spiffworkflow/test/spec/BusinessRulePropsSpec.js index 9d0cc492..5ff37d93 100644 --- a/bpmn-js-spiffworkflow/test/spec/BusinessRulePropsSpec.js +++ b/bpmn-js-spiffworkflow/test/spec/BusinessRulePropsSpec.js @@ -1,70 +1,88 @@ +import { getBpmnJS } from 'bpmn-js/test/helper'; + import { - bootstrapPropertiesPanel, changeInput, + BpmnPropertiesPanelModule, + BpmnPropertiesProviderModule, +} from 'bpmn-js-properties-panel'; +import { getBusinessObject } from 'bpmn-js/lib/util/ModelUtil'; +import spiffModdleExtension from '../../app/spiffworkflow/moddle/spiffworkflow.json'; +import { + bootstrapPropertiesPanel, + changeInput, expectSelected, findEntry, - getPropertiesPanel + findSelect, + getPropertiesPanel, } from './helpers'; -import { - query as domQuery, -} from 'min-dom'; - -import spiffModdleExtension from '../../app/spiffworkflow/moddle/spiffworkflow.json'; -import { BpmnPropertiesPanelModule, BpmnPropertiesProviderModule } from 'bpmn-js-properties-panel'; -import { getBusinessObject } from 'bpmn-js/lib/util/ModelUtil'; import extensions from '../../app/spiffworkflow/extensions'; -describe('Business Rule Properties Panel', function() { +describe('Business Rule Properties Panel', function () { const xml = require('./bpmn/diagram.bpmn').default; - beforeEach(bootstrapPropertiesPanel(xml, { - debounceInput: false, - additionalModules: [ - extensions, - BpmnPropertiesPanelModule, - BpmnPropertiesProviderModule, - ], - moddleExtensions: { - spiffworkflow: spiffModdleExtension - }, - })); + beforeEach( + bootstrapPropertiesPanel(xml, { + debounceInput: false, + additionalModules: [ + BpmnPropertiesPanelModule, + BpmnPropertiesProviderModule, + extensions, + ], + moddleExtensions: { + spiffworkflow: spiffModdleExtension, + }, + }) + ); - it('should display a text box to select the called decision id', async function() { + function addOptionsToEventBus(bpmnModeler) { + bpmnModeler.on('spiff.dmn_files.requested', (event) => { + event.eventBus.fire('spiff.dmn_files.returned', { + options: [ + { label: 'Calculate Pizza Price', value: 'Decision_Pizza_Price' }, + { label: 'Viking Availability', value: 'Decision_Vikings' }, + { label: 'Test Decision', value: 'test_decision' }, + ], + }); + }); + } + it('should display a dropdown to select from available decision tables', async function () { + const modeler = getBpmnJS(); + addOptionsToEventBus(modeler); expectSelected('business_rule_task'); // THEN - a properties panel exists with a section for editing that script - let entry = findEntry('extension_called_decision', getPropertiesPanel()); - expect(entry).to.exist; - const textInput = domQuery('input', entry); - expect(textInput).to.exist; + const entry = findEntry('extension_spiffworkflow:calledDecisionId', getPropertiesPanel()); + expect(entry, 'No Entry').to.exist; + const selectList = findSelect(entry); + expect(selectList, 'No Select').to.exist; }); - it('should update the spiffworkflow:calledDecisionId tag when you modify the called decision text input', async function() { - + it('should update the spiffworkflow:calledDecisionId tag when you modify the called decision select box', async function () { // IF - a script tag is selected, and you change the script in the properties panel + const modeler = getBpmnJS(); + addOptionsToEventBus(modeler); const businessRuleTask = await expectSelected('business_rule_task'); - const entry = findEntry('extension_called_decision', getPropertiesPanel()); - const textInput = domQuery('input', entry); - changeInput(textInput, 'wonderful'); + const entry = findEntry('extension_calledDecisionId', getPropertiesPanel()); + const selectList = findSelect(entry); + changeInput(selectList, 'Decision_Pizza_Price'); // THEN - the script tag in the BPMN Business object / XML is updated as well. const businessObject = getBusinessObject(businessRuleTask); expect(businessObject.extensionElements).to.exist; - let element = businessObject.extensionElements.values[0]; - expect(element.calledDecisionId).to.equal('wonderful'); + const element = businessObject.extensionElements.values[0]; + expect(element.value).to.equal('Decision_Pizza_Price'); }); - it('should load up the xml and the value for the called decision should match the xml', async function() { + it('should load up the xml and the value for the called decision should match the xml', async function () { const businessRuleTask = await expectSelected('business_rule_task'); - let entry = findEntry('extension_called_decision', getPropertiesPanel()); - const textInput = domQuery('input', entry); - expect(textInput.value).to.equal('test_decision'); + const entry = findEntry('extension_calledDecisionId', getPropertiesPanel()); + const selectList = findSelect(entry); + expect(selectList.value, "initial value is wrong").to.equal('test_decision'); // THEN - the script tag in the BPMN Business object / XML is updated as well. - let businessObject = getBusinessObject(businessRuleTask); + const businessObject = getBusinessObject(businessRuleTask); expect(businessObject.extensionElements).to.exist; - let element = businessObject.extensionElements.values[0]; - expect(element.calledDecisionId).to.equal('test_decision'); + const element = businessObject.extensionElements.values[0]; + expect(element.value).to.equal('test_decision'); }); - }); diff --git a/bpmn-js-spiffworkflow/test/spec/CallActivitySpec.js b/bpmn-js-spiffworkflow/test/spec/CallActivitySpec.js index 696f111a..f4749884 100644 --- a/bpmn-js-spiffworkflow/test/spec/CallActivitySpec.js +++ b/bpmn-js-spiffworkflow/test/spec/CallActivitySpec.js @@ -5,15 +5,17 @@ import { } from 'bpmn-js-properties-panel'; import { query as domQuery } from 'min-dom'; import { getBusinessObject } from 'bpmn-js/lib/util/ModelUtil'; +import { inject } from 'bpmn-js/test/helper'; import { bootstrapPropertiesPanel, changeInput, - expectSelected, - findGroupEntry, + expectSelected, findButton, + findGroupEntry, pressButton, } from './helpers'; import spiffModdleExtension from '../../app/spiffworkflow/moddle/spiffworkflow.json'; import callActivity from '../../app/spiffworkflow/callActivity'; + describe('Call Activities should work', function () { const xml = require('./bpmn/call_activity.bpmn').default; let container; @@ -57,4 +59,25 @@ describe('Call Activities should work', function () { changeInput(textInput, 'newProcessId'); expect(businessObject.get('calledElement')).to.equal('newProcessId'); }); + + /** fixme: Reenable this when we add this button back in. + it('should issue an event to the event bus if user clicks the edit button', inject( + async function(eventBus) { + const shapeElement = await expectSelected('the_call_activity'); + expect(shapeElement, "Can't find Call Activity").to.exist; + const businessObject = getBusinessObject(shapeElement); + expect(businessObject.get('calledElement')).to.equal('ProcessIdTBD1'); + + const entry = findGroupEntry('called_element', container); + const button = findButton('spiffworkflow-open-call-activity-button', entry); + expect(button).to.exist; + + let launchEvent; + eventBus.on('spiff.callactivity.edit', function (event) { + launchEvent = event; + }); + await pressButton(button); + expect(launchEvent.processId).to.exist; + })); + */ }); diff --git a/bpmn-js-spiffworkflow/test/spec/UserTaskPropsSpec.js b/bpmn-js-spiffworkflow/test/spec/UserTaskPropsSpec.js index 7e6acc32..7d2b0317 100644 --- a/bpmn-js-spiffworkflow/test/spec/UserTaskPropsSpec.js +++ b/bpmn-js-spiffworkflow/test/spec/UserTaskPropsSpec.js @@ -1,24 +1,47 @@ import { - bootstrapPropertiesPanel, changeInput, - expectSelected, - findEntry, findGroupEntry, findInput -} from './helpers'; - -import spiffModdleExtension from '../../app/spiffworkflow/moddle/spiffworkflow.json'; -import { BpmnPropertiesPanelModule, BpmnPropertiesProviderModule } from 'bpmn-js-properties-panel'; + BpmnPropertiesPanelModule, + BpmnPropertiesProviderModule, +} from 'bpmn-js-properties-panel'; +import { getBpmnJS } from 'bpmn-js/test/helper'; import { getBusinessObject } from 'bpmn-js/lib/util/ModelUtil'; import TestContainer from 'mocha-test-container-support'; +import spiffModdleExtension from '../../app/spiffworkflow/moddle/spiffworkflow.json'; +import {getExtensionProperty, getExtensionValue} from '../../app/spiffworkflow/extensions/extensionHelpers'; +import { + bootstrapPropertiesPanel, + changeInput, + expectSelected, findButton, + findEntry, + findGroupEntry, + findInput, + findSelect, pressButton, +} from './helpers'; import extensions from '../../app/spiffworkflow/extensions'; +import {query as domQuery} from 'min-dom'; +import {default as diagram_xml} from './bpmn/diagram.bpmn'; -describe('Properties Panel for User Tasks', function() { +describe('Properties Panel for User Tasks', function () { const user_form_xml = require('./bpmn/user_form.bpmn').default; const diagram_xml = require('./bpmn/diagram.bpmn').default; let container; - beforeEach(function() { + beforeEach(function () { container = TestContainer.get(this); }); + function addOptionsToEventBus(bpmnModeler) { + bpmnModeler.on('spiff.json_files.requested', (event) => { + event.eventBus.fire('spiff.json_files.returned', { + options: [ + { label: 'pizza_form.json', value: 'pizza_form.json' }, + { label: 'credit_card_form.json', value: 'credit_card_form.json' }, + { label: 'give_me_a_number_form.json', value: 'give_me_a_number_form.json' }, + { label: 'number_form_schema.json', value: 'number_form_schema.json' }, + ], + }); + }); + } + function preparePropertiesPanelWithXml(xml) { return bootstrapPropertiesPanel(xml, { container, @@ -29,12 +52,12 @@ describe('Properties Panel for User Tasks', function() { BpmnPropertiesProviderModule, ], moddleExtensions: { - spiffworkflow: spiffModdleExtension + spiffworkflow: spiffModdleExtension, }, }); } - it('should display a panel for setting the web form properties', async function() { + it('should display a panel for setting the web form properties', async function () { await preparePropertiesPanelWithXml(user_form_xml)(); // IF - you select a user task @@ -42,44 +65,72 @@ describe('Properties Panel for User Tasks', function() { expect(userTask).to.exist; // THEN - a property panel exists with a section for editing web forms - let group = findGroupEntry('user_task_properties', container); + const group = findGroupEntry('user_task_properties', container); expect(group).to.exist; }); - it('should allow you to edit a web form property.', async function() { + it('should allow you to select a json file.', async function () { await preparePropertiesPanelWithXml(user_form_xml)(); - + const modeler = getBpmnJS(); + addOptionsToEventBus(modeler); // IF - you select a user task and change the formJsonSchemaFilename text field const userTask = await expectSelected('my_user_task'); - let group = findGroupEntry('user_task_properties', container); - let entry = findEntry('extension_formJsonSchemaFilename', group); - let input = findInput('text', entry); - expect(input).to.exist; - changeInput(input, 'my_filename.json'); + const group = findGroupEntry('user_task_properties', container); + const entry = findEntry('extension_formJsonSchemaFilename', group); + const selectList = findSelect(entry); + expect(selectList).to.exist; + expect(selectList.options.length).to.equal(4); + expect(selectList.options[0].label).to.equal('pizza_form.json'); + expect(selectList.options[1].label).to.equal('credit_card_form.json'); + + changeInput(selectList, 'pizza_form.json'); // THEN - the input is updated. - let businessObject = getBusinessObject(userTask); + const businessObject = getBusinessObject(userTask); expect(businessObject.extensionElements).to.exist; - let properties = businessObject.extensionElements.values[1]; + const properties = businessObject.extensionElements.values[1]; expect(properties.properties).to.exist; const property = properties.properties[0]; - expect(property.value).to.equal('my_filename.json'); + expect(property.value).to.equal('pizza_form.json'); expect(property.name).to.equal('formJsonSchemaFilename'); }); - it('should parse the spiffworkflow:properties tag when you open an existing file', async function() { + it('should parse the spiffworkflow:properties tag when you open an existing file', async function () { await preparePropertiesPanelWithXml(diagram_xml)(); + const modeler = getBpmnJS(); + addOptionsToEventBus(modeler); - // IF - a script tag is selected, and you change the script in the properties panel + // IF - a user tag is selected, and you change the script in the properties panel await expectSelected('task_confirm'); - let group = findGroupEntry('user_task_properties', container); - let formJsonSchemaFilenameEntry = findEntry('extension_formJsonSchemaFilename', group); - let formJsonSchemaFilenameInput = findInput('text', formJsonSchemaFilenameEntry); + const group = findGroupEntry('user_task_properties', container); + const formJsonSchemaFilenameEntry = findEntry('extension_formJsonSchemaFilename', group); + const formJsonSchemaFilenameInput = findSelect(formJsonSchemaFilenameEntry); expect(formJsonSchemaFilenameInput.value).to.equal('give_me_a_number_form.json'); - - let formUiSchemaFilenameEntry = findEntry('extension_formUiSchemaFilename', group); - let formUiSchemaFilenameInput = findInput('text', formUiSchemaFilenameEntry); + const formUiSchemaFilenameEntry = findEntry('extension_formUiSchemaFilename', group); + const formUiSchemaFilenameInput = findSelect(formUiSchemaFilenameEntry); expect(formUiSchemaFilenameInput.value).to.equal('number_form_schema.json'); }); + it('should allow you to change the instructions to the end user', async function () { + // If a user task is selected + await preparePropertiesPanelWithXml(diagram_xml)(); + const modeler = getBpmnJS(); + addOptionsToEventBus(modeler); + + // AND the value of the instructions is changed + const userElement = await expectSelected('task_confirm'); + const group = findGroupEntry('instructions', container); + + const input = domQuery('textarea', group); + changeInput(input, '#Hello!'); + + // THEN - the script tag in the BPMN Business object / XML is updated as well. + const businessObject = getBusinessObject(userElement); + // The change is reflected in the business object + let instructions = getExtensionValue( + userElement, + 'spiffworkflow:instructionsForEndUser' + ); + expect(instructions).to.equal('#Hello!'); + }); }); diff --git a/spiffworkflow-backend/migrations/versions/b1647eff45c9_.py b/spiffworkflow-backend/migrations/versions/7c12964efde1_.py similarity index 95% rename from spiffworkflow-backend/migrations/versions/b1647eff45c9_.py rename to spiffworkflow-backend/migrations/versions/7c12964efde1_.py index d6ff25e3..9a285b72 100644 --- a/spiffworkflow-backend/migrations/versions/b1647eff45c9_.py +++ b/spiffworkflow-backend/migrations/versions/7c12964efde1_.py @@ -1,8 +1,8 @@ """empty message -Revision ID: b1647eff45c9 +Revision ID: 7c12964efde1 Revises: -Create Date: 2022-11-02 14:25:09.992800 +Create Date: 2022-11-08 07:48:44.265652 """ from alembic import op @@ -10,7 +10,7 @@ import sqlalchemy as sa # revision identifiers, used by Alembic. -revision = 'b1647eff45c9' +revision = '7c12964efde1' down_revision = None branch_labels = None depends_on = None @@ -115,19 +115,16 @@ def upgrade(): op.create_table('process_instance_report', sa.Column('id', sa.Integer(), nullable=False), sa.Column('identifier', sa.String(length=50), nullable=False), - sa.Column('process_model_identifier', sa.String(length=50), nullable=False), - sa.Column('process_group_identifier', sa.String(length=50), nullable=False), sa.Column('report_metadata', sa.JSON(), nullable=True), sa.Column('created_by_id', sa.Integer(), nullable=False), sa.Column('created_at_in_seconds', sa.Integer(), nullable=True), sa.Column('updated_at_in_seconds', sa.Integer(), nullable=True), sa.ForeignKeyConstraint(['created_by_id'], ['user.id'], ), sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('process_group_identifier', 'process_model_identifier', 'identifier', name='process_instance_report_unique') + sa.UniqueConstraint('created_by_id', 'identifier', name='process_instance_report_unique') ) + op.create_index(op.f('ix_process_instance_report_created_by_id'), 'process_instance_report', ['created_by_id'], unique=False) op.create_index(op.f('ix_process_instance_report_identifier'), 'process_instance_report', ['identifier'], unique=False) - op.create_index(op.f('ix_process_instance_report_process_group_identifier'), 'process_instance_report', ['process_group_identifier'], unique=False) - op.create_index(op.f('ix_process_instance_report_process_model_identifier'), 'process_instance_report', ['process_model_identifier'], unique=False) op.create_table('refresh_token', sa.Column('id', sa.Integer(), nullable=False), sa.Column('user_id', sa.Integer(), nullable=False), @@ -292,9 +289,8 @@ def downgrade(): op.drop_table('user_group_assignment') op.drop_table('secret') op.drop_table('refresh_token') - op.drop_index(op.f('ix_process_instance_report_process_model_identifier'), table_name='process_instance_report') - op.drop_index(op.f('ix_process_instance_report_process_group_identifier'), table_name='process_instance_report') op.drop_index(op.f('ix_process_instance_report_identifier'), table_name='process_instance_report') + op.drop_index(op.f('ix_process_instance_report_created_by_id'), table_name='process_instance_report') op.drop_table('process_instance_report') op.drop_index(op.f('ix_process_instance_process_model_identifier'), table_name='process_instance') op.drop_index(op.f('ix_process_instance_process_group_identifier'), table_name='process_instance') diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/api.yml b/spiffworkflow-backend/src/spiffworkflow_backend/api.yml index a85a7730..7f2fbc91 100755 --- a/spiffworkflow-backend/src/spiffworkflow_backend/api.yml +++ b/spiffworkflow-backend/src/spiffworkflow_backend/api.yml @@ -674,14 +674,8 @@ paths: schema: $ref: "#/components/schemas/OkTrue" - /process-models/{modified_process_model_identifier}/process-instances/reports: + /process-instances/reports: parameters: - - name: modified_process_model_identifier - in: path - required: true - description: The unique id of an existing process model - schema: - type: string - name: page in: query required: false @@ -748,14 +742,8 @@ paths: schema: $ref: "#/components/schemas/OkTrue" - /process-models/{modified_process_model_identifier}/process-instances/reports/{report_identifier}: + /process-instances/reports/{report_identifier}: parameters: - - name: modified_process_model_identifier - in: path - required: true - description: The unique id of an existing process model. - schema: - type: string - name: report_identifier in: path required: true diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/models/file.py b/spiffworkflow-backend/src/spiffworkflow_backend/models/file.py index 0c844766..02ad5fc1 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/models/file.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/models/file.py @@ -4,6 +4,7 @@ from dataclasses import field from datetime import datetime from typing import Optional +import marshmallow from marshmallow import INCLUDE from marshmallow import Schema @@ -61,6 +62,20 @@ CONTENT_TYPES = { } +@dataclass() +class FileReference: + """File Reference Information. + + Includes items such as the process id and name for a BPMN, + or the Decision id and Decision name for a DMN file. There may be more than + one reference that points to a particular file. + """ + + id: str + name: str + type: str # can be 'process', 'decision', or just 'file' + + @dataclass(order=True) class File: """File.""" @@ -70,17 +85,12 @@ class File: content_type: str name: str type: str - document: dict last_modified: datetime size: int - process_instance_id: Optional[int] = None - irb_doc_code: Optional[str] = None - data_store: Optional[dict] = field(default_factory=dict) - user_uid: Optional[str] = None + references: Optional[list[FileReference]] = None file_contents: Optional[bytes] = None process_model_id: Optional[str] = None process_group_id: Optional[str] = None - archived: bool = False def __post_init__(self) -> None: """__post_init__.""" @@ -100,7 +110,6 @@ class File: name=file_name, content_type=content_type, type=file_type.value, - document={}, last_modified=last_modified, size=file_size, ) @@ -118,32 +127,29 @@ class FileSchema(Schema): "id", "name", "content_type", - "process_instance_id", - "irb_doc_code", "last_modified", "type", - "archived", "size", "data_store", - "document", "user_uid", "url", "file_contents", - "process_model_id", + "references", "process_group_id", + "process_model_id", ] unknown = INCLUDE + references = marshmallow.fields.List( + marshmallow.fields.Nested("FileReferenceSchema") + ) - # url = Method("get_url") - # - # def get_url(self, obj): - # token = 'not_available' - # if hasattr(obj, 'id') and obj.id is not None: - # file_url = url_for("/v1_0.crc_api_file_get_file_data_link", file_id=obj.id, _external=True) - # if hasattr(flask.g, 'user'): - # token = flask.g.user.encode_auth_token() - # url = file_url + '?auth_token=' + urllib.parse.quote_plus(token) - # return url - # else: - # return "" - # + +class FileReferenceSchema(Schema): + """FileSchema.""" + + class Meta: + """Meta.""" + + model = FileReference + fields = ["id", "name", "type"] + unknown = INCLUDE diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/models/process_instance_report.py b/spiffworkflow-backend/src/spiffworkflow_backend/models/process_instance_report.py index 35d5d688..478858a6 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/models/process_instance_report.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/models/process_instance_report.py @@ -21,7 +21,6 @@ from spiffworkflow_backend.models.user import UserModel from spiffworkflow_backend.services.process_instance_processor import ( ProcessInstanceProcessor, ) -from spiffworkflow_backend.services.process_model_service import ProcessModelService ReportMetadata = dict[str, Any] @@ -58,8 +57,7 @@ class ProcessInstanceReportModel(SpiffworkflowBaseDBModel): __tablename__ = "process_instance_report" __table_args__ = ( db.UniqueConstraint( - "process_group_identifier", - "process_model_identifier", + "created_by_id", "identifier", name="process_instance_report_unique", ), @@ -67,21 +65,53 @@ class ProcessInstanceReportModel(SpiffworkflowBaseDBModel): id = db.Column(db.Integer, primary_key=True) identifier: str = db.Column(db.String(50), nullable=False, index=True) - process_model_identifier: str = db.Column(db.String(50), nullable=False, index=True) - process_group_identifier = db.Column(db.String(50), nullable=False, index=True) report_metadata: dict = deferred(db.Column(db.JSON)) # type: ignore - created_by_id = db.Column(ForeignKey(UserModel.id), nullable=False) + created_by_id = db.Column(ForeignKey(UserModel.id), nullable=False, index=True) created_by = relationship("UserModel") created_at_in_seconds = db.Column(db.Integer) updated_at_in_seconds = db.Column(db.Integer) + @classmethod + def default_report(cls, user: UserModel) -> ProcessInstanceReportModel: + """Default_report.""" + identifier = "default" + process_instance_report = ProcessInstanceReportModel.query.filter_by( + identifier=identifier, created_by_id=user.id + ).first() + + if process_instance_report is None: + report_metadata = { + "columns": [ + {"Header": "id", "accessor": "id"}, + { + "Header": "process_group_identifier", + "accessor": "process_group_identifier", + }, + { + "Header": "process_model_identifier", + "accessor": "process_model_identifier", + }, + {"Header": "start_in_seconds", "accessor": "start_in_seconds"}, + {"Header": "end_in_seconds", "accessor": "end_in_seconds"}, + {"Header": "status", "accessor": "status"}, + ], + } + + process_instance_report = cls( + identifier=identifier, + created_by_id=user.id, + report_metadata=report_metadata, + ) + + return process_instance_report # type: ignore + @classmethod def add_fixtures(cls) -> None: """Add_fixtures.""" try: - process_model = ProcessModelService().get_process_model( - process_model_id="sartography-admin/ticket" - ) + # process_model = ProcessModelService().get_process_model( + # process_model_id="sartography-admin/ticket" + # ) user = UserModel.query.first() columns = [ {"Header": "id", "accessor": "id"}, @@ -96,29 +126,21 @@ class ProcessInstanceReportModel(SpiffworkflowBaseDBModel): cls.create_report( identifier="standard", - process_group_identifier=process_model.process_group_id, - process_model_identifier=process_model.id, user=user, report_metadata=json, ) cls.create_report( identifier="for-month", - process_group_identifier="sartography-admin", - process_model_identifier="ticket", user=user, report_metadata=cls.ticket_for_month_report(), ) cls.create_report( identifier="for-month-3", - process_group_identifier="sartography-admin", - process_model_identifier="ticket", user=user, report_metadata=cls.ticket_for_month_3_report(), ) cls.create_report( identifier="hot-report", - process_group_identifier="category_number_one", - process_model_identifier="process-model-with-form", user=user, report_metadata=cls.process_model_with_form_report_fixture(), ) @@ -130,23 +152,18 @@ class ProcessInstanceReportModel(SpiffworkflowBaseDBModel): def create_report( cls, identifier: str, - process_group_identifier: str, - process_model_identifier: str, user: UserModel, report_metadata: ReportMetadata, ) -> None: """Make_fixture_report.""" process_instance_report = ProcessInstanceReportModel.query.filter_by( identifier=identifier, - process_group_identifier=process_group_identifier, - process_model_identifier=process_model_identifier, + created_by_id=user.id, ).first() if process_instance_report is None: process_instance_report = cls( identifier=identifier, - process_group_identifier=process_group_identifier, - process_model_identifier=process_model_identifier, created_by_id=user.id, report_metadata=report_metadata, ) @@ -217,19 +234,22 @@ class ProcessInstanceReportModel(SpiffworkflowBaseDBModel): def create_with_attributes( cls, identifier: str, - process_group_identifier: str, - process_model_identifier: str, report_metadata: dict, user: UserModel, ) -> ProcessInstanceReportModel: """Create_with_attributes.""" - process_model = ProcessModelService().get_process_model( - process_model_id=f"{process_model_identifier}" - ) +# <<<<<<< HEAD +# process_model = ProcessModelService().get_process_model( +# process_model_id=f"{process_model_identifier}" +# ) +# process_instance_report = cls( +# identifier=identifier, +# process_group_identifier="process_model.process_group_id", +# process_model_identifier=process_model.id, +# ======= process_instance_report = cls( identifier=identifier, - process_group_identifier="process_model.process_group_id", - process_model_identifier=process_model.id, +# >>>>>>> main created_by_id=user.id, report_metadata=report_metadata, ) diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py index b424ded9..f27cf957 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py @@ -65,6 +65,7 @@ from spiffworkflow_backend.services.error_handling_service import ErrorHandlingS from spiffworkflow_backend.services.file_system_service import FileSystemService from spiffworkflow_backend.services.git_service import GitService from spiffworkflow_backend.services.message_service import MessageService +from spiffworkflow_backend.services.process_instance_processor import MyCustomParser from spiffworkflow_backend.services.process_instance_processor import ( ProcessInstanceProcessor, ) @@ -280,6 +281,10 @@ def process_model_show(modified_process_model_identifier: str) -> Any: # process_model.id = process_model_identifier files = sorted(SpecFileService.get_files(process_model)) process_model.files = files + for file in process_model.files: + file.references = SpecFileService.get_references_for_file( + file, process_model, MyCustomParser + ) process_model_json = ProcessModelInfoSchema().dump(process_model) return process_model_json @@ -717,10 +722,29 @@ def process_instance_list( ProcessInstanceModel.start_in_seconds.desc(), ProcessInstanceModel.id.desc() # type: ignore ).paginate(page=page, per_page=per_page, error_out=False) + process_instance_report = ProcessInstanceReportModel.default_report(g.user) + + # TODO need to look into this more - how the filter here interacts with the + # one defined in the report. + # TODO need to look into test failures when the results from result_dict is + # used instead of the process instances + + # substitution_variables = request.args.to_dict() + # result_dict = process_instance_report.generate_report( + # process_instances.items, substitution_variables + # ) + + # results = result_dict["results"] + # report_metadata = result_dict["report_metadata"] + + results = process_instances.items + report_metadata = process_instance_report.report_metadata + response_json = { - "results": process_instances.items, + "report_metadata": report_metadata, + "results": results, "pagination": { - "count": len(process_instances.items), + "count": len(results), "total": process_instances.total, "pages": process_instances.pages, }, @@ -769,26 +793,20 @@ def process_instance_delete( def process_instance_report_list( - modified_process_model_identifier: str, page: int = 1, per_page: int = 100 + page: int = 1, per_page: int = 100 ) -> flask.wrappers.Response: """Process_instance_report_list.""" - process_model_identifier = modified_process_model_identifier.replace(":", "/") - process_instance_reports = ProcessInstanceReportModel.query.filter_by( - process_model_identifier=process_model_identifier, + created_by_id=g.user.id, ).all() return make_response(jsonify(process_instance_reports), 200) -def process_instance_report_create( - process_group_id: str, process_model_id: str, body: Dict[str, Any] -) -> flask.wrappers.Response: +def process_instance_report_create(body: Dict[str, Any]) -> flask.wrappers.Response: """Process_instance_report_create.""" ProcessInstanceReportModel.create_report( identifier=body["identifier"], - process_group_identifier=process_group_id, - process_model_identifier=process_model_id, user=g.user, report_metadata=body["report_metadata"], ) @@ -797,16 +815,13 @@ def process_instance_report_create( def process_instance_report_update( - process_group_id: str, - process_model_id: str, report_identifier: str, body: Dict[str, Any], ) -> flask.wrappers.Response: """Process_instance_report_create.""" process_instance_report = ProcessInstanceReportModel.query.filter_by( identifier=report_identifier, - process_group_identifier=process_group_id, - process_model_identifier=process_model_id, + created_by_id=g.user.id, ).first() if process_instance_report is None: raise ApiError( @@ -822,15 +837,12 @@ def process_instance_report_update( def process_instance_report_delete( - process_group_id: str, - process_model_id: str, report_identifier: str, ) -> flask.wrappers.Response: """Process_instance_report_create.""" process_instance_report = ProcessInstanceReportModel.query.filter_by( identifier=report_identifier, - process_group_identifier=process_group_id, - process_model_identifier=process_model_id, + created_by_id=g.user.id, ).first() if process_instance_report is None: raise ApiError( @@ -883,24 +895,21 @@ def authentication_callback( def process_instance_report_show( - modified_process_model_identifier: str, report_identifier: str, page: int = 1, per_page: int = 100, ) -> flask.wrappers.Response: """Process_instance_list.""" - process_model_identifier = modified_process_model_identifier.replace(":", "/") - process_instances = ( - ProcessInstanceModel.query.filter_by(process_model_identifier=process_model_identifier) - .order_by( - ProcessInstanceModel.start_in_seconds.desc(), ProcessInstanceModel.id.desc() # type: ignore - ) - .paginate(page=page, per_page=per_page, error_out=False) + process_instances = ProcessInstanceModel.query.order_by( # .filter_by(process_model_identifier=process_model.id) + ProcessInstanceModel.start_in_seconds.desc(), ProcessInstanceModel.id.desc() # type: ignore + ).paginate( + page=page, per_page=per_page, error_out=False ) process_instance_report = ProcessInstanceReportModel.query.filter_by( - identifier=report_identifier + identifier=report_identifier, + created_by_id=g.user.id, ).first() if process_instance_report is None: raise ApiError( diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_service.py index 61a89602..a8b027d3 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_service.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_service.py @@ -7,6 +7,7 @@ from typing import Optional from flask import current_app from flask_bpmn.api.api_error import ApiError from flask_bpmn.models.db import db +from SpiffWorkflow.util.deep_merge import DeepMerge # type: ignore from SpiffWorkflow.task import Task as SpiffTask # type: ignore from spiffworkflow_backend.models.process_instance import ProcessInstanceApi @@ -104,6 +105,20 @@ class ProcessInstanceService: title=title_value, ) + next_task_trying_again = next_task + if ( + not next_task + ): # The Next Task can be requested to be a certain task, useful for parallel tasks. + # This may or may not work, sometimes there is no next task to complete. + next_task_trying_again = processor.next_task() + + if next_task_trying_again is not None: + process_instance_api.next_task = ( + ProcessInstanceService.spiff_task_to_api_task( + next_task_trying_again, add_docs_and_forms=True + ) + ) + return process_instance_api def get_process_instance(self, process_instance_id: int) -> Any: diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/spec_file_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/spec_file_service.py index 4ec49307..b710394c 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/spec_file_service.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/spec_file_service.py @@ -2,6 +2,7 @@ import os import shutil from datetime import datetime +from typing import Any from typing import List from typing import Optional @@ -14,6 +15,7 @@ from SpiffWorkflow.bpmn.parser.ValidationException import ValidationException # from spiffworkflow_backend.models.bpmn_process_id_lookup import BpmnProcessIdLookup from spiffworkflow_backend.models.file import File +from spiffworkflow_backend.models.file import FileReference from spiffworkflow_backend.models.file import FileType from spiffworkflow_backend.models.message_correlation_property import ( MessageCorrelationPropertyModel, @@ -55,6 +57,41 @@ class SpecFileService(FileSystemService): ) return files + @staticmethod + def get_references_for_file( + file: File, process_model_info: ProcessModelInfo, parser_class: Any + ) -> list[FileReference]: + """Uses spiffworkflow to parse BPMN and DMN files to determine how they can be externally referenced. + + Returns a list of Reference objects that contain the type of reference, the id, the name. + Ex. + id = {str} 'Level3' + name = {str} 'Level 3' + type = {str} 'process' + """ + references: list[FileReference] = [] + file_path = SpecFileService.file_path(process_model_info, file.name) + parser = parser_class() + parser_type = None + sub_parser = None + if file.type == FileType.bpmn.value: + parser.add_bpmn_file(file_path) + parser_type = "process" + sub_parsers = list(parser.process_parsers.values()) + elif file.type == FileType.dmn.value: + parser.add_dmn_file(file_path) + sub_parsers = list(parser.dmn_parsers.values()) + parser_type = "decision" + else: + return references + for sub_parser in sub_parsers: + references.append( + FileReference( + id=sub_parser.get_id(), name=sub_parser.get_name(), type=parser_type + ) + ) + return references + @staticmethod def add_file( process_model_info: ProcessModelInfo, file_name: str, binary_data: bytes diff --git a/spiffworkflow-backend/tests/data/call_activity_nested/schema.json b/spiffworkflow-backend/tests/data/call_activity_nested/schema.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/spiffworkflow-backend/tests/data/call_activity_nested/schema.json @@ -0,0 +1 @@ +{} diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_process_api.py b/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_process_api.py index b1685e94..3ecc32c0 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_process_api.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_process_api.py @@ -1255,7 +1255,7 @@ class TestProcessApi(BaseTest): ) assert response.json is not None - # assert response.json['next_task'] is not None + assert response.json['next_task'] is not None active_tasks = ( db.session.query(ActiveTaskModel) @@ -1518,13 +1518,11 @@ class TestProcessApi(BaseTest): report_metadata = {"order_by": ["month"]} ProcessInstanceReportModel.create_with_attributes( identifier=report_identifier, - process_group_identifier="", - process_model_identifier=process_model_identifier, report_metadata=report_metadata, user=with_super_admin_user, ) response = client.get( - f"/v1.0/process-models/{modified_process_model_identifier}/process-instances/reports", + "/v1.0/process-instances/reports", headers=self.logged_in_headers(with_super_admin_user), ) assert response.status_code == 200 @@ -1578,14 +1576,12 @@ class TestProcessApi(BaseTest): ProcessInstanceReportModel.create_with_attributes( identifier="sure", - process_group_identifier="test_process_group_id", - process_model_identifier=process_model_identifier, report_metadata=report_metadata, user=with_super_admin_user, ) response = client.get( - f"/v1.0/process-models/{modified_process_model_identifier}/process-instances/reports/sure", + "/v1.0/process-instances/reports/sure", headers=self.logged_in_headers(with_super_admin_user), ) assert response.status_code == 200 @@ -1613,10 +1609,6 @@ class TestProcessApi(BaseTest): setup_process_instances_for_reports: list[ProcessInstanceModel], ) -> None: """Test_process_instance_report_show_with_default_list.""" - test_process_group_id = "runs_without_input" - process_model_dir_name = "sample" - process_model_identifier = f"{test_process_group_id}/{process_model_dir_name}" - modified_process_model_identifier = process_model_identifier.replace("/", ":") report_metadata = { "filter_by": [ @@ -1630,14 +1622,12 @@ class TestProcessApi(BaseTest): ProcessInstanceReportModel.create_with_attributes( identifier="sure", - process_group_identifier="test_process_group_id", - process_model_identifier=process_model_identifier, report_metadata=report_metadata, user=with_super_admin_user, ) response = client.get( - f"/v1.0/process-models/{modified_process_model_identifier}/process-instances/reports/sure?grade_level=1", + "/v1.0/process-instances/reports/sure?grade_level=1", headers=self.logged_in_headers(with_super_admin_user), ) assert response.status_code == 200 @@ -1653,11 +1643,8 @@ class TestProcessApi(BaseTest): setup_process_instances_for_reports: list[ProcessInstanceModel], ) -> None: """Test_process_instance_report_show_with_default_list.""" - test_process_group_id = "runs_without_input" - process_model_dir_name = "sample" - response = client.get( - f"/v1.0/process-models/{test_process_group_id}:{process_model_dir_name}/process-instances/reports/sure?grade_level=1", + "/v1.0/process-instances/reports/sure?grade_level=1", headers=self.logged_in_headers(with_super_admin_user), ) assert response.status_code == 404 diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_file.py b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_file.py index 22f2fb15..c5819587 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_file.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_file.py @@ -21,7 +21,6 @@ def create_test_file(type: str, name: str) -> File: type=type, name=name, content_type=type, - document={}, last_modified=datetime.now(), size=1, ) diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_instance_report.py b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_instance_report.py index acfac138..48239507 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_instance_report.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_instance_report.py @@ -128,8 +128,6 @@ def do_report_with_metadata_and_instances( """Do_report_with_metadata_and_instances.""" process_instance_report = ProcessInstanceReportModel.create_with_attributes( identifier="sure", - process_group_identifier=process_instances[0].process_group_identifier, - process_model_identifier=process_instances[0].process_model_identifier, report_metadata=report_metadata, user=BaseTest.find_or_create_user(), ) diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_script_unit_test_runner.py b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_script_unit_test_runner.py index 53aa777c..f9cc7cfa 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_script_unit_test_runner.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_script_unit_test_runner.py @@ -40,7 +40,7 @@ class TestScriptUnitTestRunner(BaseTest): ) ) task = ProcessInstanceProcessor.get_task_by_bpmn_identifier( - "Activity_RunScript", bpmn_process_instance + "Activity_CalculateNewData", bpmn_process_instance ) assert task is not None @@ -81,7 +81,7 @@ class TestScriptUnitTestRunner(BaseTest): ) ) task = ProcessInstanceProcessor.get_task_by_bpmn_identifier( - "Activity_RunScript", bpmn_process_instance + "Activity_CalculateNewData", bpmn_process_instance ) assert task is not None diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_spec_file_service.py b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_spec_file_service.py index 44621e19..edfade0d 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_spec_file_service.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_spec_file_service.py @@ -6,11 +6,14 @@ from flask import Flask from flask.testing import FlaskClient from flask_bpmn.api.api_error import ApiError from flask_bpmn.models.db import db +from SpiffWorkflow.dmn.parser.BpmnDmnParser import BpmnDmnParser # type: ignore from tests.spiffworkflow_backend.helpers.base_test import BaseTest from tests.spiffworkflow_backend.helpers.test_data import load_test_spec from spiffworkflow_backend.models.bpmn_process_id_lookup import BpmnProcessIdLookup from spiffworkflow_backend.models.user import UserModel +from spiffworkflow_backend.services.process_model_service import ProcessModelService +from spiffworkflow_backend.services.spec_file_service import SpecFileService class TestSpecFileService(BaseTest): @@ -108,3 +111,55 @@ class TestSpecFileService(BaseTest): bpmn_process_id_lookups[0].bpmn_file_relative_path == self.call_activity_nested_relative_file_path ) + + def test_load_reference_information( + self, app: Flask, client: FlaskClient, with_db_and_bpmn_file_cleanup: None, with_super_admin_user: UserModel + ) -> None: + """Test_load_reference_information. + + When getting files from the spec_file service, each file includes + details about how the file can be referenced -- for instance + it is possible to reference a DMN file with a Decision.id or + a bpmn file with a process.id. These Decisions and processes + can also have human readable display names, which should also be avaiable. + Note that a single bpmn file can contain many processes, and + a DMN file can (theoretically) contain many decisions. So this + is an array. + """ + process_group_id = "test_group" + process_model_id = "call_activity_nested" + bpmn_file_name = "call_activity_nested.bpmn" + process_model_identifier = self.basic_test_setup( + client=client, + user=with_super_admin_user, + process_group_id=process_group_id, + process_model_id=process_model_id, + # bpmn_file_name=bpmn_file_name, + bpmn_file_location=process_model_id + ) + # load_test_spec( + # , + # process_model_source_directory="call_activity_nested", + # ) + process_model_info = ProcessModelService().get_process_model( + process_model_identifier + ) + files = SpecFileService.get_files(process_model_info) + + file = next(filter(lambda f: f.name == "call_activity_level_3.bpmn", files)) + ca_3 = SpecFileService.get_references_for_file( + file, process_model_info, BpmnDmnParser + ) + assert len(ca_3) == 1 + assert ca_3[0].name == "Level 3" + assert ca_3[0].id == "Level3" + assert ca_3[0].type == "process" + + file = next(filter(lambda f: f.name == "level2c.dmn", files)) + dmn1 = SpecFileService.get_references_for_file( + file, process_model_info, BpmnDmnParser + ) + assert len(dmn1) == 1 + assert dmn1[0].name == "Decision 1" + assert dmn1[0].id == "Decision_0vrtcmk" + assert dmn1[0].type == "decision" diff --git a/spiffworkflow-frontend/.gitignore b/spiffworkflow-frontend/.gitignore index 0f43a99f..a694da80 100644 --- a/spiffworkflow-frontend/.gitignore +++ b/spiffworkflow-frontend/.gitignore @@ -27,3 +27,6 @@ cypress/screenshots # i keep accidentally committing these /test*.json + +# Editors +.idea \ No newline at end of file diff --git a/spiffworkflow-frontend/package-lock.json b/spiffworkflow-frontend/package-lock.json index a08036b5..eee5ba6c 100644 --- a/spiffworkflow-frontend/package-lock.json +++ b/spiffworkflow-frontend/package-lock.json @@ -25,12 +25,13 @@ "@types/node": "^18.6.5", "@types/react": "^18.0.17", "@types/react-dom": "^18.0.6", + "@uiw/react-md-editor": "^3.19.5", "autoprefixer": "10.4.8", "axios": "^0.27.2", "bootstrap": "^5.2.0", "bpmn-js": "^9.3.2", "bpmn-js-properties-panel": "^1.10.0", - "bpmn-js-spiffworkflow": "sartography/bpmn-js-spiffworkflow#main", + "bpmn-js-spiffworkflow": "sartography/bpmn-js-spiffworkflow#feature/more_launch_buttons_and_dropdowns", "craco": "^0.0.3", "date-fns": "^2.28.0", "diagram-js": "^8.5.0", @@ -5404,6 +5405,11 @@ "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.0.tgz", "integrity": "sha512-RI1L7N4JnW5gQw2spvL7Sllfuf1SaHdrZpCHiBlCXjIlufi1SMNnbu2teze3/QE67Fg2tBlH7W+mi4hVNk4p0A==" }, + "node_modules/@types/prismjs": { + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.0.tgz", + "integrity": "sha512-ZTaqn/qSqUuAq1YwvOFQfVW1AR/oQJlLSZVustdjwI+GZ8kr0MSHBj0tsXPW1EqHubx50gtBEjbPGsdZwQwCjQ==" + }, "node_modules/@types/prop-types": { "version": "15.7.5", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", @@ -5883,6 +5889,48 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@uiw/copy-to-clipboard": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@uiw/copy-to-clipboard/-/copy-to-clipboard-1.0.12.tgz", + "integrity": "sha512-3tt7FVSbjtBCNBhffy7k26rpnEmk8GQj9QkTGZBIfpHU7mG3Buryt69u6xooYM7/gmv7GIqD4QxxIauIp2HHKg==" + }, + "node_modules/@uiw/react-markdown-preview": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@uiw/react-markdown-preview/-/react-markdown-preview-4.1.5.tgz", + "integrity": "sha512-CYiwzrarfg/jCsSHSfLkgxpLiqwZjmXhMJmwMa2SCct/PX/vt9wJJ84aNgUVbSjfQFKgmTlSWpDiN+kGnf8D/w==", + "dependencies": { + "@babel/runtime": "^7.17.2", + "@uiw/copy-to-clipboard": "~1.0.12", + "react-markdown": "~8.0.0", + "rehype-attr": "~2.0.7", + "rehype-autolink-headings": "~6.1.1", + "rehype-ignore": "^1.0.1", + "rehype-prism-plus": "~1.5.0", + "rehype-raw": "^6.1.1", + "rehype-rewrite": "~3.0.6", + "rehype-slug": "~5.0.1", + "remark-gfm": "~3.0.1", + "unist-util-visit": "^4.1.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@uiw/react-md-editor": { + "version": "3.19.5", + "resolved": "https://registry.npmjs.org/@uiw/react-md-editor/-/react-md-editor-3.19.5.tgz", + "integrity": "sha512-uhFGLOrKEtADM8QUauTdG5x8wqNS15ry2jCYMBr/E1Or3njvg7jpB0KRv+QTgZDnglevCTjuQxZPH7I7hG2uKw==", + "dependencies": { + "@babel/runtime": "^7.14.6", + "@uiw/react-markdown-preview": "^4.1.5", + "rehype": "~12.0.1" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, "node_modules/@webassemblyjs/ast": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.9.0.tgz", @@ -7094,6 +7142,15 @@ "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==" }, + "node_modules/bcp-47-match": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bcp-47-match/-/bcp-47-match-2.0.2.tgz", + "integrity": "sha512-zy5swVXwQ25ttElhoN9Dgnqm6VFlMkeDNljvHSGqGNr4zClUosdFzxD+fQHJVmx3g3KY+r//wV/fmBHsa1ErnA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/bcrypt-pbkdf": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", @@ -7428,7 +7485,7 @@ }, "node_modules/bpmn-js-spiffworkflow": { "version": "0.0.8", - "resolved": "git+ssh://git@github.com/sartography/bpmn-js-spiffworkflow.git#999ea7e4c0e5157546625fbd7b1eb5cc8a79c0c0", + "resolved": "git+ssh://git@github.com/sartography/bpmn-js-spiffworkflow.git#84593aee1ead7328efdc7da03ab3c9cd34364496", "license": "MIT", "dependencies": { "inherits": "^2.0.4", @@ -7978,6 +8035,33 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/check-more-types": { "version": "2.24.0", "resolved": "https://registry.npmjs.org/check-more-types/-/check-more-types-2.24.0.tgz", @@ -9003,6 +9087,11 @@ "resolved": "https://registry.npmjs.org/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz", "integrity": "sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w==" }, + "node_modules/css-selector-parser": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/css-selector-parser/-/css-selector-parser-1.4.1.tgz", + "integrity": "sha512-HYPSb7y/Z7BNDCOrakL4raGO2zltZkbeXyAd6Tg9obzix6QhzxCotdBl6VT0Dv4vZfJGVz3WL/xaEI9Ly3ul0g==" + }, "node_modules/css-tree": { "version": "1.0.0-alpha.37", "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.37.tgz", @@ -9781,6 +9870,18 @@ "node": ">=8" } }, + "node_modules/direction": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/direction/-/direction-2.0.1.tgz", + "integrity": "sha512-9S6m9Sukh1cZNknO1CWAr2QAWsbKLafQiyM5gZ7VgXHeuaoUwffKN4q6NC4A/Mf9iiPlOXQEKW/Mv/mh9/3YFA==", + "bin": { + "direction": "cli.js" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/dlv": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", @@ -12273,6 +12374,11 @@ "assert-plus": "^1.0.0" } }, + "node_modules/github-slugger": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-1.5.0.tgz", + "integrity": "sha512-wIh+gKBI9Nshz2o46B0B3f5k/W+WI9ZAv6y5Dn5WJ5SK1t0TnDimB4WE5rmTD05ZAIn8HALCZVmCsvj0w0v0lw==" + }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -12607,6 +12713,199 @@ "minimalistic-assert": "^1.0.1" } }, + "node_modules/hast-to-hyperscript": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/hast-to-hyperscript/-/hast-to-hyperscript-10.0.1.tgz", + "integrity": "sha512-dhIVGoKCQVewFi+vz3Vt567E4ejMppS1haBRL6TEmeLeJVB1i/FJIIg/e6s1Bwn0g5qtYojHEKvyGA+OZuyifw==", + "dependencies": { + "@types/unist": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "property-information": "^6.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-object": "^0.3.0", + "unist-util-is": "^5.0.0", + "web-namespaces": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-from-parse5": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-7.1.0.tgz", + "integrity": "sha512-m8yhANIAccpU4K6+121KpPP55sSl9/samzQSQGpb0mTExcNh2WlvjtMwSWFhg6uqD4Rr6Nfa8N6TMypQM51rzQ==", + "dependencies": { + "@types/hast": "^2.0.0", + "@types/parse5": "^6.0.0", + "@types/unist": "^2.0.0", + "hastscript": "^7.0.0", + "property-information": "^6.0.0", + "vfile": "^5.0.0", + "vfile-location": "^4.0.0", + "web-namespaces": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-has-property": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hast-util-has-property/-/hast-util-has-property-2.0.0.tgz", + "integrity": "sha512-4Qf++8o5v14us4Muv3HRj+Er6wTNGA/N9uCaZMty4JWvyFKLdhULrv4KE1b65AthsSO9TXSZnjuxS8ecIyhb0w==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-heading-rank": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/hast-util-heading-rank/-/hast-util-heading-rank-2.1.0.tgz", + "integrity": "sha512-w+Rw20Q/iWp2Bcnr6uTrYU6/ftZLbHKhvc8nM26VIWpDqDMlku2iXUVTeOlsdoih/UKQhY7PHQ+vZ0Aqq8bxtQ==", + "dependencies": { + "@types/hast": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-is-element": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-2.1.2.tgz", + "integrity": "sha512-thjnlGAnwP8ef/GSO1Q8BfVk2gundnc2peGQqEg2kUt/IqesiGg/5mSwN2fE7nLzy61pg88NG6xV+UrGOrx9EA==", + "dependencies": { + "@types/hast": "^2.0.0", + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-parse-selector": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-3.1.0.tgz", + "integrity": "sha512-AyjlI2pTAZEOeu7GeBPZhROx0RHBnydkQIXlhnFzDi0qfXTmGUWoCYZtomHbrdrheV4VFUlPcfJ6LMF5T6sQzg==", + "dependencies": { + "@types/hast": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-raw": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-7.2.2.tgz", + "integrity": "sha512-0x3BhhdlBcqRIKyc095lBSDvmQNMY3Eulj2PLsT5XCyKYrxssI5yr3P4Kv/PBo1s/DMkZy2voGkMXECnFCZRLQ==", + "dependencies": { + "@types/hast": "^2.0.0", + "@types/parse5": "^6.0.0", + "hast-util-from-parse5": "^7.0.0", + "hast-util-to-parse5": "^7.0.0", + "html-void-elements": "^2.0.0", + "parse5": "^6.0.0", + "unist-util-position": "^4.0.0", + "unist-util-visit": "^4.0.0", + "vfile": "^5.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-select": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/hast-util-select/-/hast-util-select-5.0.2.tgz", + "integrity": "sha512-QGN5o7N8gq1BhUX96ApLE8izOXlf+IPkOVGXcp9Dskdd3w0OqZrn6faPAmS0/oVogwJOd0lWFSYmBK75e+030g==", + "dependencies": { + "@types/hast": "^2.0.0", + "@types/unist": "^2.0.0", + "bcp-47-match": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "css-selector-parser": "^1.0.0", + "direction": "^2.0.0", + "hast-util-has-property": "^2.0.0", + "hast-util-is-element": "^2.0.0", + "hast-util-to-string": "^2.0.0", + "hast-util-whitespace": "^2.0.0", + "not": "^0.1.0", + "nth-check": "^2.0.0", + "property-information": "^6.0.0", + "space-separated-tokens": "^2.0.0", + "unist-util-visit": "^4.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-select/node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/hast-util-to-html": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-8.0.3.tgz", + "integrity": "sha512-/D/E5ymdPYhHpPkuTHOUkSatxr4w1ZKrZsG0Zv/3C2SRVT0JFJG53VS45AMrBtYk0wp5A7ksEhiC8QaOZM95+A==", + "dependencies": { + "@types/hast": "^2.0.0", + "ccount": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-is-element": "^2.0.0", + "hast-util-whitespace": "^2.0.0", + "html-void-elements": "^2.0.0", + "property-information": "^6.0.0", + "space-separated-tokens": "^2.0.0", + "stringify-entities": "^4.0.2", + "unist-util-is": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-parse5": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-7.0.0.tgz", + "integrity": "sha512-YHiS6aTaZ3N0Q3nxaY/Tj98D6kM8QX5Q8xqgg8G45zR7PvWnPGPP0vcKCgb/moIydEJ/QWczVrX0JODCVeoV7A==", + "dependencies": { + "@types/hast": "^2.0.0", + "@types/parse5": "^6.0.0", + "hast-to-hyperscript": "^10.0.0", + "property-information": "^6.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hast-util-to-string/-/hast-util-to-string-2.0.0.tgz", + "integrity": "sha512-02AQ3vLhuH3FisaMM+i/9sm4OXGSq1UhOOCpTLLQtHdL3tZt7qil69r8M8iDkZYyC0HCFylcYoP+8IO7ddta1A==", + "dependencies": { + "@types/hast": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/hast-util-whitespace": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-2.0.0.tgz", @@ -12616,6 +12915,22 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/hastscript": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-7.1.0.tgz", + "integrity": "sha512-uBjaTTLN0MkCZxY/R2fWUOcu7FRtUVzKRO5P/RAfgsu3yFiMB1JWCO4AjeVkgHxAira1f2UecHK5WfS9QurlWA==", + "dependencies": { + "@types/hast": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-parse-selector": "^3.0.0", + "property-information": "^6.0.0", + "space-separated-tokens": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/hat": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/hat/-/hat-0.0.3.tgz", @@ -12748,6 +13063,15 @@ "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" }, + "node_modules/html-void-elements": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-2.0.1.tgz", + "integrity": "sha512-0quDb7s97CfemeJAnW9wC0hw78MtW7NU3hqtCD75g2vFlDLt36llsYD7uB7SUzojLMP24N5IatXf7ylGXiGG9A==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/htmlparser2": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", @@ -13165,6 +13489,28 @@ "node": ">=0.10.0" } }, + "node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", @@ -13271,6 +13617,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/is-descriptor": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", @@ -13344,6 +13699,15 @@ "node": ">=0.10.0" } }, + "node_modules/is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/is-in-browser": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/is-in-browser/-/is-in-browser-1.1.3.tgz", @@ -20003,6 +20367,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/not": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/not/-/not-0.1.0.tgz", + "integrity": "sha512-5PDmaAsVfnWUgTUbJ3ERwn7u79Z0dYxN9ErxCpVJJqe2RK0PJ3z+iFUxuqjwtlDDegXvtWoxD/3Fzxox7tFGWA==" + }, "node_modules/npm-run-path": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", @@ -20462,6 +20831,25 @@ "safe-buffer": "^5.1.1" } }, + "node_modules/parse-entities": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.0.tgz", + "integrity": "sha512-5nk9Fn03x3rEhGaX1FU6IDwG/k+GxLXlFAkgrbM1asuAFl3BhdQWvASaIsmwWypRNcZKHPYnIuOSfIWEyEQnPQ==", + "dependencies": { + "@types/unist": "^2.0.0", + "character-entities": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/parse-json": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", @@ -20488,6 +20876,11 @@ "node": ">=6" } }, + "node_modules/parse-numeric-range": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/parse-numeric-range/-/parse-numeric-range-1.3.0.tgz", + "integrity": "sha512-twN+njEipszzlMJd4ONUYgSfZPDxgHhT9Ahed5uTigpQn90FggW4SA/AIPq/6a149fTbE9qBEcSwE3FAEp6wQQ==" + }, "node_modules/parse5": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", @@ -23811,6 +24204,21 @@ "@babel/runtime": "^7.9.2" } }, + "node_modules/refractor": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/refractor/-/refractor-4.8.0.tgz", + "integrity": "sha512-SVOnWUJiEBFNiBlHbudSpSpDfDhDY1UHF0CMKgdvPsMYNQQ4rnqFxyGvP07UmteNC8V12mTF2c0lEsGS7lKaGw==", + "dependencies": { + "@types/hast": "^2.0.0", + "@types/prismjs": "^1.0.0", + "hastscript": "^7.0.0", + "parse-entities": "^4.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/regenerate": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", @@ -23961,6 +24369,151 @@ "jsesc": "bin/jsesc" } }, + "node_modules/rehype": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/rehype/-/rehype-12.0.1.tgz", + "integrity": "sha512-ey6kAqwLM3X6QnMDILJthGvG1m1ULROS9NT4uG9IDCuv08SFyLlreSuvOa//DgEvbXx62DS6elGVqusWhRUbgw==", + "dependencies": { + "@types/hast": "^2.0.0", + "rehype-parse": "^8.0.0", + "rehype-stringify": "^9.0.0", + "unified": "^10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-attr": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/rehype-attr/-/rehype-attr-2.0.8.tgz", + "integrity": "sha512-EISuHQNPUN2InK+R1tmiAp3IxvNw4utF3MY6+6QoltAXwSqgvC3RVhfaOVUxyakTb4KJmfii3s0b/a7DlQYZTg==", + "dependencies": { + "unified": "~10.1.1", + "unist-util-visit": "~4.1.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/rehype-autolink-headings": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/rehype-autolink-headings/-/rehype-autolink-headings-6.1.1.tgz", + "integrity": "sha512-NMYzZIsHM3sA14nC5rAFuUPIOfg+DFmf9EY1YMhaNlB7+3kK/ZlE6kqPfuxr1tsJ1XWkTrMtMoyHosU70d35mA==", + "dependencies": { + "@types/hast": "^2.0.0", + "extend": "^3.0.0", + "hast-util-has-property": "^2.0.0", + "hast-util-heading-rank": "^2.0.0", + "hast-util-is-element": "^2.0.0", + "unified": "^10.0.0", + "unist-util-visit": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-ignore": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/rehype-ignore/-/rehype-ignore-1.0.3.tgz", + "integrity": "sha512-N8eAcq9+QUFhTZqB4oxJ8WL1D2EWp+Jr+j6DKlV21j8+TBLkPdotWNPSzo4FNJbIy6SWxjS/dgDpHmJm8YD6XQ==", + "dependencies": { + "hast-util-select": "~5.0.1", + "unified": "~10.1.2", + "unist-util-visit": "~4.1.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/rehype-parse": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/rehype-parse/-/rehype-parse-8.0.4.tgz", + "integrity": "sha512-MJJKONunHjoTh4kc3dsM1v3C9kGrrxvA3U8PxZlP2SjH8RNUSrb+lF7Y0KVaUDnGH2QZ5vAn7ulkiajM9ifuqg==", + "dependencies": { + "@types/hast": "^2.0.0", + "hast-util-from-parse5": "^7.0.0", + "parse5": "^6.0.0", + "unified": "^10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-prism-plus": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/rehype-prism-plus/-/rehype-prism-plus-1.5.0.tgz", + "integrity": "sha512-KNJYMQHqN+53ZbT5Pa/lO7uorMpBIR3x9RjFeG1lPlQherZDZiPqyOFS464L4BniZ4VG5PnG5DXVqjGtwxWJew==", + "dependencies": { + "hast-util-to-string": "^2.0.0", + "parse-numeric-range": "^1.3.0", + "refractor": "^4.7.0", + "rehype-parse": "^8.0.2", + "unist-util-filter": "^4.0.0", + "unist-util-visit": "^4.0.0" + } + }, + "node_modules/rehype-raw": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/rehype-raw/-/rehype-raw-6.1.1.tgz", + "integrity": "sha512-d6AKtisSRtDRX4aSPsJGTfnzrX2ZkHQLE5kiUuGOeEoLpbEulFF4hj0mLPbsa+7vmguDKOVVEQdHKDSwoaIDsQ==", + "dependencies": { + "@types/hast": "^2.0.0", + "hast-util-raw": "^7.2.0", + "unified": "^10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-rewrite": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/rehype-rewrite/-/rehype-rewrite-3.0.6.tgz", + "integrity": "sha512-REDTNCvsKcAazy8IQWzKp66AhSUDSOIKssSCqNqCcT9sN7JCwAAm3mWGTUdUzq80ABuy8d0D6RBwbnewu1aY1g==", + "dependencies": { + "hast-util-select": "~5.0.1", + "unified": "~10.1.1", + "unist-util-visit": "~4.1.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/rehype-slug": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/rehype-slug/-/rehype-slug-5.0.1.tgz", + "integrity": "sha512-X5v3wV/meuOX9NFcGhJvUpEjIvQl2gDvjg3z40RVprYFt7q3th4qMmYLULiu3gXvbNX1ppx+oaa6JyY1W67pTA==", + "dependencies": { + "@types/hast": "^2.0.0", + "github-slugger": "^1.1.1", + "hast-util-has-property": "^2.0.0", + "hast-util-heading-rank": "^2.0.0", + "hast-util-to-string": "^2.0.0", + "unified": "^10.0.0", + "unist-util-visit": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-stringify": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/rehype-stringify/-/rehype-stringify-9.0.3.tgz", + "integrity": "sha512-kWiZ1bgyWlgOxpqD5HnxShKAdXtb2IUljn3hQAhySeak6IOQPPt6DeGnsIh4ixm7yKJWzm8TXFuC/lPfcWHJqw==", + "dependencies": { + "@types/hast": "^2.0.0", + "hast-util-to-html": "^8.0.0", + "unified": "^10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/relateurl": { "version": "0.2.7", "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", @@ -25665,6 +26218,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/stringify-entities": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.3.tgz", + "integrity": "sha512-BP9nNHMhhfcMbiuQKCqMjhDP5yBCAxsPu4pHFFzJ6Alo9dZgY4VLDPutXqIjpRiMoKdp7Av85Gr73Q5uH9k7+g==", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/stringify-object": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-3.3.0.tgz", @@ -27631,6 +28197,16 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/unist-util-filter": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-filter/-/unist-util-filter-4.0.0.tgz", + "integrity": "sha512-H4iTOv2p+n83xjhx7eGFA3zSx7Xcv3Iv9lNQRpXiR8dmm9LtslhyjVlQrZLbkk4jwUrJgc8PPGkOOrfhb76s4Q==", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0", + "unist-util-visit-parents": "^5.0.0" + } + }, "node_modules/unist-util-generated": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/unist-util-generated/-/unist-util-generated-2.0.0.tgz", @@ -28210,6 +28786,19 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/vfile-location": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-4.0.1.tgz", + "integrity": "sha512-JDxPlTbZrZCQXogGheBHjbRWjESSPEak770XwWPfw5mTc1v1nWGLB/apzZxsx8a0SJVfF8HK8ql8RD308vXRUw==", + "dependencies": { + "@types/unist": "^2.0.0", + "vfile": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/vfile-message": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.2.tgz", @@ -28571,6 +29160,15 @@ "minimalistic-assert": "^1.0.0" } }, + "node_modules/web-namespaces": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", + "integrity": "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/web-vitals": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-3.0.2.tgz", @@ -33566,6 +34164,11 @@ "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.0.tgz", "integrity": "sha512-RI1L7N4JnW5gQw2spvL7Sllfuf1SaHdrZpCHiBlCXjIlufi1SMNnbu2teze3/QE67Fg2tBlH7W+mi4hVNk4p0A==" }, + "@types/prismjs": { + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.0.tgz", + "integrity": "sha512-ZTaqn/qSqUuAq1YwvOFQfVW1AR/oQJlLSZVustdjwI+GZ8kr0MSHBj0tsXPW1EqHubx50gtBEjbPGsdZwQwCjQ==" + }, "@types/prop-types": { "version": "15.7.5", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", @@ -33931,6 +34534,40 @@ "eslint-visitor-keys": "^3.3.0" } }, + "@uiw/copy-to-clipboard": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@uiw/copy-to-clipboard/-/copy-to-clipboard-1.0.12.tgz", + "integrity": "sha512-3tt7FVSbjtBCNBhffy7k26rpnEmk8GQj9QkTGZBIfpHU7mG3Buryt69u6xooYM7/gmv7GIqD4QxxIauIp2HHKg==" + }, + "@uiw/react-markdown-preview": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@uiw/react-markdown-preview/-/react-markdown-preview-4.1.5.tgz", + "integrity": "sha512-CYiwzrarfg/jCsSHSfLkgxpLiqwZjmXhMJmwMa2SCct/PX/vt9wJJ84aNgUVbSjfQFKgmTlSWpDiN+kGnf8D/w==", + "requires": { + "@babel/runtime": "^7.17.2", + "@uiw/copy-to-clipboard": "~1.0.12", + "react-markdown": "~8.0.0", + "rehype-attr": "~2.0.7", + "rehype-autolink-headings": "~6.1.1", + "rehype-ignore": "^1.0.1", + "rehype-prism-plus": "~1.5.0", + "rehype-raw": "^6.1.1", + "rehype-rewrite": "~3.0.6", + "rehype-slug": "~5.0.1", + "remark-gfm": "~3.0.1", + "unist-util-visit": "^4.1.0" + } + }, + "@uiw/react-md-editor": { + "version": "3.19.5", + "resolved": "https://registry.npmjs.org/@uiw/react-md-editor/-/react-md-editor-3.19.5.tgz", + "integrity": "sha512-uhFGLOrKEtADM8QUauTdG5x8wqNS15ry2jCYMBr/E1Or3njvg7jpB0KRv+QTgZDnglevCTjuQxZPH7I7hG2uKw==", + "requires": { + "@babel/runtime": "^7.14.6", + "@uiw/react-markdown-preview": "^4.1.5", + "rehype": "~12.0.1" + } + }, "@webassemblyjs/ast": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.9.0.tgz", @@ -34868,6 +35505,11 @@ "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==" }, + "bcp-47-match": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bcp-47-match/-/bcp-47-match-2.0.2.tgz", + "integrity": "sha512-zy5swVXwQ25ttElhoN9Dgnqm6VFlMkeDNljvHSGqGNr4zClUosdFzxD+fQHJVmx3g3KY+r//wV/fmBHsa1ErnA==" + }, "bcrypt-pbkdf": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", @@ -35113,8 +35755,8 @@ } }, "bpmn-js-spiffworkflow": { - "version": "git+ssh://git@github.com/sartography/bpmn-js-spiffworkflow.git#999ea7e4c0e5157546625fbd7b1eb5cc8a79c0c0", - "from": "bpmn-js-spiffworkflow@sartography/bpmn-js-spiffworkflow#main", + "version": "git+ssh://git@github.com/sartography/bpmn-js-spiffworkflow.git#84593aee1ead7328efdc7da03ab3c9cd34364496", + "from": "bpmn-js-spiffworkflow@sartography/bpmn-js-spiffworkflow#feature/more_launch_buttons_and_dropdowns", "requires": { "inherits": "^2.0.4", "inherits-browser": "^0.0.1", @@ -35521,6 +36163,21 @@ "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==" }, + "character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==" + }, + "character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==" + }, + "character-reference-invalid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==" + }, "check-more-types": { "version": "2.24.0", "resolved": "https://registry.npmjs.org/check-more-types/-/check-more-types-2.24.0.tgz", @@ -36339,6 +36996,11 @@ "resolved": "https://registry.npmjs.org/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz", "integrity": "sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w==" }, + "css-selector-parser": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/css-selector-parser/-/css-selector-parser-1.4.1.tgz", + "integrity": "sha512-HYPSb7y/Z7BNDCOrakL4raGO2zltZkbeXyAd6Tg9obzix6QhzxCotdBl6VT0Dv4vZfJGVz3WL/xaEI9Ly3ul0g==" + }, "css-tree": { "version": "1.0.0-alpha.37", "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.37.tgz", @@ -36930,6 +37592,11 @@ "path-type": "^4.0.0" } }, + "direction": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/direction/-/direction-2.0.1.tgz", + "integrity": "sha512-9S6m9Sukh1cZNknO1CWAr2QAWsbKLafQiyM5gZ7VgXHeuaoUwffKN4q6NC4A/Mf9iiPlOXQEKW/Mv/mh9/3YFA==" + }, "dlv": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", @@ -38828,6 +39495,11 @@ "assert-plus": "^1.0.0" } }, + "github-slugger": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-1.5.0.tgz", + "integrity": "sha512-wIh+gKBI9Nshz2o46B0B3f5k/W+WI9ZAv6y5Dn5WJ5SK1t0TnDimB4WE5rmTD05ZAIn8HALCZVmCsvj0w0v0lw==" + }, "glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -39069,11 +39741,171 @@ "minimalistic-assert": "^1.0.1" } }, + "hast-to-hyperscript": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/hast-to-hyperscript/-/hast-to-hyperscript-10.0.1.tgz", + "integrity": "sha512-dhIVGoKCQVewFi+vz3Vt567E4ejMppS1haBRL6TEmeLeJVB1i/FJIIg/e6s1Bwn0g5qtYojHEKvyGA+OZuyifw==", + "requires": { + "@types/unist": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "property-information": "^6.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-object": "^0.3.0", + "unist-util-is": "^5.0.0", + "web-namespaces": "^2.0.0" + } + }, + "hast-util-from-parse5": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-7.1.0.tgz", + "integrity": "sha512-m8yhANIAccpU4K6+121KpPP55sSl9/samzQSQGpb0mTExcNh2WlvjtMwSWFhg6uqD4Rr6Nfa8N6TMypQM51rzQ==", + "requires": { + "@types/hast": "^2.0.0", + "@types/parse5": "^6.0.0", + "@types/unist": "^2.0.0", + "hastscript": "^7.0.0", + "property-information": "^6.0.0", + "vfile": "^5.0.0", + "vfile-location": "^4.0.0", + "web-namespaces": "^2.0.0" + } + }, + "hast-util-has-property": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hast-util-has-property/-/hast-util-has-property-2.0.0.tgz", + "integrity": "sha512-4Qf++8o5v14us4Muv3HRj+Er6wTNGA/N9uCaZMty4JWvyFKLdhULrv4KE1b65AthsSO9TXSZnjuxS8ecIyhb0w==" + }, + "hast-util-heading-rank": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/hast-util-heading-rank/-/hast-util-heading-rank-2.1.0.tgz", + "integrity": "sha512-w+Rw20Q/iWp2Bcnr6uTrYU6/ftZLbHKhvc8nM26VIWpDqDMlku2iXUVTeOlsdoih/UKQhY7PHQ+vZ0Aqq8bxtQ==", + "requires": { + "@types/hast": "^2.0.0" + } + }, + "hast-util-is-element": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-2.1.2.tgz", + "integrity": "sha512-thjnlGAnwP8ef/GSO1Q8BfVk2gundnc2peGQqEg2kUt/IqesiGg/5mSwN2fE7nLzy61pg88NG6xV+UrGOrx9EA==", + "requires": { + "@types/hast": "^2.0.0", + "@types/unist": "^2.0.0" + } + }, + "hast-util-parse-selector": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-3.1.0.tgz", + "integrity": "sha512-AyjlI2pTAZEOeu7GeBPZhROx0RHBnydkQIXlhnFzDi0qfXTmGUWoCYZtomHbrdrheV4VFUlPcfJ6LMF5T6sQzg==", + "requires": { + "@types/hast": "^2.0.0" + } + }, + "hast-util-raw": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-7.2.2.tgz", + "integrity": "sha512-0x3BhhdlBcqRIKyc095lBSDvmQNMY3Eulj2PLsT5XCyKYrxssI5yr3P4Kv/PBo1s/DMkZy2voGkMXECnFCZRLQ==", + "requires": { + "@types/hast": "^2.0.0", + "@types/parse5": "^6.0.0", + "hast-util-from-parse5": "^7.0.0", + "hast-util-to-parse5": "^7.0.0", + "html-void-elements": "^2.0.0", + "parse5": "^6.0.0", + "unist-util-position": "^4.0.0", + "unist-util-visit": "^4.0.0", + "vfile": "^5.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" + } + }, + "hast-util-select": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/hast-util-select/-/hast-util-select-5.0.2.tgz", + "integrity": "sha512-QGN5o7N8gq1BhUX96ApLE8izOXlf+IPkOVGXcp9Dskdd3w0OqZrn6faPAmS0/oVogwJOd0lWFSYmBK75e+030g==", + "requires": { + "@types/hast": "^2.0.0", + "@types/unist": "^2.0.0", + "bcp-47-match": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "css-selector-parser": "^1.0.0", + "direction": "^2.0.0", + "hast-util-has-property": "^2.0.0", + "hast-util-is-element": "^2.0.0", + "hast-util-to-string": "^2.0.0", + "hast-util-whitespace": "^2.0.0", + "not": "^0.1.0", + "nth-check": "^2.0.0", + "property-information": "^6.0.0", + "space-separated-tokens": "^2.0.0", + "unist-util-visit": "^4.0.0", + "zwitch": "^2.0.0" + }, + "dependencies": { + "nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "requires": { + "boolbase": "^1.0.0" + } + } + } + }, + "hast-util-to-html": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-8.0.3.tgz", + "integrity": "sha512-/D/E5ymdPYhHpPkuTHOUkSatxr4w1ZKrZsG0Zv/3C2SRVT0JFJG53VS45AMrBtYk0wp5A7ksEhiC8QaOZM95+A==", + "requires": { + "@types/hast": "^2.0.0", + "ccount": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-is-element": "^2.0.0", + "hast-util-whitespace": "^2.0.0", + "html-void-elements": "^2.0.0", + "property-information": "^6.0.0", + "space-separated-tokens": "^2.0.0", + "stringify-entities": "^4.0.2", + "unist-util-is": "^5.0.0" + } + }, + "hast-util-to-parse5": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-7.0.0.tgz", + "integrity": "sha512-YHiS6aTaZ3N0Q3nxaY/Tj98D6kM8QX5Q8xqgg8G45zR7PvWnPGPP0vcKCgb/moIydEJ/QWczVrX0JODCVeoV7A==", + "requires": { + "@types/hast": "^2.0.0", + "@types/parse5": "^6.0.0", + "hast-to-hyperscript": "^10.0.0", + "property-information": "^6.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" + } + }, + "hast-util-to-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hast-util-to-string/-/hast-util-to-string-2.0.0.tgz", + "integrity": "sha512-02AQ3vLhuH3FisaMM+i/9sm4OXGSq1UhOOCpTLLQtHdL3tZt7qil69r8M8iDkZYyC0HCFylcYoP+8IO7ddta1A==", + "requires": { + "@types/hast": "^2.0.0" + } + }, "hast-util-whitespace": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-2.0.0.tgz", "integrity": "sha512-Pkw+xBHuV6xFeJprJe2BBEoDV+AvQySaz3pPDRUs5PNZEMQjpXJJueqrpcHIXxnWTcAGi/UOCgVShlkY6kLoqg==" }, + "hastscript": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-7.1.0.tgz", + "integrity": "sha512-uBjaTTLN0MkCZxY/R2fWUOcu7FRtUVzKRO5P/RAfgsu3yFiMB1JWCO4AjeVkgHxAira1f2UecHK5WfS9QurlWA==", + "requires": { + "@types/hast": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-parse-selector": "^3.0.0", + "property-information": "^6.0.0", + "space-separated-tokens": "^2.0.0" + } + }, "hat": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/hat/-/hat-0.0.3.tgz", @@ -39183,6 +40015,11 @@ } } }, + "html-void-elements": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-2.0.1.tgz", + "integrity": "sha512-0quDb7s97CfemeJAnW9wC0hw78MtW7NU3hqtCD75g2vFlDLt36llsYD7uB7SUzojLMP24N5IatXf7ylGXiGG9A==" + }, "htmlparser2": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", @@ -39488,6 +40325,20 @@ "kind-of": "^6.0.0" } }, + "is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==" + }, + "is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "requires": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + } + }, "is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", @@ -39561,6 +40412,11 @@ "has-tostringtag": "^1.0.0" } }, + "is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==" + }, "is-descriptor": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", @@ -39607,6 +40463,11 @@ "is-extglob": "^2.1.1" } }, + "is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==" + }, "is-in-browser": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/is-in-browser/-/is-in-browser-1.1.3.tgz", @@ -44591,6 +45452,11 @@ "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==" }, + "not": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/not/-/not-0.1.0.tgz", + "integrity": "sha512-5PDmaAsVfnWUgTUbJ3ERwn7u79Z0dYxN9ErxCpVJJqe2RK0PJ3z+iFUxuqjwtlDDegXvtWoxD/3Fzxox7tFGWA==" + }, "npm-run-path": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", @@ -44931,6 +45797,21 @@ "safe-buffer": "^5.1.1" } }, + "parse-entities": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.0.tgz", + "integrity": "sha512-5nk9Fn03x3rEhGaX1FU6IDwG/k+GxLXlFAkgrbM1asuAFl3BhdQWvASaIsmwWypRNcZKHPYnIuOSfIWEyEQnPQ==", + "requires": { + "@types/unist": "^2.0.0", + "character-entities": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + } + }, "parse-json": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", @@ -44948,6 +45829,11 @@ "integrity": "sha512-kHt7kzLoS9VBZfUsiKjv43mr91ea+U05EyKkEtqp7vNbHxmaVuEqN7XxeEVnGrMtYOAxGrDElSi96K7EgO1zCA==", "dev": true }, + "parse-numeric-range": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/parse-numeric-range/-/parse-numeric-range-1.3.0.tgz", + "integrity": "sha512-twN+njEipszzlMJd4ONUYgSfZPDxgHhT9Ahed5uTigpQn90FggW4SA/AIPq/6a149fTbE9qBEcSwE3FAEp6wQQ==" + }, "parse5": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", @@ -47179,6 +48065,17 @@ "@babel/runtime": "^7.9.2" } }, + "refractor": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/refractor/-/refractor-4.8.0.tgz", + "integrity": "sha512-SVOnWUJiEBFNiBlHbudSpSpDfDhDY1UHF0CMKgdvPsMYNQQ4rnqFxyGvP07UmteNC8V12mTF2c0lEsGS7lKaGw==", + "requires": { + "@types/hast": "^2.0.0", + "@types/prismjs": "^1.0.0", + "hastscript": "^7.0.0", + "parse-entities": "^4.0.0" + } + }, "regenerate": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", @@ -47300,6 +48197,118 @@ } } }, + "rehype": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/rehype/-/rehype-12.0.1.tgz", + "integrity": "sha512-ey6kAqwLM3X6QnMDILJthGvG1m1ULROS9NT4uG9IDCuv08SFyLlreSuvOa//DgEvbXx62DS6elGVqusWhRUbgw==", + "requires": { + "@types/hast": "^2.0.0", + "rehype-parse": "^8.0.0", + "rehype-stringify": "^9.0.0", + "unified": "^10.0.0" + } + }, + "rehype-attr": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/rehype-attr/-/rehype-attr-2.0.8.tgz", + "integrity": "sha512-EISuHQNPUN2InK+R1tmiAp3IxvNw4utF3MY6+6QoltAXwSqgvC3RVhfaOVUxyakTb4KJmfii3s0b/a7DlQYZTg==", + "requires": { + "unified": "~10.1.1", + "unist-util-visit": "~4.1.0" + } + }, + "rehype-autolink-headings": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/rehype-autolink-headings/-/rehype-autolink-headings-6.1.1.tgz", + "integrity": "sha512-NMYzZIsHM3sA14nC5rAFuUPIOfg+DFmf9EY1YMhaNlB7+3kK/ZlE6kqPfuxr1tsJ1XWkTrMtMoyHosU70d35mA==", + "requires": { + "@types/hast": "^2.0.0", + "extend": "^3.0.0", + "hast-util-has-property": "^2.0.0", + "hast-util-heading-rank": "^2.0.0", + "hast-util-is-element": "^2.0.0", + "unified": "^10.0.0", + "unist-util-visit": "^4.0.0" + } + }, + "rehype-ignore": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/rehype-ignore/-/rehype-ignore-1.0.3.tgz", + "integrity": "sha512-N8eAcq9+QUFhTZqB4oxJ8WL1D2EWp+Jr+j6DKlV21j8+TBLkPdotWNPSzo4FNJbIy6SWxjS/dgDpHmJm8YD6XQ==", + "requires": { + "hast-util-select": "~5.0.1", + "unified": "~10.1.2", + "unist-util-visit": "~4.1.0" + } + }, + "rehype-parse": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/rehype-parse/-/rehype-parse-8.0.4.tgz", + "integrity": "sha512-MJJKONunHjoTh4kc3dsM1v3C9kGrrxvA3U8PxZlP2SjH8RNUSrb+lF7Y0KVaUDnGH2QZ5vAn7ulkiajM9ifuqg==", + "requires": { + "@types/hast": "^2.0.0", + "hast-util-from-parse5": "^7.0.0", + "parse5": "^6.0.0", + "unified": "^10.0.0" + } + }, + "rehype-prism-plus": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/rehype-prism-plus/-/rehype-prism-plus-1.5.0.tgz", + "integrity": "sha512-KNJYMQHqN+53ZbT5Pa/lO7uorMpBIR3x9RjFeG1lPlQherZDZiPqyOFS464L4BniZ4VG5PnG5DXVqjGtwxWJew==", + "requires": { + "hast-util-to-string": "^2.0.0", + "parse-numeric-range": "^1.3.0", + "refractor": "^4.7.0", + "rehype-parse": "^8.0.2", + "unist-util-filter": "^4.0.0", + "unist-util-visit": "^4.0.0" + } + }, + "rehype-raw": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/rehype-raw/-/rehype-raw-6.1.1.tgz", + "integrity": "sha512-d6AKtisSRtDRX4aSPsJGTfnzrX2ZkHQLE5kiUuGOeEoLpbEulFF4hj0mLPbsa+7vmguDKOVVEQdHKDSwoaIDsQ==", + "requires": { + "@types/hast": "^2.0.0", + "hast-util-raw": "^7.2.0", + "unified": "^10.0.0" + } + }, + "rehype-rewrite": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/rehype-rewrite/-/rehype-rewrite-3.0.6.tgz", + "integrity": "sha512-REDTNCvsKcAazy8IQWzKp66AhSUDSOIKssSCqNqCcT9sN7JCwAAm3mWGTUdUzq80ABuy8d0D6RBwbnewu1aY1g==", + "requires": { + "hast-util-select": "~5.0.1", + "unified": "~10.1.1", + "unist-util-visit": "~4.1.0" + } + }, + "rehype-slug": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/rehype-slug/-/rehype-slug-5.0.1.tgz", + "integrity": "sha512-X5v3wV/meuOX9NFcGhJvUpEjIvQl2gDvjg3z40RVprYFt7q3th4qMmYLULiu3gXvbNX1ppx+oaa6JyY1W67pTA==", + "requires": { + "@types/hast": "^2.0.0", + "github-slugger": "^1.1.1", + "hast-util-has-property": "^2.0.0", + "hast-util-heading-rank": "^2.0.0", + "hast-util-to-string": "^2.0.0", + "unified": "^10.0.0", + "unist-util-visit": "^4.0.0" + } + }, + "rehype-stringify": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/rehype-stringify/-/rehype-stringify-9.0.3.tgz", + "integrity": "sha512-kWiZ1bgyWlgOxpqD5HnxShKAdXtb2IUljn3hQAhySeak6IOQPPt6DeGnsIh4ixm7yKJWzm8TXFuC/lPfcWHJqw==", + "requires": { + "@types/hast": "^2.0.0", + "hast-util-to-html": "^8.0.0", + "unified": "^10.0.0" + } + }, "relateurl": { "version": "0.2.7", "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", @@ -48658,6 +49667,15 @@ "es-abstract": "^1.19.5" } }, + "stringify-entities": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.3.tgz", + "integrity": "sha512-BP9nNHMhhfcMbiuQKCqMjhDP5yBCAxsPu4pHFFzJ6Alo9dZgY4VLDPutXqIjpRiMoKdp7Av85Gr73Q5uH9k7+g==", + "requires": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + } + }, "stringify-object": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-3.3.0.tgz", @@ -50160,6 +51178,16 @@ "@types/unist": "^2.0.0" } }, + "unist-util-filter": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-filter/-/unist-util-filter-4.0.0.tgz", + "integrity": "sha512-H4iTOv2p+n83xjhx7eGFA3zSx7Xcv3Iv9lNQRpXiR8dmm9LtslhyjVlQrZLbkk4jwUrJgc8PPGkOOrfhb76s4Q==", + "requires": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0", + "unist-util-visit-parents": "^5.0.0" + } + }, "unist-util-generated": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/unist-util-generated/-/unist-util-generated-2.0.0.tgz", @@ -50614,6 +51642,15 @@ } } }, + "vfile-location": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-4.0.1.tgz", + "integrity": "sha512-JDxPlTbZrZCQXogGheBHjbRWjESSPEak770XwWPfw5mTc1v1nWGLB/apzZxsx8a0SJVfF8HK8ql8RD308vXRUw==", + "requires": { + "@types/unist": "^2.0.0", + "vfile": "^5.0.0" + } + }, "vfile-message": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.2.tgz", @@ -50903,6 +51940,11 @@ "minimalistic-assert": "^1.0.0" } }, + "web-namespaces": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", + "integrity": "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==" + }, "web-vitals": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-3.0.2.tgz", diff --git a/spiffworkflow-frontend/package.json b/spiffworkflow-frontend/package.json index c9127d7c..37f9e345 100644 --- a/spiffworkflow-frontend/package.json +++ b/spiffworkflow-frontend/package.json @@ -20,12 +20,13 @@ "@types/node": "^18.6.5", "@types/react": "^18.0.17", "@types/react-dom": "^18.0.6", + "@uiw/react-md-editor": "^3.19.5", "autoprefixer": "10.4.8", "axios": "^0.27.2", "bootstrap": "^5.2.0", "bpmn-js": "^9.3.2", "bpmn-js-properties-panel": "^1.10.0", - "bpmn-js-spiffworkflow": "sartography/bpmn-js-spiffworkflow#main", + "bpmn-js-spiffworkflow": "sartography/bpmn-js-spiffworkflow#feature/more_launch_buttons_and_dropdowns", "craco": "^0.0.3", "date-fns": "^2.28.0", "diagram-js": "^8.5.0", diff --git a/spiffworkflow-frontend/src/components/ButtonWithConfirmation.tsx b/spiffworkflow-frontend/src/components/ButtonWithConfirmation.tsx index 25aaadc1..88386919 100644 --- a/spiffworkflow-frontend/src/components/ButtonWithConfirmation.tsx +++ b/spiffworkflow-frontend/src/components/ButtonWithConfirmation.tsx @@ -4,10 +4,14 @@ import { Button, Modal } from '@carbon/react'; type OwnProps = { description?: string; - buttonLabel: string; + buttonLabel?: string; onConfirmation: (..._args: any[]) => any; title?: string; confirmButtonLabel?: string; + kind?: string; + renderIcon?: boolean; + iconDescription?: string | null; + hasIconOnly?: boolean; }; export default function ButtonWithConfirmation({ @@ -16,6 +20,10 @@ export default function ButtonWithConfirmation({ onConfirmation, title = 'Are you sure?', confirmButtonLabel = 'OK', + kind = 'danger', + renderIcon = false, + iconDescription = null, + hasIconOnly = false, }: OwnProps) { const [showConfirmationPrompt, setShowConfirmationPrompt] = useState(false); @@ -49,7 +57,13 @@ export default function ButtonWithConfirmation({ return ( <> - {confirmationDialog()} diff --git a/spiffworkflow-frontend/src/components/ProcessBreadcrumb.tsx b/spiffworkflow-frontend/src/components/ProcessBreadcrumb.tsx index 241aaeb7..d5d2eda2 100644 --- a/spiffworkflow-frontend/src/components/ProcessBreadcrumb.tsx +++ b/spiffworkflow-frontend/src/components/ProcessBreadcrumb.tsx @@ -1,12 +1,12 @@ -import { Link } from 'react-router-dom'; -import Breadcrumb from 'react-bootstrap/Breadcrumb'; -import { BreadcrumbItem } from '../interfaces'; +// @ts-ignore +import { Breadcrumb, BreadcrumbItem } from '@carbon/react'; +import { HotCrumbItem } from '../interfaces'; type OwnProps = { processModelId?: string; processGroupId?: string; linkProcessModel?: boolean; - hotCrumbs?: BreadcrumbItem[]; + hotCrumbs?: HotCrumbItem[]; }; export default function ProcessBreadcrumb({ @@ -22,18 +22,20 @@ export default function ProcessBreadcrumb({ if (lastItem === undefined) { return null; } - const lastCrumb = {lastItem[0]}; - const leadingCrumbLinks = hotCrumbs.map((crumb) => { + const lastCrumb = ( + {lastItem[0]} + ); + const leadingCrumbLinks = hotCrumbs.map((crumb: any) => { const valueLabel = crumb[0]; const url = crumb[1]; return ( - + {valueLabel} - + ); }); return ( - + {leadingCrumbLinks} {lastCrumb} @@ -42,42 +44,38 @@ export default function ProcessBreadcrumb({ if (processModelId) { if (linkProcessModel) { processModelBreadcrumb = ( - - Process Model: {processModelId} - + {`Process Model: ${processModelId}`} + ); } else { processModelBreadcrumb = ( - - Process Model: {processModelId} - + + {`Process Model: ${processModelId}`} + ); } processGroupBreadcrumb = ( - - Process Group: {processGroupId} - + {`Process Group: ${processGroupId}`} + ); } else if (processGroupId) { processGroupBreadcrumb = ( - Process Group: {processGroupId} + + {`Process Group: ${processGroupId}`} + ); } return ( - - - Process Groups - + + Process Groups {processGroupBreadcrumb} {processModelBreadcrumb} diff --git a/spiffworkflow-frontend/src/components/ReactDiagramEditor.tsx b/spiffworkflow-frontend/src/components/ReactDiagramEditor.tsx index 8ba2bd89..e9ddb445 100644 --- a/spiffworkflow-frontend/src/components/ReactDiagramEditor.tsx +++ b/spiffworkflow-frontend/src/components/ReactDiagramEditor.tsx @@ -68,8 +68,14 @@ type OwnProps = { diagramXML?: string | null; fileName?: string; onLaunchScriptEditor?: (..._args: any[]) => any; + onLaunchMarkdownEditor?: (..._args: any[]) => any; + onLaunchBpmnEditor?: (..._args: any[]) => any; + onLaunchJsonEditor?: (..._args: any[]) => any; + onLaunchDmnEditor?: (..._args: any[]) => any; onElementClick?: (..._args: any[]) => any; onServiceTasksRequested?: (..._args: any[]) => any; + onJsonFilesRequested?: (..._args: any[]) => any; + onDmnFilesRequested?: (..._args: any[]) => any; url?: string; }; @@ -85,8 +91,14 @@ export default function ReactDiagramEditor({ diagramXML, fileName, onLaunchScriptEditor, + onLaunchMarkdownEditor, + onLaunchBpmnEditor, + onLaunchJsonEditor, + onLaunchDmnEditor, onElementClick, onServiceTasksRequested, + onJsonFilesRequested, + onDmnFilesRequested, url, }: OwnProps) { const [diagramXMLString, setDiagramXMLString] = useState(''); @@ -189,6 +201,17 @@ export default function ReactDiagramEditor({ } } + function handleLaunchMarkdownEditor( + element: any, + value: string, + eventBus: any + ) { + if (onLaunchMarkdownEditor) { + setPerformingXmlUpdates(true); + onLaunchMarkdownEditor(element, value, eventBus); + } + } + function handleElementClick(event: any) { if (onElementClick) { onElementClick(event.element); @@ -203,7 +226,7 @@ export default function ReactDiagramEditor({ setDiagramModelerState(diagramModeler); - diagramModeler.on('script.editor.launch', (event: any) => { + diagramModeler.on('spiff.script.edit', (event: any) => { const { error, element, scriptType, script, eventBus } = event; if (error) { console.log(error); @@ -211,6 +234,35 @@ export default function ReactDiagramEditor({ handleLaunchScriptEditor(element, script, scriptType, eventBus); }); + diagramModeler.on('spiff.markdown.edit', (event: any) => { + const { error, element, value, eventBus } = event; + if (error) { + console.log(error); + } + handleLaunchMarkdownEditor(element, value, eventBus); + }); + + /** + * fixme: this is not in use yet, we need the ability to find bpmn files by id. + */ + diagramModeler.on('spiff.callactivity.edit', (event: any) => { + if (onLaunchBpmnEditor) { + onLaunchBpmnEditor(event.processId); + } + }); + + diagramModeler.on('spiff.file.edit', (event: any) => { + if (onLaunchJsonEditor) { + onLaunchJsonEditor(event.value); + } + }); + + diagramModeler.on('spiff.dmn.edit', (event: any) => { + if (onLaunchDmnEditor) { + onLaunchDmnEditor(event.value); + } + }); + // 'element.hover', // 'element.out', // 'element.click', @@ -224,12 +276,34 @@ export default function ReactDiagramEditor({ diagramModeler.on('spiff.service_tasks.requested', (event: any) => { handleServiceTasksRequested(event); }); + + diagramModeler.on('spiff.json_files.requested', (event: any) => { + if (onJsonFilesRequested) { + onJsonFilesRequested(event); + } + }); + + diagramModeler.on('spiff.dmn_files.requested', (event: any) => { + if (onDmnFilesRequested) { + onDmnFilesRequested(event); + } + }); + + diagramModeler.on('spiff.json_files.requested', (event: any) => { + handleServiceTasksRequested(event); + }); }, [ diagramModelerState, diagramType, onLaunchScriptEditor, + onLaunchMarkdownEditor, + onLaunchBpmnEditor, + onLaunchDmnEditor, + onLaunchJsonEditor, onElementClick, onServiceTasksRequested, + onJsonFilesRequested, + onDmnFilesRequested, ]); useEffect(() => { diff --git a/spiffworkflow-frontend/src/index.css b/spiffworkflow-frontend/src/index.css index 988fae27..c9ddd249 100644 --- a/spiffworkflow-frontend/src/index.css +++ b/spiffworkflow-frontend/src/index.css @@ -40,6 +40,10 @@ span.bjs-crumb { opacity: .4; } +.accordion-item-label { + vertical-align: middle; +} + .diagram-editor-canvas { border:1px solid #000000; height:70vh; diff --git a/spiffworkflow-frontend/src/index.scss b/spiffworkflow-frontend/src/index.scss index ee9ca2c2..2adade11 100644 --- a/spiffworkflow-frontend/src/index.scss +++ b/spiffworkflow-frontend/src/index.scss @@ -1,10 +1,22 @@ // @use '@carbon/react/scss/themes'; // @use '@carbon/react/scss/theme' with ($theme: themes.$g100); + +// @use '@carbon/react/scss/theme' with +// ( +// $theme: ( +// cds-link-primary: #525252 +// ) +// ); + @use '@carbon/react'; @use '@carbon/styles'; // @include grid.flex-grid(); +@use '@carbon/colors'; // @use '@carbon/react/scss/colors'; +@use '@carbon/react/scss/themes'; + +// var(--cds-link-text-color, var(--cds-link-primary, #0f62fe)) // site is mainly using white theme. // header is mainly using g100 @@ -13,3 +25,60 @@ // background-color: colors.$gray-100; color: white; } + +.cds--breadcrumb-item a.cds--link:hover { + color: #525252; +} +.cds--breadcrumb-item a.cds--link:visited { + color: #525252; +} +.cds--breadcrumb-item a.cds--link:visited:hover { + color: #525252; +} +.cds--breadcrumb-item a.cds--link { + color: #525252; +} + +.cds--btn--ghost { + color: black; +} +.cds--btn--ghost:visited { + color: black; +} +.cds--btn--ghost:hover { + color: black; +} +.cds--btn--ghost:visited:hover { + color: black; +} + +$slightly-lighter-gray: #474747; +$spiff-header-background-color: #161616; + +.cds--header__global .cds--btn--primary { + background-color: $spiff-header-background-color; +} +.cds--btn--primary { + background-color: #393939; +} +.cds--btn--primary:hover { + background-color: $slightly-lighter-gray; +} +// .cds--btn--ghost:visited { +// color: black; +// } +// .cds--btn--ghost:hover { +// color: black; +// } +// .cds--btn--ghost:visited:hover { +// color: black; +// } + + +// :root { +// --cds-link-primary: #525252; +// } +// .card { +// background: var(--orange); +// --orange: hsl(255, 72%, var(--lightness)); +// } diff --git a/spiffworkflow-frontend/src/interfaces.ts b/spiffworkflow-frontend/src/interfaces.ts index 14ead3ba..342785ec 100644 --- a/spiffworkflow-frontend/src/interfaces.ts +++ b/spiffworkflow-frontend/src/interfaces.ts @@ -17,15 +17,35 @@ export interface ProcessGroup { description?: string | null; } +export interface ProcessFileReference { + id: string; // The unique id of the process or decision table. + name: string; // The process or decision table name. + type: string; // either "decision" or "process" +} + +export interface ProcessFile { + content_type: string; + last_modified: string; + name: string; + process_group_id: string; + process_model_id: string; + references: ProcessFileReference[]; + size: number; + type: string; + file_contents?: string; +} + export interface ProcessModel { id: string; + description: string; process_group_id: string; display_name: string; primary_file_name: string; + files: ProcessFile[]; } // tuple of display value and URL -export type BreadcrumbItem = [displayValue: string, url?: string]; +export type HotCrumbItem = [displayValue: string, url?: string]; export interface ErrorForDisplay { message: string; diff --git a/spiffworkflow-frontend/src/routes/ProcessInstanceList.tsx b/spiffworkflow-frontend/src/routes/ProcessInstanceList.tsx index bec73f11..a267a359 100644 --- a/spiffworkflow-frontend/src/routes/ProcessInstanceList.tsx +++ b/spiffworkflow-frontend/src/routes/ProcessInstanceList.tsx @@ -51,6 +51,7 @@ export default function ProcessInstanceList() { const navigate = useNavigate(); const [processInstances, setProcessInstances] = useState([]); + const [reportMetadata, setReportMetadata] = useState({}); const [pagination, setPagination] = useState(null); const oneHourInSeconds = 3600; @@ -97,6 +98,7 @@ export default function ProcessInstanceList() { function setProcessInstancesFromResult(result: any) { const processInstancesFromApi = result.results; setProcessInstances(processInstancesFromApi); + setReportMetadata(result.report_metadata); setPagination(result.pagination); } function getProcessInstances() { @@ -380,53 +382,79 @@ export default function ProcessInstanceList() { }; const buildTable = () => { - const rows = processInstances.map((row: any) => { - const formattedStartDate = - convertSecondsToFormattedDate(row.start_in_seconds) || '-'; - const formattedEndDate = - convertSecondsToFormattedDate(row.end_in_seconds) || '-'; + const headerLabels: Record = { + id: 'Process Instance Id', + process_group_identifier: 'Process Group', + process_model_identifier: 'Process Model', + start_in_seconds: 'Start Time', + end_in_seconds: 'End Time', + status: 'Status', + spiff_step: 'SpiffWorkflow Step', + }; + const getHeaderLabel = (header: string) => { + return headerLabels[header] ?? header; + }; + const headers = (reportMetadata as any).columns.map((column: any) => { + return {getHeaderLabel((column as any).Header)}; + }); + + const formatProcessInstanceId = (row: any, id: any) => { const modifiedProcessModelId: String = modifyProcessModelPath( (row as any).process_model_identifier ); - const groupId = getGroupFromModifiedModelId(modifiedProcessModelId); - return ( - - - - {row.id} - - - - {groupId} - - - - {modifiedProcessModelId} - - - {formattedStartDate} - {formattedEndDate} - - {row.status} - - + + {id} + ); + }; + const formatProcessGroupIdentifier = (row: any, identifier: any) => { + return ( + {identifier} + ); + }; + const formatProcessModelIdentifier = (row: any, identifier: any) => { + return ( + + {identifier} + + ); + }; + const formatSecondsForDisplay = (row: any, seconds: any) => { + return convertSecondsToFormattedDate(seconds) || '-'; + }; + const defaultFormatter = (row: any, value: any) => { + return value; + }; + + const columnFormatters: Record = { + id: formatProcessInstanceId, + process_group_identifier: formatProcessGroupIdentifier, + process_model_identifier: formatProcessModelIdentifier, + start_in_seconds: formatSecondsForDisplay, + end_in_seconds: formatSecondsForDisplay, + }; + const formattedColumn = (row: any, column: any) => { + const formatter = columnFormatters[column.accessor] ?? defaultFormatter; + const value = row[column.accessor]; + return {formatter(row, value)}; + }; + + const rows = processInstances.map((row) => { + const currentRow = (reportMetadata as any).columns.map((column: any) => { + return formattedColumn(row, column); + }); + return {currentRow}; }); return ( - - - - - - - - + {headers}{rows}
Process Instance IdProcess GroupProcess ModelStart TimeEnd TimeStatus
diff --git a/spiffworkflow-frontend/src/routes/ProcessInstanceReportEdit.tsx b/spiffworkflow-frontend/src/routes/ProcessInstanceReportEdit.tsx index 10ad2d92..bdc04b49 100644 --- a/spiffworkflow-frontend/src/routes/ProcessInstanceReportEdit.tsx +++ b/spiffworkflow-frontend/src/routes/ProcessInstanceReportEdit.tsx @@ -56,7 +56,7 @@ export default function ProcessInstanceReportEdit() { }; function getProcessInstanceReport() { HttpService.makeCallToBackend({ - path: `/process-models/${params.process_group_id}/${params.process_model_id}/process-instances/reports/${params.report_identifier}?per_page=1`, + path: `/process-instances/reports/${params.report_identifier}?per_page=1`, successCallback: processResult, }); } @@ -88,7 +88,7 @@ export default function ProcessInstanceReportEdit() { .filter((n) => n); HttpService.makeCallToBackend({ - path: `/process-models/${params.process_group_id}/${params.process_model_id}/process-instances/reports/${params.report_identifier}`, + path: `/process-instances/reports/${params.report_identifier}`, successCallback: navigateToProcessInstanceReport, httpMethod: 'PUT', postBody: { @@ -103,7 +103,7 @@ export default function ProcessInstanceReportEdit() { const deleteProcessInstanceReport = () => { HttpService.makeCallToBackend({ - path: `/process-models/${params.process_group_id}/${params.process_model_id}/process-instances/reports/${params.report_identifier}`, + path: `/process-instances/reports/${params.report_identifier}`, successCallback: navigateToProcessInstanceReports, httpMethod: 'DELETE', }); diff --git a/spiffworkflow-frontend/src/routes/ProcessInstanceReportList.tsx b/spiffworkflow-frontend/src/routes/ProcessInstanceReportList.tsx index e5f03fb8..e3fae10b 100644 --- a/spiffworkflow-frontend/src/routes/ProcessInstanceReportList.tsx +++ b/spiffworkflow-frontend/src/routes/ProcessInstanceReportList.tsx @@ -15,7 +15,7 @@ export default function ProcessInstanceReportList() { useEffect(() => { HttpService.makeCallToBackend({ - path: `/process-models/${modifiedProcessModelId}/process-instances/reports`, + path: `/process-instances/reports`, successCallback: setProcessInstanceReports, }); }, [params]); diff --git a/spiffworkflow-frontend/src/routes/ProcessInstanceReportNew.tsx b/spiffworkflow-frontend/src/routes/ProcessInstanceReportNew.tsx index 5112617c..16d575fd 100644 --- a/spiffworkflow-frontend/src/routes/ProcessInstanceReportNew.tsx +++ b/spiffworkflow-frontend/src/routes/ProcessInstanceReportNew.tsx @@ -42,7 +42,7 @@ export default function ProcessInstanceReportNew() { .filter((n) => n); HttpService.makeCallToBackend({ - path: `/process-models/${params.process_group_id}/${params.process_model_id}/process-instances/reports`, + path: `/process-instances/reports`, successCallback: navigateToNewProcessInstance, httpMethod: 'POST', postBody: { diff --git a/spiffworkflow-frontend/src/routes/ProcessInstanceReportShow.tsx b/spiffworkflow-frontend/src/routes/ProcessInstanceReportShow.tsx index 1d26e57f..20bfc18f 100644 --- a/spiffworkflow-frontend/src/routes/ProcessInstanceReportShow.tsx +++ b/spiffworkflow-frontend/src/routes/ProcessInstanceReportShow.tsx @@ -39,7 +39,7 @@ export default function ProcessInstanceReport() { } }); HttpService.makeCallToBackend({ - path: `/process-models/${params.process_group_id}/${params.process_model_id}/process-instances/reports/${params.report_identifier}?${query}`, + path: `/process-instances/reports/${params.report_identifier}${query}`, successCallback: processResult, }); } diff --git a/spiffworkflow-frontend/src/routes/ProcessModelEdit.tsx b/spiffworkflow-frontend/src/routes/ProcessModelEdit.tsx index c74c3b69..8ef70b49 100644 --- a/spiffworkflow-frontend/src/routes/ProcessModelEdit.tsx +++ b/spiffworkflow-frontend/src/routes/ProcessModelEdit.tsx @@ -57,6 +57,7 @@ export default function ProcessModelEdit() { }); }; + // share with or delete from ProcessModelEditDiagram const deleteProcessModel = () => { setErrorMessage(null); const processModelToUse = processModel as any; diff --git a/spiffworkflow-frontend/src/routes/ProcessModelEditDiagram.tsx b/spiffworkflow-frontend/src/routes/ProcessModelEditDiagram.tsx index 179b5e07..71327078 100644 --- a/spiffworkflow-frontend/src/routes/ProcessModelEditDiagram.tsx +++ b/spiffworkflow-frontend/src/routes/ProcessModelEditDiagram.tsx @@ -1,24 +1,30 @@ import { useContext, useEffect, useRef, useState } from 'react'; -import { useNavigate, useParams, useSearchParams } from 'react-router-dom'; +import { + generatePath, + useNavigate, + useParams, + useSearchParams, +} from 'react-router-dom'; // @ts-ignore import { Button, Modal, Stack, Content } from '@carbon/react'; -// import Container from 'react-bootstrap/Container'; import Row from 'react-bootstrap/Row'; import Col from 'react-bootstrap/Col'; import Editor from '@monaco-editor/react'; +import MDEditor from '@uiw/react-md-editor'; import ReactDiagramEditor from '../components/ReactDiagramEditor'; import ProcessBreadcrumb from '../components/ProcessBreadcrumb'; import HttpService from '../services/HttpService'; import ErrorContext from '../contexts/ErrorContext'; import { makeid } from '../helpers'; -import { ProcessModel } from '../interfaces'; import { modifyProcessModelPath } from '../helpers'; +import { ProcessFile, ProcessModel } from '../interfaces'; export default function ProcessModelEditDiagram() { const [showFileNameEditor, setShowFileNameEditor] = useState(false); const handleShowFileNameEditor = () => setShowFileNameEditor(true); + const [processModel, setProcessModel] = useState(null); const [scriptText, setScriptText] = useState(''); const [scriptType, setScriptType] = useState(''); @@ -28,6 +34,12 @@ export default function ProcessModelEditDiagram() { const [showScriptEditor, setShowScriptEditor] = useState(false); const handleShowScriptEditor = () => setShowScriptEditor(true); + const [markdownText, setMarkdownText] = useState(''); + const [markdownEventBus, setMarkdownEventBus] = useState(null); + const [showMarkdownEditor, setShowMarkdownEditor] = useState(false); + + const handleShowMarkdownEditor = () => setShowMarkdownEditor(true); + const editorRef = useRef(null); const monacoRef = useRef(null); @@ -71,8 +83,6 @@ export default function ProcessModelEditDiagram() { const [bpmnXmlForDiagramRendering, setBpmnXmlForDiagramRendering] = useState(null); - const [processModel, setProcessModel] = useState(null); - const modifiedProcessModelId = modifyProcessModelPath( (params as any).process_model_id ); @@ -90,7 +100,7 @@ export default function ProcessModelEditDiagram() { }, [processModelPath]); useEffect(() => { - const processResult = (result: any) => { + const fileResult = (result: any) => { setProcessModelFile(result); setBpmnXmlForDiagramRendering(result.file_contents); }; @@ -99,7 +109,7 @@ export default function ProcessModelEditDiagram() { console.log(`processModelPath: ${processModelPath}`); HttpService.makeCallToBackend({ path: `/${processModelPath}/files/${params.file_name}`, - successCallback: processResult, + successCallback: fileResult, }); } }, [processModelPath, params]); @@ -162,9 +172,7 @@ export default function ProcessModelEditDiagram() { const httpMethod = 'DELETE'; const navigateToProcessModelShow = (_httpResult: any) => { - navigate( - `/admin/process-models/${modifiedProcessModelId}` - ); + navigate(`/admin/process-models/${modifiedProcessModelId}`); }; HttpService.makeCallToBackend({ path: url, @@ -252,6 +260,34 @@ export default function ProcessModelEditDiagram() { }); }; + const onJsonFilesRequested = (event: any) => { + if (processModel) { + const jsonFiles = processModel.files.filter((f) => f.type === 'json'); + const options = jsonFiles.map((f) => { + return { label: f.name, value: f.name }; + }); + event.eventBus.fire('spiff.json_files.returned', { options }); + } else { + console.log('There is no process Model.'); + } + }; + + const onDmnFilesRequested = (event: any) => { + if (processModel) { + const dmnFiles = processModel.files.filter((f) => f.type === 'dmn'); + const options: any[] = []; + dmnFiles.forEach((file) => { + file.references.forEach((ref) => { + options.push({ label: ref.name, value: ref.id }); + }); + }); + console.log('Options', options); + event.eventBus.fire('spiff.dmn_files.returned', { options }); + } else { + console.log('There is no process model.'); + } + }; + const getScriptUnitTestElements = (element: any) => { const { extensionElements } = element.businessObject; if (extensionElements && extensionElements.values.length > 0) { @@ -298,7 +334,7 @@ export default function ProcessModelEditDiagram() { }; const handleScriptEditorClose = () => { - scriptEventBus.fire('script.editor.update', { + scriptEventBus.fire('spiff.script.update', { scriptType, script: scriptText, element: scriptElement, @@ -586,6 +622,107 @@ export default function ProcessModelEditDiagram() { ); }; + const onLaunchMarkdownEditor = ( + element: any, + markdown: string, + eventBus: any + ) => { + setMarkdownText(markdown || ''); + setMarkdownEventBus(eventBus); + handleShowMarkdownEditor(); + }; + const handleMarkdownEditorClose = () => { + markdownEventBus.fire('spiff.markdown.update', { + value: markdownText, + }); + setShowMarkdownEditor(false); + }; + + const markdownEditor = () => { + return ( + + + Edit Markdown Content + + + + + + + + + ); + }; + + const findFileNameForReferenceId = ( + id: string, + type: string + ): ProcessFile | null => { + // Given a reference id (like a process_id, or decision_id) finds the file + // that contains that reference and returns it. + let matchFile = null; + if (processModel) { + const files = processModel.files.filter((f) => f.type === type); + files.some((file) => { + if (file.references.some((ref) => ref.id === id)) { + matchFile = file; + return true; + } + return false; + }); + } + return matchFile; + }; + + /** + * fixme: Not currently in use. This would only work for bpmn files within the process model. Which is right for DMN and json, but not right here. Need to merge in work on the nested process groups before tackling this. + * @param processId + */ + const onLaunchBpmnEditor = (processId: string) => { + const file = findFileNameForReferenceId(processId, 'bpmn'); + if (file) { + const path = generatePath( + '/admin/process-models/:process_group_id/:process_model_id/files/:file_name', + { + process_group_id: params.process_group_id, + process_model_id: params.process_model_id, + file_name: file.name, + } + ); + window.open(path); + } + }; + const onLaunchJsonEditor = (fileName: string) => { + const path = generatePath( + '/admin/process-models/:process_group_id/:process_model_id/form/:file_name', + { + process_group_id: params.process_group_id, + process_model_id: params.process_model_id, + file_name: fileName, + } + ); + window.open(path); + }; + const onLaunchDmnEditor = (processId: string) => { + const file = findFileNameForReferenceId(processId, 'dmn'); + if (file) { + const path = generatePath( + '/admin/process-models/:process_group_id/:process_model_id/files/:file_name', + { + process_group_id: params.process_group_id, + process_model_id: params.process_model_id, + file_name: file.name, + } + ); + window.open(path); + } + }; const isDmn = () => { const fileName = params.file_name || ''; @@ -626,12 +763,18 @@ export default function ProcessModelEditDiagram() { diagramType="bpmn" onLaunchScriptEditor={onLaunchScriptEditor} onServiceTasksRequested={onServiceTasksRequested} + onLaunchMarkdownEditor={onLaunchMarkdownEditor} + onLaunchBpmnEditor={onLaunchBpmnEditor} + onLaunchJsonEditor={onLaunchJsonEditor} + onJsonFilesRequested={onJsonFilesRequested} + onLaunchDmnEditor={onLaunchDmnEditor} + onDmnFilesRequested={onDmnFilesRequested} /> ); }; // if a file name is not given then this is a new model and the ReactDiagramEditor component will handle it - if (bpmnXmlForDiagramRendering || !params.file_name) { + if ((bpmnXmlForDiagramRendering || !params.file_name) && processModel) { return ( <> diff --git a/spiffworkflow-frontend/src/routes/ProcessModelShow.tsx b/spiffworkflow-frontend/src/routes/ProcessModelShow.tsx index ba9f1b92..416aef8b 100644 --- a/spiffworkflow-frontend/src/routes/ProcessModelShow.tsx +++ b/spiffworkflow-frontend/src/routes/ProcessModelShow.tsx @@ -1,13 +1,44 @@ import { useContext, useEffect, useState } from 'react'; -import { Link, useParams } from 'react-router-dom'; -// @ts-ignore -import { Button, Stack } from '@carbon/react'; +import { Link, useNavigate, useParams } from 'react-router-dom'; +import { + Add, + Upload, + Download, + TrashCan, + Favorite, + Edit, + // @ts-ignore +} from '@carbon/icons-react'; +import { + Accordion, + AccordionItem, + Button, + Stack, + ButtonSet, + Modal, + FileUploader, + Table, + TableHead, + TableHeader, + TableRow, + TableCell, + TableBody, + // @ts-ignore +} from '@carbon/react'; import ProcessBreadcrumb from '../components/ProcessBreadcrumb'; -import FileInput from '../components/FileInput'; import HttpService from '../services/HttpService'; import ErrorContext from '../contexts/ErrorContext'; -import { RecentProcessModel } from '../interfaces'; -import {modifyProcessModelPath, unModifyProcessModelPath} from '../helpers'; +import { modifyProcessModelPath, unModifyProcessModelPath } from '../helpers'; +import { ProcessFile, ProcessModel, RecentProcessModel } from '../interfaces'; +import ButtonWithConfirmation from '../components/ButtonWithConfirmation'; + +// interface ProcessModelFileCarbonDropdownItem { +// label: string; +// action: string; +// processModelFile: ProcessFile; +// needsConfirmation: boolean; +// icon: any; +// } const storeRecentProcessModelInLocalStorage = ( processModelForStorage: any, @@ -67,16 +98,20 @@ export default function ProcessModelShow() { const params = useParams(); const setErrorMessage = (useContext as any)(ErrorContext)[1]; - const [processModel, setProcessModel] = useState({}); + const [processModel, setProcessModel] = useState(null); const [processInstanceResult, setProcessInstanceResult] = useState(null); - const [reloadModel, setReloadModel] = useState(false); + const [reloadModel, setReloadModel] = useState(false); + const [filesToUpload, setFilesToUpload] = useState(null); + const [showFileUploadModal, setShowFileUploadModal] = + useState(false); + const navigate = useNavigate(); const modifiedProcessModelId = modifyProcessModelPath( `${params.process_model_id}` ); useEffect(() => { - const processResult = (result: object) => { + const processResult = (result: ProcessModel) => { setProcessModel(result); setReloadModel(false); storeRecentProcessModelInLocalStorage(result, params); @@ -105,85 +140,241 @@ export default function ProcessModelShow() { }); }; - let processInstanceResultTag = null; - if (processInstanceResult) { - let takeMeToMyTaskBlurb = null; - // FIXME: ensure that the task is actually for the current user as well - const processInstanceId = (processInstanceResult as any).id; - const nextTask = (processInstanceResult as any).next_task; - if (nextTask && nextTask.state === 'READY') { - takeMeToMyTaskBlurb = ( - - You have a task to complete. Go to{' '} - my task - . - + const processInstanceResultTag = () => { + if (processModel && processInstanceResult) { + let takeMeToMyTaskBlurb = null; + // FIXME: ensure that the task is actually for the current user as well + const processInstanceId = (processInstanceResult as any).id; + const nextTask = (processInstanceResult as any).next_task; + if (nextTask && nextTask.state === 'READY') { + takeMeToMyTaskBlurb = ( + + You have a task to complete. Go to{' '} + + my task + + . + + ); + } + return ( +
+

+ Process Instance {processInstanceId} kicked off ( + + view + + ). {takeMeToMyTaskBlurb} +

+
); } - processInstanceResultTag = ( -
-

- Process Instance {processInstanceId} kicked off ( - - view - - ). {takeMeToMyTaskBlurb} -

-
- ); - } + return null; + }; const onUploadedCallback = () => { setReloadModel(true); }; + const reloadModelOhYeah = (_httpResult: any) => { + setReloadModel(!reloadModel); + }; - const processModelFileList = () => { - let constructedTag; - const tags = (processModel as any).files.map((processModelFile: any) => { + // Remove this code from + const onDeleteFile = (fileName: string) => { + const url = `/process-models/${params.process_group_id}/${params.process_model_id}/files/${fileName}`; + const httpMethod = 'DELETE'; + HttpService.makeCallToBackend({ + path: url, + successCallback: reloadModelOhYeah, + httpMethod, + }); + }; + + // const onProcessModelFileAction = (selection: any) => { + // const { selectedItem } = selection; + // if (selectedItem.action === 'delete') { + // onDeleteFile(selectedItem.processModelFile.name); + // } + // }; + + const onSetPrimaryFile = (fileName: string) => { + const url = `/process-models/${params.process_group_id}/${params.process_model_id}`; + const httpMethod = 'PUT'; + + const processModelToPass = { + primary_file_name: fileName, + }; + HttpService.makeCallToBackend({ + path: url, + successCallback: onUploadedCallback, + httpMethod, + postBody: processModelToPass, + }); + }; + const handleProcessModelFileResult = (processModelFile: ProcessFile) => { + if ( + !('file_contents' in processModelFile) || + processModelFile.file_contents === undefined + ) { + setErrorMessage({ + message: `Could not file file contents for file: ${processModelFile.name}`, + }); + return; + } + let contentType = 'application/xml'; + if (processModelFile.type === 'json') { + contentType = 'application/json'; + } + const element = document.createElement('a'); + const file = new Blob([processModelFile.file_contents], { + type: contentType, + }); + const downloadFileName = processModelFile.name; + element.href = URL.createObjectURL(file); + element.download = downloadFileName; + document.body.appendChild(element); + element.click(); + }; + + const downloadFile = (fileName: string) => { + setErrorMessage(null); + const processModelPath = `process-models/${params.process_group_id}/${params.process_model_id}`; + HttpService.makeCallToBackend({ + path: `/${processModelPath}/files/${fileName}`, + successCallback: handleProcessModelFileResult, + }); + }; + + const navigateToFileEdit = (processModelFile: ProcessFile) => { + if (processModel) { if (processModelFile.name.match(/\.(dmn|bpmn)$/)) { - let primarySuffix = ''; - if (processModelFile.name === (processModel as any).primary_file_name) { - primarySuffix = '- Primary File'; - } - // const modifiedProcessModelId = modifyProcessModelPath( - // (processModel as any).id - // ); - constructedTag = ( -
  • - - {processModelFile.name} - - {primarySuffix} -
  • + navigate( + `/admin/process-models/${modifiedProcessModelId}/files/${processModelFile.name}` ); } else if (processModelFile.name.match(/\.(json|md)$/)) { - constructedTag = ( -
  • - - {processModelFile.name} - -
  • - ); - } else { - constructedTag = ( -
  • {processModelFile.name}
  • + navigate( + `/admin/process-models/${modifiedProcessModelId}/form/${processModelFile.name}` ); } + } + }; + + const renderButtonElements = ( + processModelFile: ProcessFile, + isPrimaryBpmnFile: boolean + ) => { + const elements = []; + elements.push( + - - - - - - + + setFilesToUpload(event.target.files)} + /> + ); }; - if (Object.keys(processModel).length > 1) { + const processModelButtons = () => { + if (!processModel) { + return null; + } + return ( + + + + + + + } + > + + + + + + + +
    + {processModelFileList()} +
    +
    + ); + }; + + if (processModel) { return ( <> + {fileUploadModal()} - {processInstanceResultTag} - +

    {processModel.display_name}

    +

    {processModel.description}

    + + + +
    +
    + {processInstanceResultTag()} {processModelButtons()}

    Process Instances

    {processInstancesUl()} -
    -
    -

    Files

    - {processModelFileList()} ); }