From 6467e967b89f2c631fddac085e517fe16a1e32c1 Mon Sep 17 00:00:00 2001 From: Dan Date: Mon, 31 Oct 2022 16:20:05 -0400 Subject: [PATCH] Adding a Launch Button for Call Activity Json files are now a selection list, not a text field -- see app.json for usage. New SpiffExtensionSelect can be used to add select boxes anywhere you want. --- app/app.js | 38 +++++++ .../CallActivityPropertiesProvider.js | 41 +++++-- .../extensions/extensionHelpers.js | 57 ++++++++++ .../ExtensionsPropertiesProvider.js | 7 +- .../propertiesPanel/SpiffExtensionSelect.js | 100 ++++++++++++++++++ .../SpiffExtensionTextInput.js | 61 ++--------- test/spec/CallActivitySpec.js | 25 ++++- test/spec/UserTaskPropsSpec.js | 88 +++++++++------ 8 files changed, 324 insertions(+), 93 deletions(-) create mode 100644 app/spiffworkflow/extensions/extensionHelpers.js create mode 100644 app/spiffworkflow/extensions/propertiesPanel/SpiffExtensionSelect.js diff --git a/app/app.js b/app/app.js index eb0c5bd..d54a680 100644 --- a/app/app.js +++ b/app/app.js @@ -126,6 +126,44 @@ saveMarkdownBtn.addEventListener('click', (_event) => { document.getElementById('markdown_overlay').style.display = 'none'; }); +/** + * Also can be good to launch an editor for a call activity. + * Not implemented here but imagine opening up a new browser tab + * and showing a different process. + */ +bpmnModeler.on('callactivity.editor.launch', (newEvent) => { + console.log( + 'Open new window with editor for call activity: ', + newEvent.processId + ); +}); + +/** + * 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.options.requested', (event) => { + console.log('Requested!', event); + if (event.optionType === 'json') { + console.log("Firing the json") + event.eventBus.fire('spiff.options.returned.json', { + options: [ + { label: 'pizza_form.json', value: 'pizza_form.json' }, + { label: 'credit_card_form.json', value: 'credit_card_form.json' }, + ], + }); + } else if (event.optionType === 'dmn') { + console.log("Firing the dmn") + event.eventBus.fire('spiff.options.returned.dmn', { + options: [ + { label: 'Pizza Special Prices', value: 'pizza_prices' }, + { label: 'Topping Prices', value: 'topping_prices' }, + ], + }); + } +}); + + // 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/app/spiffworkflow/callActivity/propertiesPanel/CallActivityPropertiesProvider.js b/app/spiffworkflow/callActivity/propertiesPanel/CallActivityPropertiesProvider.js index cf847d6..7f223e6 100644 --- a/app/spiffworkflow/callActivity/propertiesPanel/CallActivityPropertiesProvider.js +++ b/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,33 @@ function createCalledElementGroup(element, translate, moddle, commandStack) { commandStack, translate, }, + { + 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 +87,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('callactivity.editor.launch', { + element, + processId, + }); + }, + children: 'Launch Editor', + }); +} diff --git a/app/spiffworkflow/extensions/extensionHelpers.js b/app/spiffworkflow/extensions/extensionHelpers.js new file mode 100644 index 0000000..fa697f8 --- /dev/null +++ b/app/spiffworkflow/extensions/extensionHelpers.js @@ -0,0 +1,57 @@ +const SPIFF_PARENT_PROP = 'spiffworkflow:properties'; +const SPIFF_PROP = 'spiffworkflow:property'; + +export function getExtensionProperties(element) { + const bizObj = element.businessObject; + if (!bizObj.extensionElements) { + return null; + } + const extensionElements = bizObj.extensionElements.get('values'); + return extensionElements.filter(function (extensionElement) { + if (extensionElement.$instanceOf(SPIFF_PARENT_PROP)) { + return extensionElement; + } + return null; + })[0]; +} + +export function getExtensionProperty(element, name) { + const parentElement = getExtensionProperties(element); + if (parentElement) { + return parentElement.get('properties').filter(function (propertyElement) { + return ( + propertyElement.$instanceOf(SPIFF_PROP) && propertyElement.name === name + ); + })[0]; + } + return null; +} + +export function setExtensionProperty(element, name, value, moddle, commandStack) { + let properties = getExtensionProperties(element); + let property = getExtensionProperty(element, name); + const { businessObject } = element; + 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, + }, + }); +} diff --git a/app/spiffworkflow/extensions/propertiesPanel/ExtensionsPropertiesProvider.js b/app/spiffworkflow/extensions/propertiesPanel/ExtensionsPropertiesProvider.js index f53eba1..4791053 100644 --- a/app/spiffworkflow/extensions/propertiesPanel/ExtensionsPropertiesProvider.js +++ b/app/spiffworkflow/extensions/propertiesPanel/ExtensionsPropertiesProvider.js @@ -8,6 +8,7 @@ import { ServiceTaskParameterArray, ServiceTaskOperatorSelect, ServiceTaskResultTextInput, } from './SpiffExtensionServiceProperties'; +import {OPTION_TYPE, SpiffExtensionSelect} from './SpiffExtensionSelect'; const LOW_PRIORITY = 500; @@ -140,7 +141,8 @@ function createUserGroup(element, translate, moddle, commandStack) { element, moddle, commandStack, - component: SpiffExtensionTextInput, + component: SpiffExtensionSelect, + optionType: OPTION_TYPE.json, label: translate('JSON Schema Filename'), description: translate('RJSF Json Data Structure Filename'), name: 'formJsonSchemaFilename', @@ -149,7 +151,8 @@ function createUserGroup(element, translate, moddle, commandStack) { element, moddle, commandStack, - component: SpiffExtensionTextInput, + component: SpiffExtensionSelect, + optionType: OPTION_TYPE.json, label: translate('UI Schema Filename'), description: translate('RJSF User Interface Filename'), name: 'formUiSchemaFilename', diff --git a/app/spiffworkflow/extensions/propertiesPanel/SpiffExtensionSelect.js b/app/spiffworkflow/extensions/propertiesPanel/SpiffExtensionSelect.js new file mode 100644 index 0000000..9a980cd --- /dev/null +++ b/app/spiffworkflow/extensions/propertiesPanel/SpiffExtensionSelect.js @@ -0,0 +1,100 @@ +import { SelectEntry } from '@bpmn-io/properties-panel'; +import { useService } from 'bpmn-js-properties-panel'; +import { + getExtensionProperty, + setExtensionProperty, +} from '../extensionHelpers'; + +const spiffExtensionOptions = {}; + +export const OPTION_TYPE = { + json: 'json', + dmn: 'dmn', +}; + +/** + * 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.options.requested" will be fired. The event will include + * a 'type' attribute that will be one of the following: + * * jsonFiles + * * dmnFiles + * The response should be sent to "spiff.options.returned.___" where the final + * section is the name requested, ie "spiff.options.returned.jsonFiles" 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 = () => { + const property = getExtensionProperty(element, name); + if (property) { + return property.value; + } + return ''; + }; + + const setValue = (value) => { + console.log(`Set Value called with ${ value}`); + setExtensionProperty(element, name, value, moddle, commandStack); + }; + + if ( + !(optionType in spiffExtensionOptions) || + spiffExtensionOptions[optionType].length === 0 + ) { + spiffExtensionOptions[optionType] = []; + requestOptions(eventBus, element, commandStack, optionType); + } else { + console.log("Getting here.", spiffExtensionOptions) + } + 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.options.returned.json', (event) => { + spiffExtensionOptions[optionType] = event.options; + commandStack.execute('element.updateProperties', { + element, + properties: {}, + }); + }); + eventBus.fire('spiff.options.requested', { eventBus, optionType }); +} diff --git a/app/spiffworkflow/extensions/propertiesPanel/SpiffExtensionTextInput.js b/app/spiffworkflow/extensions/propertiesPanel/SpiffExtensionTextInput.js index c08b80f..c8445f5 100644 --- a/app/spiffworkflow/extensions/propertiesPanel/SpiffExtensionTextInput.js +++ b/app/spiffworkflow/extensions/propertiesPanel/SpiffExtensionTextInput.js @@ -1,8 +1,12 @@ import {useService } from 'bpmn-js-properties-panel'; import { TextFieldEntry } from '@bpmn-io/properties-panel'; +import { + addOrUpdateExtensionProperty, + getExtensionProperties, + getExtensionPropertiesObject, + getExtensionProperty, setExtensionProperty +} from '../extensionHelpers'; -const SPIFF_PARENT_PROP = "spiffworkflow:properties" -const SPIFF_PROP = "spiffworkflow:property" /** * A generic properties' editor for text input. @@ -25,32 +29,8 @@ 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() + const property = getExtensionProperty(element, name) if (property) { return property.value; } @@ -58,32 +38,7 @@ export function SpiffExtensionTextInput(props) { } 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 - } - }); + setExtensionProperty(element, name, value, moddle, commandStack) }; return { + if (event.optionType === 'json') { + event.eventBus.fire('spiff.options.returned.json', { + 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,49 @@ 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 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'); }); - });