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:
parent
32775973ca
commit
f612f26dd2
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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(/\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)}`
|
||||
);
|
||||
}
|
||||
);
|
||||
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)}`
|
||||
);
|
||||
});
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(() => {
|
||||
|
|
|
@ -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()}
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -20,7 +20,7 @@ const doRender = () => {
|
|||
);
|
||||
};
|
||||
|
||||
UserService.getAuthTokenFromParams();
|
||||
UserService.loginIfNeeded();
|
||||
doRender();
|
||||
|
||||
// If you want to start measuring performance in your app, pass a function
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -42,7 +42,7 @@ export default function AuthenticationList() {
|
|||
row.id
|
||||
}?redirect_url=${redirectUrl}/${
|
||||
row.id
|
||||
}?token=${UserService.getAuthToken()}`}
|
||||
}?token=${UserService.getAccessToken()}`}
|
||||
>
|
||||
{row.id}
|
||||
</a>
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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()}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
>
|
||||
« 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 »
|
||||
</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()}
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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/, '');
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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.
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue