Squashed 'spiffworkflow-frontend/' changes from d012f3a2c..f099c181a

f099c181a added script to add test keycloak users and moved all keycloak stuff to keycloak directory w/ burnettk
fc2ee7bc3 show start events in logs as well and added bpmn process identifiers to log table w/ burnettk
261b992b0 upgrade certifi to fix security vulnerability
94f785c7f do not navigate away from diagram editor page if there are changes w/ burnettk
4d16ad34e added End Event to simple log view w/ burnettk
d9f9a316d only show milestones for simple log view w/ burnettk
a3195fe1b fix lint issues
ff3fd9bc3 remove jsonpath
29b1dba9a added proof of concept to validate date fields in json schema form w/ burnettk
201dbebf2 fixed cypress tests
e2bedfd94 Merge branch 'main' of github.com:sartography/spiff-arena
990ccc119 added ability to add in custom validation error messages for text input fields w/ burnettk
94f31579a Merge pull request #102 from sartography/feature/waku-fault-message
32290c21d added tasks table to process instance show page w/ burnettk
2033634e1 Merge branch 'main' into feature/waku-fault-message
f03553acf added tabs to find by id page and install pre commit libraries if they fail to run help w/ burnettk
626d9056e stop at call activity as well when getting calling subprocesses by child id w/ burnettk
bf4099345 Merge branch 'main' into feature/waku-fault-message
bdeb03c42 added detailed area to process instance show page w/ burnettk
38341a987 a little more cleanup w/ burnettk
3f0b2f9fa remove several debug print statements
b920be55c logout works now and queryparams are getting passed correctly on login now
b84276aea Merge branch 'main' into feature/waku-fault-message
19574ad36 linting
96bf60a41 remove unneeded protocol variable w/ burnettk
a552ca6e7 use the cookie from the frontend w/ burnettk
e4f354e37 this somewhat works and sets cookies w/ burnettk
1a12b7ac8 debugging cookies w/ burnettk
11efb5ec4 updated rjsf to beta.16 and updated validations from v6 to v8 w/ burnettk
d67af9719 show the error and success notifications when appropriate when editing xml for json w/ burnettk
7ac328843 only load file references when needed to avoid unnecessary xml errors w/ burnettk
4834434c7 Merge remote-tracking branch 'origin/main' into feature/add_some_xml_validations
85449e564 some updates to validate xml when uploading and saving w/ burnettk
c1f271723 Merge branch 'main' into feature/waku-fault-message
d63269bec handle subprocesses in navigation
e680734a9 call proceses through setProcesses to ensure we have up to date value and removed debug logs w/ burnettk
9ad1e0ced more debug logs w/ burnettk
172d171cb more debugging and do not watch ProcessModel with getting processes w/ burnettk
73cab1863 added in debug logging for launching call activity editor w/ burnettk
248661b6e fixed cypress config w/ burnettk
3cee57987 Add `fault_or_suspend_on_exception` and `exception_notification_addresses` to Process Model interface and create/update form.
2463e1411 actually filter by process initiator w/ burnettk
cd4fda787 highlight tasks even if they are in subprocesses of called activities w/ burnettk

git-subtree-dir: spiffworkflow-frontend
git-subtree-split: f099c181a4b434a8f86dbca2942dae6c20534970
This commit is contained in:
jasquat 2023-01-19 13:44:54 -05:00
parent 32775973ca
commit f612f26dd2
42 changed files with 1260 additions and 539 deletions

View File

@ -87,7 +87,7 @@ jobs:
run: ./bin/wait_for_frontend_to_be_up 5
- name: wait_for_keycloak
working-directory: ./spiffworkflow-backend
run: ./bin/wait_for_keycloak 5
run: ./keycloak/bin/wait_for_keycloak 5
- name: Cypress run
uses: cypress-io/github-action@v4
with:

View File

