From 7db152f01049e0b788ccbfd3ce2358987df67cdc Mon Sep 17 00:00:00 2001 From: burnettk Date: Wed, 9 Aug 2023 16:14:32 -0400 Subject: [PATCH] Squashed 'bpmn-js-spiffworkflow/' changes from 0a9db509a..9dcca6c80 9dcca6c80 Merge pull request #39 from sartography/message_fixes 9de4d9a2e update github action for tests. 84183ffd3 we weren't setting the property when updating a start event. 66a26cc85 does adding a new check prevent an error that only seems to happen whenthe frontend is engaged 877424a55 Merge pull request #37 from sartography/bugfix/bugfixes-for-mi-and-payloads afb071d01 apparently didn't finish search and replace when creating the escalation panels c8040aab5 remove unused MI attributes from XML 1bc43155d Merge pull request #34 from sartography/dependabot/github_actions/dependabot/fetch-metadata-1.6.0 a645c08f5 Merge pull request #36 from sartography/feature/events-with-payloads 8e0f84fbe Merge pull request #35 from sartography/bug/data_objects_in_pools 4b732edd3 add events with payloads 3247a197c update event select to include code field 91e012582 add generic event selector 021f53bb5 add generic event list b19c69080 Assure we delete reference objects when the visible entity is removed. And remove all those console.logs. d46741ffd A few more fixes to prevent bugs from showing up later ... * Deleting a pool was erroring out when it contained a list of data objects, now it works ok. * We were getting duplicate DataObjectReferences in the XML when doing a copy paste operation. Duplicates are no longer generated. f40cecc05 * Assure that Data object in pools can be changed to reference other data objects within the same pool. * In the runnable demo, add the keyboard bindings to copy/paste/delete etc... work. * Added a test for data objects in pools. 2f835fc7f Bump dependabot/fetch-metadata from 1.4.0 to 1.6.0 f6a79440e Merge pull request #33 from sartography/bugfix/restore-references-without-breaking-messages 2556a4599 better method for fixing references 5c49d665f Merge pull request #32 from sartography/bugfix/add-mi-to-subprocess e138c4c26 add mi panel to subprocesses 462a5e777 Merge pull request #27 from sartography/feature/multi-instance-task-panel 63dc415fc add MI for call activities 61f2e5db3 add custom importer to handle loop input/output e504af9bb add multi instance configuration panel git-subtree-dir: bpmn-js-spiffworkflow git-subtree-split: 9dcca6c80b8ab8ed0d79658456047b90e8483541 --- .../workflows/auto-merge-dependabot-prs.yml | 2 +- .github/workflows/tests.yml | 2 +- app/app.js | 21 +- app/fileOperations.js | 12 +- .../DataObject/DataObjectHelpers.js | 5 +- .../DataObject/DataObjectInterceptor.js | 18 +- .../propertiesPanel/DataObjectSelect.js | 3 +- app/spiffworkflow/errors/index.js | 6 + .../ErrorPropertiesProvider.js | 49 + app/spiffworkflow/escalations/index.js | 6 + .../EscalationPropertiesProvider.js | 49 + app/spiffworkflow/eventList.js | 163 + app/spiffworkflow/eventSelect.js | 251 ++ .../SpiffExtensionLaunchButton.js | 1 - .../propertiesPanel/SpiffExtensionSelect.js | 1 - app/spiffworkflow/helpers.js | 24 + app/spiffworkflow/index.js | 15 + .../loops/propertiesPanel/LoopProperty.js | 30 + .../MultiInstancePropertiesProvider.js | 221 ++ .../StandardLoopPropertiesProvider.js | 133 + app/spiffworkflow/messages/MessageHelpers.js | 16 +- .../messages/propertiesPanel/MessageSelect.js | 3 + app/spiffworkflow/moddle/spiffworkflow.json | 22 + app/spiffworkflow/signals/index.js | 6 + .../SignalPropertiesProvider.js | 49 + test/spec/DataObjectInPoolsSpec.js | 66 + test/spec/DataObjectPropsSpec.js | 16 +- test/spec/bpmn/data_objects_in_pools.bpmn | 44 + test/spec/bpmn/request_new_role.bpmn | 2984 +++++++++++++++++ 29 files changed, 4190 insertions(+), 28 deletions(-) create mode 100644 app/spiffworkflow/errors/index.js create mode 100644 app/spiffworkflow/errors/propertiesPanel/ErrorPropertiesProvider.js create mode 100644 app/spiffworkflow/escalations/index.js create mode 100644 app/spiffworkflow/escalations/propertiesPanel/EscalationPropertiesProvider.js create mode 100644 app/spiffworkflow/eventList.js create mode 100644 app/spiffworkflow/eventSelect.js create mode 100644 app/spiffworkflow/loops/propertiesPanel/LoopProperty.js create mode 100644 app/spiffworkflow/loops/propertiesPanel/MultiInstancePropertiesProvider.js create mode 100644 app/spiffworkflow/loops/propertiesPanel/StandardLoopPropertiesProvider.js create mode 100644 app/spiffworkflow/signals/index.js create mode 100644 app/spiffworkflow/signals/propertiesPanel/SignalPropertiesProvider.js create mode 100644 test/spec/DataObjectInPoolsSpec.js create mode 100644 test/spec/bpmn/data_objects_in_pools.bpmn create mode 100644 test/spec/bpmn/request_new_role.bpmn diff --git a/.github/workflows/auto-merge-dependabot-prs.yml b/.github/workflows/auto-merge-dependabot-prs.yml index a8971e0bf..34a14c72a 100644 --- a/.github/workflows/auto-merge-dependabot-prs.yml +++ b/.github/workflows/auto-merge-dependabot-prs.yml @@ -15,7 +15,7 @@ jobs: steps: - name: Dependabot metadata id: metadata - uses: dependabot/fetch-metadata@v1.4.0 + uses: dependabot/fetch-metadata@v1.6.0 with: github-token: "${{ secrets.GITHUB_TOKEN }}" - name: Enable auto-merge for Dependabot PRs diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 5484ad1cc..1195838d9 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -20,7 +20,7 @@ jobs: steps: - uses: actions/checkout@v3 #Checkout Repo - uses: actions/setup-node@v3 #Setup Node - - uses: nanasess/setup-chromedriver@v1 + - uses: nanasess/setup-chromedriver@v2 #Setup ChromeDriver with: node-version: '18' - name: Run Karma Tests diff --git a/app/app.js b/app/app.js index 2bdc7afb9..a96012e46 100644 --- a/app/app.js +++ b/app/app.js @@ -20,6 +20,7 @@ let bpmnModeler; try { bpmnModeler = new BpmnModeler({ container: modelerEl, + keyboard: { bindTo: document }, propertiesPanel: { parent: panelEl, }, @@ -41,9 +42,6 @@ try { throw error; } -// import XML -bpmnModeler.importXML(diagramXML).then(() => {}); - /** * It is possible to populate certain components using API calls to * a backend. Here we mock out the API call, but this gives you @@ -191,6 +189,23 @@ bpmnModeler.on('spiff.callactivity.search', (event) => { }); }); +/* This restores unresolved references that camunda removes */ + +bpmnModeler.on('import.parse.complete', event => { + const refs = event.references.filter(r => r.property === 'bpmn:loopDataInputRef' || r.property === 'bpmn:loopDataOutputRef'); + const desc = bpmnModeler._moddle.registry.getEffectiveDescriptor('bpmn:ItemAwareElement'); + refs.forEach(ref => { + const props = { + id: ref.id, + name: ref.id ? typeof(ref.name) === 'undefined': ref.name, + }; + let elem = bpmnModeler._moddle.create(desc, props); + elem.$parent = ref.element; + ref.element.set(ref.property, elem); + }); +}); + +bpmnModeler.importXML(diagramXML).then(() => {}); // 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 diff --git a/app/fileOperations.js b/app/fileOperations.js index 170a75c39..86a4a24d3 100644 --- a/app/fileOperations.js +++ b/app/fileOperations.js @@ -12,6 +12,7 @@ export default function setupFileOperations(bpmnModeler) { * Just a quick bit of code so we can save the XML that is output. * Helps for debugging against other libraries (like SpiffWorkflow) */ + const btn = document.getElementById('downloadButton'); btn.addEventListener('click', (_event) => { saveXML(); @@ -29,12 +30,8 @@ export default function setupFileOperations(bpmnModeler) { */ const uploadBtn = document.getElementById('uploadButton'); uploadBtn.addEventListener('click', (_event) => { - openFile(displayFile); + openFile(bpmnModeler); }); - - function displayFile(contents) { - bpmnModeler.importXML(contents).then(() => {}); - } } function clickElem(elem) { @@ -59,7 +56,7 @@ function clickElem(elem) { elem.dispatchEvent(eventMouse); } -export function openFile(func) { +export function openFile(bpmnModeler) { const readFile = function readFileCallback(e) { const file = e.target.files[0]; if (!file) { @@ -68,7 +65,7 @@ export function openFile(func) { const reader = new FileReader(); reader.onload = function onloadCallback(onloadEvent) { const contents = onloadEvent.target.result; - fileInput.func(contents); + bpmnModeler.importXML(contents); document.body.removeChild(fileInput); }; reader.readAsText(file); @@ -77,7 +74,6 @@ export function openFile(func) { fileInput.type = 'file'; fileInput.style.display = 'none'; fileInput.onchange = readFile; - fileInput.func = func; document.body.appendChild(fileInput); clickElem(fileInput); } diff --git a/app/spiffworkflow/DataObject/DataObjectHelpers.js b/app/spiffworkflow/DataObject/DataObjectHelpers.js index 1c62efc5a..dd343789d 100644 --- a/app/spiffworkflow/DataObject/DataObjectHelpers.js +++ b/app/spiffworkflow/DataObject/DataObjectHelpers.js @@ -36,6 +36,9 @@ export function findDataObject(process, id) { } export function findDataObjectReferences(children, dataObjectId) { + if (children == null) { + return []; + } return children.flatMap((child) => { if (child.$type == 'bpmn:DataObjectReference' && child.dataObjectRef.id == dataObjectId) return [child]; @@ -58,7 +61,7 @@ export function findDataObjectReferenceShapes(children, dataObjectId) { } export function idToHumanReadableName(id) { - const words = id.match(/[A-Za-z][a-z]*/g) || [id]; + const words = id.match(/[A-Za-z][a-z]*|[0-9]+/g) || [id]; return words.map(capitalize).join(' '); function capitalize(word) { diff --git a/app/spiffworkflow/DataObject/DataObjectInterceptor.js b/app/spiffworkflow/DataObject/DataObjectInterceptor.js index df544bd7c..a8eb3f7d7 100644 --- a/app/spiffworkflow/DataObject/DataObjectInterceptor.js +++ b/app/spiffworkflow/DataObject/DataObjectInterceptor.js @@ -42,7 +42,10 @@ export default class DataObjectInterceptor extends CommandInterceptor { // when the shape is deleted, but not interested in refactoring at the moment. if (realParent != null) { const flowElements = realParent.get('flowElements'); - flowElements.push(businessObject); + const existingElement = flowElements.find(i => i.id === 1); + if (!existingElement) { + flowElements.push(businessObject); + } } } else if (is(businessObject, 'bpmn:DataObject')) { // For data objects, only update the flowElements for new data objects, and set the parent so it doesn't get moved. @@ -120,12 +123,17 @@ export default class DataObjectInterceptor extends CommandInterceptor { const { shape } = context; if (is(shape, 'bpmn:DataObjectReference') && shape.type !== 'label') { const dataObject = shape.businessObject.dataObjectRef; - let flowElements = shape.businessObject.$parent.get('flowElements'); + let parent = shape.businessObject.$parent; + if (parent.processRef) { + // Our immediate parent may be a pool, so we need to get the process + parent = parent.processRef; + } + const flowElements = parent.get('flowElements'); collectionRemove(flowElements, shape.businessObject); - let references = findDataObjectReferences(flowElements, dataObject.id); + const references = findDataObjectReferences(flowElements, dataObject.id); if (references.length === 0) { - let flowElements = dataObject.$parent.get('flowElements'); - collectionRemove(flowElements, dataObject); + const dataFlowElements = dataObject.$parent.get('flowElements'); + collectionRemove(dataFlowElements, dataObject); } } }); diff --git a/app/spiffworkflow/DataObject/propertiesPanel/DataObjectSelect.js b/app/spiffworkflow/DataObject/propertiesPanel/DataObjectSelect.js index e079e0a75..2804355f6 100644 --- a/app/spiffworkflow/DataObject/propertiesPanel/DataObjectSelect.js +++ b/app/spiffworkflow/DataObject/propertiesPanel/DataObjectSelect.js @@ -31,7 +31,8 @@ export function DataObjectSelect(props) { const setValue = value => { const businessObject = element.businessObject; - for (const flowElem of businessObject.$parent.flowElements) { + const dataObjects = findDataObjects(businessObject.$parent) + for (const flowElem of dataObjects) { if (flowElem.$type === 'bpmn:DataObject' && flowElem.id === value) { commandStack.execute('element.updateModdleProperties', { element, diff --git a/app/spiffworkflow/errors/index.js b/app/spiffworkflow/errors/index.js new file mode 100644 index 000000000..88e872b36 --- /dev/null +++ b/app/spiffworkflow/errors/index.js @@ -0,0 +1,6 @@ +import ErrorPropertiesProvider from './propertiesPanel/ErrorPropertiesProvider'; + +export default { + __init__: ['errorPropertiesProvider'], + errorPropertiesProvider: ['type', ErrorPropertiesProvider], +} diff --git a/app/spiffworkflow/errors/propertiesPanel/ErrorPropertiesProvider.js b/app/spiffworkflow/errors/propertiesPanel/ErrorPropertiesProvider.js new file mode 100644 index 000000000..b307aac11 --- /dev/null +++ b/app/spiffworkflow/errors/propertiesPanel/ErrorPropertiesProvider.js @@ -0,0 +1,49 @@ +import { is } from 'bpmn-js/lib/util/ModelUtil'; +import { getRoot } from '../../helpers'; +import { getArrayForType, getListGroupForType } from '../../eventList.js'; +import { hasEventType, + replaceGroup, + getSelectorForType, + getConfigureGroupForType +} from '../../eventSelect.js'; + +const LOW_PRIORITY = 500; + +const eventDetails = { + 'eventType': 'bpmn:Error', + 'eventDefType': 'bpmn:ErrorEventDefinition', + 'referenceType': 'errorRef', + 'idPrefix': 'error', +}; + +export default function ErrorPropertiesProvider( + propertiesPanel, + translate, + moddle, + commandStack, +) { + + this.getGroups = function (element) { + return function (groups) { + if (is(element, 'bpmn:Process') || is(element, 'bpmn:Collaboration')) { + const getErrorArray = getArrayForType('bpmn:Error', 'errorRef', 'Error'); + const errorGroup = getListGroupForType('errors', 'Errors', getErrorArray); + groups.push(errorGroup({ element, translate, moddle, commandStack })); + } else if (hasEventType(element, 'bpmn:ErrorEventDefinition')) { + const getErrorSelector = getSelectorForType(eventDetails); + const errorGroup = getConfigureGroupForType(eventDetails, 'Error', true, getErrorSelector); + const group = errorGroup({ element, translate, moddle, commandStack }); + replaceGroup('error', groups, group); + } + return groups; + }; + }; + propertiesPanel.registerProvider(LOW_PRIORITY, this); +} + +ErrorPropertiesProvider.$inject = [ + 'propertiesPanel', + 'translate', + 'moddle', + 'commandStack', +]; diff --git a/app/spiffworkflow/escalations/index.js b/app/spiffworkflow/escalations/index.js new file mode 100644 index 000000000..402aa84b5 --- /dev/null +++ b/app/spiffworkflow/escalations/index.js @@ -0,0 +1,6 @@ +import EscalationPropertiesProvider from './propertiesPanel/EscalationPropertiesProvider'; + +export default { + __init__: ['escalationPropertiesProvider'], + escalationrrorPropertiesProvider: ['type', EscalationPropertiesProvider], +} diff --git a/app/spiffworkflow/escalations/propertiesPanel/EscalationPropertiesProvider.js b/app/spiffworkflow/escalations/propertiesPanel/EscalationPropertiesProvider.js new file mode 100644 index 000000000..97ddeac37 --- /dev/null +++ b/app/spiffworkflow/escalations/propertiesPanel/EscalationPropertiesProvider.js @@ -0,0 +1,49 @@ +import { is } from 'bpmn-js/lib/util/ModelUtil'; +import { getRoot } from '../../helpers'; +import { getArrayForType, getListGroupForType } from '../../eventList.js'; +import { hasEventType, + replaceGroup, + getSelectorForType, + getConfigureGroupForType +} from '../../eventSelect.js'; + +const LOW_PRIORITY = 500; + +const eventDetails = { + 'eventType': 'bpmn:Escalation', + 'eventDefType': 'bpmn:EscalationEventDefinition', + 'referenceType': 'escalationRef', + 'idPrefix': 'escalation', +}; + +export default function EscalationPropertiesProvider( + propertiesPanel, + translate, + moddle, + commandStack, +) { + + this.getGroups = function (element) { + return function (groups) { + if (is(element, 'bpmn:Process') || is(element, 'bpmn:Collaboration')) { + const getEscalationArray = getArrayForType('bpmn:Escalation', 'escalationRef', 'Escalation'); + const escalationGroup = getListGroupForType('escalations', 'Escalations', getEscalationArray); + groups.push(escalationGroup({ element, translate, moddle, commandStack })); + } else if (hasEventType(element, 'bpmn:EscalationEventDefinition')) { + const getEscalationSelector = getSelectorForType(eventDetails); + const escalationGroup = getConfigureGroupForType(eventDetails, 'Escalation', true, getEscalationSelector); + const group = escalationGroup({ element, translate, moddle, commandStack }); + replaceGroup('escalation', groups, group); + } + return groups; + }; + }; + propertiesPanel.registerProvider(LOW_PRIORITY, this); +} + +EscalationPropertiesProvider.$inject = [ + 'propertiesPanel', + 'translate', + 'moddle', + 'commandStack', +]; diff --git a/app/spiffworkflow/eventList.js b/app/spiffworkflow/eventList.js new file mode 100644 index 000000000..a1604cbd4 --- /dev/null +++ b/app/spiffworkflow/eventList.js @@ -0,0 +1,163 @@ +import { is } from 'bpmn-js/lib/util/ModelUtil'; +import { useService } from 'bpmn-js-properties-panel'; +import { + ListGroup, + TextFieldEntry, + isTextFieldEntryEdited +} from '@bpmn-io/properties-panel'; +import { getRoot } from './helpers'; + +/* This function creates a list of a particular event type at the process level using the item list + * and add function provided by `getArray`. + * + * Usage: + * const getArray = getArrayForType('bpmn:Signal', 'signalRef', 'Signal'); + * const signalGroup = createGroupForType('signals', 'Signals', getArray); + */ + +function getListGroupForType(groupId, label, getArray) { + + return function (props) { + const { element, translate, moddle, commandStack } = props; + const eventArray = { + id: groupId, + element, + label: label, + component: ListGroup, + ...getArray({ element, moddle, commandStack, translate }), + }; + + if (eventArray.items) { + return eventArray; + } + } +} + +function getArrayForType(itemType, referenceType, prefix) { + + return function (props) { + const { element, moddle, commandStack, translate } = props; + const root = getRoot(element.businessObject); + const matching = root.rootElements ? root.rootElements.filter(elem => elem.$type === itemType) : []; + + function removeModelReferences(flowElements, match) { + flowElements.map(elem => { + if (elem.eventDefinitions) + elem.eventDefinitions = elem.eventDefinitions.filter(def => def.get(referenceType) != match); + else if (elem.flowElements) + removeModelReferences(elem.flowElements, match); + }); + } + + function removeElementReferences(children, match) { + children.map(child => { + if (child.businessObject.eventDefinitions) { + const bo = child.businessObject; + bo.eventDefinitions = bo.eventDefinitions.filter(def => def.get(referenceType) != match); + commandStack.execute('element.updateProperties', { + element: child, + moddleElement: bo, + properties: {} + }); + } + if (child.children) + removeElementReferences(child.children, match); + }); + } + + function removeFactory(item) { + return function (event) { + event.stopPropagation(); + if (root.rootElements) { + root.rootElements = root.rootElements.filter(elem => elem != item); + // This updates visible elements + removeElementReferences(element.children, item); + // This handles everything else (eg collapsed subprocesses) but does not update the shapes + // I can't figure out how to do that + root.rootElements.filter(elem => elem.$type === 'bpmn:Process').map( + process => removeModelReferences(process.flowElements, item) + ); + commandStack.execute('element.updateProperties', { + element, + properties: {}, + }); + } + } + } + + const items = matching.map((item, idx) => { + const itemId = `${prefix}-${idx}`; + return { + id: itemId, + label: item.name, + entries: getItemEditor({ + itemId, + element, + item, + commandStack, + translate, + }), + autoFocusEntry: itemId, + remove: removeFactory(item), + }; + }); + + function add(event) { + event.stopPropagation(); + const item = moddle.create(itemType); + item.id = moddle.ids.nextPrefixed(`${prefix}_`); + item.name = item.id; + if (root.rootElements) + root.rootElements.push(item); + commandStack.execute('element.updateProperties', { + element, + properties: {}, + }); + }; + + return { items, add }; + } +} + +function getItemEditor(props) { + const { itemId, element, item, commandStack, translate } = props; + return [ + { + id: `${itemId}-name`, + component: ItemTextField, + item, + commandStack, + translate, + }, + ]; +} + +function ItemTextField(props) { + const { itemId, element, item, commandStack, translate } = props; + + const debounce = useService('debounceInput'); + + const setValue = (value) => { + commandStack.execute('element.updateModdleProperties', { + element, + moddleElement: item, + properties: { + id: value, + name: value, + }, + }); + }; + + const getValue = () => { return item.id; } + + return TextFieldEntry({ + element, + id: `${itemId}-id-textField`, + label: translate('ID'), + getValue, + setValue, + debounce, + }); +} + +export { getArrayForType, getListGroupForType }; diff --git a/app/spiffworkflow/eventSelect.js b/app/spiffworkflow/eventSelect.js new file mode 100644 index 000000000..3bee58aef --- /dev/null +++ b/app/spiffworkflow/eventSelect.js @@ -0,0 +1,251 @@ +import { is, isAny } from 'bpmn-js/lib/util/ModelUtil'; +import { useService } from 'bpmn-js-properties-panel'; +import { + ListGroup, + TextFieldEntry, + TextAreaEntry, + SelectEntry, + isTextFieldEntryEdited +} from '@bpmn-io/properties-panel'; +import { getRoot } from './helpers'; + +function hasEventType(element, eventType) { + const events = element.businessObject.eventDefinitions; + return events && events.filter(item => is(item, eventType)).length > 0; +} + +function replaceGroup(groupId, groups, group) { + const idx = groups.map(g => g.id).indexOf(groupId); + if (idx > -1) + groups.splice(idx, 1, group); + else + groups.push(group); + group.shouldOpen = true; +} + +function isCatchingEvent(element) { + return isAny(element, ['bpmn:StartEvent', 'bpmn:IntermediateCatchEvent', 'bpmn:BoundaryEvent']); +} + +function isThrowingEvent(element) { + return isAny(element, ['bpmn:EndEvent', 'bpmn:IntermediateThrowEvent']); +} + +function getConfigureGroupForType(eventDetails, label, includeCode, getSelect) { + + const { eventType, eventDefType, referenceType, idPrefix } = eventDetails; + + return function (props) { + const { element, translate, moddle, commandStack } = props; + + const variableName = getTextFieldForExtension(eventDetails, 'Variable Name', 'The name of the variable to store the payload in', true); + const payloadDefinition = getTextFieldForExtension(eventDetails, 'Payload', 'The expression to create the payload with', false); + + const entries = [ + { + id: `${idPrefix}-select`, + element, + component: getSelect, + isEdited: isTextFieldEntryEdited, + moddle, + commandStack, + }, + ]; + + if (includeCode) { + const codeField = getCodeTextField(eventDetails, `${label} Code`); + entries.push({ + id: `${idPrefix}-code`, + element, + component: codeField, + isEdited: isTextFieldEntryEdited, + moddle, + commandStack, + }); + } + + + if (isCatchingEvent(element)) { + entries.push({ + id: `${idPrefix}-variable`, + element, + component: variableName, + isEdited: isTextFieldEntryEdited, + moddle, + commandStack, + }); + } else if (isThrowingEvent(element)) { + entries.push({ + id: `${idPrefix}-payload`, + element, + component: payloadDefinition, + isEdited: isTextFieldEntryEdited, + moddle, + commandStack, + }); + }; + return { + id: `${idPrefix}-group`, + label: label, + entries, + } + } +} + +function getSelectorForType(eventDetails) { + + const { eventType, eventDefType, referenceType, idPrefix } = eventDetails; + + return function (props) { + const { element, translate, moddle, commandStack } = props; + const debounce = useService('debounceInput'); + const root = getRoot(element.businessObject); + + const getValue = () => { + const eventDef = element.businessObject.eventDefinitions.find(v => v.$type == eventDefType); + return (eventDef && eventDef.get(referenceType)) ? eventDef.get(referenceType).id : ''; + }; + + const setValue = (value) => { + const bpmnEvent = root.rootElements.find(e => e.id == value); + // not sure how to handle multiple event definitions + const eventDef = element.businessObject.eventDefinitions.find(v => v.$type == eventDefType); + // really not sure what to do here if one of these can't be found either + if (bpmnEvent && eventDef) + eventDef.set(referenceType, bpmnEvent); + commandStack.execute('element.updateProperties', { + element, + moddleElement: element.businessObject, + properties: {}, + }); + }; + + const getOptions = (val) => { + const matching = root.rootElements ? root.rootElements.filter(elem => elem.$type === eventType) : []; + const options = []; + matching.map(option => options.push({label: option.name, value: option.id})); + return options; + } + + return SelectEntry({ + id: `${idPrefix}-select`, + element, + description: 'Select item', + getValue, + setValue, + getOptions, + debounce, + }); + } +} + +function getTextFieldForExtension(eventDetails, label, description, catching) { + + const { eventType, eventDefType, referenceType, idPrefix } = eventDetails; + + return function (props) { + const { element, moddle, commandStack } = props; + const debounce = useService('debounceInput'); + const translate = useService('translate'); + const root = getRoot(element.businessObject); + const extensionName = (catching) ? 'spiffworkflow:variableName' : 'spiffworkflow:payloadExpression'; + + const getEvent = () => { + const eventDef = element.businessObject.eventDefinitions.find(v => v.$type == eventDefType); + const bpmnEvent = eventDef.get(referenceType); + return bpmnEvent; + }; + + const getValue = () => { + // I've put the variable name (and payload) on the event for consistency with messages. + // However, when I think about this, I wonder if it shouldn't be on the event definition. + // I think that's something we should address in the future. + // Creating a payload and defining access to it are both process-specific, and that's an argument for leaving + // it in the event definition + const bpmnEvent = getEvent(); + if (bpmnEvent && bpmnEvent.extensionElements) { + const extension = bpmnEvent.extensionElements.get('values').find(ext => ext.$instanceOf(extensionName)); + return (extension) ? extension.value : null; + } + } + + const setValue = (value) => { + const bpmnEvent = getEvent(); + if (bpmnEvent) { + if (!bpmnEvent.extensionElements) + bpmnEvent.extensionElements = moddle.create('bpmn:ExtensionElements'); + const extensions = bpmnEvent.extensionElements.get('values'); + const extension = extensions.find(ext => ext.$instanceOf(extensionName)); + if (!extension) { + const newExt = moddle.create(extensionName); + newExt.value = value; + extensions.push(newExt); + } else + extension.value = value; + } // not sure what to do if the event hasn't been set + }; + + if (catching) { + return TextFieldEntry({ + element, + id: `${idPrefix}-variable-name`, + description: description, + label: translate(label), + getValue, + setValue, + debounce, + }); + } else { + return TextAreaEntry({ + element, + id: `${idPrefix}-payload-expression`, + description: description, + label: translate(label), + getValue, + setValue, + debounce, + }); + } + } +} + +function getCodeTextField(eventDetails, label) { + + const { eventType, eventDefType, referenceType, idPrefix } = eventDetails; + + return function (props) { + + const { element, moddle, commandStack } = props; + const translate = useService('translate'); + const debounce = useService('debounceInput'); + const attrName = `${idPrefix}Code`; + + const getEvent = () => { + const eventDef = element.businessObject.eventDefinitions.find(v => v.$type == eventDefType); + const bpmnEvent = eventDef.get(referenceType); + return bpmnEvent; + }; + + const getValue = () => { + const bpmnEvent = getEvent(); + return (bpmnEvent) ? bpmnEvent.get(attrName) : null; + }; + + const setValue = (value) => { + const bpmnEvent = getEvent(); + if (bpmnEvent) + bpmnEvent.set(attrName, value); + }; + + return TextFieldEntry({ + element, + id: `${idPrefix}-code-value`, + label: translate(label), + getValue, + setValue, + debounce, + }); + } +} + +export { hasEventType, getSelectorForType, getConfigureGroupForType, replaceGroup }; diff --git a/app/spiffworkflow/extensions/propertiesPanel/SpiffExtensionLaunchButton.js b/app/spiffworkflow/extensions/propertiesPanel/SpiffExtensionLaunchButton.js index 233646db6..7f7d5f5b6 100644 --- a/app/spiffworkflow/extensions/propertiesPanel/SpiffExtensionLaunchButton.js +++ b/app/spiffworkflow/extensions/propertiesPanel/SpiffExtensionLaunchButton.js @@ -28,7 +28,6 @@ export function SpiffExtensionLaunchButton(props) { 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); }); } diff --git a/app/spiffworkflow/extensions/propertiesPanel/SpiffExtensionSelect.js b/app/spiffworkflow/extensions/propertiesPanel/SpiffExtensionSelect.js index 33fb0c67e..e2307cf3c 100644 --- a/app/spiffworkflow/extensions/propertiesPanel/SpiffExtensionSelect.js +++ b/app/spiffworkflow/extensions/propertiesPanel/SpiffExtensionSelect.js @@ -40,7 +40,6 @@ export function SpiffExtensionSelect(props) { }; const setValue = (value) => { - console.log(`Set Value called with ${ value}`); setExtensionValue(element, name, value, moddle, commandStack); }; diff --git a/app/spiffworkflow/helpers.js b/app/spiffworkflow/helpers.js index ec68280e8..e576eaab8 100644 --- a/app/spiffworkflow/helpers.js +++ b/app/spiffworkflow/helpers.js @@ -12,3 +12,27 @@ export function removeExtensionElementsIfEmpty(moddleElement) { moddleElement.extensionElements = null; } } + +/** + * loops up until it can find the root. + * @param element + */ +export function getRoot(businessObject, moddle) { + // HACK: get the root element. need a more formal way to do this + if (moddle) { + for (const elementId in moddle.ids._seed.hats) { + if (elementId.startsWith('Definitions_')) { + return moddle.ids._seed.hats[elementId]; + } + } + } else { + // todo: Do we want businessObject to be a shape or moddle object? + if (businessObject.$type === 'bpmn:Definitions') { + return businessObject; + } + if (typeof businessObject.$parent !== 'undefined') { + return getRoot(businessObject.$parent); + } + } + return businessObject; +} diff --git a/app/spiffworkflow/index.js b/app/spiffworkflow/index.js index 53277731a..5ab4febf0 100644 --- a/app/spiffworkflow/index.js +++ b/app/spiffworkflow/index.js @@ -9,7 +9,12 @@ import DataObjectPropertiesProvider from './DataObject/propertiesPanel/DataObjec import ConditionsPropertiesProvider from './conditions/propertiesPanel/ConditionsPropertiesProvider'; import ExtensionsPropertiesProvider from './extensions/propertiesPanel/ExtensionsPropertiesProvider'; import MessagesPropertiesProvider from './messages/propertiesPanel/MessagesPropertiesProvider'; +import SignalPropertiesProvider from './signals/propertiesPanel/SignalPropertiesProvider'; +import ErrorPropertiesProvider from './errors/propertiesPanel/ErrorPropertiesProvider'; +import EscalationPropertiesProvider from './escalations/propertiesPanel/EscalationPropertiesProvider'; import CallActivityPropertiesProvider from './callActivity/propertiesPanel/CallActivityPropertiesProvider'; +import StandardLoopPropertiesProvider from './loops/propertiesPanel/StandardLoopPropertiesProvider'; +import MultiInstancePropertiesProvider from './loops/propertiesPanel/MultiInstancePropertiesProvider'; export default { __depends__: [RulesModule], @@ -20,11 +25,16 @@ export default { 'conditionsPropertiesProvider', 'extensionsPropertiesProvider', 'messagesPropertiesProvider', + 'signalPropertiesProvider', + 'errorPropertiesProvider', + 'escalationPropertiesProvider', 'callActivityPropertiesProvider', 'ioPalette', 'ioRules', 'ioInterceptor', 'dataObjectRenderer', + 'multiInstancePropertiesProvider', + 'standardLoopPropertiesProvider', ], dataObjectInterceptor: ['type', DataObjectInterceptor], dataObjectRules: ['type', DataObjectRules], @@ -32,9 +42,14 @@ export default { dataObjectPropertiesProvider: ['type', DataObjectPropertiesProvider], conditionsPropertiesProvider: ['type', ConditionsPropertiesProvider], extensionsPropertiesProvider: ['type', ExtensionsPropertiesProvider], + signalPropertiesProvider: ['type', SignalPropertiesProvider], + errorPropertiesProvider: ['type', ErrorPropertiesProvider], + escalationPropertiesProvider: ['type', EscalationPropertiesProvider], messagesPropertiesProvider: ['type', MessagesPropertiesProvider], callActivityPropertiesProvider: ['type', CallActivityPropertiesProvider], ioPalette: ['type', IoPalette], ioRules: ['type', IoRules], ioInterceptor: ['type', IoInterceptor], + multiInstancePropertiesProvider: ['type', MultiInstancePropertiesProvider], + standardLoopPropertiesProvider: ['type', StandardLoopPropertiesProvider], }; diff --git a/app/spiffworkflow/loops/propertiesPanel/LoopProperty.js b/app/spiffworkflow/loops/propertiesPanel/LoopProperty.js new file mode 100644 index 000000000..779d2c990 --- /dev/null +++ b/app/spiffworkflow/loops/propertiesPanel/LoopProperty.js @@ -0,0 +1,30 @@ +export function getLoopProperty(element, propertyName) { + + const loopCharacteristics = element.businessObject.loopCharacteristics; + const prop = loopCharacteristics.get(propertyName); + + let value = ''; + if (typeof(prop) !== 'object') { + value = prop; + } else if (typeof(prop) !== 'undefined') { + if (prop.$type === 'bpmn:FormalExpression') + value = prop.get('body'); + else + value = prop.get('id'); + } + return value; +} + +export function setLoopProperty(element, propertyName, value, commandStack) { + const loopCharacteristics = element.businessObject.loopCharacteristics; + if (typeof(value) === 'object') + value.$parent = loopCharacteristics; + let properties = { [propertyName]: value }; + if (propertyName === 'loopCardinality') properties['loopDataInputRef'] = undefined; + if (propertyName === 'loopDataInputRef') properties['loopCardinality'] = undefined; + commandStack.execute('element.updateModdleProperties', { + element, + moddleElement: loopCharacteristics, + properties: properties, + }); +} diff --git a/app/spiffworkflow/loops/propertiesPanel/MultiInstancePropertiesProvider.js b/app/spiffworkflow/loops/propertiesPanel/MultiInstancePropertiesProvider.js new file mode 100644 index 000000000..afebae1e7 --- /dev/null +++ b/app/spiffworkflow/loops/propertiesPanel/MultiInstancePropertiesProvider.js @@ -0,0 +1,221 @@ +import { is } from 'bpmn-js/lib/util/ModelUtil'; +import { useService } from 'bpmn-js-properties-panel'; +import { TextFieldEntry, isTextFieldEntryEdited } from '@bpmn-io/properties-panel'; +import { getLoopProperty, setLoopProperty } from './LoopProperty'; + +const LOW_PRIORITY = 500; + +export default function MultiInstancePropertiesProvider(propertiesPanel) { + this.getGroups = function getGroupsCallback(element) { + return function pushGroup(groups) { + if (is(element, 'bpmn:Task') || is(element, 'bpmn:CallActivity') || is(element, 'bpmn:SubProcess')) { + let group = groups.filter(g => g.id == 'multiInstance'); + if (group.length == 1) + updateMultiInstanceGroup(element, group[0]); + } + return groups; + }; + }; + propertiesPanel.registerProvider(LOW_PRIORITY, this); +} + +MultiInstancePropertiesProvider.$inject = ['propertiesPanel']; + +function updateMultiInstanceGroup(element, group) { + group.entries = MultiInstanceProps({element}); + group.shouldOpen = true; +} + +function MultiInstanceProps(props) { + const { element } = props; + + const entries = [{ + id: 'loopCardinality', + component: LoopCardinality, + isEdited: isTextFieldEntryEdited + }, { + id: 'loopDataInputRef', + component: InputCollection, + isEdited: isTextFieldEntryEdited + }, { + id: 'dataInputItem', + component: InputItem, + isEdited: isTextFieldEntryEdited + }, { + id: 'loopDataOutputRef', + component: OutputCollection, + isEdited: isTextFieldEntryEdited + }, { + id: 'dataOutputItem', + component: OutputItem, + isEdited: isTextFieldEntryEdited + }, { + id: 'completionCondition', + component: CompletionCondition, + isEdited: isTextFieldEntryEdited + }]; + return entries; +} + +function LoopCardinality(props) { + const { element } = props; + const debounce = useService('debounceInput'); + const translate = useService('translate'); + const commandStack = useService('commandStack'); + const bpmnFactory = useService('bpmnFactory'); + + const getValue = () => { + return getLoopProperty(element, 'loopCardinality'); + }; + + const setValue = value => { + const loopCardinality = bpmnFactory.create('bpmn:FormalExpression', {body: value}) + setLoopProperty(element, 'loopCardinality', loopCardinality, commandStack); + }; + + return TextFieldEntry({ + element, + id: 'loopCardinality', + label: translate('Loop Cardinality'), + getValue, + setValue, + debounce, + description: 'Explicitly set the number of instances' + }); +} + +function InputCollection(props) { + const { element } = props; + const debounce = useService('debounceInput'); + const translate = useService('translate'); + const commandStack = useService('commandStack'); + const bpmnFactory = useService('bpmnFactory'); + + const getValue = () => { + return getLoopProperty(element, 'loopDataInputRef'); + }; + + const setValue = value => { + const collection = bpmnFactory.create('bpmn:ItemAwareElement', {id: value}); + setLoopProperty(element, 'loopDataInputRef', collection, commandStack); + }; + + return TextFieldEntry({ + element, + id: 'loopDataInputRef', + label: translate('Input Collection'), + getValue, + setValue, + debounce, + description: 'Create an instance for each item in this collection' + }); +} + +function InputItem(props) { + const { element } = props; + const debounce = useService('debounceInput'); + const translate = useService('translate'); + const commandStack = useService('commandStack'); + const bpmnFactory = useService('bpmnFactory'); + + const getValue = () => { + return getLoopProperty(element, 'inputDataItem'); + }; + + const setValue = value => { + const item = (typeof(value) !== 'undefined') ? bpmnFactory.create('bpmn:DataInput', {id: value, name: value}) : undefined; + setLoopProperty(element, 'inputDataItem', item, commandStack); + }; + + return TextFieldEntry({ + element, + id: 'inputDataItem', + label: translate('Input Element'), + getValue, + setValue, + debounce, + description: 'Each item in the collection will be copied to this variable' + }); +} + +function OutputCollection(props) { + const { element } = props; + const debounce = useService('debounceInput'); + const translate = useService('translate'); + const commandStack = useService('commandStack'); + const bpmnFactory = useService('bpmnFactory'); + + const getValue = () => { + return getLoopProperty(element, 'loopDataOutputRef'); + }; + + const setValue = value => { + const collection = bpmnFactory.create('bpmn:ItemAwareElement', {id: value}); + setLoopProperty(element, 'loopDataOutputRef', collection, commandStack); + }; + + return TextFieldEntry({ + element, + id: 'loopDataOutputRef', + label: translate('Output Collection'), + getValue, + setValue, + debounce, + description: 'Create or update this collection with the instance results' + }); +} + +function OutputItem(props) { + const { element } = props; + const debounce = useService('debounceInput'); + const translate = useService('translate'); + const commandStack = useService('commandStack'); + const bpmnFactory = useService('bpmnFactory'); + + const getValue = () => { + return getLoopProperty(element, 'outputDataItem'); + }; + + const setValue = value => { + const item = (typeof(value) !== 'undefined') ? bpmnFactory.create('bpmn:DataOutput', {id: value, name: value}) : undefined; + setLoopProperty(element, 'outputDataItem', item, commandStack); + }; + + return TextFieldEntry({ + element, + id: 'outputDataItem', + label: translate('Output Element'), + getValue, + setValue, + debounce, + description: 'The value of this variable will be added to the output collection' + }); +} + +function CompletionCondition(props) { + const { element } = props; + const debounce = useService('debounceInput'); + const translate = useService('translate'); + const commandStack = useService('commandStack'); + const bpmnFactory = useService('bpmnFactory'); + + const getValue = () => { + return getLoopProperty(element, 'completionCondition'); + }; + + const setValue = value => { + const completionCondition = bpmnFactory.create('bpmn:FormalExpression', {body: value}) + setLoopProperty(element, 'completionCondition', completionCondition, commandStack); + }; + + return TextFieldEntry({ + element, + id: 'completionCondition', + label: translate('Completion Condition'), + getValue, + setValue, + debounce, + description: 'Stop executing this task when this condition is met' + }); +} + diff --git a/app/spiffworkflow/loops/propertiesPanel/StandardLoopPropertiesProvider.js b/app/spiffworkflow/loops/propertiesPanel/StandardLoopPropertiesProvider.js new file mode 100644 index 000000000..491e199f5 --- /dev/null +++ b/app/spiffworkflow/loops/propertiesPanel/StandardLoopPropertiesProvider.js @@ -0,0 +1,133 @@ +import { is } from 'bpmn-js/lib/util/ModelUtil'; +import { useService } from 'bpmn-js-properties-panel'; +import { + Group, + TextFieldEntry, + isTextFieldEntryEdited, + CheckboxEntry, + isCheckboxEntryEdited, +} from '@bpmn-io/properties-panel'; + +import { getLoopProperty, setLoopProperty } from './LoopProperty'; + +const LOW_PRIORITY = 500; + +export default function StandardLoopPropertiesProvider(propertiesPanel) { + this.getGroups = function getGroupsCallback(element) { + return function pushGroup(groups) { + if ( + (is(element, 'bpmn:Task') || is(element, 'bpmn:CallActivity') || is(element, 'bpmn:SubProcess')) && + typeof(element.businessObject.loopCharacteristics) !== 'undefined' && + element.businessObject.loopCharacteristics.$type === 'bpmn:StandardLoopCharacteristics' + ) { + const group = { + id: 'standardLoopCharacteristics', + component: Group, + label: 'Standard Loop', + entries: StandardLoopProps(element), + shouldOpen: true, + }; + if (groups.length < 3) + groups.push(group); + else + groups.splice(2, 0, group); + } + return groups; + }; + }; + propertiesPanel.registerProvider(LOW_PRIORITY, this); +} + +StandardLoopPropertiesProvider.$inject = ['propertiesPanel']; + +function StandardLoopProps(props) { + const { element } = props; + return [{ + id: 'loopMaximum', + component: LoopMaximum, + isEdited: isTextFieldEntryEdited + }, { + id: 'loopCondition', + component: LoopCondition, + isEdited: isTextFieldEntryEdited + }, { + id: 'testBefore', + component: TestBefore, + isEdited: isCheckboxEntryEdited + }]; +} + +function LoopMaximum(props) { + const { element } = props; + const debounce = useService('debounceInput'); + const translate = useService('translate'); + const commandStack = useService('commandStack'); + const bpmnFactory = useService('bpmnFactory'); + + const getValue = () => { + return getLoopProperty(element, 'loopMaximum'); + }; + + const setValue = value => { + setLoopProperty(element, 'loopMaximum', value, commandStack); + }; + + return TextFieldEntry({ + element, + id: 'loopMaximum', + label: translate('Loop Maximum'), + getValue, + setValue, + debounce + }); +} + +function TestBefore(props) { + const { element } = props; + const debounce = useService('debounceInput'); + const translate = useService('translate'); + const commandStack = useService('commandStack'); + const bpmnFactory = useService('bpmnFactory'); + + const getValue = () => { + return getLoopProperty(element, 'testBefore'); + }; + + const setValue = value => { + setLoopProperty(element, 'testBefore', value, commandStack); + }; + + return CheckboxEntry({ + element, + id: 'testBefore', + label: translate('Test Before'), + getValue, + setValue, + }); +} + +function LoopCondition(props) { + const { element } = props; + const debounce = useService('debounceInput'); + const translate = useService('translate'); + const commandStack = useService('commandStack'); + const bpmnFactory = useService('bpmnFactory'); + + const getValue = () => { + return getLoopProperty(element, 'loopCondition'); + }; + + const setValue = value => { + const loopCondition = bpmnFactory.create('bpmn:FormalExpression', {body: value}) + setLoopProperty(element, 'loopCondition', loopCondition, commandStack); + }; + + return TextFieldEntry({ + element, + id: 'loopCondition', + label: translate('Loop Condition'), + getValue, + setValue, + debounce + }); +} diff --git a/app/spiffworkflow/messages/MessageHelpers.js b/app/spiffworkflow/messages/MessageHelpers.js index 9c4e6fb36..dd06efaf8 100644 --- a/app/spiffworkflow/messages/MessageHelpers.js +++ b/app/spiffworkflow/messages/MessageHelpers.js @@ -148,14 +148,24 @@ function getRetrievalExpressionFromCorrelationProperty( export function findCorrelationProperties(businessObject, moddle) { const root = getRoot(businessObject, moddle); const correlationProperties = []; - for (const rootElement of root.rootElements) { - if (rootElement.$type === 'bpmn:CorrelationProperty') { - correlationProperties.push(rootElement); + if (isIterable(root.rootElements)) { + for (const rootElement of root.rootElements) { + if (rootElement.$type === 'bpmn:CorrelationProperty') { + correlationProperties.push(rootElement); + } } } return correlationProperties; } +function isIterable(obj) { + // checks for null and undefined + if (obj == null) { + return false; + } + return typeof obj[Symbol.iterator] === 'function'; +} + export function findCorrelationKeys(businessObject, moddle) { const root = getRoot(businessObject, moddle); const correlationKeys = []; diff --git a/app/spiffworkflow/messages/propertiesPanel/MessageSelect.js b/app/spiffworkflow/messages/propertiesPanel/MessageSelect.js index e1df995db..e6a171f6f 100644 --- a/app/spiffworkflow/messages/propertiesPanel/MessageSelect.js +++ b/app/spiffworkflow/messages/propertiesPanel/MessageSelect.js @@ -35,6 +35,9 @@ export function MessageSelect(props) { commandStack.execute('element.updateModdleProperties', { element: shapeElement, moddleElement: businessObject, + properties: { + messageRef: message, + }, }); } else if ( businessObject.$type === 'bpmn:ReceiveTask' || diff --git a/app/spiffworkflow/moddle/spiffworkflow.json b/app/spiffworkflow/moddle/spiffworkflow.json index 5987f874b..3228e4747 100644 --- a/app/spiffworkflow/moddle/spiffworkflow.json +++ b/app/spiffworkflow/moddle/spiffworkflow.json @@ -219,6 +219,28 @@ "type": "string" } ] + }, + { + "name": "payloadExpression", + "superClass": [ "Element" ], + "properties": [ + { + "name": "value", + "isBody": true, + "type": "String" + } + ] + }, + { + "name": "variableName", + "superClass": [ "Element" ], + "properties": [ + { + "name": "value", + "isBody": true, + "type": "String" + } + ] } ] } diff --git a/app/spiffworkflow/signals/index.js b/app/spiffworkflow/signals/index.js new file mode 100644 index 000000000..faf161efe --- /dev/null +++ b/app/spiffworkflow/signals/index.js @@ -0,0 +1,6 @@ +import SignalPropertiesProvider from './propertiesPanel/SignalPropertiesProvider'; + +export default { + __init__: ['signalPropertiesProvider'], + signalPropertiesProvider: ['type', SignalPropertiesProvider], +} diff --git a/app/spiffworkflow/signals/propertiesPanel/SignalPropertiesProvider.js b/app/spiffworkflow/signals/propertiesPanel/SignalPropertiesProvider.js new file mode 100644 index 000000000..8a493d28e --- /dev/null +++ b/app/spiffworkflow/signals/propertiesPanel/SignalPropertiesProvider.js @@ -0,0 +1,49 @@ +import { is } from 'bpmn-js/lib/util/ModelUtil'; +import { getRoot } from '../../helpers'; +import { getArrayForType, getListGroupForType } from '../../eventList.js'; +import { hasEventType, + replaceGroup, + getSelectorForType, + getConfigureGroupForType +} from '../../eventSelect.js'; + +const LOW_PRIORITY = 500; + +const eventDetails = { + 'eventType': 'bpmn:Signal', + 'eventDefType': 'bpmn:SignalEventDefinition', + 'referenceType': 'signalRef', + 'idPrefix': 'signal', +}; + +export default function SignalPropertiesProvider( + propertiesPanel, + translate, + moddle, + commandStack, +) { + + this.getGroups = function (element) { + return function (groups) { + if (is(element, 'bpmn:Process') || is(element, 'bpmn:Collaboration')) { + const getSignalArray = getArrayForType('bpmn:Signal', 'signalRef', 'Signal'); + const signalGroup = getListGroupForType('signals', 'Signals', getSignalArray); + groups.push(signalGroup({ element, translate, moddle, commandStack })); + } else if (hasEventType(element, 'bpmn:SignalEventDefinition')) { + const getSignalSelector = getSelectorForType(eventDetails); + const signalGroup = getConfigureGroupForType(eventDetails, 'Signal', false, getSignalSelector); + const group = signalGroup({ element, translate, moddle, commandStack }); + replaceGroup('signal', groups, group); + } + return groups; + }; + }; + propertiesPanel.registerProvider(LOW_PRIORITY, this); +} + +SignalPropertiesProvider.$inject = [ + 'propertiesPanel', + 'translate', + 'moddle', + 'commandStack', +]; diff --git a/test/spec/DataObjectInPoolsSpec.js b/test/spec/DataObjectInPoolsSpec.js new file mode 100644 index 000000000..e3a9f8ca4 --- /dev/null +++ b/test/spec/DataObjectInPoolsSpec.js @@ -0,0 +1,66 @@ +import { + BpmnPropertiesPanelModule, + BpmnPropertiesProviderModule, +} from 'bpmn-js-properties-panel'; +import TestContainer from 'mocha-test-container-support'; +import { + bootstrapPropertiesPanel, + changeInput, + expectSelected, + findEntry, + findSelect, +} from './helpers'; +import spiffModdleExtension from '../../app/spiffworkflow/moddle/spiffworkflow.json'; +import DataObject from '../../app/spiffworkflow/DataObject'; + +describe('Properties Panel for Data Objects', function () { + const xml = require('./bpmn/data_objects_in_pools.bpmn').default; + let container; + + beforeEach(function () { + container = TestContainer.get(this); + }); + + beforeEach( + bootstrapPropertiesPanel(xml, { + container, + debounceInput: false, + additionalModules: [ + DataObject, + BpmnPropertiesPanelModule, + BpmnPropertiesProviderModule, + ], + moddleExtensions: { + spiffworkflow: spiffModdleExtension, + }, + }) + ); + + it('should allow you to select other data objects within the same participant', async function () { + // IF - a data object reference is selected + const doREF = await expectSelected('pool1Do1_REF'); + expect(doREF).to.exist; + + // THEN - a select Data Object section should appear in the properties panel + const entry = findEntry('selectDataObject', container); + const selector = findSelect(entry); + changeInput(selector, 'pool1Do2'); + // then this data reference object now references that data object. + const { businessObject } = doREF; + expect(businessObject.get('dataObjectRef').id).to.equal('pool1Do2'); + }); + + it('should NOT allow you to select data objects within other participants', async function () { + // IF - a data object reference is selected + const doREF = await expectSelected('pool1Do1_REF'); + expect(doREF).to.exist; + + // THEN - a select Data Object section should appear in the properties panel but pool2Do1 should not be an option + const entry = findEntry('selectDataObject', container); + const selector = findSelect(entry); + expect(selector.length).to.equal(2); + expect(selector[0].value === 'pool1Do2'); + expect(selector[1].value === 'pool1Do1'); + }); + +}); diff --git a/test/spec/DataObjectPropsSpec.js b/test/spec/DataObjectPropsSpec.js index 49b1abbba..aa97a50d4 100644 --- a/test/spec/DataObjectPropsSpec.js +++ b/test/spec/DataObjectPropsSpec.js @@ -6,9 +6,6 @@ import { import { BpmnPropertiesPanelModule, BpmnPropertiesProviderModule } from 'bpmn-js-properties-panel'; import spiffModdleExtension from '../../app/spiffworkflow/moddle/spiffworkflow.json'; import TestContainer from 'mocha-test-container-support'; -import DataObjectPropertiesProvider - from '../../app/spiffworkflow/DataObject/propertiesPanel/DataObjectPropertiesProvider'; -import spiffworkflow from '../../app/spiffworkflow'; import DataObject from '../../app/spiffworkflow/DataObject'; describe('Properties Panel for Data Objects', function() { @@ -78,4 +75,17 @@ describe('Properties Panel for Data Objects', function() { expect(my_data_ref_1.businessObject.name).to.equal('My Nifty New Name'); }); + it('renaming a data object creates a lable without losing the numbers', async function() { + + // IF - a process is selected, and the name of a data object is changed. + let entry = findEntry('ProcessTest-dataObj-2-id', container); + let textInput = findInput('text', entry); + changeInput(textInput, 'MyObject1'); + let my_data_ref_1 = await expectSelected('my_data_ref_1'); + + // THEN - both the data object itself, and the label of any references are updated. + expect(my_data_ref_1.businessObject.dataObjectRef.id).to.equal('MyObject1'); + expect(my_data_ref_1.businessObject.name).to.equal('My Object 1'); + }); + }); diff --git a/test/spec/bpmn/data_objects_in_pools.bpmn b/test/spec/bpmn/data_objects_in_pools.bpmn new file mode 100644 index 000000000..420d116a2 --- /dev/null +++ b/test/spec/bpmn/data_objects_in_pools.bpmn @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/spec/bpmn/request_new_role.bpmn b/test/spec/bpmn/request_new_role.bpmn new file mode 100644 index 000000000..c34d2e3df --- /dev/null +++ b/test/spec/bpmn/request_new_role.bpmn @@ -0,0 +1,2984 @@ + + + + + + + + + Gateway_1bz36qs + Gateway_1ci8c4i + Event_15gc86r + Event_1lr6j50 + Gateway_0dvdlwh + Gateway_0gpvfm8 + Gateway_0z3562z + Activity_05solt4 + Activity_05ou4v1 + Activity_1vw9byu + Activity_0o5rhui + Activity_1e64lzx + Activity_1dpvgq0 + + + Activity_0h7ft7p + Gateway_0ip2pbu + Activity_0apmo1t + Activity_0adfss7 + Activity_00bqo01 + + + Activity_04ium7h + Gateway_09pngbv + + + Event_14e2or3 + Activity_1el69ef + Activity_0cac2ge + + + + + + + Flow_1rpyrgl + Flow_0m414q7 + Flow_154casc + + + Flow_1gpngr0 + Flow_1g8h223 + Flow_0xz0rte + + + Flow_1awy4f9 + + + + Process Recap +--- +The request has been cancelled and will be removed from Instances waiting on you. + +**Status**: {{ trvl_details["status"] }} + +**Requestor**: {{ trvl_details["requestor"] }} + + Flow_1g8h223 + + + Flow_1ieb889 + Flow_0dxc4uz + Flow_1r7byc8 + + + Flow_0idzytl + Flow_0m414q7 + Flow_0mi1iph + + + Flow_1r7byc8 + Flow_1hb6m2j + Flow_1svj7af + + + Flow_0xz0rte + Flow_0nom5oy + # Set/Clear Last Approval Outcome +last_approval_outcome = "" + +# Set Approval Type Agnostic Requestor Authentication Username +requestor_username = trvl_details["requestor_auth_username"] +cc_requested_for_username = trvl_details["core_contributor_work_email"] + +# Set variables for Waku messages +waku_msg_project = trvl_details["project_name"] +waku_msg_category = trvl_details["category_name"] +waku_msg_total_amount = trvl_details["total_request_amount_usd"] +waku_msg_criticality = trvl_details["criticality"] +waku_msg_requestor = trvl_details["requestor"] + +# Delete Unneeded variables +del(is_request_cancelled) + + + + # Set Current Role +current_approver_role = "Budget Owner" + +# Set Current Requestor Bamboo API ID +requestor_bamboo_eid = trvl_details["requestor_bamboo_eid"] + # Clear Last Approval Role +del(current_approver_role) + +# Resat Feviewer Outcome +reviewer_outcome = "" + + + Flow_0nom5oy + Flow_1ieb889 + + + + # Set Current Role +current_approver_role = "PeopleOps Partner" + +# Default Escalation to Budget Owner to False +is_escalate = False + # Clear Last Approval Role +del(current_approver_role) + +# Resat Feviewer Outcome +reviewer_outcome = "" + + Flow_0dxc4uz + Flow_1hb6m2j + + + Flow_1awy4f9 + Flow_1rpyrgl + + + + + + Flow_1k6ur0k + Flow_0c9yzka + + + Flow_1k6ur0k + + + + + + + Flow_0c9yzka + Flow_1n7gshu + # Create Details Dictionary +trvl_details = {} + +# Set Requestor First, Last and Preferred Names +trvl_details["requestor_first_name"] = bamboo_get_employee["firstName"] +trvl_details["requestor_last_name"] = bamboo_get_employee["lastName"] +trvl_details["requestor_preferred_name"] = bamboo_get_employee["preferredName"] + +# Set Requestor Full and Greet Names +if bamboo_get_employee["preferredName"] is None: + trvl_details["requestor"] = bamboo_get_employee["firstName"] + " " + bamboo_get_employee["lastName"] + trvl_details["requestor_greet_name"] = bamboo_get_employee["firstName"] + +else: + trvl_details["requestor"] = bamboo_get_employee["preferredName"] + " " + bamboo_get_employee["lastName"] + trvl_details["requestor_greet_name"] = bamboo_get_employee["preferredName"] + +# Set Approval Source Agnostic Requestor Greet Name +requestor_greet_name = trvl_details["requestor_greet_name"] + +# Set Requestor Ids +trvl_details["requestor_status_key"] = bamboo_get_employee["customStatusPublicKey"] +trvl_details["requestor_bamboo_eid"] = bamboo_get_employee["id"] +trvl_details["requestor_auth_username"] = auth_user_info["username"] + +# Set Default Core Contributor Request is for to Requestor +# trvl_details["core_contributor_test"] = r'{\"id\":\"' + trvl_details["requestor_bamboo_eid"] + r'\",\"last_name\":\"' + trvl_details["requestor_last_name"] + r'\",\"first_name\":\"' + trvl_details["requestor_first_name"] + r'\"}' +req_as_cc = {} +req_as_cc["id"] = trvl_details["requestor_bamboo_eid"] +req_as_cc["last_name"] = trvl_details["requestor_last_name"] +if bamboo_get_employee["preferredName"] is None: + req_as_cc["first_name"] = trvl_details["requestor_first_name"] +else: + req_as_cc["first_name"] = trvl_details["requestor_first_name"] + " (" + trvl_details["requestor_preferred_name"] + ")" +req_as_cc["workEmail"] = bamboo_get_employee["workEmail"] + +trvl_details["core_contributor"] = json.dumps(req_as_cc) + +# Delete Unneeded Variables +del( bamboo_get_employee) +del(auth_user_info) + +# Initialize Approver List +approvers = [] + +# Initialize Approval History +approval_history = [] + +# Set Approveal Source +approval_source = "TRVL" + +# Set Reviewer Comments List +review_comment_list = [] +review_comment_cnt = 0 + +# Set Defalt reviewer Comment +nai_reviewer_comment = {} +nai_reviewer_comment["Reviewer Comment"] = "" + +# Set Frontend URLs +prod_frontend_url = "https://prod.mod.spiff.status.im" + +# Add additional variables +is_additional_information = False +is_supporting_files = False +is_supporting_information = False +requestor_edit_option = False +non_book_check_cnt = 0 +last_approval_outcome = "" + + + Flow_1n7gshu + + + + + + + # Delete Unneeded Variables +del(travel_sub_category_enum_list) +del(currency_type_enum_list) +del(currency_enum_list) +del(dept_enum_list) +del(category_enum_list) +del(event_type_enum_list) + # Initiate Request Cancelled to False +is_request_cancelled = False + + Flow_154casc + Flow_1gpngr0 + + Flow_13sdhgh + Flow_0fhz1yp + + Flow_0rt9rpn + + + Flow_0rt9rpn + Flow_1yboqgc + Flow_1regjqs + Flow_0ijiqtp + Flow_1lvqe7v + Flow_0org7jx + Flow_1e8hadk + + + Flow_0rydcxn + Flow_1ihkyvi + Flow_0zaclsu + Flow_0h3piq7 + Flow_1dcxvqq + Flow_18n2gwv + Flow_0n82r7f + + + Get Form Data Call Activity is used for getting required data for User Tasks. + + # Set Task Name +which_bamboo_depart_div_data_rule = "Department Enum List" + # Exclude non-Project departments +dept_enum_list = [x for x in dept_enum_list if x["value"] not in ["18589","19125","19068","19110","19111","19126","19032","18737","19004","19147","19009","19033","19115"]] + +del(which_bamboo_depart_div_data_rule) + Loading Projects from Bamboo + + Flow_1yboqgc + Flow_0rydcxn + + + + + + + + # Set Enum List Parameters +whichSubject = "Sub-Category" +whichGrouping = "travel" +addNew = False +addBlank = False + # Copy to Category Enum List +travel_sub_category_enum_list = enumerations_list + +# Remove Meals Sub-category +travel_sub_category_enum_list = [x for x in travel_sub_category_enum_list if x["value"] != "meals"] + +# Delete generic list +del(enumerations_list) +del(whichSubject) +del(whichGrouping) +del(addNew) +del(addBlank) + + Flow_1regjqs + Flow_0zaclsu + + + + + Flow_1m73brt + + + Flow_0n82r7f + Flow_1m73brt + # Set Currency Type Enum List +currency_type_enum_list = [ + { + "label": "Crypto", + "value": "crypto" + }, + { + "label": "Fiat", + "value": "fiat" + } +] + +no_budget_owners_proceed = "" + +# Set Travel Category Enum List +category_enum_list = [ + { + "label": "Travel", + "value": "travel" + } +] + + + + + + # Set Enum List Parameters +whichSubject = "Event Type" +whichGrouping = "" +addNew = False +addBlank = False + # Copy to Category Enum List +event_type_enum_list = enumerations_list + +# Delete generic list +del(enumerations_list) +del(whichSubject) +del(whichGrouping) +del(addNew) +del(addBlank) + + Flow_1lvqe7v + Flow_0h3piq7 + + + + + + currency_enum_list = [ + { + "label": "United Arab Emirates Dirham (AED)", + "value": "AED" + }, + { + "label": "Australian Dollar (AUD)", + "value": "AUD" + }, + { + "label": "Canadian Dollar (CAD)", + "value": "CAD" + }, + { + "label": "Swiss Franc (CHF)", + "value": "CHF" + }, + { + "label": "Chinese Yuan (CNY)", + "value": "CNY" + }, + { + "label": "Colombian Peso (COP)", + "value": "COP" + }, + { + "label": "Euro (EUR)", + "value": "EUR" + }, + { + "label": "British Pound (GBP)", + "value": "GBP" + }, + { + "label": "Hong Kong Dollar (HKD)", + "value": "HKD" + }, + { + "label": "Japanese Yen (JPY)", + "value": "JPY" + }, + { + "label": "New Zealand Dollar (NZD)", + "value": "NZD" + }, + { + "label": "Singapore Dollar (SGD)", + "value": "SGD" + }, + { + "label": "United States Dollar (USD)", + "value": "USD" + } + ] + + + + Loading currencies from Xero + + Flow_0ijiqtp + Flow_1ihkyvi + + + Flow_0org7jx + Flow_1dcxvqq + + + Flow_1e8hadk + Flow_18n2gwv + + + + + + + + Departments, Divisions + + + temp fix + + + + Create a list of Programs, list of Projects + + + + PostgresDB or Spiff? + + + + Talent Level Guidance - to make it a step within the process + + + + Suggest it will be hard-coded in process model + + + + Connect to MercerDB? + + + + Can we use a spreadsheet as a source of truth? + + + + + Flow_0fhz1yp + Flow_1e5xai6 + Flow_1w54t94 + Flow_0dgrlbd + + + Flow_0zen0hg + Flow_1wk4ctc + + + Flow_1uda7lq + Flow_1b7wncj + + + Flow_18911q5 + Flow_1wixb6f + + + + Flow_13sdhgh + + + + + + + Flow_17xdzln + + + + Flow_1wixb6f + Flow_0zb8ksl + Flow_17xdzln + + + + Flow_0gs9a10 + Flow_0zb8ksl + # Set Request to Cancelled +is_request_cancelled = True + +# Set Final Status to Cancelled +trvl_details["status"] = "Cancelled" + + + + **Figma - New Demand request (Procurement) - SUBMIT**, https://www.figma.com/file/9NP2BUoLuwHUCGStvDMgOw/Form?node-id=0%3A1&t=BABeo58RkNviJr4N-0 + +**Fields details** - https://docs.google.com/spreadsheets/d/19S85IJeNXffPa1oAe6kSkmTleMWE92A4kWJFFodCEZU/edit#gid=1736626389 + + + + + + --- +Please ensure to provide all the necessary details regarding your travel request. These details are crucial for the Project Leads and PeopleOps Partners to review your request and make the necessary arrangements. + +If you need guidance on the travel policy, it may be helpful to check <a href="https://contributors.status.im/contributing-to-status/travelling-for-status.html" target="_blank" >Contributors Guide</a>. + +To get an overview of the process, you can check the process model or read <a href="https://www.notion.so/Request-Travel-06db3450784e4320adb425688d8dbe3f?pvs=4" target="_blank" >Process Description</a>. + +--- + +{% if is_additional_information %} +### Reviewer Additional Information Request + +| Requested by | <div style="width:90px">Date<div> | <div style="width:65px">Time<div> | Additional Information Requested | +| ----------------- | ----- | ----- | -------------------------------------- | +| {{ nai_reviewer_comment["Reviewer Role"] }} | {{ nai_reviewer_comment["Reviewer Comment Date"] }} | {{ nai_reviewer_comment["Reviewer Comment Time"] }} | {{ nai_reviewer_comment["Reviewer Comment"] }} | + + +--- + +{% endif %} + # Get Frontend URL +frontend_url = get_frontend_url() + +# Set BO check variable +budget_owner_check = trvl_details["project"] + +# Get Project Label +temp_project_name = [x["label"] for x in dept_enum_list if x["value"] == trvl_details["project"]] +trvl_details["project_name"] = temp_project_name[0] +del(temp_project_name) + +# Get Category Label +temp_category_name = [x["label"] for x in category_enum_list if x["value"] == trvl_details["category"]] +trvl_details["category_name"] = temp_category_name[0] +del(temp_category_name) + +# Get Event Type Label +temp_event_type_name = [x["label"] for x in event_type_enum_list if x["value"] == trvl_details["event_type"]] +trvl_details["event_type_name"] = temp_event_type_name[0] +del(temp_event_type_name) + +# Set Period (Event Date) Display Date Format +trvl_details["period_display"] = trvl_details["period"][8:10] + "-" + trvl_details["period"][5:7] + "-" + trvl_details["period"][:4] + +# Remove any linefeeds from from Purpose +purpose_double = trvl_details["purpose"] +purpose_single = purpose_double.replace("\n\n", " ") +purpose_removed = purpose_single.replace("\n", " ") +trvl_details["purpose"] = purpose_removed +del(purpose_double) +del(purpose_single) +del(purpose_removed) + +# Set Period (Start Date) Display Date Format +trvl_details["start_date_display"] = trvl_details["start_date"][8:10] + "-" + trvl_details["start_date"][5:7] + "-" + trvl_details["start_date"][:4] + +# Set Period (End Date) Display Date Format +trvl_details["end_date_display"] = trvl_details["end_date"][8:10] + "-" + trvl_details["end_date"][5:7] + "-" + trvl_details["end_date"][:4] + +# Get Frontend URL +frontend_url = get_frontend_url() + +# Set BO check variable +budget_owner_check = trvl_details["project"] + +# Set Edit Option to True +requestor_edit_option = True + +# Set Event Destination Display +csc_json_string_event = trvl_details["event_destination"] +csc_dict_event = json.loads(csc_json_string_event) +city_event = csc_dict_event["name"] +state_event = csc_dict_event["state"] +country_event = csc_dict_event["country"] +trvl_details["event_destination_display"] = city_event + " (" + state_event + ", " + country_event + ")" + +# Set Departure From Display +csc_json_string_dep = trvl_details["departure_from"] +csc_dict_dep = json.loads(csc_json_string_dep) +city_dep = csc_dict_dep["name"] +state_dep = csc_dict_dep["state"] +country_dep = csc_dict_dep["country"] +trvl_details["departure_from_display"] = city_dep + " (" + state_dep + ", " + country_dep + ")" + +# Set Return To Display +csc_json_string_ret = trvl_details["return_to"] +csc_dict_ret = json.loads(csc_json_string_ret) +city_ret = csc_dict_ret["name"] +state_ret = csc_dict_ret["state"] +country_ret = csc_dict_ret["country"] +trvl_details["return_to_display"] = city_ret + " (" + state_ret + ", " + country_ret + ")" + +# Set Core Contributor Values and Display +csc_json_string_cc = trvl_details["core_contributor"] +csc_dict_cc = json.loads(csc_json_string_cc) +last_name_cc = csc_dict_cc["last_name"] +first_name_cc = csc_dict_cc["first_name"] +trvl_details["core_contributor_display"] = first_name_cc + " " + last_name_cc +trvl_details["core_contributor_id"] = csc_dict_cc["id"] +trvl_details["core_contributor_work_email"] = csc_dict_cc["workEmail"] + +# Set Visa display - Replace True or False with actual value +trvl_details["visa_display"] = "" +if trvl_details["visa"] == True: + trvl_details["visa_display"] = "Yes" +else: + trvl_details["visa_display"] = "No" + +del(city_event) +del(state_event) +del(country_event) +del(csc_dict_event) +del(csc_json_string_event) +del(city_dep) +del(state_dep) +del(country_dep) +del(csc_dict_dep) +del(csc_json_string_dep) +del(city_ret) +del(state_ret) +del(country_ret) +del(csc_dict_ret) +del(csc_json_string_ret) +del(csc_json_string_cc) +del(csc_dict_cc) +del(last_name_cc) +del(first_name_cc) + + + Flow_1sx4un4 + Flow_0xus3om + + + Flow_0xus3om + Flow_0ch4cxy + Flow_0op21ag + + + + # Delete Unneeded Variable +del(frontend_url) + + Flow_0op21ag + Flow_119j0el + + + + # Delete Unneeded Variable +del(frontend_url) + + Flow_0ch4cxy + Flow_18qe7ut + + + Flow_05hzfur + Flow_1whl6cg + + + # Set Xe Conversion Parameters +xe_convert_from_currency = trvl_details["item"][item_list_cnt]["currency"] +xe_convert_to_currency = "USD" +xe_amount = trvl_details["item"][item_list_cnt]["unit_price_num"] +xe_decimal_places = 2 + # Delete Unneeded Variables +del(xe_convert_from_currency) +del(xe_convert_to_currency) +del(xe_amount) +del(xe_decimal_places) + Getting currency rates from XE + + Flow_0oj2rzg + Flow_0yzymjg + + + Flow_04gxo4i + Flow_0e95pd0 + # Set List Count and Counter +item_list_cnt = 0 +item_list_total = len(trvl_details["item"]) + +# Initially set Request Exceeds Threshold to False +request_exceeds_threshold = False + +# Set Default Value for Crypto Conversion Check +is_xe = False + +# Set Item Name and URl lists +is_item_url_list = [] +item_url_list = [] +item_name_list = [] + + + Flow_11jp113 + Flow_1s5j2un + # Get Process Instance +process_info = get_toplevel_process_info() +process_instance_id = process_info["process_instance_id"] +process_instance_id_str = str(process_instance_id) +del(process_info) + +# Add Process Instance Id, for Postgres extract +trvl_details["item"][item_list_cnt]["process_instance_id"] = process_instance_id_str + +# If selected, add Fiat conversion info +if trvl_details["item"][item_list_cnt]["currency_type"] == "fiat": + trvl_details["item"][item_list_cnt]["converted_to_unit_price"] = round(xe_convert_from["to"][0]["mid"], 2) + trvl_details["item"][item_list_cnt]["xe_usd_mid"] = xe_convert_from["to"][0]["mid"] + trvl_details["item"][item_list_cnt]["unit_price_total"] = round(trvl_details["item"][item_list_cnt]["qty"] * trvl_details["item"][item_list_cnt]["unit_price_num"], 2) + trvl_details["item"][item_list_cnt]["xe_usd_mid_date"] = local_date_str + trvl_details["item"][item_list_cnt]["cg_usd_mid_time"] = local_time_str + del(xe_convert_from) + +# If selected, add Crypto conversion info +if trvl_details["item"][item_list_cnt]["currency_type"] == "crypto": + trvl_details["item"][item_list_cnt]["converted_to_unit_price"] = round(cg_usd_conversion_rate * trvl_details["item"][item_list_cnt]["unit_price_num"], 4) + trvl_details["item"][item_list_cnt]["cg_usd_conversion_rate"] = cg_usd_conversion_rate + trvl_details["item"][item_list_cnt]["unit_price_total"] = round(trvl_details["item"][item_list_cnt]["qty"] * trvl_details["item"][item_list_cnt]["unit_price_num"], 4) + trvl_details["item"][item_list_cnt]["cg_usd_conversion_rate_date"] = local_date_str + trvl_details["item"][item_list_cnt]["cg_usd_conversion_rate_time"] = local_time_str + +# Calculate Converted to unit price total +trvl_details["item"][item_list_cnt]["converted_to_currency"] = "USD" +trvl_details["item"][item_list_cnt]["converted_to_unit_price_total"] = round(trvl_details["item"][item_list_cnt]["qty"] * trvl_details["item"][item_list_cnt]["converted_to_unit_price"], 2) + +# Delete Unneeded Variables +del(local_date_str) +del(local_time_str) + + + Flow_04gxo4i + + + Flow_1fr8h2a + Flow_03hk9xl + Flow_0wmxs2e + + + Flow_0e95pd0 + Flow_0wmxs2e + Flow_0r6exl3 + + + Flow_08d1pda + Flow_1c12pyi + # Set Item Unit Price to Numeric Value +trvl_details["item"][item_list_cnt]["unit_price_num"] = float((trvl_details["item"][item_list_cnt]["unit_price"]).replace(',', '')) + +# Set Sub-Category Name +temp_sub_category_name = [x["label"] for x in travel_sub_category_enum_list if x["value"] == trvl_details["item"][item_list_cnt]["sub_category"]] + +trvl_details["item"][item_list_cnt]["sub_category_name"] = temp_sub_category_name[0] +del(temp_sub_category_name) + +# Get Currency Type Label +temp_currency_type_name = [x["label"] for x in currency_type_enum_list if x["value"] == trvl_details["item"][item_list_cnt]["currency_type"]] +trvl_details["item"][item_list_cnt]["currency_type_name"] = temp_currency_type_name[0] +del(temp_currency_type_name) + + + Flow_11bg9z9 + Flow_077coya + # Calculate total UDS anount +converted_to_unit_price_total_sum = sum(v.get('converted_to_unit_price_total', 0) for v in trvl_details["item"]) +trvl_details["meals_cost"] +converted_to_unit_price_total_sum_rounded = round(converted_to_unit_price_total_sum, 2) +trvl_details["total_request_amount_usd"] = converted_to_unit_price_total_sum_rounded +trvl_details["total_request_amount_usd_display"] = "{:,.2f}".format(trvl_details["total_request_amount_usd"]) + +# Delete Unneeded Variable +if is_xe: + del(xe_amount) + del(xe_convert_from) + del(xe_convert_from_currency) + del(xe_convert_to_currency) + del(xe_decimal_places) + +del(exceeds_threshold) +del(item_list_cnt) +del(is_xe) +del(converted_to_unit_price_total_sum) +del(converted_to_unit_price_total_sum_rounded) +del(process_instance_id_str) + + + Flow_1c12pyi + Flow_0oj2rzg + Flow_0y3b16d + + + Flow_0yzymjg + Flow_1gdgdff + Flow_0mj9wyr + + + + # Set Convert From Coin Symbol +coin_symbol = trvl_details["item"][item_list_cnt]["currency"] + + Getting currency rates from CoinGecko + + Flow_0y3b16d + Flow_1gdgdff + + + + # Set input variables +threshold_category = trvl_details["category"] +threshold_sub_category = trvl_details["item"][item_list_cnt]["sub_category"] +exceeds_check_unit_price = trvl_details["item"][item_list_cnt]["converted_to_unit_price"] + # Set Threshold +trvl_details["item"][item_list_cnt]["exceeds_threshold"] = exceeds_threshold + +# If any item exceeds threshold, request exceeds threshold +if exceeds_threshold: + request_exceeds_threshold = True + +# Increase Item Id +trvl_details["item"][item_list_cnt]["item_id"] = item_list_cnt + 1 + +# Increase Item Count +item_list_cnt = item_list_cnt + 1 + +# Delete variables +del(threshold_category) +del(threshold_sub_category) +del(exceeds_check_unit_price) + + Flow_1s5j2un + Flow_1fr8h2a + + + Flow_0mj9wyr + Flow_11jp113 + + + Flow_077coya + + + Flow_03hk9xl + Flow_11bg9z9 + # Calculate total per diem meal cost +start_object = datetime.strptime(trvl_details["start_date"], '%Y-%m-%d') +end_object = datetime.strptime(trvl_details["end_date"], '%Y-%m-%d') +number_of_days = (end_object - start_object).days + 1 +trvl_details["number_of_days"] = number_of_days + +# Meals in USD +meals_cost = number_of_days * 75 +trvl_details["meals_cost"] = meals_cost +trvl_details["meals_cost_display"] = "{:,.2f}".format(trvl_details["meals_cost"]) + +del(start_object) +del(end_object) +del(meals_cost) +del(number_of_days) + + + trvl_details["item"][item_list_cnt]["currency_type"] == "fiat" + + + + + + + + + + item_list_cnt < item_list_total + + + + + + trvl_details["item"][item_list_cnt]["currency_type"] == "crypto" + + + + + + + + Flow_0r6exl3 + Flow_08d1pda + # Check for Item URl +if "item_url" in trvl_details["item"][item_list_cnt]: + is_item_url_list.append(True) + item_url_list.append(trvl_details["item"][item_list_cnt]["item_url"]) +else: + is_item_url_list.append(False) + item_url_list.append("") + +# Set Ite m Name +item_name_list.append(trvl_details["item"][item_list_cnt]["item_name"]) + + + + + + Flow_119j0el + Flow_18qe7ut + Flow_0y8xob6 + + + Flow_0y8xob6 + Flow_19t1udw + Flow_05hzfur + + + + frontend_url == "https://prod.spiffworkflow.org" + + + + l1_budget_owner_bamboo_eid == 0 or l2_budget_owner_bamboo_eid == 0 + + + + + + + + + Cancel Request + + Flow_0z8xl4u + + + + + + + + + --- +The Project you selected is not configured for the approval process at this time. Click Continue to select a different Project or Cancel the Request. + +--- + + Flow_19t1udw + Flow_1e5xai6 + + + + + Cancel Request + + Flow_0zen0hg + + + + + + + + + --- + +**Requestor**: {{ trvl_details["requestor"] }} + +--- + +| | | +| --------------: | -------------------------------- | +|**Traveller** | {{ trvl_details["core_contributor_display"] }} | +|**Purpose** | {{ trvl_details["purpose"] }} | + +| Project | Event Type | Criticality | Event Date | Event Name | Event Destination | Visa required? | +| -------------------------------- | -------------------------------- | -------------------------------- | -------------------------------- | -------------------------------- | -------------------------------- | ----- | +| {{ trvl_details["project_name"] }} | {{ trvl_details["event_type_name"] }} | {{ trvl_details["criticality"] }} | {{ trvl_details["period_display"] }} | {{ trvl_details["event_name"] }} | {{ trvl_details["event_destination_display"] }} | {{ trvl_details["visa_display"] }} | + +---------- +### Itinerary + +**Departure**: {{ trvl_details["start_date_display"] }} , {{ trvl_details["departure_from_display"] }} + +**Return**: {{ trvl_details["end_date_display"] }}, {{ trvl_details["return_to_display"] }} + +**Number of days**: {{ trvl_details["number_of_days"] }} + +--- +### Items +| Item | Sub-Category | Qty | Currency | Unit Price | Total Price | USD Price | Total USD Price | +| ---- | ----------------- | :---: | :---------: | ----------: | ------------: | -----------: | -----------------: | +| Per diem | Meals | {{ trvl_details["number_of_days"] }} | USD | 75.00 | {{ trvl_details["meals_cost_display"] }} | 75.00 | {{ trvl_details["meals_cost_display"] }} | +{% for icnt in range(item_list_total) %} +| {% if is_item_url_list[icnt] %}<a href="{{ item_url_list[icnt] }}" target="_blank" >{{ item_name_list[icnt] }}</a>{% else %}{{ item_name_list[icnt] }}{% endif %} | {{ trvl_details["item"][icnt]["sub_category_name"] }} | {{ trvl_details["item"][icnt]["qty"] }} | {{ trvl_details["item"][icnt]["currency"] }} | {% if trvl_details["item"][icnt]["currency_type"] == "crypto" %}{{ "{:,.4f}".format(trvl_details["item"][icnt]["unit_price_num"]) }}{% else %}{{ "{:,.2f}".format(trvl_details["item"][icnt]["unit_price_num"]) }}{% endif %} | {% if trvl_details["item"][icnt]["currency_type"] == "crypto" %}{{ "{:,.4f}".format(trvl_details["item"][icnt]["unit_price_total"]) }}{% else %}{{ "{:,.2f}".format(trvl_details["item"][icnt]["unit_price_total"]) }}{% endif %} | {{ "{:,.2f}".format(trvl_details["item"][icnt]["converted_to_unit_price"]) }} | {{ "{:,.2f}".format(trvl_details["item"][icnt]["converted_to_unit_price_total"]) }} | +{% endfor %} +---------- +### Total Request: {{ "{:,.2f}".format(trvl_details["total_request_amount_usd"]) }} USD** + + +<sub> *Fiat - Exchange Rates used to calculate the "USD" amounts are under <a href="http://www.xe.com/" target="_blank" >license from Xe</a>. Please note that if you use this Xe Data, you are obliged to comply with <a href="http://www.xe.com/legal/dfs.php" target="_blank" >Xe's end terms of use</a>. <sub> + +<sub> *Crypto - Exchange Rates used to calculate the "USD" amounts are received from <a href="https://www.coingecko.com/en/api" target="_blank" >CoinGecko</a>.<sub> + +---------- +{% if is_additional_information %} +### Reviewer Additional Information Request + +| Requested by | <div style="width:90px">Date<div> | <div style="width:65px">Time<div> | Additional Information Requested | +| ----------------- | ----- | ----- | -------------------------------------- | +| {{ nai_reviewer_comment["Reviewer Role"] }} | {{ nai_reviewer_comment["Reviewer Comment Date"] }} | {{ nai_reviewer_comment["Reviewer Comment Time"] }} | {{ nai_reviewer_comment["Reviewer Comment"] }} | + + +--- + +{% endif %} + # If no addtion details were provided, set more_details to blank +try: + supporting_information +except NameError: + supporting_information = "" + +# Determine if additional details were provided +is_supporting_information = len(supporting_information) > 0 + +# if Supporting INformation, Remove any linefeeds +if is_supporting_information: + supporting_information_double = supporting_information + supporting_information_single = supporting_information_double.replace("\n\n", " ") + supporting_information = supporting_information_single.replace("\n", " ") + del(supporting_information_double) + del(supporting_information_single) + +# Add Supporting Information to Details +trvl_details["supporting_information"] = supporting_information + +# Check if any Supporting Files were uploaded +try: + supporting_files +except NameError: + is_supporting_files = False +else: + supporting_files = [x for x in supporting_files if len(x) > 0] + if len(supporting_files) > 0: + is_supporting_files = True + else: + del(supporting_files) + is_supporting_files = False + + Flow_1whl6cg + Flow_18911q5 + + + + Cancel Request + + Flow_1uda7lq + + + + Flow_0z8xl4u + Flow_0l0kxuh + + + + + Flow_1wk4ctc + Flow_1b7wncj + Flow_0l0kxuh + Flow_0gs9a10 + + + + + + + + + Edit Request + + Flow_1w54t94 + + + + + + + Flow_0dgrlbd + Flow_1sx4un4 + + + + User Form is different + + + + 1 request per role - to get confirmation from JB + + + + Should be Program/Service Lead, L2 is not always the same + + + + no need for items + + + + for Hiring manager - use Typeahead + + + + User Form is TBC one more time with JB + + + + currency will be always USD? + + + + Can the amount be in crypto? + + + + + Flow_1svj7af + Flow_0idzytl + + Flow_09uui77 + + + Flow_09uui77 + Flow_1bkg437 + Flow_0epa5vy + + + + Flow_1bkg437 + Flow_1wlj1lr + Flow_1ipspcm + + + + last_approval_outcome == "nai" or last_approval_outcome == "rej" + + + + Flow_0cablju + Flow_1wlj1lr + # Determine index of last Reviewer Comment +review_comment_cnt = len(review_comment_list) +nai_index = review_comment_cnt - 1 + +# Get Last Reviewer's Greet Name +reviewer_greet_names = {k: v["Greet Name"] for d in approvers for k, v in d.items()} +last_reviewer = review_comment_list[nai_index]["Reviewer"] +last_reviewer_greet_name = reviewer_greet_names[last_reviewer] + +# Get Last Reviewer Comment +nai_reviewer_comment = {} +nai_reviewer_comment["Reviewer Role"] = last_reviewer +nai_reviewer_comment["Reviewer Comment Date"] = local_date_str +nai_reviewer_comment["Reviewer Comment Time"] = local_time_str +nai_reviewer_comment["Reviewer Comment"] = review_comment_list[nai_index]["Reviewer Comment"] + +# Set Variable to Indicate Addition Information has been Requested +is_additional_information = True + +# Reset Requester's Supporting Information +supporting_information = trvl_details["supporting_information"] + +del(local_date_str) +del(local_time_str) + + + + Flow_0epa5vy + Flow_0cablju + + + + if last_approval_outcome == "rej": + message_id = "waku_request_rejected" +elif last_approval_outcome == "app": + message_id = "waku_trvl_request_approved" +elif last_approval_outcome == "nai": + message_id = "waku_additional_info_required" +else: + message_id = "not_set" + +# Set Requestor Status Key +as_message_status_keys = [] +as_requestor_status_key = trvl_details["requestor_status_key"] +as_message_status_keys.append(as_requestor_status_key) + # Delete Unneeded Variables +del(message_id) +del(as_message_status_keys) + Sending Requestor Waku message + + Flow_1ipspcm + Flow_1h6i1mp + + + + Flow_1h6i1mp + + + + + + last_approval_outcome == "nai" + + + + + is_request_cancelled + + + + + + + last_approval_outcome == "nai" or last_approval_outcome == "rej" + + + + + + + + + + + + + Flow_1cqv4yz + Flow_0ejzyvh + + Flow_062233f + + + + + + Flow_0g2pd95 + Flow_04lznc6 + + + Flow_06m0ary + + + + Flow_04lznc6 + Flow_06m0ary + + + + Flow_07xnd9q + Flow_0m61kxf + + + Flow_0m61kxf + Flow_0g2pd95 + + + + Flow_062233f + Flow_07xnd9q + + + This should look as a tick-box + link to the job description + + + + a link should be submitted + + + + review the fields submitted + levels ect + + + + should it include the 2 next tasks? + + + + + Flow_0mi1iph + Flow_0ez35ra + + Flow_0td3lsi + + + + + Flow_0td3lsi + Flow_0g7z4ew + + + Flow_0g7z4ew + Flow_1xvdncl + + + Flow_11bn803 + + + + Flow_1xvdncl + Flow_11bn803 + + + + + Flow_0ejzyvh + Flow_14udt27 + + + + + + Flow_0ez35ra + Flow_1bg71py + Flow_1cqv4yz + + + Flow_1qq3qyg + Flow_1ew5sem + + Flow_1ya9gzv + + + Flow_07ikkpu + + + + Flow_1ya9gzv + Flow_0q3ipnb + Flow_13qd471 + + + last_approval_outcome == "rej" + + + Flow_0q3ipnb + Flow_11zpk89 + + + + Flow_13qd471 + Flow_0eh8nri + + + last_approval_outcome == "app" + + + Flow_11zpk89 + Flow_0eh8nri + Flow_0p8nnkb + + + + + + Flow_0p8nnkb + Flow_07ikkpu + + Flow_0nvm07k + + + Flow_0j9rq6r + + + + + Flow_0nvm07k + Flow_1xjpyq1 + # Set Outcome Status +if last_approval_outcome == "app": + trvl_details["status"] = "Approved" +elif last_approval_outcome == "rej": + trvl_details["status"] = "Rejected" +else: + trvl_details["status"] = "Unknown" + +# Add Approver names to Postgres +approver_name_list = list({appr["Greet Name"] for appr in approval_history}) +approver_name = ",".join(approver_name_list) +del(approver_name_list) + +# Get Process Instance +process_info = get_toplevel_process_info() +process_instance_id = process_info["process_instance_id"] +process_instance_id_str = str(process_instance_id) +del(process_info) + + + + + # Set Details Data +requester_name = trvl_details["requestor_greet_name"] + " " + trvl_details["requestor_last_name"] +trvl_data = {"request_id": process_instance_id_str, "status": trvl_details["status"], "requestor_name": requester_name, "project": trvl_details["project_name"], "category": trvl_details["category_name"], "purpose": trvl_details["purpose"], "criticality": trvl_details["criticality"], "period": trvl_details["period"], "core_contributor_id": trvl_details["core_contributor_id"], "core_contributor_name": trvl_details["core_contributor_display"], "start_date": trvl_details["start_date"], "end_date": trvl_details["end_date"], "number_of_days": trvl_details["number_of_days"], "event_type": trvl_details["event_type_name"], "event_name": trvl_details["event_name"], "event_destination": trvl_details["event_destination_display"], "departure_from": trvl_details["departure_from_display"], "return_to": trvl_details["return_to_display"], "total_amount": trvl_details["total_request_amount_usd"], "details": trvl_details["supporting_information"], "approver_name": approver_name, "visa_required": trvl_details["visa_display"] } + +# Set Call Store Procedure +call_stored_proc_schema = { + "sql": "SELECT jsoninsert('demand_request_travel', %s);", + "values": [trvl_data], +} + # Delete Unneeded Data +del(call_stored_proc_schema) +del(requester_name) +del(trvl_data) +del(resp_DetailsDump) + + + + + + + + Flow_1xjpyq1 + Flow_17vkkm7 + + + + + + # Get request_id +details_request_id_cnt = len(resp_SelectValue) +details_request_id_index = details_request_id_cnt - 1 +details_request_id = resp_SelectValue[details_request_id_index][0] +del(resp_SelectValue) + + + + + + + + + Flow_17vkkm7 + Flow_19x61kv + + + + + # Set Item Data +col_names = {"process_instance_id": "request_id", "item_name": "item_name", "item_id": "item_id", "item_url": "item_url", "sub_category_name": "sub_category", "qty": "quantity", "unit_price_num": "unit_price", "currency": "currency", "currency_type": "currency_type", "unit_price_total": "total_amount_currency", "converted_to_unit_price_total": "total_amount_usd", "converted_to_unit_price": "unit_price_usd"} +trvl_item_data_temp = [{col_names[k]: v for k, v in x.items() if k in col_names} for x in trvl_details["item"]] +trvl_item_data = [dict(item, request_id=process_instance_id_str, request_id_db=details_request_id) for item in trvl_item_data_temp] +pd_item_id = len(trvl_item_data) + 1 +trvl_item_data.append({"total_amount_usd": trvl_details["meals_cost"], "currency": "USD", "currency_type": "fiat", "item_id": pd_item_id, "item_name": "Per Diem", "request_id": process_instance_id_str, "quantity": trvl_details["number_of_days"], "sub_category": "Meals", "unit_price": 75, "total_amount_currency": trvl_details["meals_cost"], "request_id_db": details_request_id, "unit_price_usd": 75}) +del(col_names) +del(trvl_item_data_temp) + + +call_stored_proc_schema = { + "sql": "SELECT pp2_jsonarrayinsert_item(%s);", + "values": [{ "trvl_item_data": trvl_item_data }], +} + # Delete Unneeded Variables +del(call_stored_proc_schema) +del(trvl_item_data) +del(resp_ItemsDump) + + + + + + + + Flow_19x61kv + Flow_0wquwy1 + + + Flow_0wquwy1 + Flow_1wwho23 + Flow_1ky54ip + + + is_supporting_files + + + Flow_0b1bfsh + Flow_1ky54ip + Flow_0gtenff + + + + + + # Set Data Format +trvl_file_data = [{"file_string": x, "request_id": process_instance_id_str, "request_id_db": details_request_id} for x in file_links] + +# Store Proc +call_stored_proc_schema = { + "sql": "SELECT pp2_jsonarrayinsert_files(%s);", + "values": [{ "trvl_file_data": trvl_file_data }], +} + # Delete Unneeded Variables +del(call_stored_proc_schema) +del(trvl_file_data) + + + + + + + + Flow_1wwho23 + Flow_0b1bfsh + + + + + + + + Flow_0gtenff + Flow_0j9rq6r + # Delete Unneeded Variables +del(process_instance_id_str) +del(is_additional_information) +del(is_approval_rejected) +del(last_approval_outcome) +del(nai_reviewer_comment) +del(review_comment_cnt) +del(reviewer_comment) +del(reviewer_outcome) +del(no_budget_owners_proceed) +del(process_instance_id) +# del(requestor_proceed_option) +del(l1_budget_owner_bamboo_eid) +del(l2_budget_owner_bamboo_eid) +del(details_request_id) +del(details_request_id_cnt) +del(details_request_id_index) +del(requestor_greet_name) +del(n) + +if is_supporting_files: + del(resp_FilesDump) + + + + + Flow_1ew5sem + Flow_17ajueg + + + Flow_14udt27 + Flow_1qq3qyg + Flow_1bg71py + + + + + + Process Recap +--- +### Approval History + +**Approval Status**: {{ trvl_details["status"] }} + + +| Reviewer | <div style="width:90px">Date<div> | <div style="width:65px">Time<div> | Outcome | Comment | +| ---------- | ------ | ----- | --------- | ----------- | +{% for i in approval_history %} +| {{ i["Approver"] }} | {{ i["Approval Date"] }} | {{ i["Approval Time"] }} | {{ i["Outcome"] }} | {{ i["Comment"] }} | +{% endfor %} + +---------- +**Requestor**: {{ trvl_details["requestor"] }} + +| | | +| --------------: | -------------------------------- | +|**Traveller** | {{ trvl_details["core_contributor_display"] }} | +|**Purpose** | {{ trvl_details["purpose"] }} | + +| Project | Event Type | Criticality | Event Date | Event Name | Event Destination | Visa required? | +| -------------------------------- | -------------------------------- | -------------------------------- | -------------------------------- | -------------------------------- | -------------------------------- | ----- | +| {{ trvl_details["project_name"] }} | {{ trvl_details["event_type_name"] }} | {{ trvl_details["criticality"] }} | {{ trvl_details["period_display"] }} | {{ trvl_details["event_name"] }} | {{ trvl_details["event_destination_display"] }} | {{ trvl_details["visa_display"] }} | + +---------- +### Itinerary + +**Departure**: {{ trvl_details["start_date_display"] }} , {{ trvl_details["departure_from_display"] }} + +**Return**: {{ trvl_details["end_date_display"] }}, {{ trvl_details["return_to_display"] }} + +**Number of days**: {{ trvl_details["number_of_days"] }} + +--- +### Items +| Item | Sub-Category | Qty | Currency | Unit Price | Total Price | USD Price | Total USD Price | +| ---- | ----------------- | :---: | :---------: | ----------: | ------------: | -----------: | -----------------: | +| Per diem | Meals | {{ trvl_details["number_of_days"] }} | USD | 75.00 | {{ trvl_details["meals_cost_display"] }} | 75 | {{ trvl_details["meals_cost_display"] }} | +{% for icnt in range(item_list_total) %} +| {% if is_item_url_list[icnt] %}<a href="{{ item_url_list[icnt] }}" target="_blank" >{{ item_name_list[icnt] }}</a>{% else %}{{ item_name_list[icnt] }}{% endif %} | {{ trvl_details["item"][icnt]["sub_category_name"] }} | {{ trvl_details["item"][icnt]["qty"] }} | {{ trvl_details["item"][icnt]["currency"] }} | {% if trvl_details["item"][icnt]["currency_type"] == "crypto" %}{{ "{:,.4f}".format(trvl_details["item"][icnt]["unit_price_num"]) }}{% else %}{{ "{:,.2f}".format(trvl_details["item"][icnt]["unit_price_num"]) }}{% endif %} | {% if trvl_details["item"][icnt]["currency_type"] == "crypto" %}{{ "{:,.4f}".format(trvl_details["item"][icnt]["unit_price_total"]) }}{% else %}{{ "{:,.2f}".format(trvl_details["item"][icnt]["unit_price_total"]) }}{% endif %} | {{ "{:,.2f}".format(trvl_details["item"][icnt]["converted_to_unit_price"]) }} | {{ "{:,.2f}".format(trvl_details["item"][icnt]["converted_to_unit_price_total"]) }} | +{% endfor %} +---------- +### Total Request: {{ "{:,.2f}".format(trvl_details["total_request_amount_usd"]) }} USD** + + +<sub> *Fiat - Exchange Rates used to calculate the "USD" amounts are under <a href="http://www.xe.com/" target="_blank" >license from Xe</a>. Please note that if you use this Xe Data, you are obliged to comply with <a href="http://www.xe.com/legal/dfs.php" target="_blank" >Xe's end terms of use</a>. <sub> + +<sub> *Crypto - Exchange Rates used to calculate the "USD" amounts are received from <a href="https://www.coingecko.com/en/api" target="_blank" >CoinGecko</a>.<sub> + +---------- +{% if is_supporting_information %} +### Supporting Information +| | | +| --------------: | -------------------------------- | +|**More details** | {{ trvl_details["supporting_information"] }} | + +---------- +{% endif %} + +{% if is_supporting_files %} + +### Attachments + +{% for link in file_links %} + {{link}} +{% endfor %} + +{% endif %} + + Flow_1ge8w7r + + + Flow_1b4y857 + Flow_1ge8w7r + + + Flow_17ajueg + Flow_1b4y857 + + + L2 BO - ? TBD + + + + How to get list of all recruiters? + + + + create a role PeopleOps (Talent), get the list of the users with this role - select a user + + + + can someone raise the request on behalf of someone else? + + + + to requestor and Head of PeopleOps partner + + + + TBC + + + + the job can be published + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +