From 32775973cada06ea26416a320b891165f65f85fd Mon Sep 17 00:00:00 2001 From: jasquat Date: Thu, 5 Jan 2023 17:35:22 -0500 Subject: [PATCH] Squashed 'spiffworkflow-frontend/' changes from 16dc9a7c4..d012f3a2c d012f3a2c added fix to SpiffWorkflow to deepcopy operation params before evaluating them w/ burnettk fa9b97410 basic support to find a process instance by id w/ burnettk 2071317e9 Merge branch 'main' into feature/process-nav-improvements e2c6c00dc fixed issue displaying task data for call activities called multiple times in a diagram w/ burnettk f42fca976 Merge branch 'main' into feature/process-nav-improvements c2a197d8b extract some duplicated text into vars to appease eslint 0221e4299 remove arbirary waits with no comments aa3d875e3 restore assertion cc5f511f0 Merge remote-tracking branch 'origin/main' into bug/cypress-tests b45a2d17b added ability to filter process instances by process initiator 2e1f02e18 pyl w/ burnettk 4875ff59a added process group display name to model search and cache the groups to avoid extra lookups w/ burnettk 2df091d71 merged in main and resolved conflicts 793d41f4c added new page to create process models using english text w/ burnettk 5107a8b5c allow specifying number of iterations 76ae5e51f adjust the process model file actions so they do not stack as easily w/ burnettk 1b72bf145 do not set baseUrl since it breaks auto-import and is not used otherwise w/ burnettk 056c08f86 cleaned up some debug code w/ burnettk ec9fb62f7 moved error display to own component w/ burnettk 357ea4a89 do not error when removing columns from instance column filters w/ burnettk dcb538896 added all users to waiting for column on task list tables w/ burnettk cf7ae42c6 Merge branch 'main' into feature/process-nav-improvements 831a956cf redirect to current step when resetting process instance 5287333e5 Merge branch 'feature/process_api_blueprint_refactor' of github.com:sartography/spiff-arena into feature/process_api_blueprint_refactor 19a57cb8f stats script cfdb682f7 allow tests to determine what paginated entity id to use to determine a table has loaded a49c985e5 improve button labels 36611724b allow option to complete single tasks with or without execution 4212645c4 add endpoint to reset process to earlier step 71a9368e7 Merge branch 'main' into feature/process-navigation d9f50a346 make pagination test work for instances as well 4d92fa739 fix another race conditions in instance cypress test 62c529528 delete videos on success and address race condition in cypress test 538b47b94 added better error message for failed tasks w/ burnettk 6fa96dc76 make sure we are using the same notification component on all pages w/ burnettk 7671f78a3 add baseUrl to cypress configs ddf835df1 Merge branch 'main' of github.com:sartography/spiff-arena 778f419e0 fixed some cypress tests and fixed issue where an invalid date caused the page to constantly reload w/ burnettk 74874b051 Revive report deletion (#85) e0cb7eef9 some cypress tests w/ burnettk bccfe02df added ability to view data objects from the process instance show page w/ burnettk 7b3c123b6 add an underscorize helper and use it for form fields where they need to be python identifiers e09ababa3 get the language a bit closer 0422a1005 link to correct instances page, fix fin users, ignore coverage files 9ab26e04d added method to add permissions based on macros w/ burnettk 9cba191fc Merge remote-tracking branch 'origin/main' into feature/bpmn_user_permissions 5ccbfcc1d fixing a linting error a4ed6c3fe pyl w/ burnettk d6ddc330f added permission to run privileged scripts w/ burnettk 7b5d78394 Merge pull request #79 from sartography/feature/better_unit_tests e969064cf Merge branch 'main' of github.com:sartography/spiff-arena into main 10b6b3537 A hot path that will assume the backend is running on a port that is one less than the front end port (rather than assuming 7000) Updating the docker-compose for all of SpiffArena so that it will fire up on ports 8000 -> 8004 rather than 7000 which has a common conflict with Apple AirPlay 152580859 lint and upgrade cypress 8ff2d6305 Merge remote-tracking branch 'origin/main' into bug/cypress-tests 8a595959a Merge remote-tracking branch 'origin/main' into feature/bpmn_user_permissions 0c053afef fixed get tasks and process instances by group w/ burnettk ef2eebc6d pyl w/ burnettk 934e7d75b merged in main and resolved conflicts w/ burnettk 3efd204ce added test for report filters w/ burnettk b0cc6c71b fix conflicts for like the thousandth time 4af838d39 process model show now only shows my instances 2577a7b9a fix getting task info for a process instance w/ burnettk f655377a8 created process instance list and show pages to handle all and for-me w/ burnettk fd4bb1981 fix conflicts yet again 42946bf09 merged in main and resolved conflicts w/ burnettk 3b3a2a531 fixed file upload and do not allow submitting task data to a suspended process instance w/ burnettk cd1ae938f updates to disallow modifying a process instance when it is not in the correct state w/ burnettk e86aeb750 do not allow editing task data for process instances that are not suspended and some code cleanup w/ burnettk 392537c03 Merge remote-tracking branch 'origin/main' into feature/bpmn_user_permissions 2082c9cd3 allow marking task complete without executing e20829e44 fix conflicts again 21df98582 added completed column to active task w/ burnettk b6b610f4d update url to allow permissions on send event e0579b9b0 Merge remote-tracking branch 'origin/main' into feature/process_instance_list_for_user 4b8dbf0be terminating a process instance is a POST w/ burnettk 9822719d4 some basic stuff for showing only relating items to user w/ burnettk 40a8ddd84 Bug fixes for Script Unit Test user interface -- don't bug out on invalid json. c9744ccf5 Merge remote-tracking branch 'origin/main' into feature/bpmn_user_permissions 7f7fddf7c fixed file upload of new files and get 1000 process models to list tiles page w/ burnettk 9a58bdab3 some more updates for text w/ burnettk 9397729e1 updated some text for task tables w/ burnettk 0cdc6654a merged in main and resolved conflicts w/ burnettk bdaaadaf8 merged in main and pyl passes 61848c75e Merge remote-tracking branch 'origin/main' into feature/better_unit_tests da1cd26d3 fix conflicts & update event url to match other process instance urls b3f984af4 fixed process model tests cb46389af fixed a path issue with the breadcrumb 02f9a2567 Merge branch 'feature/better_unit_tests' into feature/bpmn_user_permissions 940545fd7 Fixes a bug that was causing tests to be added to the incorrect task. Clean up UI for a better experience when viewing tests. fa7945778 Merge branch 'main' into feature/process-navigation 4a24f215a fix event UI 420274f2b Revert package-lock.json changes e8d449c63 Fix api endpoints for script unit tests c9a1ff0c5 working but barely functional UI for manually sending events 6c53a181c split out completed instances by group as well d07a2f5be updated group api so it is not under tasks 70098825b pyl 41b140ee1 split group task tables by group and created component for group tables bd8f68e94 some base work to try to get display names searchable for process models w/ burnettk git-subtree-dir: spiffworkflow-frontend git-subtree-split: d012f3a2c4a0516f040156997d6daf675197e294 --- bin/collect_cypress_stats | 48 ++ cypress.config.js | 31 +- cypress/e2e/process_groups.cy.js | 5 +- cypress/e2e/process_instances.cy.js | 16 +- cypress/e2e/process_models.cy.js | 64 +-- cypress/e2e/tasks.cy.js | 16 +- cypress/support/commands.js | 55 +- package-lock.json | 12 +- public/index.html | 4 +- src/App.tsx | 34 +- src/classes/ProcessInstanceClass.tsx | 5 + src/components/ErrorDisplay.tsx | 55 ++ src/components/MyCompletedInstances.tsx | 2 +- src/components/NavigationBar.tsx | 2 +- src/components/Notification.tsx | 14 +- .../ProcessInstanceListDeleteReport.tsx | 29 ++ src/components/ProcessInstanceListTable.tsx | 215 +++++--- src/components/ProcessInstanceRun.tsx | 6 +- src/components/ProcessModelListTiles.tsx | 30 +- src/components/ProcessModelSearch.tsx | 29 +- src/components/TaskListTable.tsx | 233 +++++++++ src/components/TasksForMyOpenProcesses.tsx | 160 +----- src/components/TasksWaitingForMe.tsx | 155 +----- src/components/TasksWaitingForMyGroups.tsx | 171 +------ src/config.tsx | 22 +- src/helpers.test.tsx | 21 +- src/helpers.tsx | 23 + src/hooks/UriListForPermissions.tsx | 11 +- src/index.css | 4 +- src/interfaces.ts | 34 +- src/routes/AdminRoutes.tsx | 45 +- src/routes/AuthenticationList.tsx | 6 +- src/routes/CompletedInstances.tsx | 61 ++- src/routes/Configuration.tsx | 6 +- src/routes/HomePageRoutes.tsx | 6 +- src/routes/JsonSchemaFormBuilder.tsx | 8 +- src/routes/MyTasks.tsx | 28 +- src/routes/ProcessGroupList.tsx | 2 +- src/routes/ProcessInstanceFindById.tsx | 79 +++ src/routes/ProcessInstanceList.tsx | 68 ++- src/routes/ProcessInstanceLogList.tsx | 4 +- src/routes/ProcessInstanceReportList.tsx | 4 +- src/routes/ProcessInstanceShow.tsx | 484 +++++++++++++----- src/routes/ProcessModelEditDiagram.tsx | 244 ++++++--- src/routes/ProcessModelNewExperimental.tsx | 73 +++ src/routes/ProcessModelShow.tsx | 59 ++- src/routes/TaskShow.tsx | 15 +- src/services/UserService.ts | 14 +- tsconfig.json | 8 +- 49 files changed, 1767 insertions(+), 953 deletions(-) create mode 100755 bin/collect_cypress_stats create mode 100644 src/classes/ProcessInstanceClass.tsx create mode 100644 src/components/ErrorDisplay.tsx create mode 100644 src/components/ProcessInstanceListDeleteReport.tsx create mode 100644 src/components/TaskListTable.tsx create mode 100644 src/routes/ProcessInstanceFindById.tsx create mode 100644 src/routes/ProcessModelNewExperimental.tsx diff --git a/bin/collect_cypress_stats b/bin/collect_cypress_stats new file mode 100755 index 00000000..150efc80 --- /dev/null +++ b/bin/collect_cypress_stats @@ -0,0 +1,48 @@ +#!/usr/bin/env bash + +function error_handler() { + >&2 echo "Exited with BAD EXIT CODE '${2}' in ${0} script at line: ${1}." + exit "$2" +} +trap 'error_handler ${LINENO} $?' ERR +set -o errtrace -o errexit -o nounset -o pipefail + +# see also: npx cypress run --env grep="can filter",grepFilterSpecs=true +# https://github.com/cypress-io/cypress/tree/develop/npm/grep#pre-filter-specs-grepfilterspecs + +iterations="${1:-10}" + +test_case_matches="$(rg '^ it\(')" + +stats_file="/var/tmp/cypress_stats.txt" + +function run_all_test_cases() { + local stat_index="$1" + + pushd "$NO_TERM_LIMITS_PROJECTS_DIR/github/sartography/sample-process-models" + gitc + popd + + while read -r test_case_line; do + test_case_file="$(awk -F: '{print $1}' <<< "$test_case_line")" + test_case_name_side="$(awk -F: '{print $2}' <<< "$test_case_line")" + test_case_name=$(hot_sed -E "s/^\s+it\('(.+)'.*/\1/" <<< "$test_case_name_side") + echo "running test case: $test_case_file::$test_case_name" + if ./node_modules/.bin/cypress run --e2e --browser chrome --spec "$test_case_file" --env grep="$test_case_name"; then + echo "$stat_index:::$test_case_file:::$test_case_name: PASS" >> "$stats_file" + else + echo "$stat_index:::$test_case_file:::$test_case_name: FAIL" >> "$stats_file" + fi + done <<< "$test_case_matches" +} + +# clear the stats file +echo > "$stats_file" + +for ((global_stat_index=1;global_stat_index<=$iterations;global_stat_index++)); do +# for global_stat_index in {1..$iterations}; do + run_all_test_cases "$global_stat_index" +done + +# prints summary of most-failing test cases +grep FAIL "$stats_file" | awk -F ':::' '{for (i=2; i { + const filesToDelete = [] + on('after:spec', (_spec, results) => { + if (results.stats.failures === 0 && results.video) { + filesToDelete.push(results.video) + } + }) + on('after:run', async () => { + if (filesToDelete.length) { + console.log( + 'after:run hook: Deleting %d video(s) from successful specs', + filesToDelete.length + ) + await Promise.all(filesToDelete.map((videoFile) => rm(videoFile))) + } + }) +} module.exports = defineConfig({ projectId: 'crax1q', + + // since it's slow + videoCompression: useVideoCompression, + + videoUploadOnPasses: false, chromeWebSecurity: false, e2e: { baseUrl: 'http://localhost:7001', - setupNodeEvents(_on, config) { + setupNodeEvents(on, config) { + deleteVideosOnSuccess(on) require('@cypress/grep/src/plugin')(config); return config; }, diff --git a/cypress/e2e/process_groups.cy.js b/cypress/e2e/process_groups.cy.js index bef0e560..e10c4857 100644 --- a/cypress/e2e/process_groups.cy.js +++ b/cypress/e2e/process_groups.cy.js @@ -30,7 +30,10 @@ describe('process-groups', () => { .find('.cds--btn--danger') .click(); cy.url().should('include', `process-groups`); - cy.contains(groupId).should('not.exist'); + cy.contains(newGroupDisplayName).should('not.exist'); + + // meaning the process group list page is loaded, so we can sign out safely without worrying about ajax requests failing + cy.get('.tile-process-group-content-container').should('exist'); }); // process groups no longer has pagination post-tiles diff --git a/cypress/e2e/process_instances.cy.js b/cypress/e2e/process_instances.cy.js index 4d33d13f..e582dcbb 100644 --- a/cypress/e2e/process_instances.cy.js +++ b/cypress/e2e/process_instances.cy.js @@ -68,8 +68,7 @@ describe('process-instances', () => { cy.login(); cy.navigateToProcessModel( 'Acceptance Tests Group One', - 'Acceptance Tests Model 1', - 'acceptance-tests-model-1' + 'Acceptance Tests Model 1' ); }); afterEach(() => { @@ -80,6 +79,7 @@ describe('process-instances', () => { const originalDmnOutputForKevin = 'Very wonderful'; const newDmnOutputForKevin = 'The new wonderful'; const dmnOutputForDan = 'pretty wonderful'; + const acceptanceTestOneDisplayName = 'Acceptance Tests Model 1'; const originalPythonScript = 'person = "Kevin"'; const newPythonScript = 'person = "Dan"'; @@ -95,13 +95,13 @@ describe('process-instances', () => { cy.getBySel(`edit-file-${dmnFile.replace('.', '-')}`).click(); updateDmnText(originalDmnOutputForKevin, newDmnOutputForKevin); - cy.contains('acceptance-tests-model-1').click(); + cy.contains(acceptanceTestOneDisplayName).click(); cy.runPrimaryBpmnFile(); cy.getBySel('files-accordion').click(); cy.getBySel(`edit-file-${dmnFile.replace('.', '-')}`).click(); updateDmnText(newDmnOutputForKevin, originalDmnOutputForKevin); - cy.contains('acceptance-tests-model-1').click(); + cy.contains(acceptanceTestOneDisplayName).click(); cy.runPrimaryBpmnFile(); // Change bpmn @@ -109,13 +109,13 @@ describe('process-instances', () => { cy.getBySel(`edit-file-${bpmnFile.replace('.', '-')}`).click(); cy.contains(`Process Model File: ${bpmnFile}`); updateBpmnPythonScript(newPythonScript); - cy.contains('acceptance-tests-model-1').click(); + cy.contains(acceptanceTestOneDisplayName).click(); cy.runPrimaryBpmnFile(); cy.getBySel('files-accordion').click(); cy.getBySel(`edit-file-${bpmnFile.replace('.', '-')}`).click(); updateBpmnPythonScript(originalPythonScript); - cy.contains('acceptance-tests-model-1').click(); + cy.contains(acceptanceTestOneDisplayName).click(); cy.runPrimaryBpmnFile(); }); @@ -160,6 +160,7 @@ describe('process-instances', () => { cy.getBySel('process-instance-list-link').click(); cy.getBySel('process-instance-show-link').first().click(); cy.getBySel('process-instance-log-list-link').click(); + cy.getBySel('process-instance-log-detailed').click(); cy.contains('process_model_one'); cy.contains('State change to COMPLETED'); cy.basicPaginationTest(); @@ -167,6 +168,8 @@ describe('process-instances', () => { it('can filter', () => { cy.getBySel('process-instance-list-link').click(); + cy.getBySel('process-instance-list-all').click(); + cy.contains('All Process Instances'); cy.assertAtLeastOneItemInPaginatedResults(); const statusSelect = '#process-instance-status-select'; @@ -174,6 +177,7 @@ describe('process-instances', () => { if (!['all', 'waiting'].includes(processStatus)) { cy.get(statusSelect).click(); cy.get(statusSelect).contains(processStatus).click(); + cy.get(statusSelect).click(); cy.getBySel('filter-button').click(); // FIXME: wait a little bit for the useEffects to be able to fully set processInstanceFilters cy.wait(1000); diff --git a/cypress/e2e/process_models.cy.js b/cypress/e2e/process_models.cy.js index 43fba108..3a36f710 100644 --- a/cypress/e2e/process_models.cy.js +++ b/cypress/e2e/process_models.cy.js @@ -9,16 +9,19 @@ describe('process-models', () => { cy.logout(); }); + const groupDisplayName = 'Acceptance Tests Group One'; + const deleteProcessModelButtonId = 'delete-process-model-button'; + const saveChangesButtonText = 'Save Changes'; + const fileNameInputSelector = 'input[name=file_name]'; + it('can perform crud operations', () => { const uuid = () => Cypress._.random(0, 1e6); const id = uuid(); const groupId = 'misc/acceptance-tests-group-one'; - const groupDisplayName = 'Acceptance Tests Group One'; const modelDisplayName = `Test Model 2 ${id}`; const modelId = `test-model-2-${id}`; const newModelDisplayName = `${modelDisplayName} edited`; cy.contains(miscDisplayName).click(); - cy.wait(500); cy.contains(groupDisplayName).click(); cy.createModel(groupId, modelId, modelDisplayName); cy.url().should( @@ -34,37 +37,27 @@ describe('process-models', () => { cy.contains('Submit').click(); cy.contains(`Process Model: ${newModelDisplayName}`); - // go back to process model show by clicking on the breadcrumb - cy.contains(modelDisplayName).click(); + cy.deleteProcessModelAndConfirm(deleteProcessModelButtonId, groupId); - cy.getBySel('delete-process-model-button').click(); - cy.contains('Are you sure'); - cy.getBySel('delete-process-model-button-modal-confirmation-dialog') - .find('.cds--btn--danger') - .click(); - cy.url().should( - 'include', - `process-groups/${modifyProcessIdentifierForPathParam(groupId)}` - ); cy.contains(modelId).should('not.exist'); cy.contains(modelDisplayName).should('not.exist'); }); - it('can create new bpmn, dmn, and json files', () => { + it('can create new bpmn and dmn and json files', () => { const uuid = () => Cypress._.random(0, 1e6); const id = uuid(); const directParentGroupId = 'acceptance-tests-group-one'; + const directParentGroupName = 'Acceptance Tests Group One'; const groupId = `misc/${directParentGroupId}`; - const groupDisplayName = 'Acceptance Tests Group One'; const modelDisplayName = `Test Model 2 ${id}`; const modelId = `test-model-2-${id}`; const bpmnFileName = `bpmn_test_file_${id}`; const dmnFileName = `dmn_test_file_${id}`; const jsonFileName = `json_test_file_${id}`; + const decision_acceptance_test_id = `decision_acceptance_test_${id}`; cy.contains(miscDisplayName).click(); - cy.wait(500); cy.contains(groupDisplayName).click(); cy.createModel(groupId, modelId, modelDisplayName); cy.contains(groupDisplayName).click(); @@ -89,8 +82,8 @@ describe('process-models', () => { cy.wait(500); cy.contains('Save').click(); cy.contains('Start Event Name'); - cy.get('input[name=file_name]').type(bpmnFileName); - cy.contains('Save Changes').click(); + cy.get(fileNameInputSelector).type(bpmnFileName); + cy.contains(saveChangesButtonText).click(); cy.contains(`Process Model File: ${bpmnFileName}`); cy.contains(modelDisplayName).click(); cy.contains(`Process Model: ${modelDisplayName}`); @@ -104,11 +97,11 @@ describe('process-models', () => { cy.contains('General').click(); cy.get('#bio-properties-panel-id') .clear() - .type('decision_acceptance_test_1'); + .type(decision_acceptance_test_id); cy.contains('General').click(); cy.contains('Save').click(); - cy.get('input[name=file_name]').type(dmnFileName); - cy.contains('Save Changes').click(); + cy.get(fileNameInputSelector).type(dmnFileName); + cy.contains(saveChangesButtonText).click(); cy.contains(`Process Model File: ${dmnFileName}`); cy.contains(modelDisplayName).click(); cy.contains(`Process Model: ${modelDisplayName}`); @@ -121,8 +114,8 @@ describe('process-models', () => { // Some reason, cypress evals json strings so we have to escape it it with '{{}' cy.get('.view-line').type('{{} "test_key": "test_value" }'); cy.getBySel('file-save-button').click(); - cy.get('input[name=file_name]').type(jsonFileName); - cy.contains('Save Changes').click(); + cy.get(fileNameInputSelector).type(jsonFileName); + cy.contains(saveChangesButtonText).click(); cy.contains(`Process Model File: ${jsonFileName}`); // wait for json to load before clicking away to avoid network errors cy.wait(500); @@ -131,17 +124,12 @@ describe('process-models', () => { // cy.getBySel('files-accordion').click(); cy.contains(`${jsonFileName}.json`).should('exist'); - cy.getBySel('delete-process-model-button').click(); - cy.contains('Are you sure'); - cy.getBySel('delete-process-model-button-modal-confirmation-dialog') - .find('.cds--btn--danger') - .click(); - cy.url().should( - 'include', - `process-groups/${modifyProcessIdentifierForPathParam(groupId)}` - ); + cy.deleteProcessModelAndConfirm(deleteProcessModelButtonId, groupId); cy.contains(modelId).should('not.exist'); cy.contains(modelDisplayName).should('not.exist'); + + // we go back to the parent process group after deleting the model + cy.get('.tile-process-group-content-container').should('exist'); }); it('can upload and run a bpmn file', () => { @@ -149,12 +137,10 @@ describe('process-models', () => { const id = uuid(); const directParentGroupId = 'acceptance-tests-group-one'; const groupId = `misc/${directParentGroupId}`; - const groupDisplayName = 'Acceptance Tests Group One'; const modelDisplayName = `Test Model 2 ${id}`; const modelId = `test-model-2-${id}`; cy.contains('Add a process group'); cy.contains(miscDisplayName).click(); - cy.wait(500); cy.contains(groupDisplayName).click(); cy.createModel(groupId, modelId, modelDisplayName); @@ -190,7 +176,7 @@ describe('process-models', () => { // in breadcrumb cy.contains(modelDisplayName).click(); - cy.getBySel('delete-process-model-button').click(); + cy.getBySel(deleteProcessModelButtonId).click(); cy.contains('Are you sure'); cy.getBySel('delete-process-model-button-modal-confirmation-dialog') .find('.cds--btn--danger') @@ -203,14 +189,6 @@ describe('process-models', () => { cy.contains(modelDisplayName).should('not.exist'); }); - // process models no longer has pagination post-tiles - // it.only('can paginate items', () => { - // cy.contains(miscDisplayName).click(); - // cy.wait(500); - // cy.contains('Acceptance Tests Group One').click(); - // cy.basicPaginationTest(); - // }); - it('can allow searching for model', () => { cy.getBySel('process-model-selection').click().type('model-3'); cy.contains('acceptance-tests-group-one/acceptance-tests-model-3').click(); diff --git a/cypress/e2e/tasks.cy.js b/cypress/e2e/tasks.cy.js index e58566b8..922c4209 100644 --- a/cypress/e2e/tasks.cy.js +++ b/cypress/e2e/tasks.cy.js @@ -13,11 +13,10 @@ const checkTaskHasClass = (taskName, className) => { cy.get(`g[data-element-id=${taskName}]`).should('have.class', className); }; -const kickOffModelWithForm = (modelId, formName) => { +const kickOffModelWithForm = () => { cy.navigateToProcessModel( 'Acceptance Tests Group One', - 'Acceptance Tests Model 2', - 'acceptance-tests-model-2' + 'Acceptance Tests Model 2' ); cy.runPrimaryBpmnFile(true); }; @@ -32,12 +31,11 @@ describe('tasks', () => { it('can complete and navigate a form', () => { const groupDisplayName = 'Acceptance Tests Group One'; - const modelId = `acceptance-tests-model-2`; const modelDisplayName = `Acceptance Tests Model 2`; const completedTaskClassName = 'completed-task-highlight'; const activeTaskClassName = 'active-task-highlight'; - cy.navigateToProcessModel(groupDisplayName, modelDisplayName, modelId); + cy.navigateToProcessModel(groupDisplayName, modelDisplayName); cy.runPrimaryBpmnFile(true); submitInputIntoFormField( @@ -71,7 +69,7 @@ describe('tasks', () => { ); cy.contains('Task: get_user_generated_number_four'); - cy.navigateToProcessModel(groupDisplayName, modelDisplayName, modelId); + cy.navigateToProcessModel(groupDisplayName, modelDisplayName); cy.getBySel('process-instance-list-link').click(); cy.assertAtLeastOneItemInPaginatedResults(); @@ -94,7 +92,7 @@ describe('tasks', () => { cy.contains('Tasks').should('exist'); // FIXME: this will probably need a better way to link to the proper form that we want - cy.contains('Complete Task').click(); + cy.contains('Go').click(); submitInputIntoFormField( 'get_user_generated_number_four', @@ -103,7 +101,7 @@ describe('tasks', () => { ); cy.url().should('include', '/tasks'); - cy.navigateToProcessModel(groupDisplayName, modelDisplayName, modelId); + cy.navigateToProcessModel(groupDisplayName, modelDisplayName); cy.getBySel('process-instance-list-link').click(); cy.assertAtLeastOneItemInPaginatedResults(); @@ -122,6 +120,6 @@ describe('tasks', () => { kickOffModelWithForm(); cy.navigateToHome(); - cy.basicPaginationTest(); + cy.basicPaginationTest('process-instance-show-link'); }); }); diff --git a/cypress/support/commands.js b/cypress/support/commands.js index f7c4e846..8369a22c 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -95,14 +95,16 @@ Cypress.Commands.add( } else { cy.contains(/Process Instance.*[kK]icked [oO]ff/); cy.reload(true); + cy.contains('Process Model:').should('exist'); cy.contains(/Process Instance.*[kK]icked [oO]ff/).should('not.exist'); + cy.contains('[data-qa=process-model-show-permissions-loaded]', 'true'); } } ); Cypress.Commands.add( 'navigateToProcessModel', - (groupDisplayName, modelDisplayName, modelIdentifier) => { + (groupDisplayName, modelDisplayName) => { cy.navigateToAdmin(); cy.contains(miscDisplayName).click(); cy.contains(`Process Group: ${miscDisplayName}`, { timeout: 10000 }); @@ -114,17 +116,33 @@ Cypress.Commands.add( } ); -Cypress.Commands.add('basicPaginationTest', () => { - cy.getBySel('pagination-options').scrollIntoView(); - cy.get('.cds--select__item-count').find('.cds--select-input').select('2'); +Cypress.Commands.add( + 'basicPaginationTest', + (dataQaTagToUseToEnsureTableHasLoaded = 'paginated-entity-id') => { + cy.getBySel('pagination-options').scrollIntoView(); + cy.get('.cds--select__item-count').find('.cds--select-input').select('2'); - // NOTE: this is a em dash instead of en dash - cy.contains(/\b1–2 of \d+/); - cy.get('.cds--pagination__button--forward').click(); - cy.contains(/\b3–4 of \d+/); - cy.get('.cds--pagination__button--backward').click(); - cy.contains(/\b1–2 of \d+/); -}); + // NOTE: this is a em dash instead of en dash + cy.contains(/\b1–2 of \d+/); + + // ok, trying to ensure that we have everything loaded before we leave this + // function and try to sign out. Just showing results 1-2 of blah is not good enough, + // since the ajax request may not have finished yet. + // to be sure it's finished, grab the log id from page 1. remember it. + // then use the magical contains command that waits for the element to exist AND + // for that element to contain the text we're looking for. + cy.getBySel(dataQaTagToUseToEnsureTableHasLoaded) + .first() + .then(($element) => { + const oldId = $element.text().trim(); + cy.get('.cds--pagination__button--forward').click(); + cy.contains(/\b3–4 of \d+/); + cy.get('.cds--pagination__button--backward').click(); + cy.contains(/\b1–2 of \d+/); + cy.contains(`[data-qa=${dataQaTagToUseToEnsureTableHasLoaded}]`, oldId); + }); + } +); Cypress.Commands.add('assertAtLeastOneItemInPaginatedResults', () => { cy.contains(/\b[1-9]\d*–[1-9]\d* of [1-9]\d*/); @@ -133,3 +151,18 @@ Cypress.Commands.add('assertAtLeastOneItemInPaginatedResults', () => { Cypress.Commands.add('assertNoItemInPaginatedResults', () => { cy.contains(/\b0–0 of 0 items/); }); + +Cypress.Commands.add( + 'deleteProcessModelAndConfirm', + (buttonId, groupId) => { + cy.getBySel(buttonId).click(); + cy.contains('Are you sure'); + cy.getBySel('delete-process-model-button-modal-confirmation-dialog') + .find('.cds--btn--danger') + .click(); + cy.url().should( + 'include', + `process-groups/${modifyProcessIdentifierForPathParam(groupId)}` + ); + } +); diff --git a/package-lock.json b/package-lock.json index 4ccea192..54cf2c89 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9850,9 +9850,9 @@ "integrity": "sha512-NJGVKPS81XejHcLhaLJS7plab0fK3slPh11mESeeDq2W4ZI5kUKK/LRRdVDvjJseojbPB7ZwjnyOybg3Igea/A==" }, "node_modules/cypress": { - "version": "12.1.0", - "resolved": "https://registry.npmjs.org/cypress/-/cypress-12.1.0.tgz", - "integrity": "sha512-7fz8N84uhN1+ePNDsfQvoWEl4P3/VGKKmAg+bJQFY4onhA37Ys+6oBkGbNdwGeC7n2QqibNVPhk8x3YuQLwzfw==", + "version": "12.2.0", + "resolved": "https://registry.npmjs.org/cypress/-/cypress-12.2.0.tgz", + "integrity": "sha512-kvl95ri95KK8mAy++tEU/wUgzAOMiIciZSL97LQvnOinb532m7dGvwN0mDSIGbOd71RREtmT9o4h088RjK5pKw==", "dev": true, "hasInstallScript": true, "dependencies": { @@ -38586,9 +38586,9 @@ "integrity": "sha512-NJGVKPS81XejHcLhaLJS7plab0fK3slPh11mESeeDq2W4ZI5kUKK/LRRdVDvjJseojbPB7ZwjnyOybg3Igea/A==" }, "cypress": { - "version": "12.1.0", - "resolved": "https://registry.npmjs.org/cypress/-/cypress-12.1.0.tgz", - "integrity": "sha512-7fz8N84uhN1+ePNDsfQvoWEl4P3/VGKKmAg+bJQFY4onhA37Ys+6oBkGbNdwGeC7n2QqibNVPhk8x3YuQLwzfw==", + "version": "12.2.0", + "resolved": "https://registry.npmjs.org/cypress/-/cypress-12.2.0.tgz", + "integrity": "sha512-kvl95ri95KK8mAy++tEU/wUgzAOMiIciZSL97LQvnOinb532m7dGvwN0mDSIGbOd71RREtmT9o4h088RjK5pKw==", "dev": true, "requires": { "@cypress/request": "^2.88.10", diff --git a/public/index.html b/public/index.html index ae3a2307..1a7cafa9 100644 --- a/public/index.html +++ b/public/index.html @@ -7,7 +7,7 @@ - spiffworkflow-frontend + SpiffWorkflow diff --git a/src/App.tsx b/src/App.tsx index 6357a713..ecf9fc54 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -14,15 +14,14 @@ import { ErrorForDisplay } from './interfaces'; import { AbilityContext } from './contexts/Can'; import UserService from './services/UserService'; +import ErrorDisplay from './components/ErrorDisplay'; export default function App() { - const [errorMessage, setErrorMessage] = useState( - null - ); + const [errorObject, setErrorObject] = useState(null); const errorContextValueArray = useMemo( - () => [errorMessage, setErrorMessage], - [errorMessage] + () => [errorObject, setErrorObject], + [errorObject] ); if (!UserService.isLoggedIn()) { @@ -32,29 +31,6 @@ export default function App() { const ability = defineAbility(() => {}); - let errorTag = null; - if (errorMessage) { - let sentryLinkTag = null; - if (errorMessage.sentry_link) { - sentryLinkTag = ( - - { - ': Find details about this error here (it may take a moment to become available): ' - } - - {errorMessage.sentry_link} - - - ); - } - errorTag = ( - - ); - } - return (
{/* @ts-ignore */} @@ -63,7 +39,7 @@ export default function App() { - {errorTag} + } /> diff --git a/src/classes/ProcessInstanceClass.tsx b/src/classes/ProcessInstanceClass.tsx new file mode 100644 index 00000000..d44569cd --- /dev/null +++ b/src/classes/ProcessInstanceClass.tsx @@ -0,0 +1,5 @@ +export default class ProcessInstanceClass { + static terminalStatuses() { + return ['complete', 'error', 'terminated']; + } +} diff --git a/src/components/ErrorDisplay.tsx b/src/components/ErrorDisplay.tsx new file mode 100644 index 00000000..cdbed75a --- /dev/null +++ b/src/components/ErrorDisplay.tsx @@ -0,0 +1,55 @@ +import { useContext } from 'react'; +import ErrorContext from '../contexts/ErrorContext'; +import { Notification } from './Notification'; + +export default function ErrorDisplay() { + const [errorObject, setErrorObject] = (useContext as any)(ErrorContext); + + let errorTag = null; + if (errorObject) { + let sentryLinkTag = null; + if (errorObject.sentry_link) { + sentryLinkTag = ( + + { + ': Find details about this error here (it may take a moment to become available): ' + } + + {errorObject.sentry_link} + + + ); + } + + let message =
{errorObject.message}
; + let title = 'Error:'; + if ('task_name' in errorObject && errorObject.task_name) { + title = 'Error in python script:'; + message = ( + <> +
+
+ Task: {errorObject.task_name} ({errorObject.task_id}) +
+
File name: {errorObject.file_name}
+
Line number in script task: {errorObject.line_number}
+
+
{errorObject.message}
+ + ); + } + + errorTag = ( + setErrorObject(null)} + type="error" + > + {message} + {sentryLinkTag} + + ); + } + + return errorTag; +} diff --git a/src/components/MyCompletedInstances.tsx b/src/components/MyCompletedInstances.tsx index 2d0fe26a..47042e91 100644 --- a/src/components/MyCompletedInstances.tsx +++ b/src/components/MyCompletedInstances.tsx @@ -8,7 +8,7 @@ export default function MyCompletedInstances() { filtersEnabled={false} paginationQueryParamPrefix={paginationQueryParamPrefix} perPageOptions={[2, 5, 25]} - reportIdentifier="system_report_instances_initiated_by_me" + reportIdentifier="system_report_completed_instances_initiated_by_me" showReports={false} /> ); diff --git a/src/components/NavigationBar.tsx b/src/components/NavigationBar.tsx index 7a0ffd3e..e482ae52 100644 --- a/src/components/NavigationBar.tsx +++ b/src/components/NavigationBar.tsx @@ -81,7 +81,7 @@ export default function NavigationBar() { return ( <> - {UserService.getUsername()} + {UserService.getPreferredUsername()} ; if (type === 'error') { - iconClassName = 'red-icon'; + iconComponent = ; } return (
- + {iconComponent}
{title}
{children}
diff --git a/src/components/ProcessInstanceListDeleteReport.tsx b/src/components/ProcessInstanceListDeleteReport.tsx new file mode 100644 index 00000000..ca04d516 --- /dev/null +++ b/src/components/ProcessInstanceListDeleteReport.tsx @@ -0,0 +1,29 @@ +import { ProcessInstanceReport } from '../interfaces'; +import HttpService from '../services/HttpService'; +import ButtonWithConfirmation from './ButtonWithConfirmation'; + +type OwnProps = { + onSuccess: (..._args: any[]) => any; + processInstanceReportSelection: ProcessInstanceReport; +}; + +export default function ProcessInstanceListDeleteReport({ + onSuccess, + processInstanceReportSelection, +}: OwnProps) { + const deleteProcessInstanceReport = () => { + HttpService.makeCallToBackend({ + path: `/process-instances/reports/${processInstanceReportSelection.id}`, + successCallback: onSuccess, + httpMethod: 'DELETE', + }); + }; + + return ( + + ); +} diff --git a/src/components/ProcessInstanceListTable.tsx b/src/components/ProcessInstanceListTable.tsx index a21baec0..51e8c2d6 100644 --- a/src/components/ProcessInstanceListTable.tsx +++ b/src/components/ProcessInstanceListTable.tsx @@ -1,4 +1,4 @@ -import { useContext, useEffect, useMemo, useState } from 'react'; +import { useContext, useEffect, useMemo, useRef, useState } from 'react'; import { Link, useNavigate, @@ -40,6 +40,7 @@ import { getProcessModelFullIdentifierFromSearchParams, modifyProcessIdentifierForPathParam, refreshAtInterval, + setErrorMessageSafely, } from '../helpers'; import PaginationForTable from './PaginationForTable'; @@ -59,9 +60,11 @@ import { ReportColumnForEditing, ReportMetadata, ReportFilter, + User, } from '../interfaces'; import ProcessModelSearch from './ProcessModelSearch'; import ProcessInstanceReportSearch from './ProcessInstanceReportSearch'; +import ProcessInstanceListDeleteReport from './ProcessInstanceListDeleteReport'; import ProcessInstanceListSaveAsReport from './ProcessInstanceListSaveAsReport'; import { FormatProcessModelDisplayName } from './MiniComponents'; import { Notification } from './Notification'; @@ -79,6 +82,8 @@ type OwnProps = { textToShowIfEmpty?: string; paginationClassName?: string; autoReload?: boolean; + additionalParams?: string; + variant?: string; }; interface dateParameters { @@ -90,12 +95,18 @@ export default function ProcessInstanceListTable({ processModelFullIdentifier, paginationQueryParamPrefix, perPageOptions, + additionalParams, showReports = true, reportIdentifier, textToShowIfEmpty, paginationClassName, autoReload = false, + variant = 'for-me', }: OwnProps) { + let apiPath = '/process-instances/for-me'; + if (variant === 'all') { + apiPath = '/process-instances'; + } const params = useParams(); const [searchParams] = useSearchParams(); const navigate = useNavigate(); @@ -122,7 +133,16 @@ export default function ProcessInstanceListTable({ const [endFromTimeInvalid, setEndFromTimeInvalid] = useState(false); const [endToTimeInvalid, setEndToTimeInvalid] = useState(false); - const setErrorMessage = (useContext as any)(ErrorContext)[1]; + const [errorObject, setErrorObject] = (useContext as any)(ErrorContext); + + const processInstanceListPathPrefix = + variant === 'all' + ? '/admin/process-instances/all' + : '/admin/process-instances/for-me'; + const processInstanceShowPathPrefix = + variant === 'all' + ? '/admin/process-instances' + : '/admin/process-instances/for-me'; const [processStatusAllOptions, setProcessStatusAllOptions] = useState( [] @@ -149,6 +169,12 @@ export default function ProcessInstanceListTable({ useState(null); const [reportColumnFormMode, setReportColumnFormMode] = useState(''); + const [processInstanceInitiatorOptions, setProcessInstanceInitiatorOptions] = + useState([]); + const [processInitiatorSelection, setProcessInitiatorSelection] = + useState(null); + const lastRequestedInitatorSearchTerm = useRef(); + const dateParametersToAlwaysFilterBy: dateParameters = useMemo(() => { return { start_from: [setStartFromDate, setStartFromTime], @@ -253,8 +279,12 @@ export default function ProcessInstanceListTable({ } ); + if (additionalParams) { + queryParamString += `&${additionalParams}`; + } + HttpService.makeCallToBackend({ - path: `/process-instances?${queryParamString}`, + path: `${apiPath}?${queryParamString}`, successCallback: setProcessInstancesFromResult, }); } @@ -290,7 +320,7 @@ export default function ProcessInstanceListTable({ if (filtersEnabled) { // populate process model selection HttpService.makeCallToBackend({ - path: `/process-models?per_page=1000&recursive=true`, + path: `/process-models?per_page=1000&recursive=true&include_parent_groups=true`, successCallback: processResultForProcessModels, }); } else { @@ -320,6 +350,8 @@ export default function ProcessInstanceListTable({ processModelFullIdentifier, perPageOptions, reportIdentifier, + additionalParams, + apiPath, ]); // This sets the filter data using the saved reports returned from the initial instance_list query. @@ -409,8 +441,11 @@ export default function ProcessInstanceListTable({ } }; - // TODO: after factoring this out page hangs when invalid date ranges and applying the filter - const calculateStartAndEndSeconds = () => { + // jasquat/burnettk - 2022-12-28 do not check the validity of the dates when rendering components to avoid the page being + // re-rendered while the user is still typing. NOTE that we also prevented rerendering + // with the use of the setErrorMessageSafely function. we are not sure why the context not + // changing still causes things to rerender when we call its setter without our extra check. + const calculateStartAndEndSeconds = (validate: boolean = true) => { const startFromSeconds = convertDateAndTimeStringsToSeconds( startFromDate, startFromTime || '00:00:00' @@ -428,29 +463,25 @@ export default function ProcessInstanceListTable({ endToTime || '00:00:00' ); let valid = true; - if (isTrueComparison(startFromSeconds, '>', startToSeconds)) { - setErrorMessage({ - message: '"Start date from" cannot be after "start date to"', - }); - valid = false; - } - if (isTrueComparison(endFromSeconds, '>', endToSeconds)) { - setErrorMessage({ - message: '"End date from" cannot be after "end date to"', - }); - valid = false; - } - if (isTrueComparison(startFromSeconds, '>', endFromSeconds)) { - setErrorMessage({ - message: '"Start date from" cannot be after "end date from"', - }); - valid = false; - } - if (isTrueComparison(startToSeconds, '>', endToSeconds)) { - setErrorMessage({ - message: '"Start date to" cannot be after "end date to"', - }); - valid = false; + + if (validate) { + let message = ''; + if (isTrueComparison(startFromSeconds, '>', startToSeconds)) { + message = '"Start date from" cannot be after "start date to"'; + } + if (isTrueComparison(endFromSeconds, '>', endToSeconds)) { + message = '"End date from" cannot be after "end date to"'; + } + if (isTrueComparison(startFromSeconds, '>', endFromSeconds)) { + message = '"Start date from" cannot be after "end date from"'; + } + if (isTrueComparison(startToSeconds, '>', endToSeconds)) { + message = '"Start date to" cannot be after "end date to"'; + } + if (message !== '') { + valid = false; + setErrorMessageSafely(message, errorObject, setErrorObject); + } } return { @@ -507,9 +538,9 @@ export default function ProcessInstanceListTable({ queryParamString += `&report_id=${processInstanceReportSelection.id}`; } - setErrorMessage(null); + setErrorObject(null); setProcessInstanceReportJustSaved(null); - navigate(`/admin/process-instances?${queryParamString}`); + navigate(`${processInstanceListPathPrefix}?${queryParamString}`); }; const dateComponent = ( @@ -606,9 +637,9 @@ export default function ProcessInstanceListTable({ queryParamString = `?report_id=${selectedReport.id}`; } - setErrorMessage(null); + setErrorObject(null); setProcessInstanceReportJustSaved(mode || null); - navigate(`/admin/process-instances${queryParamString}`); + navigate(`${processInstanceListPathPrefix}${queryParamString}`); }; const reportColumns = () => { @@ -638,7 +669,7 @@ export default function ProcessInstanceListTable({ startToSeconds, endFromSeconds, endToSeconds, - } = calculateStartAndEndSeconds(); + } = calculateStartAndEndSeconds(false); if (!valid || !reportMetadata) { return null; @@ -662,6 +693,19 @@ export default function ProcessInstanceListTable({ ); }; + const onDeleteReportSuccess = () => { + processInstanceReportDidChange({ selectedItem: null }); + }; + + const deleteReportComponent = () => { + return processInstanceReportSelection ? ( + + ) : null; + }; + const removeColumn = (reportColumn: ReportColumn) => { if (reportMetadata) { const reportMetadataCopy = { ...reportMetadata }; @@ -741,7 +785,6 @@ export default function ProcessInstanceListTable({ setReportMetadata(reportMetadataCopy); setReportColumnToOperateOn(null); setShowReportColumnForm(false); - setShowReportColumnForm(false); } }; @@ -762,9 +805,12 @@ export default function ProcessInstanceListTable({ }; const updateReportColumn = (event: any) => { - const reportColumnForEditing = reportColumnToReportColumnForEditing( - event.selectedItem - ); + let reportColumnForEditing = null; + if (event.selectedItem) { + reportColumnForEditing = reportColumnToReportColumnForEditing( + event.selectedItem + ); + } setReportColumnToOperateOn(reportColumnForEditing); }; @@ -794,7 +840,29 @@ export default function ProcessInstanceListTable({ if (reportColumnFormMode === '') { return null; } - const formElements = [ + const formElements = []; + if (reportColumnFormMode === 'new') { + formElements.push( + { + if (reportColumn) { + return reportColumn.accessor; + } + return null; + }} + shouldFilterItem={shouldFilterReportColumn} + placeholder="Choose a column to show" + titleText="Column" + selectedItem={reportColumnToOperateOn} + /> + ); + } + formElements.push([ , - ]; + ]); if (reportColumnToOperateOn && reportColumnToOperateOn.filterable) { formElements.push( ); } - if (reportColumnFormMode === 'new') { - formElements.push( - { - if (reportColumn) { - return reportColumn.accessor; - } - return null; - }} - shouldFilterItem={shouldFilterReportColumn} - placeholder="Choose a column to show" - titleText="Column" - /> - ); - } + formElements.push( +
+ ); const modalHeading = reportColumnFormMode === 'new' ? 'Add Column' @@ -937,6 +987,22 @@ export default function ProcessInstanceListTable({ return null; }; + const handleProcessInstanceInitiatorSearchResult = (result: any) => { + if (lastRequestedInitatorSearchTerm.current === result.username_prefix) { + setProcessInstanceInitiatorOptions(result.users); + } + }; + + const searchForProcessInitiator = (inputText: string) => { + if (inputText) { + lastRequestedInitatorSearchTerm.current = inputText; + HttpService.makeCallToBackend({ + path: `/users/search?username_prefix=${inputText}`, + successCallback: handleProcessInstanceInitiatorSearchResult, + }); + } + }; + const filterOptions = () => { if (!showFilterOptions) { return null; @@ -969,7 +1035,27 @@ export default function ProcessInstanceListTable({ selectedItem={processModelSelection} /> - {processStatusSearch()} + + { + setProcessInitiatorSelection(event.selectedItem); + }} + id="process-instance-initiator-search" + data-qa="process-instance-initiator-search" + items={processInstanceInitiatorOptions} + itemToString={(processInstanceInitatorOption: User) => { + if (processInstanceInitatorOption) { + return processInstanceInitatorOption.username; + } + return null; + }} + placeholder="Starting typing username" + titleText="Process Initiator" + selectedItem={processInitiatorSelection} + /> + + {processStatusSearch()} @@ -1043,6 +1129,7 @@ export default function ProcessInstanceListTable({ {saveAsReportComponent()} + {deleteReportComponent()} @@ -1074,10 +1161,10 @@ export default function ProcessInstanceListTable({ return ( - {id} + {id} ); }; diff --git a/src/components/ProcessInstanceRun.tsx b/src/components/ProcessInstanceRun.tsx index 05b643da..b956abcc 100644 --- a/src/components/ProcessInstanceRun.tsx +++ b/src/components/ProcessInstanceRun.tsx @@ -78,7 +78,7 @@ export default function ProcessInstanceRun({ checkPermissions = true, }: OwnProps) { const navigate = useNavigate(); - const setErrorMessage = (useContext as any)(ErrorContext)[1]; + const setErrorObject = (useContext as any)(ErrorContext)[1]; const modifiedProcessModelId = modifyProcessIdentifierForPathParam( processModel.id ); @@ -105,12 +105,12 @@ export default function ProcessInstanceRun({ }; const processModelRun = (processInstance: any) => { - setErrorMessage(null); + setErrorObject(null); storeRecentProcessModelInLocalStorage(processModel); HttpService.makeCallToBackend({ path: `/process-instances/${modifiedProcessModelId}/${processInstance.id}/run`, successCallback: onProcessInstanceRun, - failureCallback: setErrorMessage, + failureCallback: setErrorObject, httpMethod: 'POST', }); }; diff --git a/src/components/ProcessModelListTiles.tsx b/src/components/ProcessModelListTiles.tsx index 1412635c..517a0302 100644 --- a/src/components/ProcessModelListTiles.tsx +++ b/src/components/ProcessModelListTiles.tsx @@ -11,6 +11,7 @@ import { truncateString, } from '../helpers'; import ProcessInstanceRun from './ProcessInstanceRun'; +import { Notification } from './Notification'; type OwnProps = { headerElement?: ReactElement; @@ -35,7 +36,7 @@ export default function ProcessModelListTiles({ setProcessModels(result.results); }; // only allow 10 for now until we get the backend only returning certain models for user execution - let queryParams = '?per_page=20'; + let queryParams = '?per_page=1000'; if (processGroup) { queryParams = `${queryParams}&process_group_identifier=${processGroup.id}`; } else { @@ -50,20 +51,19 @@ export default function ProcessModelListTiles({ const processInstanceRunResultTag = () => { if (processInstance) { return ( -
-

- Process Instance {processInstance.id} kicked off ( - - view - - ). -

-
+ setProcessInstance(null)} + > + + view + + ); } return null; diff --git a/src/components/ProcessModelSearch.tsx b/src/components/ProcessModelSearch.tsx index 8a3c0b9f..bd995bc3 100644 --- a/src/components/ProcessModelSearch.tsx +++ b/src/components/ProcessModelSearch.tsx @@ -2,8 +2,7 @@ import { ComboBox, // @ts-ignore } from '@carbon/react'; -import { truncateString } from '../helpers'; -import { ProcessModel } from '../interfaces'; +import { ProcessGroupLite, ProcessModel } from '../interfaces'; type OwnProps = { onChange: (..._args: any[]) => any; @@ -18,12 +17,27 @@ export default function ProcessModelSearch({ onChange, titleText = 'Process model', }: OwnProps) { + const getParentGroupsDisplayName = (processModel: ProcessModel) => { + if (processModel.parent_groups) { + return processModel.parent_groups + .map((parentGroup: ProcessGroupLite) => { + return parentGroup.display_name; + }) + .join(' / '); + } + return ''; + }; + + const getFullProcessModelLabel = (processModel: ProcessModel) => { + return `${processModel.id} (${getParentGroupsDisplayName(processModel)} ${ + processModel.display_name + })`; + }; + const shouldFilterProcessModel = (options: any) => { const processModel: ProcessModel = options.item; const { inputValue } = options; - return `${processModel.id} (${processModel.display_name})`.includes( - inputValue - ); + return getFullProcessModelLabel(processModel).includes(inputValue); }; return ( { if (processModel) { - return `${processModel.id} (${truncateString( - processModel.display_name, - 75 - )})`; + return getFullProcessModelLabel(processModel); } return null; }} diff --git a/src/components/TaskListTable.tsx b/src/components/TaskListTable.tsx new file mode 100644 index 00000000..2e53bcea --- /dev/null +++ b/src/components/TaskListTable.tsx @@ -0,0 +1,233 @@ +import { useEffect, useState } from 'react'; +// @ts-ignore +import { Button, Table } from '@carbon/react'; +import { Link, useSearchParams } from 'react-router-dom'; +import UserService from '../services/UserService'; +import PaginationForTable from './PaginationForTable'; +import { + convertSecondsToFormattedDateTime, + getPageInfoFromSearchParams, + modifyProcessIdentifierForPathParam, + refreshAtInterval, +} from '../helpers'; +import HttpService from '../services/HttpService'; +import { PaginationObject, ProcessInstanceTask } from '../interfaces'; +import TableCellWithTimeAgoInWords from './TableCellWithTimeAgoInWords'; + +const PER_PAGE_FOR_TASKS_ON_HOME_PAGE = 5; +const REFRESH_INTERVAL = 5; +const REFRESH_TIMEOUT = 600; + +type OwnProps = { + apiPath: string; + tableTitle: string; + tableDescription: string; + additionalParams?: string; + paginationQueryParamPrefix?: string; + paginationClassName?: string; + autoReload?: boolean; + showStartedBy?: boolean; + showWaitingOn?: boolean; + textToShowIfEmpty?: string; +}; + +export default function TaskListTable({ + apiPath, + tableTitle, + tableDescription, + additionalParams, + paginationQueryParamPrefix, + paginationClassName, + textToShowIfEmpty, + autoReload = false, + showStartedBy = true, + showWaitingOn = true, +}: OwnProps) { + const [searchParams] = useSearchParams(); + const [tasks, setTasks] = useState(null); + const [pagination, setPagination] = useState(null); + + const preferredUsername = UserService.getPreferredUsername(); + const userEmail = UserService.getUserEmail(); + + useEffect(() => { + const getTasks = () => { + const { page, perPage } = getPageInfoFromSearchParams( + searchParams, + PER_PAGE_FOR_TASKS_ON_HOME_PAGE, + undefined, + paginationQueryParamPrefix + ); + const setTasksFromResult = (result: any) => { + setTasks(result.results); + setPagination(result.pagination); + }; + let params = `?per_page=${perPage}&page=${page}`; + if (additionalParams) { + params += `&${additionalParams}`; + } + HttpService.makeCallToBackend({ + path: `${apiPath}${params}`, + successCallback: setTasksFromResult, + }); + }; + getTasks(); + if (autoReload) { + return refreshAtInterval(REFRESH_INTERVAL, REFRESH_TIMEOUT, getTasks); + } + return undefined; + }, [ + searchParams, + additionalParams, + apiPath, + paginationQueryParamPrefix, + autoReload, + ]); + + const getWaitingForTableCellComponent = ( + processInstanceTask: ProcessInstanceTask + ) => { + let fullUsernameString = ''; + let shortUsernameString = ''; + if (processInstanceTask.assigned_user_group_identifier) { + fullUsernameString = processInstanceTask.assigned_user_group_identifier; + shortUsernameString = processInstanceTask.assigned_user_group_identifier; + } + if (processInstanceTask.potential_owner_usernames) { + fullUsernameString = processInstanceTask.potential_owner_usernames; + const usernames = + processInstanceTask.potential_owner_usernames.split(','); + const firstTwoUsernames = usernames.slice(0, 2); + if (usernames.length > 2) { + firstTwoUsernames.push('...'); + } + shortUsernameString = firstTwoUsernames.join(','); + } + return {shortUsernameString}; + }; + + const buildTable = () => { + if (!tasks) { + return null; + } + const rows = tasks.map((row: ProcessInstanceTask) => { + const taskUrl = `/tasks/${row.process_instance_id}/${row.task_id}`; + const modifiedProcessModelIdentifier = + modifyProcessIdentifierForPathParam(row.process_model_identifier); + + const regex = new RegExp(`\\b(${preferredUsername}|${userEmail})\\b`); + let hasAccessToCompleteTask = false; + if (row.potential_owner_usernames.match(regex)) { + hasAccessToCompleteTask = true; + } + return ( + + + + {row.process_instance_id} + + + + + {row.process_model_display_name} + + + + {row.task_title} + + {showStartedBy ? {row.process_initiator_username} : ''} + {showWaitingOn ? {getWaitingForTableCellComponent(row)} : ''} + + {convertSecondsToFormattedDateTime(row.created_at_in_seconds) || + '-'} + + + + + + + ); + }); + let tableHeaders = ['Id', 'Process', 'Task']; + if (showStartedBy) { + tableHeaders.push('Started By'); + } + if (showWaitingOn) { + tableHeaders.push('Waiting For'); + } + tableHeaders = tableHeaders.concat([ + 'Date Started', + 'Last Updated', + 'Actions', + ]); + return ( + + + + {tableHeaders.map((tableHeader: string) => { + return ; + })} + + + {rows} +
{tableHeader}
+ ); + }; + + const tasksComponent = () => { + if (pagination && pagination.total < 1) { + return ( +

+ {textToShowIfEmpty} +

+ ); + } + const { page, perPage } = getPageInfoFromSearchParams( + searchParams, + PER_PAGE_FOR_TASKS_ON_HOME_PAGE, + undefined, + paginationQueryParamPrefix + ); + return ( + + ); + }; + + if (tasks) { + return ( + <> +

{tableTitle}

+

{tableDescription}

+ {tasksComponent()} + + ); + } + return null; +} diff --git a/src/components/TasksForMyOpenProcesses.tsx b/src/components/TasksForMyOpenProcesses.tsx index 297f2071..be1d9042 100644 --- a/src/components/TasksForMyOpenProcesses.tsx +++ b/src/components/TasksForMyOpenProcesses.tsx @@ -1,156 +1,18 @@ -import { useEffect, useState } from 'react'; -// @ts-ignore -import { Button, Table } from '@carbon/react'; -import { Link, useSearchParams } from 'react-router-dom'; -import PaginationForTable from './PaginationForTable'; -import { - convertSecondsToFormattedDateTime, - getPageInfoFromSearchParams, - modifyProcessIdentifierForPathParam, - refreshAtInterval, -} from '../helpers'; -import HttpService from '../services/HttpService'; -import { PaginationObject } from '../interfaces'; -import TableCellWithTimeAgoInWords from './TableCellWithTimeAgoInWords'; +import TaskListTable from './TaskListTable'; -const PER_PAGE_FOR_TASKS_ON_HOME_PAGE = 5; const paginationQueryParamPrefix = 'tasks_for_my_open_processes'; -const REFRESH_INTERVAL = 5; -const REFRESH_TIMEOUT = 600; export default function MyOpenProcesses() { - const [searchParams] = useSearchParams(); - const [tasks, setTasks] = useState([]); - const [pagination, setPagination] = useState(null); - - useEffect(() => { - const getTasks = () => { - const { page, perPage } = getPageInfoFromSearchParams( - searchParams, - PER_PAGE_FOR_TASKS_ON_HOME_PAGE, - undefined, - paginationQueryParamPrefix - ); - const setTasksFromResult = (result: any) => { - setTasks(result.results); - setPagination(result.pagination); - }; - HttpService.makeCallToBackend({ - path: `/tasks/for-my-open-processes?per_page=${perPage}&page=${page}`, - successCallback: setTasksFromResult, - }); - }; - getTasks(); - return refreshAtInterval(REFRESH_INTERVAL, REFRESH_TIMEOUT, getTasks); - }, [searchParams]); - - const buildTable = () => { - const rows = tasks.map((row) => { - const rowToUse = row as any; - const taskUrl = `/tasks/${rowToUse.process_instance_id}/${rowToUse.task_id}`; - const modifiedProcessModelIdentifier = - modifyProcessIdentifierForPathParam(rowToUse.process_model_identifier); - return ( - - - - {rowToUse.process_instance_id} - - - - - {rowToUse.process_model_display_name} - - - - {rowToUse.task_title} - - {rowToUse.group_identifier || '-'} - - {convertSecondsToFormattedDateTime( - rowToUse.created_at_in_seconds - ) || '-'} - - - - - - - ); - }); - return ( - - - - - - - - - - - - - {rows} -
IdProcessTaskWaiting ForDate StartedLast UpdatedActions
- ); - }; - - const tasksComponent = () => { - if (pagination && pagination.total < 1) { - return ( -

- There are no tasks for processes you started at this time. -

- ); - } - const { page, perPage } = getPageInfoFromSearchParams( - searchParams, - PER_PAGE_FOR_TASKS_ON_HOME_PAGE, - undefined, - paginationQueryParamPrefix - ); - return ( - - ); - }; - return ( - <> -

My open instances

-

- These tasks are for processes you started which are not complete. You - may not have an action to take at this time. See below for tasks waiting - on you. -

- {tasksComponent()} - + ); } diff --git a/src/components/TasksWaitingForMe.tsx b/src/components/TasksWaitingForMe.tsx index 7d06b7a3..1939e4ba 100644 --- a/src/components/TasksWaitingForMe.tsx +++ b/src/components/TasksWaitingForMe.tsx @@ -1,149 +1,16 @@ -import { useEffect, useState } from 'react'; -// @ts-ignore -import { Button, Table } from '@carbon/react'; -import { Link, useSearchParams } from 'react-router-dom'; -import PaginationForTable from './PaginationForTable'; -import { - convertSecondsToFormattedDateTime, - getPageInfoFromSearchParams, - modifyProcessIdentifierForPathParam, -} from '../helpers'; -import HttpService from '../services/HttpService'; -import { PaginationObject } from '../interfaces'; -import TableCellWithTimeAgoInWords from './TableCellWithTimeAgoInWords'; - -const PER_PAGE_FOR_TASKS_ON_HOME_PAGE = 5; +import TaskListTable from './TaskListTable'; export default function TasksWaitingForMe() { - const [searchParams] = useSearchParams(); - const [tasks, setTasks] = useState([]); - const [pagination, setPagination] = useState(null); - - useEffect(() => { - const { page, perPage } = getPageInfoFromSearchParams( - searchParams, - PER_PAGE_FOR_TASKS_ON_HOME_PAGE, - undefined, - 'tasks_waiting_for_me' - ); - const setTasksFromResult = (result: any) => { - setTasks(result.results); - setPagination(result.pagination); - }; - HttpService.makeCallToBackend({ - path: `/tasks/for-me?per_page=${perPage}&page=${page}`, - successCallback: setTasksFromResult, - }); - }, [searchParams]); - - const buildTable = () => { - const rows = tasks.map((row) => { - const rowToUse = row as any; - const taskUrl = `/tasks/${rowToUse.process_instance_id}/${rowToUse.task_id}`; - const modifiedProcessModelIdentifier = - modifyProcessIdentifierForPathParam(rowToUse.process_model_identifier); - return ( - - - - {rowToUse.process_instance_id} - - - - - {rowToUse.process_model_display_name} - - - - {rowToUse.task_title} - - {rowToUse.username} - {rowToUse.group_identifier || '-'} - - {convertSecondsToFormattedDateTime( - rowToUse.created_at_in_seconds - ) || '-'} - - - - - - - ); - }); - return ( - - - - - - - - - - - - - - {rows} -
IdProcessTaskStarted ByWaiting ForDate StartedLast UpdatedActions
- ); - }; - - const tasksComponent = () => { - if (pagination && pagination.total < 1) { - return ( -

- You have no task assignments at this time. -

- ); - } - const { page, perPage } = getPageInfoFromSearchParams( - searchParams, - PER_PAGE_FOR_TASKS_ON_HOME_PAGE, - undefined, - 'tasks_waiting_for_me' - ); - return ( - - ); - }; - return ( - <> -

Tasks waiting for me

-

- These processes are waiting on you to complete the next task. All are - processes created by others that are now actionable by you. -

- {tasksComponent()} - + ); } diff --git a/src/components/TasksWaitingForMyGroups.tsx b/src/components/TasksWaitingForMyGroups.tsx index 5b05dcd0..dab0372b 100644 --- a/src/components/TasksWaitingForMyGroups.tsx +++ b/src/components/TasksWaitingForMyGroups.tsx @@ -1,156 +1,41 @@ import { useEffect, useState } from 'react'; -// @ts-ignore -import { Button, Table } from '@carbon/react'; -import { Link, useSearchParams } from 'react-router-dom'; -import PaginationForTable from './PaginationForTable'; -import { - convertSecondsToFormattedDateTime, - getPageInfoFromSearchParams, - modifyProcessIdentifierForPathParam, - refreshAtInterval, -} from '../helpers'; import HttpService from '../services/HttpService'; -import { PaginationObject } from '../interfaces'; -import TableCellWithTimeAgoInWords from './TableCellWithTimeAgoInWords'; - -const PER_PAGE_FOR_TASKS_ON_HOME_PAGE = 5; -const paginationQueryParamPrefix = 'tasks_waiting_for_my_groups'; -const REFRESH_INTERVAL = 5; -const REFRESH_TIMEOUT = 600; +import TaskListTable from './TaskListTable'; export default function TasksWaitingForMyGroups() { - const [searchParams] = useSearchParams(); - const [tasks, setTasks] = useState([]); - const [pagination, setPagination] = useState(null); + const [userGroups, setUserGroups] = useState(null); useEffect(() => { - const getTasks = () => { - const { page, perPage } = getPageInfoFromSearchParams( - searchParams, - PER_PAGE_FOR_TASKS_ON_HOME_PAGE, - undefined, - paginationQueryParamPrefix - ); - const setTasksFromResult = (result: any) => { - setTasks(result.results); - setPagination(result.pagination); - }; - HttpService.makeCallToBackend({ - path: `/tasks/for-my-groups?per_page=${perPage}&page=${page}`, - successCallback: setTasksFromResult, - }); - }; - getTasks(); - return refreshAtInterval(REFRESH_INTERVAL, REFRESH_TIMEOUT, getTasks); - }, [searchParams]); + HttpService.makeCallToBackend({ + path: `/user-groups/for-current-user`, + successCallback: setUserGroups, + }); + }, [setUserGroups]); - const buildTable = () => { - const rows = tasks.map((row) => { - const rowToUse = row as any; - const taskUrl = `/tasks/${rowToUse.process_instance_id}/${rowToUse.task_id}`; - const modifiedProcessModelIdentifier = - modifyProcessIdentifierForPathParam(rowToUse.process_model_identifier); + const tableComponents = () => { + if (!userGroups) { + return null; + } + + return userGroups.map((userGroup: string) => { return ( - - - - {rowToUse.process_instance_id} - - - - - {rowToUse.process_model_display_name} - - - - {rowToUse.task_title} - - {rowToUse.username} - {rowToUse.group_identifier || '-'} - - {convertSecondsToFormattedDateTime( - rowToUse.created_at_in_seconds - ) || '-'} - - - - - - + ); }); - return ( - - - - - - - - - - - - - - {rows} -
IdProcessTaskStarted ByWaiting ForDate StartedLast UpdatedActions
- ); }; - const tasksComponent = () => { - if (pagination && pagination.total < 1) { - return ( -

- Your groups have no task assignments at this time. -

- ); - } - const { page, perPage } = getPageInfoFromSearchParams( - searchParams, - PER_PAGE_FOR_TASKS_ON_HOME_PAGE, - undefined, - paginationQueryParamPrefix - ); - return ( - - ); - }; - - return ( - <> -

Tasks waiting for my groups

-

- This is a list of tasks for groups you belong to that can be completed - by any member of the group. -

- {tasksComponent()} - - ); + if (userGroups) { + return <>{tableComponents()}; + } + return null; } diff --git a/src/config.tsx b/src/config.tsx index b0816a39..abaadd5e 100644 --- a/src/config.tsx +++ b/src/config.tsx @@ -1,11 +1,23 @@ -const host = window.location.hostname; -let hostAndPort = `api.${host}`; +const { port, hostname } = window.location; +let hostAndPort = `api.${hostname}`; let protocol = 'https'; -if (/^\d+\./.test(host) || host === 'localhost') { - hostAndPort = `${host}:7000`; + +if (/^\d+\./.test(hostname) || hostname === 'localhost') { + let serverPort = 7000; + if (!Number.isNaN(Number(port))) { + serverPort = Number(port) - 1; + } + hostAndPort = `${hostname}:${serverPort}`; protocol = 'http'; } -export const BACKEND_BASE_URL = `${protocol}://${hostAndPort}/v1.0`; + +let url = `${protocol}://${hostAndPort}/v1.0`; +// Allow overriding the backend base url with an environment variable at build time. +if (process.env.REACT_APP_BACKEND_BASE_URL) { + url = process.env.REACT_APP_BACKEND_BASE_URL; +} + +export const BACKEND_BASE_URL = url; export const PROCESS_STATUSES = [ 'not_started', diff --git a/src/helpers.test.tsx b/src/helpers.test.tsx index 5a0352b8..5a7889a6 100644 --- a/src/helpers.test.tsx +++ b/src/helpers.test.tsx @@ -1,4 +1,9 @@ -import { convertSecondsToFormattedDateString, slugifyString } from './helpers'; +import { + convertSecondsToFormattedDateString, + isInteger, + slugifyString, + underscorizeString, +} from './helpers'; test('it can slugify a string', () => { expect(slugifyString('hello---world_ and then Some such-')).toEqual( @@ -6,7 +11,21 @@ test('it can slugify a string', () => { ); }); +test('it can underscorize a string', () => { + expect(underscorizeString('hello---world_ and then Some such-')).toEqual( + 'hello_world_and_then_some_such' + ); +}); + test('it can keep the correct date when converting seconds to date', () => { const dateString = convertSecondsToFormattedDateString(1666325400); expect(dateString).toEqual('2022-10-21'); }); + +test('it can validate numeric values', () => { + expect(isInteger('11')).toEqual(true); + expect(isInteger('hey')).toEqual(false); + expect(isInteger(' ')).toEqual(false); + expect(isInteger('1 2')).toEqual(false); + expect(isInteger(2)).toEqual(true); +}); diff --git a/src/helpers.tsx b/src/helpers.tsx index 8f625533..b3edd776 100644 --- a/src/helpers.tsx +++ b/src/helpers.tsx @@ -8,6 +8,7 @@ import { DEFAULT_PER_PAGE, DEFAULT_PAGE, } from './components/PaginationForTable'; +import { ErrorForDisplay } from './interfaces'; // https://www.30secondsofcode.org/js/s/slugify export const slugifyString = (str: any) => { @@ -20,6 +21,10 @@ export const slugifyString = (str: any) => { .replace(/-+$/g, ''); }; +export const underscorizeString = (inputString: string) => { + return slugifyString(inputString).replace(/-/g, '_'); +}; + export const capitalizeFirstLetter = (string: any) => { return string.charAt(0).toUpperCase() + string.slice(1); }; @@ -234,3 +239,21 @@ export const getBpmnProcessIdentifiers = (rootBpmnElement: any) => { childProcesses.push(rootBpmnElement.businessObject.id); return childProcesses; }; + +// Setting the error message state to the same string is still considered a change +// and re-renders the page so check the message first to avoid that. +export const setErrorMessageSafely = ( + newErrorMessageString: string, + oldErrorMessage: ErrorForDisplay, + errorMessageSetter: any +) => { + if (oldErrorMessage && oldErrorMessage.message === newErrorMessageString) { + return null; + } + errorMessageSetter({ message: newErrorMessageString }); + return null; +}; + +export const isInteger = (str: string | number) => { + return /^\d+$/.test(str.toString()); +}; diff --git a/src/hooks/UriListForPermissions.tsx b/src/hooks/UriListForPermissions.tsx index 4ba04352..f8e5f07f 100644 --- a/src/hooks/UriListForPermissions.tsx +++ b/src/hooks/UriListForPermissions.tsx @@ -9,13 +9,20 @@ export const useUriListForPermissions = () => { messageInstanceListPath: '/v1.0/messages', processGroupListPath: '/v1.0/process-groups', processGroupShowPath: `/v1.0/process-groups/${params.process_group_id}`, - processInstanceCreatePath: `/v1.0/process-instances/${params.process_model_id}`, processInstanceActionPath: `/v1.0/process-instances/${params.process_model_id}/${params.process_instance_id}`, + processInstanceCreatePath: `/v1.0/process-instances/${params.process_model_id}`, processInstanceListPath: '/v1.0/process-instances', processInstanceLogListPath: `/v1.0/logs/${params.process_model_id}/${params.process_instance_id}`, processInstanceReportListPath: '/v1.0/process-instances/reports', - processInstanceTaskListPath: `/v1.0/process-instances/${params.process_model_id}/${params.process_instance_id}/task-info`, + processInstanceResumePath: `/v1.0/process-instance-resume/${params.process_model_id}/${params.process_instance_id}`, + processInstanceSuspendPath: `/v1.0/process-instance-suspend/${params.process_model_id}/${params.process_instance_id}`, + processInstanceResetPath: `/v1.0/process-instance-reset/${params.process_model_id}/${params.process_instance_id}`, processInstanceTaskListDataPath: `/v1.0/task-data/${params.process_model_id}/${params.process_instance_id}`, + processInstanceSendEventPath: `/v1.0/send-event/${params.process_model_id}/${params.process_instance_id}`, + processInstanceCompleteTaskPath: `/v1.0/complete-task/${params.process_model_id}/${params.process_instance_id}`, + processInstanceTaskListPath: `/v1.0/process-instances/${params.process_model_id}/${params.process_instance_id}/task-info`, + processInstanceTaskListForMePath: `/v1.0/process-instances/for-me/${params.process_model_id}/${params.process_instance_id}/task-info`, + processInstanceTerminatePath: `/v1.0/process-instance-terminate/${params.process_model_id}/${params.process_instance_id}`, processModelCreatePath: `/v1.0/process-models/${params.process_group_id}`, processModelFileCreatePath: `/v1.0/process-models/${params.process_model_id}/files`, processModelFileShowPath: `/v1.0/process-models/${params.process_model_id}/files/${params.file_name}`, diff --git a/src/index.css b/src/index.css index 248a23d7..08e8341c 100644 --- a/src/index.css +++ b/src/index.css @@ -355,8 +355,8 @@ svg.notification-icon { word-break: normal; } -.combo-box-in-modal { - height: 300px; +.vertical-spacer-to-allow-combo-box-to-expand-in-modal { + height: 250px; } .cds--btn.narrow-button { diff --git a/src/interfaces.ts b/src/interfaces.ts index 6afb1144..97ef763c 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -1,3 +1,8 @@ +export interface User { + id: number; + username: string; +} + export interface Secret { id: number; key: string; @@ -5,6 +10,11 @@ export interface Secret { creator_user_id: string; } +export interface ProcessData { + process_data_identifier: string; + process_data_value: any; +} + export interface RecentProcessModel { processGroupIdentifier?: string; processModelIdentifier: string; @@ -12,10 +22,24 @@ export interface RecentProcessModel { } export interface ProcessInstanceTask { - id: string; + id: number; + task_id: string; + process_instance_id: number; + process_model_display_name: string; + process_model_identifier: string; + task_title: string; + lane_assignment_id: string; + process_instance_status: string; state: string; process_identifier: string; name: string; + process_initiator_username: string; + assigned_user_group_identifier: string; + created_at_in_seconds: number; + updated_at_in_seconds: number; + current_user_is_potential_owner: number; + potential_owner_usernames: string; + calling_subprocess_task_id: string; } export interface ProcessReference { @@ -46,6 +70,10 @@ export interface ProcessInstance { id: number; process_model_identifier: string; process_model_display_name: string; + status: string; + start_in_seconds: number | null; + end_in_seconds: number | null; + bpmn_xml_file_contents?: string; spiff_step?: number; } @@ -143,6 +171,10 @@ export type HotCrumbItem = HotCrumbItemArray | HotCrumbItemObject; export interface ErrorForDisplay { message: string; sentry_link?: string; + task_name?: string; + task_id?: string; + line_number?: number; + file_name?: string; } export interface AuthenticationParam { diff --git a/src/routes/AdminRoutes.tsx b/src/routes/AdminRoutes.tsx index da6cae35..8d21a5b9 100644 --- a/src/routes/AdminRoutes.tsx +++ b/src/routes/AdminRoutes.tsx @@ -22,14 +22,16 @@ import ProcessInstanceLogList from './ProcessInstanceLogList'; import MessageInstanceList from './MessageInstanceList'; import Configuration from './Configuration'; import JsonSchemaFormBuilder from './JsonSchemaFormBuilder'; +import ProcessModelNewExperimental from './ProcessModelNewExperimental'; +import ProcessInstanceFindById from './ProcessInstanceFindById'; export default function AdminRoutes() { const location = useLocation(); - const setErrorMessage = (useContext as any)(ErrorContext)[1]; + const setErrorObject = (useContext as any)(ErrorContext)[1]; useEffect(() => { - setErrorMessage(null); - }, [location, setErrorMessage]); + setErrorObject(null); + }, [location, setErrorObject]); if (UserService.hasRole(['admin'])) { return ( @@ -50,6 +52,10 @@ export default function AdminRoutes() { path="process-models/:process_group_id/new" element={} /> + } + /> } @@ -62,21 +68,25 @@ export default function AdminRoutes() { path="process-models/:process_model_id/files/:file_name" element={} /> - } - /> } /> + } + /> + } + /> } + element={} /> } + element={} /> } /> - } /> + } + /> + } + /> + } + /> } /> } /> } /> + } + /> ); } diff --git a/src/routes/AuthenticationList.tsx b/src/routes/AuthenticationList.tsx index 4f320df4..a0d15101 100644 --- a/src/routes/AuthenticationList.tsx +++ b/src/routes/AuthenticationList.tsx @@ -7,7 +7,7 @@ import HttpService from '../services/HttpService'; import UserService from '../services/UserService'; export default function AuthenticationList() { - const setErrorMessage = (useContext as any)(ErrorContext)[1]; + const setErrorObject = (useContext as any)(ErrorContext)[1]; const [authenticationList, setAuthenticationList] = useState< AuthenticationItem[] | null @@ -26,9 +26,9 @@ export default function AuthenticationList() { HttpService.makeCallToBackend({ path: `/authentications`, successCallback: processResult, - failureCallback: setErrorMessage, + failureCallback: setErrorObject, }); - }, [setErrorMessage]); + }, [setErrorObject]); const buildTable = () => { if (authenticationList) { diff --git a/src/routes/CompletedInstances.tsx b/src/routes/CompletedInstances.tsx index f97bb5d5..5c7ce445 100644 --- a/src/routes/CompletedInstances.tsx +++ b/src/routes/CompletedInstances.tsx @@ -1,6 +1,45 @@ +import { useEffect, useState } from 'react'; import ProcessInstanceListTable from '../components/ProcessInstanceListTable'; +import HttpService from '../services/HttpService'; export default function CompletedInstances() { + const [userGroups, setUserGroups] = useState(null); + + useEffect(() => { + HttpService.makeCallToBackend({ + path: `/user-groups/for-current-user`, + successCallback: setUserGroups, + }); + }, [setUserGroups]); + + const groupTableComponents = () => { + if (!userGroups) { + return null; + } + + return userGroups.map((userGroup: string) => { + return ( + <> +

With tasks completed by group: {userGroup}

+

+ This is a list of instances with tasks that were completed by the{' '} + {userGroup} group. +

+ + + ); + }); + }; + return ( <>

My completed instances

@@ -11,13 +50,13 @@ export default function CompletedInstances() { filtersEnabled={false} paginationQueryParamPrefix="my_completed_instances" perPageOptions={[2, 5, 25]} - reportIdentifier="system_report_instances_initiated_by_me" + reportIdentifier="system_report_completed_instances_initiated_by_me" showReports={false} textToShowIfEmpty="You have no completed instances at this time." paginationClassName="with-large-bottom-margin" autoReload /> -

Tasks completed by me

+

With tasks completed by me

This is a list of instances where you have completed tasks.

@@ -25,24 +64,12 @@ export default function CompletedInstances() { filtersEnabled={false} paginationQueryParamPrefix="my_completed_tasks" perPageOptions={[2, 5, 25]} - reportIdentifier="system_report_instances_with_tasks_completed_by_me" + reportIdentifier="system_report_completed_instances_with_tasks_completed_by_me" showReports={false} - textToShowIfEmpty="You have no completed tasks at this time." + textToShowIfEmpty="You have no completed instances at this time." paginationClassName="with-large-bottom-margin" /> -

Tasks completed by my groups

-

- This is a list of instances with tasks that were completed by groups you - belong to. -

- + {groupTableComponents()} ); } diff --git a/src/routes/Configuration.tsx b/src/routes/Configuration.tsx index b2e30416..bd9e59c5 100644 --- a/src/routes/Configuration.tsx +++ b/src/routes/Configuration.tsx @@ -14,7 +14,7 @@ import { usePermissionFetcher } from '../hooks/PermissionService'; export default function Configuration() { const location = useLocation(); - const setErrorMessage = (useContext as any)(ErrorContext)[1]; + const setErrorObject = (useContext as any)(ErrorContext)[1]; const [selectedTabIndex, setSelectedTabIndex] = useState(0); const navigate = useNavigate(); @@ -26,13 +26,13 @@ export default function Configuration() { const { ability } = usePermissionFetcher(permissionRequestData); useEffect(() => { - setErrorMessage(null); + setErrorObject(null); let newSelectedTabIndex = 0; if (location.pathname.match(/^\/admin\/configuration\/authentications\b/)) { newSelectedTabIndex = 1; } setSelectedTabIndex(newSelectedTabIndex); - }, [location, setErrorMessage]); + }, [location, setErrorObject]); return ( <> diff --git a/src/routes/HomePageRoutes.tsx b/src/routes/HomePageRoutes.tsx index 872a7a69..0475d4c7 100644 --- a/src/routes/HomePageRoutes.tsx +++ b/src/routes/HomePageRoutes.tsx @@ -11,12 +11,12 @@ import CreateNewInstance from './CreateNewInstance'; export default function HomePageRoutes() { const location = useLocation(); - const setErrorMessage = (useContext as any)(ErrorContext)[1]; + const setErrorObject = (useContext as any)(ErrorContext)[1]; const [selectedTabIndex, setSelectedTabIndex] = useState(0); const navigate = useNavigate(); useEffect(() => { - setErrorMessage(null); + setErrorObject(null); let newSelectedTabIndex = 0; if (location.pathname.match(/^\/tasks\/completed-instances\b/)) { newSelectedTabIndex = 1; @@ -24,7 +24,7 @@ export default function HomePageRoutes() { newSelectedTabIndex = 2; } setSelectedTabIndex(newSelectedTabIndex); - }, [location, setErrorMessage]); + }, [location, setErrorObject]); const renderTabs = () => { if (location.pathname.match(/^\/tasks\/\d+\/\b/)) { diff --git a/src/routes/JsonSchemaFormBuilder.tsx b/src/routes/JsonSchemaFormBuilder.tsx index 6d101101..d4a9c2b4 100644 --- a/src/routes/JsonSchemaFormBuilder.tsx +++ b/src/routes/JsonSchemaFormBuilder.tsx @@ -3,7 +3,11 @@ import { useEffect, useState } from 'react'; import { Button, Select, SelectItem, TextInput } from '@carbon/react'; import { useParams } from 'react-router-dom'; import { FormField } from '../interfaces'; -import { modifyProcessIdentifierForPathParam, slugifyString } from '../helpers'; +import { + modifyProcessIdentifierForPathParam, + slugifyString, + underscorizeString, +} from '../helpers'; import HttpService from '../services/HttpService'; export default function JsonSchemaFormBuilder() { @@ -75,7 +79,7 @@ export default function JsonSchemaFormBuilder() { formFieldIdHasBeenUpdatedByUser ); if (!formFieldIdHasBeenUpdatedByUser) { - setFormFieldId(slugifyString(newFormFieldTitle)); + setFormFieldId(underscorizeString(newFormFieldTitle)); } setFormFieldTitle(newFormFieldTitle); }; diff --git a/src/routes/MyTasks.tsx b/src/routes/MyTasks.tsx index 4c1cbc9b..3daaaef6 100644 --- a/src/routes/MyTasks.tsx +++ b/src/routes/MyTasks.tsx @@ -2,6 +2,7 @@ import { useEffect, useState } from 'react'; // @ts-ignore import { Button, Table } from '@carbon/react'; import { Link, useSearchParams } from 'react-router-dom'; +import { Notification } from '../components/Notification'; import PaginationForTable from '../components/PaginationForTable'; import { getPageInfoFromSearchParams, @@ -51,20 +52,19 @@ export default function MyTasks() { const processInstanceRunResultTag = () => { if (processInstance) { return ( -
-

- Process Instance {processInstance.id} kicked off ( - - view - - ). -

-
+ setProcessInstance(null)} + > + + view + + ); } return null; diff --git a/src/routes/ProcessGroupList.tsx b/src/routes/ProcessGroupList.tsx index d9ceaf59..7dee4f20 100644 --- a/src/routes/ProcessGroupList.tsx +++ b/src/routes/ProcessGroupList.tsx @@ -39,7 +39,7 @@ export default function ProcessGroupList() { }; // for search box HttpService.makeCallToBackend({ - path: `/process-models?per_page=1000&recursive=true`, + path: `/process-models?per_page=1000&recursive=true&include_parent_groups=true`, successCallback: processResultForProcessModels, }); }, [searchParams]); diff --git a/src/routes/ProcessInstanceFindById.tsx b/src/routes/ProcessInstanceFindById.tsx new file mode 100644 index 00000000..e55520ef --- /dev/null +++ b/src/routes/ProcessInstanceFindById.tsx @@ -0,0 +1,79 @@ +import { useEffect, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +// @ts-ignore +import { Button, ButtonSet, Form, Stack, TextInput } from '@carbon/react'; +import { isInteger, modifyProcessIdentifierForPathParam } from '../helpers'; +import HttpService from '../services/HttpService'; +import { ProcessInstance } from '../interfaces'; + +export default function ProcessInstanceFindById() { + const navigate = useNavigate(); + const [processInstanceId, setProcessInstanceId] = useState(''); + const [processInstanceIdValid, setProcessInstanceIdValid] = + useState(true); + + useEffect(() => {}, []); + + const handleProcessInstanceNavigation = (result: any) => { + const processInstance: ProcessInstance = result.process_instance; + let path = '/admin/process-instances/'; + if (result.uri_type === 'for-me') { + path += 'for-me/'; + } + path += `${modifyProcessIdentifierForPathParam( + processInstance.process_model_identifier + )}/${processInstance.id}`; + navigate(path); + }; + + const handleFormSubmission = (event: any) => { + event.preventDefault(); + + if (!processInstanceId) { + setProcessInstanceIdValid(false); + } + + if (processInstanceId && processInstanceIdValid) { + HttpService.makeCallToBackend({ + path: `/process-instances/find-by-id/${processInstanceId}`, + successCallback: handleProcessInstanceNavigation, + }); + } + }; + + const handleProcessInstanceIdChange = (event: any) => { + if (isInteger(event.target.value)) { + setProcessInstanceIdValid(true); + } else { + setProcessInstanceIdValid(false); + } + setProcessInstanceId(event.target.value); + }; + + const formElements = () => { + return ( + + ); + }; + + const formButtons = () => { + const buttons = []; + return {buttons}; + }; + + return ( +
+ + {formElements()} + {formButtons()} + +
+ ); +} diff --git a/src/routes/ProcessInstanceList.tsx b/src/routes/ProcessInstanceList.tsx index 1d75db56..33af0c1f 100644 --- a/src/routes/ProcessInstanceList.tsx +++ b/src/routes/ProcessInstanceList.tsx @@ -1,15 +1,33 @@ -import { useSearchParams } from 'react-router-dom'; +import { useNavigate, useSearchParams } from 'react-router-dom'; import 'react-datepicker/dist/react-datepicker.css'; import 'react-bootstrap-typeahead/css/Typeahead.css'; import 'react-bootstrap-typeahead/css/Typeahead.bs5.css'; +// @ts-ignore +import { Tabs, TabList, Tab } from '@carbon/react'; +import { Can } from '@casl/react'; import ProcessBreadcrumb from '../components/ProcessBreadcrumb'; import ProcessInstanceListTable from '../components/ProcessInstanceListTable'; import { getProcessModelFullIdentifierFromSearchParams } from '../helpers'; +import { useUriListForPermissions } from '../hooks/UriListForPermissions'; +import { PermissionsToCheck } from '../interfaces'; +import { usePermissionFetcher } from '../hooks/PermissionService'; -export default function ProcessInstanceList() { +type OwnProps = { + variant: string; +}; + +export default function ProcessInstanceList({ variant }: OwnProps) { const [searchParams] = useSearchParams(); + const navigate = useNavigate(); + + const { targetUris } = useUriListForPermissions(); + const permissionRequestData: PermissionsToCheck = { + [targetUris.processInstanceListPath]: ['GET'], + }; + const { ability } = usePermissionFetcher(permissionRequestData); + const processInstanceBreadcrumbElement = () => { const processModelFullIdentifier = getProcessModelFullIdentifierFromSearchParams(searchParams); @@ -33,13 +51,55 @@ export default function ProcessInstanceList() { }; const processInstanceTitleElement = () => { - return

Process Instances

; + if (variant === 'all') { + return

All Process Instances

; + } + return

My Process Instances

; }; + + let selectedTabIndex = 0; + if (variant === 'all') { + selectedTabIndex = 1; + } return ( <> + + + { + navigate('/admin/process-instances/for-me'); + }} + > + For Me + + + { + navigate('/admin/process-instances/all'); + }} + > + All + + + { + navigate('/admin/process-instances/find-by-id'); + }} + > + Find By Id + + + +
{processInstanceBreadcrumbElement()} {processInstanceTitleElement()} - + ); } diff --git a/src/routes/ProcessInstanceLogList.tsx b/src/routes/ProcessInstanceLogList.tsx index 37ef5519..b4a4f683 100644 --- a/src/routes/ProcessInstanceLogList.tsx +++ b/src/routes/ProcessInstanceLogList.tsx @@ -45,7 +45,7 @@ export default function ProcessInstanceLogList() { const rowToUse = row as any; return ( - {rowToUse.id} + {rowToUse.id} {rowToUse.message} {rowToUse.bpmn_task_name} {isDetailedView && ( @@ -114,6 +114,7 @@ export default function ProcessInstanceLogList() { { searchParams.set('detailed', 'false'); setSearchParams(searchParams); @@ -123,6 +124,7 @@ export default function ProcessInstanceLogList() { { searchParams.set('detailed', 'true'); setSearchParams(searchParams); diff --git a/src/routes/ProcessInstanceReportList.tsx b/src/routes/ProcessInstanceReportList.tsx index 906fb314..b753d307 100644 --- a/src/routes/ProcessInstanceReportList.tsx +++ b/src/routes/ProcessInstanceReportList.tsx @@ -31,9 +31,7 @@ export default function ProcessInstanceReportList() { return ( - + {rowToUse.identifier} diff --git a/src/routes/ProcessInstanceShow.tsx b/src/routes/ProcessInstanceShow.tsx index 1adb585b..dc0e761f 100644 --- a/src/routes/ProcessInstanceShow.tsx +++ b/src/routes/ProcessInstanceShow.tsx @@ -25,6 +25,7 @@ import { ButtonSet, Tag, Modal, + Dropdown, Stack, // @ts-ignore } from '@carbon/react'; @@ -41,12 +42,18 @@ import ErrorContext from '../contexts/ErrorContext'; import { useUriListForPermissions } from '../hooks/UriListForPermissions'; import { PermissionsToCheck, + ProcessData, ProcessInstance, ProcessInstanceTask, } from '../interfaces'; import { usePermissionFetcher } from '../hooks/PermissionService'; +import ProcessInstanceClass from '../classes/ProcessInstanceClass'; -export default function ProcessInstanceShow() { +type OwnProps = { + variant: string; +}; + +export default function ProcessInstanceShow({ variant }: OwnProps) { const navigate = useNavigate(); const params = useParams(); const [searchParams] = useSearchParams(); @@ -57,9 +64,16 @@ export default function ProcessInstanceShow() { const [tasksCallHadError, setTasksCallHadError] = useState(false); const [taskToDisplay, setTaskToDisplay] = useState(null); const [taskDataToDisplay, setTaskDataToDisplay] = useState(''); + const [processDataToDisplay, setProcessDataToDisplay] = + useState(null); const [editingTaskData, setEditingTaskData] = useState(false); + const [selectingEvent, setSelectingEvent] = useState(false); + const [eventToSend, setEventToSend] = useState({}); + const [eventPayload, setEventPayload] = useState('{}'); + const [eventTextEditorEnabled, setEventTextEditorEnabled] = + useState(false); - const setErrorMessage = (useContext as any)(ErrorContext)[1]; + const setErrorObject = (useContext as any)(ErrorContext)[1]; const unModifiedProcessModelId = unModifyProcessIdentifierForPathParam( `${params.process_model_id}` @@ -67,16 +81,24 @@ export default function ProcessInstanceShow() { const modifiedProcessModelId = params.process_model_id; const { targetUris } = useUriListForPermissions(); + const taskListPath = + variant === 'all' + ? targetUris.processInstanceTaskListPath + : targetUris.processInstanceTaskListForMePath; + const permissionRequestData: PermissionsToCheck = { + [`${targetUris.processInstanceResumePath}`]: ['POST'], + [`${targetUris.processInstanceSuspendPath}`]: ['POST'], + [`${targetUris.processInstanceTerminatePath}`]: ['POST'], + [targetUris.processInstanceResetPath]: ['POST'], [targetUris.messageInstanceListPath]: ['GET'], - [targetUris.processInstanceTaskListPath]: ['GET'], - [targetUris.processInstanceTaskListDataPath]: ['GET', 'PUT'], [targetUris.processInstanceActionPath]: ['DELETE'], [targetUris.processInstanceLogListPath]: ['GET'], + [targetUris.processInstanceTaskListDataPath]: ['GET', 'PUT'], + [targetUris.processInstanceSendEventPath]: ['POST'], + [targetUris.processInstanceCompleteTaskPath]: ['POST'], [targetUris.processModelShowPath]: ['PUT'], - [`${targetUris.processInstanceActionPath}/suspend`]: ['PUT'], - [`${targetUris.processInstanceActionPath}/terminate`]: ['PUT'], - [`${targetUris.processInstanceActionPath}/resume`]: ['PUT'], + [taskListPath]: ['GET'], }; const { ability, permissionsLoaded } = usePermissionFetcher( permissionRequestData @@ -98,8 +120,12 @@ export default function ProcessInstanceShow() { if (processIdentifier) { queryParams = `?process_identifier=${processIdentifier}`; } + let apiPath = '/process-instances/for-me'; + if (variant === 'all') { + apiPath = '/process-instances'; + } HttpService.makeCallToBackend({ - path: `/process-instances/${modifiedProcessModelId}/${params.process_instance_id}${queryParams}`, + path: `${apiPath}/${modifiedProcessModelId}/${params.process_instance_id}${queryParams}`, successCallback: setProcessInstance, }); let taskParams = '?all_tasks=true'; @@ -109,8 +135,8 @@ export default function ProcessInstanceShow() { let taskPath = ''; if (ability.can('GET', targetUris.processInstanceTaskListDataPath)) { taskPath = `${targetUris.processInstanceTaskListDataPath}${taskParams}`; - } else if (ability.can('GET', targetUris.processInstanceTaskListPath)) { - taskPath = `${targetUris.processInstanceTaskListPath}${taskParams}`; + } else if (ability.can('GET', taskListPath)) { + taskPath = `${taskListPath}${taskParams}`; } if (taskPath) { HttpService.makeCallToBackend({ @@ -129,6 +155,8 @@ export default function ProcessInstanceShow() { ability, targetUris, searchParams, + taskListPath, + variant, ]); const deleteProcessInstance = () => { @@ -146,7 +174,7 @@ export default function ProcessInstanceShow() { const terminateProcessInstance = () => { HttpService.makeCallToBackend({ - path: `${targetUris.processInstanceActionPath}/terminate`, + path: `${targetUris.processInstanceTerminatePath}`, successCallback: refreshPage, httpMethod: 'POST', }); @@ -154,7 +182,7 @@ export default function ProcessInstanceShow() { const suspendProcessInstance = () => { HttpService.makeCallToBackend({ - path: `${targetUris.processInstanceActionPath}/suspend`, + path: `${targetUris.processInstanceSuspendPath}`, successCallback: refreshPage, httpMethod: 'POST', }); @@ -162,7 +190,7 @@ export default function ProcessInstanceShow() { const resumeProcessInstance = () => { HttpService.makeCallToBackend({ - path: `${targetUris.processInstanceActionPath}/resume`, + path: `${targetUris.processInstanceResumePath}`, successCallback: refreshPage, httpMethod: 'POST', }); @@ -172,40 +200,41 @@ export default function ProcessInstanceShow() { const taskIds = { completed: [], readyOrWaiting: [] }; if (tasks) { tasks.forEach(function getUserTasksElement(task: ProcessInstanceTask) { - if (task.state === 'COMPLETED') { - (taskIds.completed as any).push(task); - } - if (task.state === 'READY' || task.state === 'WAITING') { - (taskIds.readyOrWaiting as any).push(task); + const callingSubprocessId = searchParams.get('call_activity_task_id'); + if ( + !callingSubprocessId || + callingSubprocessId === task.calling_subprocess_task_id + ) { + console.log('callingSubprocessId', callingSubprocessId); + if (task.state === 'COMPLETED') { + (taskIds.completed as any).push(task); + } + if (task.state === 'READY' || task.state === 'WAITING') { + (taskIds.readyOrWaiting as any).push(task); + } } }); } return taskIds; }; - const currentSpiffStep = (processInstanceToUse: any) => { - if (typeof params.spiff_step === 'undefined') { - return processInstanceToUse.spiff_step; + const currentSpiffStep = () => { + if (processInstance && typeof params.spiff_step === 'undefined') { + return processInstance.spiff_step || 0; } return Number(params.spiff_step); }; - const showingFirstSpiffStep = (processInstanceToUse: any) => { - return currentSpiffStep(processInstanceToUse) === 1; + const showingFirstSpiffStep = () => { + return currentSpiffStep() === 1; }; - const showingLastSpiffStep = (processInstanceToUse: any) => { - return ( - currentSpiffStep(processInstanceToUse) === processInstanceToUse.spiff_step - ); + const showingLastSpiffStep = () => { + return processInstance && currentSpiffStep() === processInstance.spiff_step; }; - const spiffStepLink = ( - processInstanceToUse: any, - label: any, - distance: number - ) => { + const spiffStepLink = (label: any, distance: number) => { const processIdentifier = searchParams.get('process_identifier'); let queryParams = ''; if (processIdentifier) { @@ -217,32 +246,47 @@ export default function ProcessInstanceShow() { data-qa="process-instance-step-link" to={`/admin/process-instances/${params.process_model_id}/${ params.process_instance_id - }/${currentSpiffStep(processInstanceToUse) + distance}${queryParams}`} + }/${currentSpiffStep() + distance}${queryParams}`} > {label} ); }; - const previousStepLink = (processInstanceToUse: any) => { - if (showingFirstSpiffStep(processInstanceToUse)) { + const previousStepLink = () => { + if (showingFirstSpiffStep()) { return null; } - return spiffStepLink(processInstanceToUse, , -1); + return spiffStepLink(, -1); }; - const nextStepLink = (processInstanceToUse: any) => { - if (showingLastSpiffStep(processInstanceToUse)) { + const nextStepLink = () => { + if (showingLastSpiffStep()) { return null; } - return spiffStepLink(processInstanceToUse, , 1); + return spiffStepLink(, 1); }; - const getInfoTag = (processInstanceToUse: any) => { + const returnToLastSpiffStep = () => { + window.location.href = `/admin/process-instances/${params.process_model_id}/${params.process_instance_id}`; + }; + + const resetProcessInstance = () => { + HttpService.makeCallToBackend({ + path: `${targetUris.processInstanceResetPath}/${currentSpiffStep()}`, + successCallback: returnToLastSpiffStep, + httpMethod: 'POST', + }); + }; + + const getInfoTag = () => { + if (!processInstance) { + return null; + } const currentEndDate = convertSecondsToFormattedDateTime( - processInstanceToUse.end_in_seconds + processInstance.end_in_seconds || 0 ); let currentEndDateTag; if (currentEndDate) { @@ -253,7 +297,7 @@ export default function ProcessInstanceShow() { {convertSecondsToFormattedDateTime( - processInstanceToUse.end_in_seconds + processInstance.end_in_seconds || 0 ) || 'N/A'} @@ -261,13 +305,13 @@ export default function ProcessInstanceShow() { } let statusIcon = ; - if (processInstanceToUse.status === 'suspended') { + if (processInstance.status === 'suspended') { statusIcon = ; - } else if (processInstanceToUse.status === 'complete') { + } else if (processInstance.status === 'complete') { statusIcon = ; - } else if (processInstanceToUse.status === 'terminated') { + } else if (processInstance.status === 'terminated') { statusIcon = ; - } else if (processInstanceToUse.status === 'error') { + } else if (processInstance.status === 'error') { statusIcon = ; } @@ -279,7 +323,7 @@ export default function ProcessInstanceShow() { {convertSecondsToFormattedDateTime( - processInstanceToUse.start_in_seconds + processInstance.start_in_seconds || 0 )} @@ -290,7 +334,7 @@ export default function ProcessInstanceShow() { - {processInstanceToUse.status} {statusIcon} + {processInstance.status} {statusIcon} @@ -333,11 +377,10 @@ export default function ProcessInstanceShow() { ); }; - const terminateButton = (processInstanceToUse: any) => { + const terminateButton = () => { if ( - ['complete', 'terminated', 'error'].indexOf( - processInstanceToUse.status - ) === -1 + processInstance && + !ProcessInstanceClass.terminalStatuses().includes(processInstance.status) ) { return ( @@ -354,11 +397,12 @@ export default function ProcessInstanceShow() { return
; }; - const suspendButton = (processInstanceToUse: any) => { + const suspendButton = () => { if ( - ['complete', 'terminated', 'error', 'suspended'].indexOf( - processInstanceToUse.status - ) === -1 + processInstance && + !ProcessInstanceClass.terminalStatuses() + .concat(['suspended']) + .includes(processInstance.status) ) { return ( - ); - buttons.push( - - ); - } else { + if (editingTaskData) { + buttons.push( + + ); + buttons.push( + + ); + } else if (selectingEvent) { + buttons.push( + + ); + buttons.push( + + ); + } else { + if (canEditTaskData(task)) { buttons.push( + ); + buttons.push( + + ); + } + if (canSendEvent(task)) { + buttons.push( + + ); + } + if (canResetProcess(task)) { + buttons.push( + + ); + } } return buttons; @@ -571,8 +781,42 @@ export default function ProcessInstanceShow() { ); }; - const taskDataDisplayArea = () => { + const eventSelector = (candidateEvents: any) => { + const editor = ( + setEventPayload(value || '{}')} + options={{ readOnly: !eventTextEditorEnabled }} + /> + ); + return selectingEvent ? ( + + item.name || item.label || item.typename} + onChange={(value: any) => { + setEventToSend(value.selectedItem); + setEventTextEditorEnabled( + value.selectedItem.typename === 'MessageEventDefinition' + ); + }} + /> + {editor} + + ) : ( + taskDataContainer() + ); + }; + + const taskUpdateDisplayArea = () => { const taskToUse: any = { ...taskToDisplay, data: taskDataToDisplay }; + const candidateEvents: any = getEvents(taskToUse); if (taskToDisplay) { return ( {taskToUse.name} ({taskToUse.type}): {taskToUse.state} - {taskDataButtons(taskToUse)} + {taskDisplayButtons(taskToUse)} - {taskDataContainer()} + {selectingEvent + ? eventSelector(candidateEvents) + : taskDataContainer()} ); } return null; }; - const stepsElement = (processInstanceToUse: any) => { + const stepsElement = () => { + if (!processInstance) { + return null; + } return ( - {previousStepLink(processInstanceToUse)} - Step {currentSpiffStep(processInstanceToUse)} of{' '} - {processInstanceToUse.spiff_step} - {nextStepLink(processInstanceToUse)} + {previousStepLink()} + Step {currentSpiffStep()} of {processInstance.spiff_step} + {nextStepLink()} ); }; - const buttonIcons = (processInstanceToUse: any) => { + const buttonIcons = () => { + if (!processInstance) { + return null; + } const elements = []; - if ( - ability.can('POST', `${targetUris.processInstanceActionPath}/terminate`) - ) { - elements.push(terminateButton(processInstanceToUse)); + if (ability.can('POST', `${targetUris.processInstanceTerminatePath}`)) { + elements.push(terminateButton()); + } + if (ability.can('POST', `${targetUris.processInstanceSuspendPath}`)) { + elements.push(suspendButton()); + } + if (ability.can('POST', `${targetUris.processInstanceResumePath}`)) { + elements.push(resumeButton()); } if ( - ability.can('POST', `${targetUris.processInstanceActionPath}/suspend`) + ability.can('DELETE', targetUris.processInstanceActionPath) && + ProcessInstanceClass.terminalStatuses().includes(processInstance.status) ) { - elements.push(suspendButton(processInstanceToUse)); - } - if (ability.can('POST', `${targetUris.processInstanceActionPath}/resume`)) { - elements.push(resumeButton(processInstanceToUse)); - } - if (ability.can('DELETE', targetUris.processInstanceActionPath)) { elements.push( @@ -639,7 +889,6 @@ export default function ProcessInstanceShow() { }; if (processInstance && (tasks || tasksCallHadError)) { - const processInstanceToUse = processInstance as any; const taskIds = getTaskIds(); const processModelId = unModifyProcessIdentifierForPathParam( params.process_model_id ? params.process_model_id : '' @@ -655,26 +904,27 @@ export default function ProcessInstanceShow() { entityType: 'process-model-id', linkLastItem: true, }, - [`Process Instance Id: ${processInstanceToUse.id}`], + [`Process Instance Id: ${processInstance.id}`], ]} />

- Process Instance Id: {processInstanceToUse.id} + Process Instance Id: {processInstance.id}

- {buttonIcons(processInstanceToUse)} + {buttonIcons()}


- {getInfoTag(processInstanceToUse)} + {getInfoTag()}
- {taskDataDisplayArea()} - {stepsElement(processInstanceToUse)} + {taskUpdateDisplayArea()} + {processDataDisplayArea()} + {stepsElement()}
(null); const [processSearchElement, setProcessSearchElement] = useState(null); const [processes, setProcesses] = useState([]); + const [displaySaveFileMessage, setDisplaySaveFileMessage] = + useState(false); const handleShowMarkdownEditor = () => setShowMarkdownEditor(true); @@ -70,10 +81,10 @@ export default function ProcessModelEditDiagram() { interface ScriptUnitTestResult { result: boolean; - context: object; - error: string; - line_number: number; - offset: number; + context?: object; + error?: string; + line_number?: number; + offset?: number; } const [currentScriptUnitTest, setCurrentScriptUnitTest] = @@ -87,7 +98,7 @@ export default function ProcessModelEditDiagram() { const navigate = useNavigate(); const [searchParams] = useSearchParams(); - const setErrorMessage = (useContext as any)(ErrorContext)[1]; + const setErrorObject = (useContext as any)(ErrorContext)[1]; const [processModelFile, setProcessModelFile] = useState( null ); @@ -148,6 +159,7 @@ export default function ProcessModelEditDiagram() { }; const navigateToProcessModelFile = (_result: any) => { + setDisplaySaveFileMessage(true); if (!params.file_name) { const fileNameWithExtension = `${newFileName}.${searchParams.get( 'file_type' @@ -158,10 +170,9 @@ export default function ProcessModelEditDiagram() { } }; - const [displaySaveFileMessage, setDisplaySaveFileMessage] = - useState(false); const saveDiagram = (bpmnXML: any, fileName = params.file_name) => { - setErrorMessage(null); + setDisplaySaveFileMessage(false); + setErrorObject(null); setBpmnXmlForDiagramRendering(bpmnXML); let url = `/process-models/${modifiedProcessModelId}/files`; @@ -187,7 +198,7 @@ export default function ProcessModelEditDiagram() { HttpService.makeCallToBackend({ path: url, successCallback: navigateToProcessModelFile, - failureCallback: setErrorMessage, + failureCallback: setErrorObject, httpMethod, postBody: formData, }); @@ -195,7 +206,6 @@ export default function ProcessModelEditDiagram() { // after saving the file, make sure we null out newFileName // so it does not get used over the params setNewFileName(''); - setDisplaySaveFileMessage(true); }; const onDeleteFile = (fileName = params.file_name) => { @@ -401,6 +411,13 @@ export default function ProcessModelEditDiagram() { }; }; + const jsonEditorOptions = () => { + return Object.assign(generalEditorOptions(), { + minimap: { enabled: false }, + folding: true, + }); + }; + const setPreviousScriptUnitTest = () => { resetUnitTextResult(); const newScriptIndex = currentScriptUnitTestIndex - 1; @@ -461,6 +478,21 @@ export default function ProcessModelEditDiagram() { const runCurrentUnitTest = () => { if (currentScriptUnitTest && scriptElement) { + let inputJson = ''; + let expectedJson = ''; + try { + inputJson = JSON.parse(currentScriptUnitTest.inputJson.value); + expectedJson = JSON.parse( + currentScriptUnitTest.expectedOutputJson.value + ); + } catch (e) { + setScriptUnitTestResult({ + result: false, + error: 'The JSON provided contains a formatting error.', + }); + return; + } + resetUnitTextResult(); HttpService.makeCallToBackend({ path: `/process-models/${modifiedProcessModelId}/script-unit-tests/run`, @@ -469,37 +501,56 @@ export default function ProcessModelEditDiagram() { postBody: { bpmn_task_identifier: (scriptElement as any).id, python_script: scriptText, - input_json: JSON.parse(currentScriptUnitTest.inputJson.value), - expected_output_json: JSON.parse( - currentScriptUnitTest.expectedOutputJson.value - ), + input_json: inputJson, + expected_output_json: expectedJson, }, }); } }; const unitTestFailureElement = () => { - if ( - scriptUnitTestResult && - scriptUnitTestResult.result === false && - !scriptUnitTestResult.line_number - ) { - let errorStringElement = null; - if (scriptUnitTestResult.error) { - errorStringElement = ( - - Received error when running script:{' '} - {JSON.stringify(scriptUnitTestResult.error)} - - ); - } - let errorContextElement = null; + if (scriptUnitTestResult && scriptUnitTestResult.result === false) { + let errorObject = ''; if (scriptUnitTestResult.context) { + errorObject = 'Unexpected result. Please see the comparison below.'; + } else if (scriptUnitTestResult.line_number) { + errorObject = `Error encountered running the script. Please check the code around line ${scriptUnitTestResult.line_number}`; + } else { + errorObject = `Error encountered running the script. ${JSON.stringify( + scriptUnitTestResult.error + )}`; + } + let errorStringElement = {errorObject}; + + let errorContextElement = null; + + if (scriptUnitTestResult.context) { + errorStringElement = ( + Unexpected result. Please see the comparison below. + ); + let outputJson = '{}'; + if (currentScriptUnitTest) { + outputJson = JSON.stringify( + JSON.parse(currentScriptUnitTest.expectedOutputJson.value), + null, + ' ' + ); + } + const contextJson = JSON.stringify( + scriptUnitTestResult.context, + null, + ' ' + ); errorContextElement = ( - - Received unexpected output:{' '} - {JSON.stringify(scriptUnitTestResult.context)} - + ); } return ( @@ -543,19 +594,35 @@ export default function ProcessModelEditDiagram() { ); } + let inputJson = currentScriptUnitTest.inputJson.value; + let outputJson = currentScriptUnitTest.expectedOutputJson.value; + try { + inputJson = JSON.stringify( + JSON.parse(currentScriptUnitTest.inputJson.value), + null, + ' ' + ); + outputJson = JSON.stringify( + JSON.parse(currentScriptUnitTest.expectedOutputJson.value), + null, + ' ' + ); + } catch (e) { + // Attemping to format the json failed -- it's invalid. + } + return (
-
); } return null; }; - const scriptEditor = () => { + return ( + + ); + }; + const scriptEditorAndTests = () => { let scriptName = ''; if (scriptElement) { scriptName = (scriptElement as any).di.bpmnElement.name; } - return ( - - {scriptUnitTestEditorElement()} + + + Script Editor + Unit Tests + + + {scriptEditor()} + {scriptUnitTestEditorElement()} + + ); }; @@ -878,7 +954,7 @@ export default function ProcessModelEditDiagram() { {saveFileMessage()} {appropriateEditor()} {newFileNameBox()} - {scriptEditor()} + {scriptEditorAndTests()} {markdownEditor()} {processModelSelector()}
diff --git a/src/routes/ProcessModelNewExperimental.tsx b/src/routes/ProcessModelNewExperimental.tsx new file mode 100644 index 00000000..af8be822 --- /dev/null +++ b/src/routes/ProcessModelNewExperimental.tsx @@ -0,0 +1,73 @@ +import { useState } from 'react'; +import { useNavigate, useParams } from 'react-router-dom'; +// @ts-ignore +import { TextArea, Button, Form } from '@carbon/react'; +import ProcessBreadcrumb from '../components/ProcessBreadcrumb'; +import { ProcessModel } from '../interfaces'; +import { modifyProcessIdentifierForPathParam } from '../helpers'; +import HttpService from '../services/HttpService'; + +export default function ProcessModelNewExperimental() { + const params = useParams(); + const navigate = useNavigate(); + const [processModelDescriptiveText, setProcessModelDescriptiveText] = + useState(''); + + const helperText = + 'Create a bug tracker process model with a bug-details form that collects summary, description, and priority'; + + const navigateToProcessModel = (result: ProcessModel) => { + if ('id' in result) { + const modifiedProcessModelPathFromResult = + modifyProcessIdentifierForPathParam(result.id); + navigate(`/admin/process-models/${modifiedProcessModelPathFromResult}`); + } + }; + + const handleFormSubmission = (event: any) => { + event.preventDefault(); + HttpService.makeCallToBackend({ + path: `/process-models-natural-language/${params.process_group_id}`, + successCallback: navigateToProcessModel, + httpMethod: 'POST', + postBody: { natural_language_text: processModelDescriptiveText }, + }); + }; + + const ohYeeeeaah = () => { + setProcessModelDescriptiveText(helperText); + }; + + return ( + <> + + {/* eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions */} +

+ Add Process Model +

+
+