@ -24,12 +24,9 @@ const deleteVideosOnSuccess = (on) => {
})
}
module.exports = defineConfig({
const cypressConfig = {
projectId: 'crax1q',
// since it's slow
videoCompression: useVideoCompression,
videoUploadOnPasses: false,
chromeWebSecurity: false,
e2e: {
@ -45,4 +42,11 @@ module.exports = defineConfig({
// https://github.com/cypress-io/cypress/issues/2353
// https://docs.cypress.io/guides/core-concepts/interacting-with-elements#Scrolling
scrollBehavior: "center",
});
}
if (!process.env.CYPRESS_RECORD_KEY) {
// since it's slow
cypressConfig.videoCompression = false
}
module.exports = defineConfig(cypressConfig)

View File

@ -1,6 +1,9 @@
import { slowCypressDown } from 'cypress-slow-down';
import { modifyProcessIdentifierForPathParam } from '../../src/helpers';
import { miscDisplayName } from '../support/helpers';
// slowCypressDown(500);
describe('process-models', () => {
beforeEach(() => {
cy.login();
@ -132,7 +135,7 @@ describe('process-models', () => {
cy.get('.tile-process-group-content-container').should('exist');
});
it('can upload and run a bpmn file', () => {
it.only('can upload and run a bpmn file', () => {
const uuid = () => Cypress._.random(0, 1e6);
const id = uuid();
const directParentGroupId = 'acceptance-tests-group-one';
@ -165,7 +168,6 @@ describe('process-models', () => {
.click();
cy.runPrimaryBpmnFile();
// cy.getBySel('process-instance-list-link').click();
cy.getBySel('process-instance-show-link').click();
cy.getBySel('process-instance-delete').click();
cy.contains('Are you sure');

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" id="Definitions_96f6665" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.0.0-dev">
<bpmn:process id="Process_bd2e724" isExecutable="true">
<bpmn:process id="Process_Model_Cypress_Test_Process" isExecutable="true">
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>Flow_07vd2ar</bpmn:outgoing>
</bpmn:startEvent>

View File

@ -97,7 +97,7 @@ Cypress.Commands.add(
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');
cy.getBySel('process-model-show-permissions-loaded').should('exist');
}
}
);
@ -152,17 +152,14 @@ Cypress.Commands.add('assertNoItemInPaginatedResults', () => {
cy.contains(/\b00 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)}`
);
}
);
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)}`
);
});

143
package-lock.json generated
View File

@ -23,7 +23,7 @@
"@rjsf/core": "*",
"@rjsf/mui": "^5.0.0-beta.13",
"@rjsf/utils": "^5.0.0-beta.13",
"@rjsf/validator-ajv6": "^5.0.0-beta.13",
"@rjsf/validator-ajv8": "^5.0.0-beta.16",
"@tanstack/react-table": "^8.2.2",
"@testing-library/jest-dom": "^5.16.4",
"@testing-library/react": "^13.3.0",
@ -39,7 +39,9 @@
"bpmn-js": "^9.3.2",
"bpmn-js-properties-panel": "^1.10.0",
"bpmn-js-spiffworkflow": "sartography/bpmn-js-spiffworkflow#main",
"cookie": "^0.5.0",
"craco": "^0.0.3",
"cypress-slow-down": "^1.2.1",
"date-fns": "^2.28.0",
"diagram-js": "^8.5.0",
"dmn-js": "^12.2.0",
@ -56,6 +58,7 @@
"react-icons": "^4.4.0",
"react-jsonschema-form": "^1.8.1",
"react-markdown": "^8.0.3",
"react-router": "^6.3.0",
"react-router-dom": "^6.3.0",
"react-scripts": "^5.0.1",
"remark-gfm": "^3.0.1",
@ -66,6 +69,7 @@
},
"devDependencies": {
"@cypress/grep": "^3.1.0",
"@types/cookie": "^0.5.1",
"@typescript-eslint/eslint-plugin": "^5.30.5",
"@typescript-eslint/parser": "^5.30.6",
"cypress": "^12",
@ -4863,9 +4867,9 @@
}
},
"node_modules/@rjsf/core": {
"version": "5.0.0-beta.13",
"resolved": "https://registry.npmjs.org/@rjsf/core/-/core-5.0.0-beta.13.tgz",
"integrity": "sha512-uQ3A9aJhMJsz9ct5tV3ogZkSFEkKUxrM9SJ9Hc8ijxmuaW7Jv8tNv5jiWZZsLvNXlIONX83s6JqkiOJf6IOAvg==",
"version": "5.0.0-beta.16",
"resolved": "https://registry.npmjs.org/@rjsf/core/-/core-5.0.0-beta.16.tgz",
"integrity": "sha512-TqOd3CKptWAswX9PU8pLSoAe5zI03J6Kk/aWAFbMj+xW/6hR5PXHbs5X5kxwpQx7IVXiJZZZpP5n1oDsu4GwNg==",
"dependencies": {
"lodash": "^4.17.15",
"lodash-es": "^4.17.15",
@ -4881,9 +4885,9 @@
}
},
"node_modules/@rjsf/mui": {
"version": "5.0.0-beta.13",
"resolved": "https://registry.npmjs.org/@rjsf/mui/-/mui-5.0.0-beta.13.tgz",
"integrity": "sha512-hwCtADpjNssq/CsT3Wj1FDVJfdCN3gptKedGjbusLUEwQqXoVzkzl25e/IRfN8y/JxYu4lMXDU89bN9nJSKWLA==",
"version": "5.0.0-beta.16",
"resolved": "https://registry.npmjs.org/@rjsf/mui/-/mui-5.0.0-beta.16.tgz",
"integrity": "sha512-QskaSc2Zcwqz+nKoACstvn5LhrAx4EmicYc/6kNoj3jKH6MlfVCA7FYumu5g6TIqMrDEvZuZqBKtjL64Tv52PQ==",
"engines": {
"node": ">=14"
},
@ -4898,9 +4902,9 @@
}
},
"node_modules/@rjsf/utils": {
"version": "5.0.0-beta.13",
"resolved": "https://registry.npmjs.org/@rjsf/utils/-/utils-5.0.0-beta.13.tgz",
"integrity": "sha512-hWWWFD2ifjSOhqWueML4OHrZe2HW5pE2nfKGhCObFbwtggHoQlj64xDBsJ1qfUG8DGvCHztJQ/sKIaOvXnpt7w==",
"version": "5.0.0-beta.16",
"resolved": "https://registry.npmjs.org/@rjsf/utils/-/utils-5.0.0-beta.16.tgz",
"integrity": "sha512-dNQ620Q6a9cB28sjjRgJkxIuD9TFd03sNMlcZVdZOuZC6wjfGc4rKG0Lc7+xgLFvSPFKwXJprzfKSM3yuy9jXg==",
"dependencies": {
"json-schema-merge-allof": "^0.8.1",
"jsonpointer": "^5.0.1",
@ -4933,12 +4937,13 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz",
"integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w=="
},
"node_modules/@rjsf/validator-ajv6": {
"version": "5.0.0-beta.13",
"resolved": "https://registry.npmjs.org/@rjsf/validator-ajv6/-/validator-ajv6-5.0.0-beta.13.tgz",
"integrity": "sha512-X9N3/HJYV23MjUN/VJHIdBhUdBuMTUsh4HAZm50eUvUAhWK95wIqjjhAs24rzeLajrjFeH7kFr89zAqDgIFhVQ==",
"node_modules/@rjsf/validator-ajv8": {
"version": "5.0.0-beta.16",
"resolved": "https://registry.npmjs.org/@rjsf/validator-ajv8/-/validator-ajv8-5.0.0-beta.16.tgz",
"integrity": "sha512-VrQzR9HEH/1BF2TW/lRJuV+kILzR4geS+iW5Th1OlPeNp1NNWZuSO1kCU9O0JA17t2WHOEl/SFZXZBnN1/zwzQ==",
"dependencies": {
"ajv": "^6.7.0",
"ajv-formats": "^2.1.1",
"ajv8": "npm:ajv@^8.11.0",
"lodash": "^4.17.15",
"lodash-es": "^4.17.15"
},
@ -4946,7 +4951,7 @@
"node": ">=14"
},
"peerDependencies": {
"@rjsf/utils": "^5.0.0-beta.1"
"@rjsf/utils": "^5.0.0-beta.12"
}
},
"node_modules/@rollup/plugin-babel": {
@ -5653,6 +5658,12 @@
"@types/node": "*"
}
},
"node_modules/@types/cookie": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.5.1.tgz",
"integrity": "sha512-COUnqfB2+ckwXXSFInsFdOAWQzCCx+a5hq2ruyj+Vjund94RJQd4LG2u9hnvJrTgunKAaax7ancBYlDrNYxA0g==",
"dev": true
},
"node_modules/@types/debug": {
"version": "4.1.7",
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.7.tgz",
@ -6822,6 +6833,27 @@
"ajv": "^6.9.1"
}
},
"node_modules/ajv8": {
"name": "ajv",
"version": "8.12.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz",
"integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==",
"dependencies": {
"fast-deep-equal": "^3.1.1",
"json-schema-traverse": "^1.0.0",
"require-from-string": "^2.0.2",
"uri-js": "^4.2.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/ajv8/node_modules/json-schema-traverse": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="
},
"node_modules/ansi-align": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz",
@ -9906,6 +9938,19 @@
"node": "^14.0.0 || ^16.0.0 || >=18.0.0"
}
},
"node_modules/cypress-plugin-config": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/cypress-plugin-config/-/cypress-plugin-config-1.2.0.tgz",
"integrity": "sha512-vgMMwjeI/L+2xptqkyhJ20LRuZrrsdbPaGMNNLVq+Cwox5+9dm0E312gpMXgXRs05uyUAzL/nCm/tdTckSAgoQ=="
},
"node_modules/cypress-slow-down": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/cypress-slow-down/-/cypress-slow-down-1.2.1.tgz",
"integrity": "sha512-Pd+nESR+Ca8I+mLGbBrPVMEFvJBWxkJcEdcIUDxSBnMoWI00hiIKxzEgVqCv5c6Oap2OPpnrPLbJBwveCNKLig==",
"dependencies": {
"cypress-plugin-config": "^1.0.0"
}
},
"node_modules/cypress/node_modules/@types/node": {
"version": "14.18.26",
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.26.tgz",
@ -34767,9 +34812,9 @@
}
},
"@rjsf/core": {
"version": "5.0.0-beta.13",
"resolved": "https://registry.npmjs.org/@rjsf/core/-/core-5.0.0-beta.13.tgz",
"integrity": "sha512-uQ3A9aJhMJsz9ct5tV3ogZkSFEkKUxrM9SJ9Hc8ijxmuaW7Jv8tNv5jiWZZsLvNXlIONX83s6JqkiOJf6IOAvg==",
"version": "5.0.0-beta.16",
"resolved": "https://registry.npmjs.org/@rjsf/core/-/core-5.0.0-beta.16.tgz",
"integrity": "sha512-TqOd3CKptWAswX9PU8pLSoAe5zI03J6Kk/aWAFbMj+xW/6hR5PXHbs5X5kxwpQx7IVXiJZZZpP5n1oDsu4GwNg==",
"requires": {
"lodash": "^4.17.15",
"lodash-es": "^4.17.15",
@ -34778,15 +34823,15 @@
}
},
"@rjsf/mui": {
"version": "5.0.0-beta.13",
"resolved": "https://registry.npmjs.org/@rjsf/mui/-/mui-5.0.0-beta.13.tgz",
"integrity": "sha512-hwCtADpjNssq/CsT3Wj1FDVJfdCN3gptKedGjbusLUEwQqXoVzkzl25e/IRfN8y/JxYu4lMXDU89bN9nJSKWLA==",
"version": "5.0.0-beta.16",
"resolved": "https://registry.npmjs.org/@rjsf/mui/-/mui-5.0.0-beta.16.tgz",
"integrity": "sha512-QskaSc2Zcwqz+nKoACstvn5LhrAx4EmicYc/6kNoj3jKH6MlfVCA7FYumu5g6TIqMrDEvZuZqBKtjL64Tv52PQ==",
"requires": {}
},
"@rjsf/utils": {
"version": "5.0.0-beta.13",
"resolved": "https://registry.npmjs.org/@rjsf/utils/-/utils-5.0.0-beta.13.tgz",
"integrity": "sha512-hWWWFD2ifjSOhqWueML4OHrZe2HW5pE2nfKGhCObFbwtggHoQlj64xDBsJ1qfUG8DGvCHztJQ/sKIaOvXnpt7w==",
"version": "5.0.0-beta.16",
"resolved": "https://registry.npmjs.org/@rjsf/utils/-/utils-5.0.0-beta.16.tgz",
"integrity": "sha512-dNQ620Q6a9cB28sjjRgJkxIuD9TFd03sNMlcZVdZOuZC6wjfGc4rKG0Lc7+xgLFvSPFKwXJprzfKSM3yuy9jXg==",
"requires": {
"json-schema-merge-allof": "^0.8.1",
"jsonpointer": "^5.0.1",
@ -34812,12 +34857,13 @@
}
}
},
"@rjsf/validator-ajv6": {
"version": "5.0.0-beta.13",
"resolved": "https://registry.npmjs.org/@rjsf/validator-ajv6/-/validator-ajv6-5.0.0-beta.13.tgz",
"integrity": "sha512-X9N3/HJYV23MjUN/VJHIdBhUdBuMTUsh4HAZm50eUvUAhWK95wIqjjhAs24rzeLajrjFeH7kFr89zAqDgIFhVQ==",
"@rjsf/validator-ajv8": {
"version": "5.0.0-beta.16",
"resolved": "https://registry.npmjs.org/@rjsf/validator-ajv8/-/validator-ajv8-5.0.0-beta.16.tgz",
"integrity": "sha512-VrQzR9HEH/1BF2TW/lRJuV+kILzR4geS+iW5Th1OlPeNp1NNWZuSO1kCU9O0JA17t2WHOEl/SFZXZBnN1/zwzQ==",
"requires": {
"ajv": "^6.7.0",
"ajv-formats": "^2.1.1",
"ajv8": "npm:ajv@^8.11.0",
"lodash": "^4.17.15",
"lodash-es": "^4.17.15"
}
@ -35307,6 +35353,12 @@
"@types/node": "*"
}
},
"@types/cookie": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.5.1.tgz",
"integrity": "sha512-COUnqfB2+ckwXXSFInsFdOAWQzCCx+a5hq2ruyj+Vjund94RJQd4LG2u9hnvJrTgunKAaax7ancBYlDrNYxA0g==",
"dev": true
},
"@types/debug": {
"version": "4.1.7",
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.7.tgz",
@ -36295,6 +36347,24 @@
"integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==",
"requires": {}
},
"ajv8": {
"version": "npm:ajv@8.12.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz",
"integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==",
"requires": {
"fast-deep-equal": "^3.1.1",
"json-schema-traverse": "^1.0.0",
"require-from-string": "^2.0.2",
"uri-js": "^4.2.2"
},
"dependencies": {
"json-schema-traverse": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="
}
}
},
"ansi-align": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz",
@ -38727,6 +38797,19 @@
}
}
},
"cypress-plugin-config": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/cypress-plugin-config/-/cypress-plugin-config-1.2.0.tgz",
"integrity": "sha512-vgMMwjeI/L+2xptqkyhJ20LRuZrrsdbPaGMNNLVq+Cwox5+9dm0E312gpMXgXRs05uyUAzL/nCm/tdTckSAgoQ=="
},
"cypress-slow-down": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/cypress-slow-down/-/cypress-slow-down-1.2.1.tgz",
"integrity": "sha512-Pd+nESR+Ca8I+mLGbBrPVMEFvJBWxkJcEdcIUDxSBnMoWI00hiIKxzEgVqCv5c6Oap2OPpnrPLbJBwveCNKLig==",
"requires": {
"cypress-plugin-config": "^1.0.0"
}
},
"damerau-levenshtein": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",

View File

@ -18,7 +18,7 @@
"@rjsf/core": "*",
"@rjsf/mui": "^5.0.0-beta.13",
"@rjsf/utils": "^5.0.0-beta.13",
"@rjsf/validator-ajv6": "^5.0.0-beta.13",
"@rjsf/validator-ajv8": "^5.0.0-beta.16",
"@tanstack/react-table": "^8.2.2",
"@testing-library/jest-dom": "^5.16.4",
"@testing-library/react": "^13.3.0",
@ -34,7 +34,9 @@
"bpmn-js": "^9.3.2",
"bpmn-js-properties-panel": "^1.10.0",
"bpmn-js-spiffworkflow": "sartography/bpmn-js-spiffworkflow#main",
"cookie": "^0.5.0",
"craco": "^0.0.3",
"cypress-slow-down": "^1.2.1",
"date-fns": "^2.28.0",
"diagram-js": "^8.5.0",
"dmn-js": "^12.2.0",
@ -51,6 +53,7 @@
"react-icons": "^4.4.0",
"react-jsonschema-form": "^1.8.1",
"react-markdown": "^8.0.3",
"react-router": "^6.3.0",
"react-router-dom": "^6.3.0",
"react-scripts": "^5.0.1",
"remark-gfm": "^3.0.1",
@ -102,6 +105,7 @@
},
"devDependencies": {
"@cypress/grep": "^3.1.0",
"@types/cookie": "^0.5.1",
"@typescript-eslint/eslint-plugin": "^5.30.5",
"@typescript-eslint/parser": "^5.30.6",
"cypress": "^12",

View File

@ -12,6 +12,7 @@ import {
ProcessModel,
ReportColumn,
ReportMetadata,
User,
} from '../interfaces';
import HttpService from '../services/HttpService';
@ -20,6 +21,7 @@ type OwnProps = {
columnArray: ReportColumn[];
orderBy: string;
processModelSelection: ProcessModel | null;
processInitiatorSelection: User | null;
processStatusSelection: string[];
startFromSeconds: string | null;
startToSeconds: string | null;
@ -36,6 +38,7 @@ export default function ProcessInstanceListSaveAsReport({
columnArray,
orderBy,
processModelSelection,
processInitiatorSelection,
processInstanceReportSelection,
processStatusSelection,
startFromSeconds,
@ -86,6 +89,13 @@ export default function ProcessInstanceListSaveAsReport({
});
}
if (processInitiatorSelection) {
filterByArray.push({
field_name: 'process_initiator_username',
field_value: processInitiatorSelection.username,
});
}
if (processStatusSelection.length > 0) {
filterByArray.push({
field_name: 'process_status',

View File

@ -193,11 +193,42 @@ export default function ProcessInstanceListTable({
setEndToTime,
]);
const handleProcessInstanceInitiatorSearchResult = (
result: any,
inputText: string
) => {
if (lastRequestedInitatorSearchTerm.current === result.username_prefix) {
setProcessInstanceInitiatorOptions(result.users);
result.users.forEach((user: User) => {
if (user.username === inputText) {
setProcessInitiatorSelection(user);
}
});
}
};
const searchForProcessInitiator = (inputText: string) => {
if (inputText) {
lastRequestedInitatorSearchTerm.current = inputText;
HttpService.makeCallToBackend({
path: `/users/search?username_prefix=${inputText}`,
successCallback: (result: any) =>
handleProcessInstanceInitiatorSearchResult(result, inputText),
});
}
};
const parametersToGetFromSearchParams = useMemo(() => {
const figureOutProcessInitiator = (processInitiatorSearchText: string) => {
searchForProcessInitiator(processInitiatorSearchText);
};
return {
process_model_identifier: null,
process_status: null,
process_initiator_username: figureOutProcessInitiator,
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// eslint-disable-next-line sonarjs/cognitive-complexity
@ -384,6 +415,12 @@ export default function ProcessInstanceListTable({
}
});
if (filters.process_initiator_username) {
const functionToCall =
parametersToGetFromSearchParams.process_initiator_username;
functionToCall(filters.process_initiator_username);
}
const processStatusSelectedArray: string[] = [];
if (filters.process_status) {
PROCESS_STATUSES.forEach((processStatusOption: any) => {
@ -538,8 +575,13 @@ export default function ProcessInstanceListTable({
queryParamString += `&report_id=${processInstanceReportSelection.id}`;
}
if (processInitiatorSelection) {
queryParamString += `&process_initiator_username=${processInitiatorSelection.username}`;
}
setErrorObject(null);
setProcessInstanceReportJustSaved(null);
setProcessInstanceFilters({});
navigate(`${processInstanceListPathPrefix}?${queryParamString}`);
};
@ -682,6 +724,7 @@ export default function ProcessInstanceListTable({
orderBy=""
buttonText="Save"
processModelSelection={processModelSelection}
processInitiatorSelection={processInitiatorSelection}
processStatusSelection={processStatusSelection}
processInstanceReportSelection={processInstanceReportSelection}
reportMetadata={reportMetadata}
@ -987,22 +1030,6 @@ 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;
@ -1144,7 +1171,7 @@ export default function ProcessInstanceListTable({
start_in_seconds: 'Start Time',
end_in_seconds: 'End Time',
status: 'Status',
username: 'Started By',
process_initiator_username: 'Started By',
spiff_step: 'SpiffWorkflow Step',
};
const getHeaderLabel = (header: string) => {

View File

@ -0,0 +1,63 @@
// @ts-ignore
import { Tabs, TabList, Tab } from '@carbon/react';
import { Can } from '@casl/react';
import { useNavigate } from 'react-router-dom';
import { usePermissionFetcher } from '../hooks/PermissionService';
import { useUriListForPermissions } from '../hooks/UriListForPermissions';
import { PermissionsToCheck } from '../interfaces';
type OwnProps = {
variant: string;
};
export default function ProcessInstanceListTabs({ variant }: OwnProps) {
const navigate = useNavigate();
const { targetUris } = useUriListForPermissions();
const permissionRequestData: PermissionsToCheck = {
[targetUris.processInstanceListPath]: ['GET'],
};
const { ability } = usePermissionFetcher(permissionRequestData);
let selectedTabIndex = 0;
if (variant === 'all') {
selectedTabIndex = 1;
} else if (variant === 'find-by-id') {
selectedTabIndex = 2;
}
return (
<Tabs selectedIndex={selectedTabIndex}>
<TabList aria-label="List of tabs">
<Tab
title="Only show process instances for the current user."
data-qa="process-instance-list-for-me"
onClick={() => {
navigate('/admin/process-instances/for-me');
}}
>
For Me
</Tab>
<Can I="GET" a={targetUris.processInstanceListPath} ability={ability}>
<Tab
title="Show all process instances for all users."
data-qa="process-instance-list-all"
onClick={() => {
navigate('/admin/process-instances/all');
}}
>
All
</Tab>
</Can>
<Tab
title="Search for a process instance by id."
data-qa="process-instance-list-find-by-id"
onClick={() => {
navigate('/admin/process-instances/find-by-id');
}}
>
Find By Id
</Tab>
</TabList>
</Tabs>
);
}

View File

@ -8,6 +8,8 @@ import {
TextInput,
Grid,
Column,
Select,
SelectItem,
// @ts-ignore
} from '@carbon/react';
// @ts-ignore
@ -76,6 +78,9 @@ export default function ProcessModelForm({
display_name: processModel.display_name,
description: processModel.description,
metadata_extraction_paths: processModel.metadata_extraction_paths,
fault_or_suspend_on_exception: processModel.fault_or_suspend_on_exception,
exception_notification_addresses:
processModel.exception_notification_addresses,
};
if (mode === 'new') {
Object.assign(postBody, {
@ -173,6 +178,69 @@ export default function ProcessModelForm({
updateProcessModel({ metadata_extraction_paths: cep });
};
const notificationAddressForm = (
index: number,
notificationAddress: string
) => {
return (
<Grid>
<Column md={3} lg={7} sm={1}>
<TextInput
id={`process-model-notification-address-key-${index}`}
labelText="Address"
value={notificationAddress}
onChange={(event: any) => {
const notificationAddresses: string[] =
processModel.exception_notification_addresses || [];
notificationAddresses[index] = event.target.value;
updateProcessModel({
exception_notification_addresses: notificationAddresses,
});
}}
/>
</Column>
<Column md={1} lg={1} sm={1}>
<Button
kind="ghost"
renderIcon={TrashCan}
iconDescription="Remove Address"
hasIconOnly
size="lg"
className="with-extra-top-margin"
onClick={() => {
const notificationAddresses: string[] =
processModel.exception_notification_addresses || [];
notificationAddresses.splice(index, 1);
updateProcessModel({
exception_notification_addresses: notificationAddresses,
});
}}
/>
</Column>
</Grid>
);
};
const notificationAddressFormArea = () => {
if (processModel.exception_notification_addresses) {
return processModel.exception_notification_addresses.map(
(notificationAddress: string, index: number) => {
return notificationAddressForm(index, notificationAddress);
}
);
}
return null;
};
const addBlankNotificationAddress = () => {
const notificationAddresses: string[] =
processModel.exception_notification_addresses || [];
notificationAddresses.push('');
updateProcessModel({
exception_notification_addresses: notificationAddresses,
});
};
const onDisplayNameChanged = (newDisplayName: any) => {
setDisplayNameInvalid(false);
const updateDict = { display_name: newDisplayName };
@ -182,6 +250,11 @@ export default function ProcessModelForm({
updateProcessModel(updateDict);
};
const onNotificationTypeChanged = (newNotificationType: string) => {
const updateDict = { fault_or_suspend_on_exception: newNotificationType };
updateProcessModel(updateDict);
};
const formElements = () => {
const textInputs = [
<TextInput
@ -230,6 +303,49 @@ export default function ProcessModelForm({
/>
);
textInputs.push(
<Select
id="notification-type"
defaultValue="fault"
labelText="Notification Type"
onChange={(event: any) => {
onNotificationTypeChanged(event.target.value);
}}
>
<SelectItem value="fault" text="Fault" />
<SelectItem value="suspend" text="Suspend" />
</Select>
);
textInputs.push(<h2>Notification Addresses</h2>);
textInputs.push(
<Grid>
<Column md={8} lg={16} sm={4}>
<p className="data-table-description">
You can provide one or more addresses to notify if this model fails.
</p>
</Column>
</Grid>
);
textInputs.push(<>{notificationAddressFormArea()}</>);
textInputs.push(
<Grid>
<Column md={4} lg={8} sm={2}>
<Button
data-qa="add-notification-address-button"
renderIcon={AddAlt}
className="button-white-background"
kind=""
size="sm"
onClick={() => {
addBlankNotificationAddress();
}}
>
Add Notification Address
</Button>
</Column>
</Grid>
);
textInputs.push(<h2>Metadata Extractions</h2>);
textInputs.push(
<Grid>

View File

@ -37,7 +37,9 @@ export default function ProcessModelSearch({
const shouldFilterProcessModel = (options: any) => {
const processModel: ProcessModel = options.item;
const { inputValue } = options;
return getFullProcessModelLabel(processModel).includes(inputValue);
return getFullProcessModelLabel(processModel)
.toLowerCase()
.includes((inputValue || '').toLowerCase());
};
return (
<ComboBox

View File

@ -84,6 +84,7 @@ type OwnProps = {
onJsonFilesRequested?: (..._args: any[]) => any;
onDmnFilesRequested?: (..._args: any[]) => any;
onSearchProcessModels?: (..._args: any[]) => any;
onElementsChanged?: (..._args: any[]) => any;
url?: string;
};
@ -109,6 +110,7 @@ export default function ReactDiagramEditor({
onJsonFilesRequested,
onDmnFilesRequested,
onSearchProcessModels,
onElementsChanged,
url,
}: OwnProps) {
const [diagramXMLString, setDiagramXMLString] = useState('');
@ -264,9 +266,6 @@ export default function ReactDiagramEditor({
handleLaunchMarkdownEditor(element, value, eventBus);
});
/**
* fixme: this is not in use yet, we need the ability to find bpmn files by id.
*/
diagramModeler.on('spiff.callactivity.edit', (event: any) => {
if (onLaunchBpmnEditor) {
onLaunchBpmnEditor(event.processId);
@ -294,6 +293,11 @@ export default function ReactDiagramEditor({
diagramModeler.on('element.click', (element: any) => {
handleElementClick(element);
});
diagramModeler.on('elements.changed', (event: any) => {
if (onElementsChanged) {
onElementsChanged(event);
}
});
diagramModeler.on('spiff.service_tasks.requested', (event: any) => {
handleServiceTasksRequested(event);
@ -333,6 +337,7 @@ export default function ReactDiagramEditor({
onJsonFilesRequested,
onDmnFilesRequested,
onSearchProcessModels,
onElementsChanged,
]);
useEffect(() => {

View File

@ -29,6 +29,13 @@ type OwnProps = {
showStartedBy?: boolean;
showWaitingOn?: boolean;
textToShowIfEmpty?: string;
shouldPaginateTable?: boolean;
showProcessId?: boolean;
showProcessModelIdentifier?: boolean;
showTableDescriptionAsTooltip?: boolean;
showDateStarted?: boolean;
showLastUpdated?: boolean;
hideIfNoTasks?: boolean;
};
export default function TaskListTable({
@ -42,6 +49,13 @@ export default function TaskListTable({
autoReload = false,
showStartedBy = true,
showWaitingOn = true,
shouldPaginateTable = true,
showProcessId = true,
showProcessModelIdentifier = true,
showTableDescriptionAsTooltip = false,
showDateStarted = true,
showLastUpdated = true,
hideIfNoTasks = false,
}: OwnProps) {
const [searchParams] = useSearchParams();
const [tasks, setTasks] = useState<ProcessInstanceTask[] | null>(null);
@ -89,10 +103,6 @@ export default function TaskListTable({
) => {
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 =
@ -103,82 +113,133 @@ export default function TaskListTable({
}
shortUsernameString = firstTwoUsernames.join(',');
}
if (processInstanceTask.assigned_user_group_identifier) {
fullUsernameString = processInstanceTask.assigned_user_group_identifier;
shortUsernameString = processInstanceTask.assigned_user_group_identifier;
}
return <span title={fullUsernameString}>{shortUsernameString}</span>;
};
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 getTableRow = (processInstanceTask: ProcessInstanceTask) => {
const taskUrl = `/tasks/${processInstanceTask.process_instance_id}/${processInstanceTask.task_id}`;
const modifiedProcessModelIdentifier = modifyProcessIdentifierForPathParam(
processInstanceTask.process_model_identifier
);
const regex = new RegExp(`\\b(${preferredUsername}|${userEmail})\\b`);
let hasAccessToCompleteTask = false;
if (row.potential_owner_usernames.match(regex)) {
hasAccessToCompleteTask = true;
}
return (
<tr key={row.id}>
<td>
<Link
data-qa="process-instance-show-link"
to={`/admin/process-instances/for-me/${modifiedProcessModelIdentifier}/${row.process_instance_id}`}
title={`View process instance ${row.process_instance_id}`}
>
{row.process_instance_id}
</Link>
</td>
<td>
<Link
data-qa="process-model-show-link"
to={`/admin/process-models/${modifiedProcessModelIdentifier}`}
title={row.process_model_identifier}
>
{row.process_model_display_name}
</Link>
</td>
<td
title={`task id: ${row.name}, spiffworkflow task guid: ${row.id}`}
const regex = new RegExp(`\\b(${preferredUsername}|${userEmail})\\b`);
let hasAccessToCompleteTask = false;
if ((processInstanceTask.potential_owner_usernames || '').match(regex)) {
hasAccessToCompleteTask = true;
}
const rowElements = [];
if (showProcessId) {
rowElements.push(
<td>
<Link
data-qa="process-instance-show-link"
to={`/admin/process-instances/for-me/${modifiedProcessModelIdentifier}/${processInstanceTask.process_instance_id}`}
title={`View process instance ${processInstanceTask.process_instance_id}`}
>
{row.task_title}
</td>
{showStartedBy ? <td>{row.process_initiator_username}</td> : ''}
{showWaitingOn ? <td>{getWaitingForTableCellComponent(row)}</td> : ''}
<td>
{convertSecondsToFormattedDateTime(row.created_at_in_seconds) ||
'-'}
</td>
<TableCellWithTimeAgoInWords
timeInSeconds={row.updated_at_in_seconds}
/>
<td>
<Button
variant="primary"
href={taskUrl}
hidden={row.process_instance_status === 'suspended'}
disabled={!hasAccessToCompleteTask}
>
Go
</Button>
</td>
</tr>
{processInstanceTask.process_instance_id}
</Link>
</td>
);
});
let tableHeaders = ['Id', 'Process', 'Task'];
}
if (showProcessModelIdentifier) {
rowElements.push(
<td>
<Link
data-qa="process-model-show-link"
to={`/admin/process-models/${modifiedProcessModelIdentifier}`}
title={processInstanceTask.process_model_identifier}
>
{processInstanceTask.process_model_display_name}
</Link>
</td>
);
}
rowElements.push(
<td
title={`task id: ${processInstanceTask.name}, spiffworkflow task guid: ${processInstanceTask.id}`}
>
{processInstanceTask.task_title}
</td>
);
if (showStartedBy) {
rowElements.push(
<td>{processInstanceTask.process_initiator_username}</td>
);
}
if (showWaitingOn) {
rowElements.push(
<td>{getWaitingForTableCellComponent(processInstanceTask)}</td>
);
}
if (showDateStarted) {
rowElements.push(
<td>
{convertSecondsToFormattedDateTime(
processInstanceTask.created_at_in_seconds
) || '-'}
</td>
);
}
if (showLastUpdated) {
rowElements.push(
<TableCellWithTimeAgoInWords
timeInSeconds={processInstanceTask.updated_at_in_seconds}
/>
);
}
rowElements.push(
<td>
<Button
variant="primary"
href={taskUrl}
hidden={processInstanceTask.process_instance_status === 'suspended'}
disabled={!hasAccessToCompleteTask}
>
Go
</Button>
</td>
);
return <tr key={processInstanceTask.id}>{rowElements}</tr>;
};
const getTableHeaders = () => {
let tableHeaders = [];
if (showProcessId) {
tableHeaders.push('Id');
}
if (showProcessModelIdentifier) {
tableHeaders.push('Process');
}
tableHeaders.push('Task');
if (showStartedBy) {
tableHeaders.push('Started By');
}
if (showWaitingOn) {
tableHeaders.push('Waiting For');
}
tableHeaders = tableHeaders.concat([
'Date Started',
'Last Updated',
'Actions',
]);
if (showDateStarted) {
tableHeaders.push('Date Started');
}
if (showLastUpdated) {
tableHeaders.push('Last Updated');
}
tableHeaders = tableHeaders.concat(['Actions']);
return tableHeaders;
};
const buildTable = () => {
if (!tasks) {
return null;
}
const tableHeaders = getTableHeaders();
const rows = tasks.map((processInstanceTask: ProcessInstanceTask) => {
return getTableRow(processInstanceTask);
});
return (
<Table striped bordered>
<thead>
@ -207,24 +268,41 @@ export default function TaskListTable({
undefined,
paginationQueryParamPrefix
);
return (
<PaginationForTable
page={page}
perPage={perPage}
perPageOptions={[2, PER_PAGE_FOR_TASKS_ON_HOME_PAGE, 25]}
pagination={pagination}
tableToDisplay={buildTable()}
paginationQueryParamPrefix={paginationQueryParamPrefix}
paginationClassName={paginationClassName}
/>
let tableElement = (
<div className={paginationClassName}>{buildTable()}</div>
);
if (shouldPaginateTable) {
tableElement = (
<PaginationForTable
page={page}
perPage={perPage}
perPageOptions={[2, PER_PAGE_FOR_TASKS_ON_HOME_PAGE, 25]}
pagination={pagination}
tableToDisplay={buildTable()}
paginationQueryParamPrefix={paginationQueryParamPrefix}
paginationClassName={paginationClassName}
/>
);
}
return tableElement;
};
if (tasks) {
const tableAndDescriptionElement = () => {
if (showTableDescriptionAsTooltip) {
return <h2 title={tableDescription}>{tableTitle}</h2>;
}
return (
<>
<h2>{tableTitle}</h2>
<p className="data-table-description">{tableDescription}</p>
</>
);
};
if (tasks && (tasks.length > 0 || hideIfNoTasks === false)) {
return (
<>
{tableAndDescriptionElement()}
{tasksComponent()}
</>
);

View File

@ -12,7 +12,6 @@ if (/^\d+\./.test(hostname) || hostname === 'localhost') {
}
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;
}

View File

@ -219,6 +219,20 @@ export const refreshAtInterval = (
};
};
// bpmn:SubProcess shape elements do not have children
// but their moddle elements / businessOjects have flowElements
// that can include the moddleElement of the subprocesses
const getChildProcessesFromModdleElement = (bpmnModdleElement: any) => {
let elements: string[] = [];
bpmnModdleElement.flowElements.forEach((c: any) => {
if (c.$type === 'bpmn:SubProcess') {
elements.push(c.id);
elements = [...elements, ...getChildProcessesFromModdleElement(c)];
}
});
return elements;
};
const getChildProcesses = (bpmnElement: any) => {
let elements: string[] = [];
bpmnElement.children.forEach((c: any) => {
@ -229,6 +243,10 @@ const getChildProcesses = (bpmnElement: any) => {
elements = [...elements, ...getChildProcesses(c)];
} else if (c.type === 'bpmn:SubProcess') {
elements.push(c.id);
elements = [
...elements,
...getChildProcessesFromModdleElement(c.businessObject),
];
}
});
return elements;

58
src/hooks/UsePrompt.tsx Normal file
View File

@ -0,0 +1,58 @@
/**
* These hooks re-implement the now removed useBlocker and usePrompt hooks in 'react-router-dom'.
* Thanks for the idea @piecyk https://github.com/remix-run/react-router/issues/8139#issuecomment-953816315
* Source: https://github.com/remix-run/react-router/commit/256cad70d3fd4500b1abcfea66f3ee622fb90874#diff-b60f1a2d4276b2a605c05e19816634111de2e8a4186fe9dd7de8e344b65ed4d3L344-L381
*/
import { useCallback, useContext, useEffect } from 'react';
import { UNSAFE_NavigationContext as NavigationContext } from 'react-router-dom';
/**
* Blocks all navigation attempts. This is useful for preventing the page from
* changing until some condition is met, like saving form data.
*
* @param blocker
* @param when
* @see https://reactrouter.com/api/useBlocker
*/
export function useBlocker(blocker: any, when: any = true) {
const { navigator } = useContext(NavigationContext);
useEffect(() => {
if (!when) return null;
const unblock = (navigator as any).block((tx: any) => {
const autoUnblockingTx = {
...tx,
retry() {
// Automatically unblock the transition so it can play all the way
// through before retrying it. TODO: Figure out how to re-enable
// this block if the transition is cancelled for some reason.
unblock();
tx.retry();
},
};
blocker(autoUnblockingTx);
});
return unblock;
}, [navigator, blocker, when]);
}
/**
* Prompts the user with an Alert before they leave the current screen.
*
* @param message
* @param when
*/
export function usePrompt(message: any, when: any = true) {
const blocker = useCallback(
(tx: any) => {
// eslint-disable-next-line no-alert
if (window.confirm(message)) tx.retry();
},
[message]
);
useBlocker(blocker, when);
}

View File

@ -228,6 +228,10 @@ h1.with-icons {
background: rgba(0,0,0,.05);
}
.form-instructions {
margin-bottom: 10em;
}
/* Json Web Form CSS Fix - Bootstrap now requries that each li have a "list-inline-item." Also have a PR
in on this with the react-jsonschema-form repo. This is just a patch fix to allow date inputs to layout a little more cleanly */
.list-inline>li {

View File

@ -20,7 +20,7 @@ const doRender = () => {
);
};
UserService.getAuthTokenFromParams();
UserService.loginIfNeeded();
doRender();
// If you want to start measuring performance in your app, pass a function

View File

@ -34,12 +34,12 @@ export interface ProcessInstanceTask {
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;
potential_owner_usernames?: string;
assigned_user_group_identifier?: string;
}
export interface ProcessReference {
@ -73,8 +73,13 @@ export interface ProcessInstance {
status: string;
start_in_seconds: number | null;
end_in_seconds: number | null;
process_initiator_username: string;
bpmn_xml_file_contents?: string;
spiff_step?: number;
created_at_in_seconds: number;
updated_at_in_seconds: number;
bpmn_version_control_identifier: string;
bpmn_version_control_type: string;
}
export interface MessageCorrelationProperties {
@ -146,6 +151,8 @@ export interface ProcessModel {
files: ProcessFile[];
parent_groups?: ProcessGroupLite[];
metadata_extraction_paths?: MetadataExtractionPath[];
fault_or_suspend_on_exception?: string;
exception_notification_addresses?: string[];
}
export interface ProcessGroup {

View File

@ -42,7 +42,7 @@ export default function AuthenticationList() {
row.id
}?redirect_url=${redirectUrl}/${
row.id
}?token=${UserService.getAuthToken()}`}
}?token=${UserService.getAccessToken()}`}
>
{row.id}
</a>

View File

@ -73,11 +73,6 @@ export default function JsonSchemaFormBuilder() {
};
const onFormFieldTitleChange = (newFormFieldTitle: string) => {
console.log('newFormFieldTitle', newFormFieldTitle);
console.log(
'setFormFieldIdHasBeenUpdatedByUser',
formFieldIdHasBeenUpdatedByUser
);
if (!formFieldIdHasBeenUpdatedByUser) {
setFormFieldId(underscorizeString(newFormFieldTitle));
}

View File

@ -4,6 +4,7 @@ import { useNavigate } from 'react-router-dom';
import { Button, ButtonSet, Form, Stack, TextInput } from '@carbon/react';
import { isInteger, modifyProcessIdentifierForPathParam } from '../helpers';
import HttpService from '../services/HttpService';
import ProcessInstanceListTabs from '../components/ProcessInstanceListTabs';
import { ProcessInstance } from '../interfaces';
export default function ProcessInstanceFindById() {
@ -69,11 +70,15 @@ export default function ProcessInstanceFindById() {
};
return (
<Form onSubmit={handleFormSubmission}>
<Stack gap={5}>
{formElements()}
{formButtons()}
</Stack>
</Form>
<>
<ProcessInstanceListTabs variant="find-by-id" />
<br />
<Form onSubmit={handleFormSubmission}>
<Stack gap={5}>
{formElements()}
{formButtons()}
</Stack>
</Form>
</>
);
}

View File

@ -1,18 +1,13 @@
import { useNavigate, useSearchParams } from 'react-router-dom';
import { 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';
import ProcessInstanceListTabs from '../components/ProcessInstanceListTabs';
type OwnProps = {
variant: string;
@ -20,13 +15,6 @@ type OwnProps = {
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 =
@ -57,45 +45,9 @@ export default function ProcessInstanceList({ variant }: OwnProps) {
return <h1>My Process Instances</h1>;
};
let selectedTabIndex = 0;
if (variant === 'all') {
selectedTabIndex = 1;
}
return (
<>
<Tabs selectedIndex={selectedTabIndex}>
<TabList aria-label="List of tabs">
<Tab
title="Only show process instances for the current user."
data-qa="process-instance-list-for-me"
onClick={() => {
navigate('/admin/process-instances/for-me');
}}
>
For Me
</Tab>
<Can I="GET" a={targetUris.processInstanceListPath} ability={ability}>
<Tab
title="Show all process instances for all users."
data-qa="process-instance-list-all"
onClick={() => {
navigate('/admin/process-instances/all');
}}
>
All
</Tab>
</Can>
<Tab
title="Search for a process instance by id."
data-qa="process-instance-list-find-by-id"
onClick={() => {
navigate('/admin/process-instances/find-by-id');
}}
>
Find By Id
</Tab>
</TabList>
</Tabs>
<ProcessInstanceListTabs variant={variant} />
<br />
{processInstanceBreadcrumbElement()}
{processInstanceTitleElement()}

View File

@ -46,15 +46,21 @@ export default function ProcessInstanceLogList() {
return (
<tr key={rowToUse.id}>
<td data-qa="paginated-entity-id">{rowToUse.id}</td>
<td>{rowToUse.message}</td>
<td>{rowToUse.bpmn_task_name}</td>
<td>
{rowToUse.bpmn_task_name ||
(rowToUse.bpmn_task_type === 'Default Start Event'
? 'Process Started'
: '') ||
(rowToUse.bpmn_task_type === 'End Event' ? 'Process Ended' : '')}
</td>
{isDetailedView && (
<>
<td>{rowToUse.message}</td>
<td>{rowToUse.bpmn_task_identifier}</td>
<td>{rowToUse.bpmn_task_type}</td>
<td>{rowToUse.bpmn_process_identifier}</td>
</>
)}
<td>{rowToUse.bpmn_process_identifier}</td>
<td>{rowToUse.username}</td>
<td>
<Link
@ -72,15 +78,15 @@ export default function ProcessInstanceLogList() {
<thead>
<tr>
<th>Id</th>
<th>Message</th>
<th>Task Name</th>
{isDetailedView && (
<>
<th>Message</th>
<th>Task Identifier</th>
<th>Task Type</th>
<th>Bpmn Process Identifier</th>
</>
)}
<th>Bpmn Process Identifier</th>
<th>User</th>
<th>Timestamp</th>
</tr>

View File

@ -7,12 +7,12 @@ import {
useSearchParams,
} from 'react-router-dom';
import {
CaretRight,
TrashCan,
StopOutline,
PauseOutline,
PlayOutline,
CaretLeft,
CaretRight,
InProgress,
Checkmark,
Warning,
@ -48,6 +48,7 @@ import {
} from '../interfaces';
import { usePermissionFetcher } from '../hooks/PermissionService';
import ProcessInstanceClass from '../classes/ProcessInstanceClass';
import TaskListTable from '../components/TaskListTable';
type OwnProps = {
variant: string;
@ -72,6 +73,7 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
const [eventPayload, setEventPayload] = useState<string>('{}');
const [eventTextEditorEnabled, setEventTextEditorEnabled] =
useState<boolean>(false);
const [displayDetails, setDisplayDetails] = useState<boolean>(false);
const setErrorObject = (useContext as any)(ErrorContext)[1];
@ -199,20 +201,21 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
const getTaskIds = () => {
const taskIds = { completed: [], readyOrWaiting: [] };
if (tasks) {
const callingSubprocessId = searchParams.get('call_activity_task_id');
tasks.forEach(function getUserTasksElement(task: ProcessInstanceTask) {
const callingSubprocessId = searchParams.get('call_activity_task_id');
if (
!callingSubprocessId ||
callingSubprocessId === task.calling_subprocess_task_id
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 null;
}
if (task.state === 'COMPLETED') {
(taskIds.completed as any).push(task);
}
if (task.state === 'READY' || task.state === 'WAITING') {
(taskIds.readyOrWaiting as any).push(task);
}
return null;
});
}
return taskIds;
@ -281,6 +284,70 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
});
};
const detailedViewElement = () => {
if (!processInstance) {
return null;
}
if (displayDetails) {
return (
<>
<Grid condensed fullWidth>
<Button
kind="ghost"
className="button-link"
onClick={() => setDisplayDetails(false)}
title="Hide Details"
>
&laquo; Hide Details
</Button>
</Grid>
<Grid condensed fullWidth>
<Column sm={1} md={1} lg={2} className="grid-list-title">
Updated At:{' '}
</Column>
<Column sm={3} md={3} lg={3} className="grid-date">
{convertSecondsToFormattedDateTime(
processInstance.updated_at_in_seconds
)}
</Column>
</Grid>
<Grid condensed fullWidth>
<Column sm={1} md={1} lg={2} className="grid-list-title">
Created At:{' '}
</Column>
<Column sm={3} md={3} lg={3} className="grid-date">
{convertSecondsToFormattedDateTime(
processInstance.created_at_in_seconds
)}
</Column>
</Grid>
<Grid condensed fullWidth>
<Column sm={1} md={1} lg={2} className="grid-list-title">
Process model revision:{' '}
</Column>
<Column sm={3} md={3} lg={3} className="grid-date">
{processInstance.bpmn_version_control_identifier} (
{processInstance.bpmn_version_control_type})
</Column>
</Grid>
</>
);
}
return (
<Grid condensed fullWidth>
<Button
kind="ghost"
className="button-link"
onClick={() => setDisplayDetails(true)}
title="Show Details"
>
View Details &raquo;
</Button>
</Grid>
);
};
const getInfoTag = () => {
if (!processInstance) {
return null;
@ -317,6 +384,14 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
return (
<>
<Grid condensed fullWidth>
<Column sm={1} md={1} lg={2} className="grid-list-title">
Started By:{' '}
</Column>
<Column sm={3} md={3} lg={3} className="grid-date">
{processInstance.process_initiator_username}
</Column>
</Grid>
<Grid condensed fullWidth>
<Column sm={1} md={1} lg={2} className="grid-list-title">
Started:{' '}
@ -338,6 +413,7 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
</Tag>
</Column>
</Grid>
{detailedViewElement()}
<br />
<Grid condensed fullWidth>
<Column sm={2} md={2} lg={2}>
@ -529,11 +605,24 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
}
};
const isCurrentTask = (task: any) => {
const subprocessTypes = [
'Subprocess',
'Call Activity',
'Transactional Subprocess',
];
return (
(task.state === 'WAITING' &&
subprocessTypes.filter((t) => t === task.type).length > 0) ||
task.state === 'READY'
);
};
const canEditTaskData = (task: any) => {
return (
processInstance &&
ability.can('PUT', targetUris.processInstanceTaskListDataPath) &&
task.state === 'READY' &&
isCurrentTask(task) &&
processInstance.status === 'suspended' &&
showingLastSpiffStep()
);
@ -557,7 +646,7 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
processInstance &&
processInstance.status === 'suspended' &&
ability.can('POST', targetUris.processInstanceCompleteTaskPath) &&
task.state === 'READY' &&
isCurrentTask(task) &&
showingLastSpiffStep()
);
};
@ -818,6 +907,10 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
const taskToUse: any = { ...taskToDisplay, data: taskDataToDisplay };
const candidateEvents: any = getEvents(taskToUse);
if (taskToDisplay) {
let taskTitleText = taskToUse.id;
if (taskToUse.title) {
taskTitleText += ` (${taskToUse.title})`;
}
return (
<Modal
open={!!taskToUse}
@ -825,7 +918,9 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
onRequestClose={handleTaskDataDisplayClose}
>
<Stack orientation="horizontal" gap={2}>
{taskToUse.name} ({taskToUse.type}): {taskToUse.state}
<span title={taskTitleText}>{taskToUse.name}</span> (
{taskToUse.type}
): {taskToUse.state}
{taskDisplayButtons(taskToUse)}
</Stack>
{selectingEvent
@ -915,6 +1010,26 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
</Stack>
<br />
<br />
<Grid condensed fullWidth>
<Column md={6} lg={8} sm={4}>
<TaskListTable
apiPath="/tasks"
additionalParams={`process_instance_id=${processInstance.id}`}
tableTitle="Tasks I can complete"
tableDescription="These are tasks that can be completed by you, either because they were assigned to a group you are in, or because they were assigned directly to you."
paginationClassName="with-large-bottom-margin"
textToShowIfEmpty="There are no tasks you can complete for this process instance."
shouldPaginateTable={false}
showProcessModelIdentifier={false}
showProcessId={false}
showStartedBy={false}
showTableDescriptionAsTooltip
showDateStarted={false}
showLastUpdated={false}
hideIfNoTasks
/>
</Column>
</Grid>
{getInfoTag()}
<br />
{taskUpdateDisplayArea()}

View File

@ -35,11 +35,13 @@ import {
} from '../interfaces';
import ProcessSearch from '../components/ProcessSearch';
import { Notification } from '../components/Notification';
import { usePrompt } from '../hooks/UsePrompt';
export default function ProcessModelEditDiagram() {
const [showFileNameEditor, setShowFileNameEditor] = useState(false);
const handleShowFileNameEditor = () => setShowFileNameEditor(true);
const [processModel, setProcessModel] = useState<ProcessModel | null>(null);
const [diagramHasChanges, setDiagramHasChanges] = useState<boolean>(false);
const [scriptText, setScriptText] = useState<string>('');
const [scriptType, setScriptType] = useState<string>('');
@ -112,6 +114,8 @@ export default function ProcessModelEditDiagram() {
const processModelPath = `process-models/${modifiedProcessModelId}`;
usePrompt('Changes you made may not be saved.', diagramHasChanges);
useEffect(() => {
// Grab all available process models in case we need to search for them.
// Taken from the Process Group List
@ -127,14 +131,14 @@ export default function ProcessModelEditDiagram() {
path: `/processes`,
successCallback: processResults,
});
}, [processModel]);
}, []);
useEffect(() => {
const processResult = (result: ProcessModel) => {
setProcessModel(result);
};
HttpService.makeCallToBackend({
path: `/${processModelPath}`,
path: `/${processModelPath}?include_file_references=true`,
successCallback: processResult,
});
}, [processModelPath]);
@ -206,6 +210,11 @@ export default function ProcessModelEditDiagram() {
// after saving the file, make sure we null out newFileName
// so it does not get used over the params
setNewFileName('');
setDiagramHasChanges(false);
};
const onElementsChanged = () => {
setDiagramHasChanges(true);
};
const onDeleteFile = (fileName = params.file_name) => {
@ -731,7 +740,7 @@ export default function ProcessModelEditDiagram() {
);
};
const onLaunchMarkdownEditor = (
element: any,
_element: any,
markdown: string,
eventBus: any
) => {
@ -767,7 +776,7 @@ export default function ProcessModelEditDiagram() {
};
const onSearchProcessModels = (
processId: string,
_processId: string,
eventBus: any,
element: any
) => {
@ -792,6 +801,7 @@ export default function ProcessModelEditDiagram() {
open={showProcessSearch}
modalHeading="Select Process Model"
primaryButtonText="Close"
onRequestClose={processSearchOnClose}
onRequestSubmit={processSearchOnClose}
size="lg"
>
@ -826,22 +836,30 @@ export default function ProcessModelEditDiagram() {
};
const onLaunchBpmnEditor = (processId: string) => {
const processRef = processes.find((p) => {
return p.identifier === processId;
// using the "setState" method with a function gives us access to the
// most current state of processes. Otherwise it uses the stale state
// when passing the callback to a non-React component like bpmn-js:
// https://stackoverflow.com/a/60643670/6090676
setProcesses((upToDateProcesses: ProcessReference[]) => {
const processRef = upToDateProcesses.find((p) => {
return p.identifier === processId;
});
if (processRef) {
const path = generatePath(
'/admin/process-models/:process_model_path/files/:file_name',
{
process_model_path: modifyProcessIdentifierForPathParam(
processRef.process_model_id
),
file_name: processRef.file_name,
}
);
window.open(path);
}
return upToDateProcesses;
});
if (processRef) {
const path = generatePath(
'/admin/process-models/:process_model_path/files/:file_name',
{
process_model_path: modifyProcessIdentifierForPathParam(
processRef.process_model_id
),
file_name: processRef.file_name,
}
);
window.open(path);
}
};
const onLaunchJsonEditor = (fileName: string) => {
const path = generatePath(
'/admin/process-models/:process_model_id/form/:file_name',
@ -913,6 +931,7 @@ export default function ProcessModelEditDiagram() {
onLaunchDmnEditor={onLaunchDmnEditor}
onDmnFilesRequested={onDmnFilesRequested}
onSearchProcessModels={onSearchProcessModels}
onElementsChanged={onElementsChanged}
/>
);
};

View File

@ -374,6 +374,7 @@ export default function ProcessModelShow() {
const doFileUpload = (event: any) => {
event.preventDefault();
setErrorObject(null);
const url = `/process-models/${modifiedProcessModelId}/files`;
const formData = new FormData();
formData.append('file', filesToUpload[0]);
@ -383,6 +384,7 @@ export default function ProcessModelShow() {
successCallback: onUploadedCallback,
httpMethod: 'POST',
postBody: formData,
failureCallback: setErrorObject,
});
setFilesToUpload(null);
};
@ -685,7 +687,7 @@ export default function ProcessModelShow() {
perPageOptions={[2, 5, 25]}
showReports={false}
/>
<span data-qa="process-model-show-permissions-loaded">true</span>
<span data-qa="process-model-show-permissions-loaded" />
</Can>
</>
);

View File

@ -1,4 +1,4 @@
import { useEffect, useState } from 'react';
import { useContext, useEffect, useState } from 'react';
import Editor from '@monaco-editor/react';
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
// @ts-ignore
@ -8,18 +8,25 @@ import HttpService from '../services/HttpService';
import ButtonWithConfirmation from '../components/ButtonWithConfirmation';
import { modifyProcessIdentifierForPathParam } from '../helpers';
import { ProcessFile } from '../interfaces';
import ErrorContext from '../contexts/ErrorContext';
import { Notification } from '../components/Notification';
// NOTE: This is mostly the same as ProcessModelEditDiagram and if we go this route could
// possibly be merged into it. I'm leaving as a separate file now in case it does
// end up diverging greatly
export default function ReactFormEditor() {
const params = useParams();
const setErrorObject = (useContext as any)(ErrorContext)[1];
const [showFileNameEditor, setShowFileNameEditor] = useState(false);
const [newFileName, setNewFileName] = useState('');
const searchParams = useSearchParams()[0];
const handleShowFileNameEditor = () => setShowFileNameEditor(true);
const navigate = useNavigate();
const [displaySaveFileMessage, setDisplaySaveFileMessage] =
useState<boolean>(false);
const [processModelFile, setProcessModelFile] = useState<ProcessFile | null>(
null
);
@ -70,6 +77,7 @@ export default function ReactFormEditor() {
}, [params, modifiedProcessModelId]);
const navigateToProcessModelFile = (_result: any) => {
setDisplaySaveFileMessage(true);
if (!params.file_name) {
const fileNameWithExtension = `${newFileName}.${fileExtension}`;
navigate(
@ -79,6 +87,9 @@ export default function ReactFormEditor() {
};
const saveFile = () => {
setErrorObject(null);
setDisplaySaveFileMessage(false);
let url = `/process-models/${modifiedProcessModelId}/files`;
let httpMethod = 'PUT';
let fileNameWithExtension = params.file_name;
@ -105,6 +116,7 @@ export default function ReactFormEditor() {
HttpService.makeCallToBackend({
path: url,
successCallback: navigateToProcessModelFile,
failureCallback: setErrorObject,
httpMethod,
postBody: formData,
});
@ -162,6 +174,20 @@ export default function ReactFormEditor() {
);
};
const saveFileMessage = () => {
if (displaySaveFileMessage) {
return (
<Notification
title="File Saved: "
onClose={() => setDisplaySaveFileMessage(false)}
>
Changes to the file were saved.
</Notification>
);
}
return null;
};
if (processModelFile || !params.file_name) {
const processModelFileName = processModelFile ? processModelFile.name : '';
return (
@ -182,6 +208,7 @@ export default function ReactFormEditor() {
{processModelFileName}
</h1>
{newFileNameBox()}
{saveFileMessage()}
<Button onClick={saveFile} variant="danger" data-qa="file-save-button">
Save
</Button>

View File

@ -1,13 +1,6 @@
import { useContext, useEffect, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
// FIXME: npm install @rjsf/validator-ajv8 and use it as soon as
// rawErrors is fixed.
// https://react-jsonschema-form.readthedocs.io/en/latest/usage/validation/
// https://github.com/rjsf-team/react-jsonschema-form/issues/2309 links to a codesandbox that might be useful to fork
// if we wanted to file a defect against rjsf to show the difference between validator-ajv6 and validator-ajv8.
// https://github.com/rjsf-team/react-jsonschema-form/blob/main/docs/api-reference/uiSchema.md talks about rawErrors
import validator from '@rjsf/validator-ajv6';
import validator from '@rjsf/validator-ajv8';
import {
TabList,
@ -145,6 +138,34 @@ export default function TaskShow() {
return null;
};
const getFieldsWithDateValidations = (
jsonSchema: any,
formData: any,
errors: any
) => {
if ('properties' in jsonSchema) {
Object.keys(jsonSchema.properties).forEach((propertyKey: string) => {
const propertyMetadata = jsonSchema.properties[propertyKey];
if (
'minimumDate' in propertyMetadata &&
propertyMetadata.minimumDate === 'today'
) {
const dateToday = new Date();
const dateValue = formData[propertyKey];
if (dateValue) {
const dateValueObject = new Date(dateValue);
const dateValueString = dateValueObject.toISOString().split('T')[0];
const dateTodayString = dateToday.toISOString().split('T')[0];
if (dateTodayString > dateValueString) {
errors[propertyKey].addError('must be today or after');
}
}
}
});
}
return errors;
};
const formElement = (taskToUse: any) => {
let formUiSchema;
let taskData = taskToUse.data;
@ -191,6 +212,10 @@ export default function TaskShow() {
);
}
const customValidate = (formData: any, errors: any) => {
return getFieldsWithDateValidations(jsonSchema, formData, errors);
};
return (
<Grid fullWidth condensed>
<Column md={5} lg={8} sm={4}>
@ -200,6 +225,7 @@ export default function TaskShow() {
schema={jsonSchema}
uiSchema={formUiSchema}
validator={validator}
customValidate={customValidate}
>
{reactFragmentToHideSubmitButton}
</Form>

View File

@ -11,7 +11,7 @@ const HttpMethods = {
const getBasicHeaders = (): object => {
if (UserService.isLoggedIn()) {
return {
Authorization: `Bearer ${UserService.getAuthToken()}`,
Authorization: `Bearer ${UserService.getAccessToken()}`,
};
}
return {};
@ -64,6 +64,7 @@ backendCallProps) => {
Object.assign(httpArgs, {
headers: new Headers(headers as any),
method: httpMethod,
credentials: 'include',
});
const updatedPath = path.replace(/^\/v1\.0/, '');

View File

@ -1,4 +1,5 @@
import jwt from 'jwt-decode';
import cookie from 'cookie';
import { BACKEND_BASE_URL } from '../config';
// NOTE: this currently stores the jwt token in local storage
@ -10,33 +11,46 @@ import { BACKEND_BASE_URL } from '../config';
// Some explanation:
// https://dev.to/nilanth/how-to-secure-jwt-in-a-single-page-application-cko
const getCurrentLocation = () => {
// to trim off any query params
return `${window.location.origin}${window.location.pathname}`;
const getCookie = (key: string) => {
const parsedCookies = cookie.parse(document.cookie);
if (key in parsedCookies) {
return parsedCookies[key];
}
return null;
};
const getCurrentLocation = (queryParams: string = window.location.search) => {
let queryParamString = '';
if (queryParams) {
queryParamString = `${queryParams}`;
}
return encodeURIComponent(
`${window.location.origin}${window.location.pathname}${queryParamString}`
);
};
const doLogin = () => {
const url = `${BACKEND_BASE_URL}/login?redirect_url=${getCurrentLocation()}`;
window.location.href = url;
};
// required for logging out
const getIdToken = () => {
return localStorage.getItem('jwtIdToken');
return getCookie('id_token');
};
const doLogout = () => {
const idToken = getIdToken();
localStorage.removeItem('jwtAccessToken');
localStorage.removeItem('jwtIdToken');
const redirectUrl = `${window.location.origin}`;
const url = `${BACKEND_BASE_URL}/logout?redirect_url=${redirectUrl}&id_token=${idToken}`;
window.location.href = url;
};
const getAuthToken = () => {
return localStorage.getItem('jwtAccessToken');
const getAccessToken = () => {
return getCookie('access_token');
};
const isLoggedIn = () => {
return !!getAuthToken();
return !!getAccessToken();
};
const getUserEmail = () => {
@ -57,22 +71,8 @@ const getPreferredUsername = () => {
return null;
};
// FIXME: we could probably change this search to a hook
// and then could use useSearchParams here instead
const getAuthTokenFromParams = () => {
const queryParams = window.location.search;
const accessTokenMatch = queryParams.match(/.*\baccess_token=([^&]+).*/);
const idTokenMatch = queryParams.match(/.*\bid_token=([^&]+).*/);
if (accessTokenMatch) {
const accessToken = accessTokenMatch[1];
localStorage.setItem('jwtAccessToken', accessToken);
if (idTokenMatch) {
const idToken = idTokenMatch[1];
localStorage.setItem('jwtIdToken', idToken);
}
// to remove token query param
window.location.href = getCurrentLocation();
} else if (!isLoggedIn()) {
const loginIfNeeded = () => {
if (!isLoggedIn()) {
doLogin();
}
};
@ -85,8 +85,8 @@ const UserService = {
doLogin,
doLogout,
isLoggedIn,
getAuthToken,
getAuthTokenFromParams,
getAccessToken,
loginIfNeeded,
getPreferredUsername,
getUserEmail,
hasRole,

View File

@ -1,12 +1,23 @@
import React, { CSSProperties } from 'react';
import Box from '@mui/material/Box';
import Grid from '@mui/material/Grid';
import Paper from '@mui/material/Paper';
import { ArrayFieldTemplateItemType } from '@rjsf/utils';
import {
ArrayFieldTemplateItemType,
FormContextType,
RJSFSchema,
StrictRJSFSchema,
} from '@rjsf/utils';
function ArrayFieldItemTemplate(props: ArrayFieldTemplateItemType) {
/** The `ArrayFieldItemTemplate` component is the template used to render an items of an array.
*
* @param props - The `ArrayFieldTemplateItemType` props for the component
*/
export default function ArrayFieldItemTemplate<
T = any,
S extends StrictRJSFSchema = RJSFSchema,
F extends FormContextType = any
>(props: ArrayFieldTemplateItemType<T, S, F>) {
const {
children,
className,
disabled,
hasToolbar,
hasMoveDown,
@ -16,8 +27,8 @@ function ArrayFieldItemTemplate(props: ArrayFieldTemplateItemType) {
onDropIndexClick,
onReorderClick,
readonly,
uiSchema,
registry,
uiSchema,
} = props;
const { MoveDownButton, MoveUpButton, RemoveButton } =
registry.templates.ButtonTemplates;
@ -26,47 +37,49 @@ function ArrayFieldItemTemplate(props: ArrayFieldTemplateItemType) {
paddingLeft: 6,
paddingRight: 6,
fontWeight: 'bold',
minWidth: 0,
};
return (
<Grid container alignItems="center">
<Grid item xs style={{ overflow: 'auto' }}>
<Box mb={2}>
<Paper elevation={2}>
<Box p={2}>{children}</Box>
</Paper>
</Box>
</Grid>
<div className={className}>
<div className={hasToolbar ? 'col-xs-9' : 'col-xs-12'}>{children}</div>
{hasToolbar && (
<Grid item>
{(hasMoveUp || hasMoveDown) && (
<MoveUpButton
style={btnStyle}
disabled={disabled || readonly || !hasMoveUp}
onClick={onReorderClick(index, index - 1)}
uiSchema={uiSchema}
/>
)}
{(hasMoveUp || hasMoveDown) && (
<MoveDownButton
style={btnStyle}
disabled={disabled || readonly || !hasMoveDown}
onClick={onReorderClick(index, index + 1)}
uiSchema={uiSchema}
/>
)}
{hasRemove && (
<RemoveButton
style={btnStyle}
disabled={disabled || readonly}
onClick={onDropIndexClick(index)}
uiSchema={uiSchema}
/>
)}
</Grid>
<div className="col-xs-3 array-item-toolbox">
<div
className="btn-group"
style={{
display: 'flex',
justifyContent: 'space-around',
}}
>
{(hasMoveUp || hasMoveDown) && (
<MoveUpButton
style={btnStyle}
disabled={disabled || readonly || !hasMoveUp}
onClick={onReorderClick(index, index - 1)}
uiSchema={uiSchema}
registry={registry}
/>
)}
{(hasMoveUp || hasMoveDown) && (
<MoveDownButton
style={btnStyle}
disabled={disabled || readonly || !hasMoveDown}
onClick={onReorderClick(index, index + 1)}
uiSchema={uiSchema}
registry={registry}
/>
)}
{hasRemove && (
<RemoveButton
style={btnStyle}
disabled={disabled || readonly}
onClick={onDropIndexClick(index)}
uiSchema={uiSchema}
registry={registry}
/>
)}
</div>
</div>
)}
</Grid>
</div>
);
}
export default ArrayFieldItemTemplate;

View File

@ -1,17 +1,26 @@
import React from 'react';
import Box from '@mui/material/Box';
import Grid from '@mui/material/Grid';
import Paper from '@mui/material/Paper';
import {
ArrayFieldTemplateItemType,
ArrayFieldTemplateProps,
getTemplate,
getUiOptions,
ArrayFieldTemplateProps,
ArrayFieldTemplateItemType,
FormContextType,
RJSFSchema,
StrictRJSFSchema,
} from '@rjsf/utils';
function ArrayFieldTemplate(props: ArrayFieldTemplateProps) {
/** The `ArrayFieldTemplate` component is the template used to render all items in an array.
*
* @param props - The `ArrayFieldTemplateItemType` props for the component
*/
export default function ArrayFieldTemplate<
T = any,
S extends StrictRJSFSchema = RJSFSchema,
F extends FormContextType = any
>(props: ArrayFieldTemplateProps<T, S, F>) {
const {
canAdd,
className,
disabled,
idSchema,
uiSchema,
@ -23,68 +32,62 @@ function ArrayFieldTemplate(props: ArrayFieldTemplateProps) {
schema,
title,
} = props;
const uiOptions = getUiOptions(uiSchema);
const ArrayFieldDescriptionTemplate =
getTemplate<'ArrayFieldDescriptionTemplate'>(
'ArrayFieldDescriptionTemplate',
registry,
uiOptions
);
const ArrayFieldItemTemplate = getTemplate<'ArrayFieldItemTemplate'>(
const uiOptions = getUiOptions<T, S, F>(uiSchema);
const ArrayFieldDescriptionTemplate = getTemplate<
'ArrayFieldDescriptionTemplate',
T,
S,
F
>('ArrayFieldDescriptionTemplate', registry, uiOptions);
const ArrayFieldItemTemplate = getTemplate<'ArrayFieldItemTemplate', T, S, F>(
'ArrayFieldItemTemplate',
registry,
uiOptions
);
const ArrayFieldTitleTemplate = getTemplate<'ArrayFieldTitleTemplate'>(
const ArrayFieldTitleTemplate = getTemplate<
'ArrayFieldTitleTemplate',
registry,
uiOptions
);
T,
S,
F
>('ArrayFieldTitleTemplate', registry, uiOptions);
// Button templates are not overridden in the uiSchema
const {
ButtonTemplates: { AddButton },
} = registry.templates;
return (
<Paper elevation={2}>
<Box p={2}>
<ArrayFieldTitleTemplate
idSchema={idSchema}
title={uiOptions.title || title}
schema={schema}
uiSchema={uiSchema}
required={required}
registry={registry}
/>
<ArrayFieldDescriptionTemplate
idSchema={idSchema}
description={uiOptions.description || schema.description}
schema={schema}
uiSchema={uiSchema}
registry={registry}
/>
<Grid container key={`array-item-list-${idSchema.$id}`}>
{items &&
items.map(({ key, ...itemProps }: ArrayFieldTemplateItemType) => (
<fieldset className={className} id={idSchema.$id}>
<ArrayFieldTitleTemplate
idSchema={idSchema}
title={uiOptions.title || title}
required={required}
schema={schema}
uiSchema={uiSchema}
registry={registry}
/>
<ArrayFieldDescriptionTemplate
idSchema={idSchema}
description={uiOptions.description || schema.description}
schema={schema}
uiSchema={uiSchema}
registry={registry}
/>
<div className="row array-item-list">
{items &&
items.map(
({ key, ...itemProps }: ArrayFieldTemplateItemType<T, S, F>) => (
<ArrayFieldItemTemplate key={key} {...itemProps} />
))}
{canAdd && (
<Grid container justifyContent="flex-end">
<Grid item>
<Box mt={2}>
<AddButton
className="array-item-add"
onClick={onAddClick}
disabled={disabled || readonly}
uiSchema={uiSchema}
/>
</Box>
</Grid>
</Grid>
)
)}
</Grid>
</Box>
</Paper>
</div>
{canAdd && (
<AddButton
className="array-item-add"
onClick={onAddClick}
disabled={disabled || readonly}
uiSchema={uiSchema}
registry={registry}
/>
)}
</fieldset>
);
}
export default ArrayFieldTemplate;

View File

@ -45,7 +45,6 @@ export default function BaseInputTemplate<
// Note: since React 15.2.0 we can't forward unknown element attributes, so we
// exclude the "options" and "schema" ones here.
if (!id) {
console.log('No id for', props);
throw new Error(`no id for props ${JSON.stringify(props)}`);
}
const inputProps = {
@ -90,7 +89,11 @@ export default function BaseInputTemplate<
let errorMessageForField = null;
if (rawErrors && rawErrors.length > 0) {
invalid = true;
errorMessageForField = `${labelToUse.replace(/\*$/, '')} ${rawErrors[0]}`;
if ('validationErrorMessage' in schema) {
errorMessageForField = (schema as any).validationErrorMessage;
} else {
errorMessageForField = `${labelToUse.replace(/\*$/, '')} ${rawErrors[0]}`;
}
}
return (
@ -98,6 +101,7 @@ export default function BaseInputTemplate<
<TextInput
id={id}
name={id}
className="input"
labelText={labelToUse}
invalid={invalid}
invalidText={errorMessageForField}

View File

@ -1,17 +1,34 @@
import React from 'react';
import Typography from '@mui/material/Typography';
import { DescriptionFieldProps } from '@rjsf/utils';
import {
DescriptionFieldProps,
FormContextType,
RJSFSchema,
StrictRJSFSchema,
} from '@rjsf/utils';
function DescriptionField({ description, id }: DescriptionFieldProps) {
if (description) {
/** The `DescriptionField` is the template to use to render the description of a field
*
* @param props - The `DescriptionFieldProps` for this component
*/
export default function DescriptionField<
T = any,
S extends StrictRJSFSchema = RJSFSchema,
F extends FormContextType = any
>(props: DescriptionFieldProps<T, S, F>) {
const { id, description } = props;
if (!description) {
return null;
}
if (typeof description === 'string') {
return (
<Typography id={id} variant="subtitle2" style={{ marginTop: '5px' }}>
<p id={id} className="field-description">
{description}
</Typography>
</p>
);
}
return null;
return (
<div id={id} className="field-description">
{description}
</div>
);
}
export default DescriptionField;

View File

@ -0,0 +1,29 @@
import React from "react";
const REQUIRED_FIELD_SYMBOL = "*";
export type LabelProps = {
/** The label for the field */
label?: string;
/** A boolean value stating if the field is required */
required?: boolean;
/** The id of the input field being labeled */
id?: string;
};
/** Renders a label for a field
*
* @param props - The `LabelProps` for this component
*/
export default function Label(props: LabelProps) {
const { label, required, id } = props;
if (!label) {
return null;
}
return (
<label className="control-label" htmlFor={id}>
{label}
{required && <span className="required">{REQUIRED_FIELD_SYMBOL}</span>}
</label>
);
}

View File

@ -1,89 +1,87 @@
import React from 'react';
import Grid from '@mui/material/Grid';
import {
FormContextType,
ObjectFieldTemplatePropertyType,
ObjectFieldTemplateProps,
RJSFSchema,
StrictRJSFSchema,
canExpand,
getTemplate,
getUiOptions,
} from '@rjsf/utils';
function ObjectFieldTemplate({
description,
title,
properties,
required,
disabled,
readonly,
uiSchema,
idSchema,
schema,
formData,
onAddClick,
registry,
}: ObjectFieldTemplateProps) {
const uiOptions = getUiOptions(uiSchema);
const TitleFieldTemplate = getTemplate<'TitleFieldTemplate'>(
/** The `ObjectFieldTemplate` is the template to use to render all the inner properties of an object along with the
* title and description if available. If the object is expandable, then an `AddButton` is also rendered after all
* the properties.
*
* @param props - The `ObjectFieldTemplateProps` for this component
*/
export default function ObjectFieldTemplate<
T = any,
S extends StrictRJSFSchema = RJSFSchema,
F extends FormContextType = any
>(props: ObjectFieldTemplateProps<T, S, F>) {
const {
description,
disabled,
formData,
idSchema,
onAddClick,
properties,
readonly,
registry,
required,
schema,
title,
uiSchema,
} = props;
const options = getUiOptions<T, S, F>(uiSchema);
const TitleFieldTemplate = getTemplate<'TitleFieldTemplate', T, S, F>(
'TitleFieldTemplate',
registry,
uiOptions
options
);
const DescriptionFieldTemplate = getTemplate<'DescriptionFieldTemplate'>(
const DescriptionFieldTemplate = getTemplate<
'DescriptionFieldTemplate',
registry,
uiOptions
);
T,
S,
F
>('DescriptionFieldTemplate', registry, options);
// Button templates are not overridden in the uiSchema
const {
ButtonTemplates: { AddButton },
} = registry.templates;
return (
<>
{(uiOptions.title || title) && (
<fieldset id={idSchema.$id}>
{(options.title || title) && (
<TitleFieldTemplate
id={`${idSchema.$id}-title`}
title={title}
id={`${idSchema.$id}__title`}
title={options.title || title}
required={required}
schema={schema}
uiSchema={uiSchema}
registry={registry}
/>
)}
{(uiOptions.description || description) && (
{(options.description || description) && (
<DescriptionFieldTemplate
id={`${idSchema.$id}-description`}
description={uiOptions.description || description!}
id={`${idSchema.$id}__description`}
description={options.description || description!}
schema={schema}
uiSchema={uiSchema}
registry={registry}
/>
)}
<Grid container spacing={2} style={{ marginTop: '10px' }}>
{properties.map((element, index) =>
// Remove the <Grid> if the inner element is hidden as the <Grid>
// itself would otherwise still take up space.
element.hidden ? (
element.content
) : (
<Grid item xs={12} key={index} style={{ marginBottom: '10px' }}>
{element.content}
</Grid>
)
)}
{canExpand(schema, uiSchema, formData) && (
<Grid container justifyContent="flex-end">
<Grid item>
<AddButton
className="object-property-expand"
onClick={onAddClick(schema)}
disabled={disabled || readonly}
uiSchema={uiSchema}
/>
</Grid>
</Grid>
)}
</Grid>
</>
{properties.map((prop: ObjectFieldTemplatePropertyType) => prop.content)}
{canExpand<T, S, F>(schema, uiSchema, formData) && (
<AddButton
className="object-property-expand"
onClick={onAddClick(schema)}
disabled={disabled || readonly}
uiSchema={uiSchema}
registry={registry}
/>
)}
</fieldset>
);
}
export default ObjectFieldTemplate;

View File

@ -0,0 +1,9 @@
### Custom Validation Error Message for String Inputs
If you have a property in your json schema like:
"user_generated_number_1": {"type": "string", "title": "User Generated Number", "default": "0", "minLength": 3}
it will generate this error message by default: "User Generated Number must NOT have fewer than 3 characters."
If you add the `validationErrorMessage` key to the property json it will print that message instead.

View File

@ -1,16 +1,27 @@
import React from 'react';
import Box from '@mui/material/Box';
import Divider from '@mui/material/Divider';
import Typography from '@mui/material/Typography';
import { TitleFieldProps } from '@rjsf/utils';
import {
FormContextType,
TitleFieldProps,
RJSFSchema,
StrictRJSFSchema,
} from '@rjsf/utils';
function TitleField({ id, title }: TitleFieldProps) {
const REQUIRED_FIELD_SYMBOL = '*';
/** The `TitleField` is the template to use to render the title of a field
*
* @param props - The `TitleFieldProps` for this component
*/
export default function TitleField<
T = any,
S extends StrictRJSFSchema = RJSFSchema,
F extends FormContextType = any
>(props: TitleFieldProps<T, S, F>) {
const { id, title, required } = props;
return (
<Box id={id} mb={1} mt={1}>
<Typography variant="h5">{title}</Typography>
<Divider />
</Box>
<legend id={id} className="header">
{title}
{required && <span className="required">{REQUIRED_FIELD_SYMBOL}</span>}
</legend>
);
}
export default TitleField;

View File

@ -1,80 +1,73 @@
import React, { CSSProperties } from 'react';
import FormControl from '@mui/material/FormControl';
import Grid from '@mui/material/Grid';
import InputLabel from '@mui/material/InputLabel';
import Input from '@mui/material/OutlinedInput';
import {
ADDITIONAL_PROPERTY_FLAG,
FormContextType,
RJSFSchema,
StrictRJSFSchema,
WrapIfAdditionalTemplateProps,
} from '@rjsf/utils';
function WrapIfAdditionalTemplate({
children,
classNames,
disabled,
id,
label,
onDropPropertyClick,
onKeyChange,
readonly,
required,
schema,
uiSchema,
registry,
}: WrapIfAdditionalTemplateProps) {
import Label from '../FieldTemplate/Label';
/** The `WrapIfAdditional` component is used by the `FieldTemplate` to rename, or remove properties that are
* part of an `additionalProperties` part of a schema.
*
* @param props - The `WrapIfAdditionalProps` for this component
*/
export default function WrapIfAdditionalTemplate<
T = any,
S extends StrictRJSFSchema = RJSFSchema,
F extends FormContextType = any
>(props: WrapIfAdditionalTemplateProps<T, S, F>) {
const {
id,
classNames,
disabled,
label,
onKeyChange,
onDropPropertyClick,
readonly,
required,
schema,
children,
uiSchema,
registry,
} = props;
// Button templates are not overridden in the uiSchema
const { RemoveButton } = registry.templates.ButtonTemplates;
const keyLabel = `${label} Key`; // i18n ?
const additional = ADDITIONAL_PROPERTY_FLAG in schema;
const btnStyle: CSSProperties = {
flex: 1,
paddingLeft: 6,
paddingRight: 6,
fontWeight: 'bold',
};
if (!additional) {
return <div className={classNames}>{children}</div>;
}
const handleBlur = ({ target }: React.FocusEvent<HTMLInputElement>) =>
onKeyChange(target.value);
return (
<Grid
container
key={`${id}-key`}
alignItems="center"
spacing={2}
className={classNames}
>
<Grid item xs>
<FormControl fullWidth required={required}>
<InputLabel>{keyLabel}</InputLabel>
<Input
defaultValue={label}
<div className={classNames}>
<div className="row">
<div className="col-xs-5 form-additional">
<div className="form-group">
<Label label={keyLabel} required={required} id={`${id}-key`} />
<input
className="form-control"
type="text"
id={`${id}-key`}
onBlur={(event) => onKeyChange(event.target.value)}
defaultValue={label}
/>
</div>
</div>
<div className="form-additional form-group col-xs-5">{children}</div>
<div className="col-xs-2">
<RemoveButton
className="array-item-remove btn-block"
style={{ border: '0' }}
disabled={disabled || readonly}
id={`${id}-key`}
name={`${id}-key`}
onBlur={!readonly ? handleBlur : undefined}
type="text"
onClick={onDropPropertyClick(label)}
uiSchema={uiSchema}
registry={registry}
/>
</FormControl>
</Grid>
<Grid item xs>
{children}
</Grid>
<Grid item>
<RemoveButton
iconType="default"
style={btnStyle}
disabled={disabled || readonly}
onClick={onDropPropertyClick(label)}
uiSchema={uiSchema}
/>
</Grid>
</Grid>
</div>
</div>
</div>
);
}
export default WrapIfAdditionalTemplate;

View File

@ -1,3 +1,22 @@
button.react-json-schema-form-submit-button {
margin-top: 1.5em;
}
.rjsf .header {
font-weight: 400;
font-size: 20px;
line-height: 28px;
color: #161616;
}
.rjsf .field-description {
font-size: 14px;
line-height: 18px;
letter-spacing: 0.16px;
color: #525252;
margin-bottom: 1em;
}
.rjsf .input {
margin-bottom: 2em;
}