diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 37c6c2fa9..464ed5ebd 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -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: diff --git a/cypress.config.js b/cypress.config.js index 3d6c7410e..770a3b0d9 100644 --- a/cypress.config.js +++ b/cypress.config.js @@ -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) diff --git a/cypress/e2e/process_models.cy.js b/cypress/e2e/process_models.cy.js index 3a36f710e..0e9250463 100644 --- a/cypress/e2e/process_models.cy.js +++ b/cypress/e2e/process_models.cy.js @@ -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'); diff --git a/cypress/fixtures/test_bpmn_file_upload.bpmn b/cypress/fixtures/test_bpmn_file_upload.bpmn index e9160b8d9..6d355c1b8 100644 --- a/cypress/fixtures/test_bpmn_file_upload.bpmn +++ b/cypress/fixtures/test_bpmn_file_upload.bpmn @@ -1,6 +1,6 @@ - + Flow_07vd2ar diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 8369a22c3..05126412d 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -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)}` + ); +}); diff --git a/package-lock.json b/package-lock.json index 54cf2c890..c52d1a86b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 6a84cea9e..da1665264 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/components/ProcessInstanceListSaveAsReport.tsx b/src/components/ProcessInstanceListSaveAsReport.tsx index a3d50d94b..6953a20cb 100644 --- a/src/components/ProcessInstanceListSaveAsReport.tsx +++ b/src/components/ProcessInstanceListSaveAsReport.tsx @@ -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', diff --git a/src/components/ProcessInstanceListTable.tsx b/src/components/ProcessInstanceListTable.tsx index 51e8c2d6a..02a6395fd 100644 --- a/src/components/ProcessInstanceListTable.tsx +++ b/src/components/ProcessInstanceListTable.tsx @@ -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) => { diff --git a/src/components/ProcessInstanceListTabs.tsx b/src/components/ProcessInstanceListTabs.tsx new file mode 100644 index 000000000..8da9d8c1a --- /dev/null +++ b/src/components/ProcessInstanceListTabs.tsx @@ -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 ( + + + { + navigate('/admin/process-instances/for-me'); + }} + > + For Me + + + { + navigate('/admin/process-instances/all'); + }} + > + All + + + { + navigate('/admin/process-instances/find-by-id'); + }} + > + Find By Id + + + + ); +} diff --git a/src/components/ProcessModelForm.tsx b/src/components/ProcessModelForm.tsx index 7cfd4d616..999356952 100644 --- a/src/components/ProcessModelForm.tsx +++ b/src/components/ProcessModelForm.tsx @@ -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 ( + + + { + const notificationAddresses: string[] = + processModel.exception_notification_addresses || []; + notificationAddresses[index] = event.target.value; + updateProcessModel({ + exception_notification_addresses: notificationAddresses, + }); + }} + /> + + + + + + ); + textInputs.push(

Metadata Extractions

); textInputs.push( diff --git a/src/components/ProcessModelSearch.tsx b/src/components/ProcessModelSearch.tsx index bd995bc3e..b7debc6bb 100644 --- a/src/components/ProcessModelSearch.tsx +++ b/src/components/ProcessModelSearch.tsx @@ -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 ( 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(() => { diff --git a/src/components/TaskListTable.tsx b/src/components/TaskListTable.tsx index 2e53bcea6..287742e92 100644 --- a/src/components/TaskListTable.tsx +++ b/src/components/TaskListTable.tsx @@ -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(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 {shortUsernameString}; }; - const buildTable = () => { - if (!tasks) { - return null; - } - const rows = tasks.map((row: ProcessInstanceTask) => { - const taskUrl = `/tasks/${row.process_instance_id}/${row.task_id}`; - const modifiedProcessModelIdentifier = - modifyProcessIdentifierForPathParam(row.process_model_identifier); + const 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 ( - - - - {row.process_instance_id} - - - - - {row.process_model_display_name} - - - + - {row.task_title} - - {showStartedBy ? {row.process_initiator_username} : ''} - {showWaitingOn ? {getWaitingForTableCellComponent(row)} : ''} - - {convertSecondsToFormattedDateTime(row.created_at_in_seconds) || - '-'} - - - - - - + {processInstanceTask.process_instance_id} + + ); - }); - let tableHeaders = ['Id', 'Process', 'Task']; + } + if (showProcessModelIdentifier) { + rowElements.push( + + + {processInstanceTask.process_model_display_name} + + + ); + } + + rowElements.push( + + {processInstanceTask.task_title} + + ); + if (showStartedBy) { + rowElements.push( + {processInstanceTask.process_initiator_username} + ); + } + if (showWaitingOn) { + rowElements.push( + {getWaitingForTableCellComponent(processInstanceTask)} + ); + } + if (showDateStarted) { + rowElements.push( + + {convertSecondsToFormattedDateTime( + processInstanceTask.created_at_in_seconds + ) || '-'} + + ); + } + if (showLastUpdated) { + rowElements.push( + + ); + } + rowElements.push( + + + + ); + return {rowElements}; + }; + + 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 ( @@ -207,24 +268,41 @@ export default function TaskListTable({ undefined, paginationQueryParamPrefix ); - return ( - + let tableElement = ( +
{buildTable()}
); + if (shouldPaginateTable) { + tableElement = ( + + ); + } + return tableElement; }; - if (tasks) { + const tableAndDescriptionElement = () => { + if (showTableDescriptionAsTooltip) { + return

{tableTitle}

; + } return ( <>

{tableTitle}

{tableDescription}

+ + ); + }; + + if (tasks && (tasks.length > 0 || hideIfNoTasks === false)) { + return ( + <> + {tableAndDescriptionElement()} {tasksComponent()} ); diff --git a/src/config.tsx b/src/config.tsx index abaadd5ef..36e0ed4e0 100644 --- a/src/config.tsx +++ b/src/config.tsx @@ -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; } diff --git a/src/helpers.tsx b/src/helpers.tsx index b3edd7766..8b91367dd 100644 --- a/src/helpers.tsx +++ b/src/helpers.tsx @@ -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; diff --git a/src/hooks/UsePrompt.tsx b/src/hooks/UsePrompt.tsx new file mode 100644 index 000000000..63b873616 --- /dev/null +++ b/src/hooks/UsePrompt.tsx @@ -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); +} diff --git a/src/index.css b/src/index.css index 08e8341cf..614bbf739 100644 --- a/src/index.css +++ b/src/index.css @@ -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 { diff --git a/src/index.tsx b/src/index.tsx index 7a602be60..42eddc027 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -20,7 +20,7 @@ const doRender = () => { ); }; -UserService.getAuthTokenFromParams(); +UserService.loginIfNeeded(); doRender(); // If you want to start measuring performance in your app, pass a function diff --git a/src/interfaces.ts b/src/interfaces.ts index 97ef763c1..f6d3020a2 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -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 { diff --git a/src/routes/AuthenticationList.tsx b/src/routes/AuthenticationList.tsx index a0d151018..3fdb748be 100644 --- a/src/routes/AuthenticationList.tsx +++ b/src/routes/AuthenticationList.tsx @@ -42,7 +42,7 @@ export default function AuthenticationList() { row.id }?redirect_url=${redirectUrl}/${ row.id - }?token=${UserService.getAuthToken()}`} + }?token=${UserService.getAccessToken()}`} > {row.id} diff --git a/src/routes/JsonSchemaFormBuilder.tsx b/src/routes/JsonSchemaFormBuilder.tsx index d4a9c2b44..b180ed2a0 100644 --- a/src/routes/JsonSchemaFormBuilder.tsx +++ b/src/routes/JsonSchemaFormBuilder.tsx @@ -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)); } diff --git a/src/routes/ProcessInstanceFindById.tsx b/src/routes/ProcessInstanceFindById.tsx index e55520ef6..0c0c1974e 100644 --- a/src/routes/ProcessInstanceFindById.tsx +++ b/src/routes/ProcessInstanceFindById.tsx @@ -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 ( -
- - {formElements()} - {formButtons()} - - + <> + +
+
+ + {formElements()} + {formButtons()} + + + ); } diff --git a/src/routes/ProcessInstanceList.tsx b/src/routes/ProcessInstanceList.tsx index 33af0c1fb..ca69eb7d3 100644 --- a/src/routes/ProcessInstanceList.tsx +++ b/src/routes/ProcessInstanceList.tsx @@ -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

My Process Instances

; }; - let selectedTabIndex = 0; - if (variant === 'all') { - selectedTabIndex = 1; - } return ( <> - - - { - navigate('/admin/process-instances/for-me'); - }} - > - For Me - - - { - navigate('/admin/process-instances/all'); - }} - > - All - - - { - navigate('/admin/process-instances/find-by-id'); - }} - > - Find By Id - - - +
{processInstanceBreadcrumbElement()} {processInstanceTitleElement()} diff --git a/src/routes/ProcessInstanceLogList.tsx b/src/routes/ProcessInstanceLogList.tsx index b4a4f683a..acfb4020c 100644 --- a/src/routes/ProcessInstanceLogList.tsx +++ b/src/routes/ProcessInstanceLogList.tsx @@ -46,15 +46,21 @@ export default function ProcessInstanceLogList() { return (
- - + {isDetailedView && ( <> + - )} + - {isDetailedView && ( <> + - )} + diff --git a/src/routes/ProcessInstanceShow.tsx b/src/routes/ProcessInstanceShow.tsx index dc0e761f9..279322a63 100644 --- a/src/routes/ProcessInstanceShow.tsx +++ b/src/routes/ProcessInstanceShow.tsx @@ -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('{}'); const [eventTextEditorEnabled, setEventTextEditorEnabled] = useState(false); + const [displayDetails, setDisplayDetails] = useState(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 ( + <> + + + + + + Updated At:{' '} + + + {convertSecondsToFormattedDateTime( + processInstance.updated_at_in_seconds + )} + + + + + Created At:{' '} + + + {convertSecondsToFormattedDateTime( + processInstance.created_at_in_seconds + )} + + + + + Process model revision:{' '} + + + {processInstance.bpmn_version_control_identifier} ( + {processInstance.bpmn_version_control_type}) + + + + ); + } + return ( + + + + ); + }; + const getInfoTag = () => { if (!processInstance) { return null; @@ -317,6 +384,14 @@ export default function ProcessInstanceShow({ variant }: OwnProps) { return ( <> + + + Started By:{' '} + + + {processInstance.process_initiator_username} + + Started:{' '} @@ -338,6 +413,7 @@ export default function ProcessInstanceShow({ variant }: OwnProps) { + {detailedViewElement()}
@@ -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 ( - {taskToUse.name} ({taskToUse.type}): {taskToUse.state} + {taskToUse.name} ( + {taskToUse.type} + ): {taskToUse.state} {taskDisplayButtons(taskToUse)} {selectingEvent @@ -915,6 +1010,26 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {

+ + + + + {getInfoTag()}
{taskUpdateDisplayArea()} diff --git a/src/routes/ProcessModelEditDiagram.tsx b/src/routes/ProcessModelEditDiagram.tsx index cc3ac8789..af38d8bc4 100644 --- a/src/routes/ProcessModelEditDiagram.tsx +++ b/src/routes/ProcessModelEditDiagram.tsx @@ -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(null); + const [diagramHasChanges, setDiagramHasChanges] = useState(false); const [scriptText, setScriptText] = useState(''); const [scriptType, setScriptType] = useState(''); @@ -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} /> ); }; diff --git a/src/routes/ProcessModelShow.tsx b/src/routes/ProcessModelShow.tsx index a46d02a25..cdbe38a96 100644 --- a/src/routes/ProcessModelShow.tsx +++ b/src/routes/ProcessModelShow.tsx @@ -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} /> - true + ); diff --git a/src/routes/ReactFormEditor.tsx b/src/routes/ReactFormEditor.tsx index 5d1f55279..58b2819b1 100644 --- a/src/routes/ReactFormEditor.tsx +++ b/src/routes/ReactFormEditor.tsx @@ -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(false); + const [processModelFile, setProcessModelFile] = useState( 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 ( + setDisplaySaveFileMessage(false)} + > + Changes to the file were saved. + + ); + } + return null; + }; + if (processModelFile || !params.file_name) { const processModelFileName = processModelFile ? processModelFile.name : ''; return ( @@ -182,6 +208,7 @@ export default function ReactFormEditor() { {processModelFileName} {newFileNameBox()} + {saveFileMessage()} diff --git a/src/routes/TaskShow.tsx b/src/routes/TaskShow.tsx index b6068568c..79bc52635 100644 --- a/src/routes/TaskShow.tsx +++ b/src/routes/TaskShow.tsx @@ -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 ( @@ -200,6 +225,7 @@ export default function TaskShow() { schema={jsonSchema} uiSchema={formUiSchema} validator={validator} + customValidate={customValidate} > {reactFragmentToHideSubmitButton} diff --git a/src/services/HttpService.ts b/src/services/HttpService.ts index 78a29d07e..b60802487 100644 --- a/src/services/HttpService.ts +++ b/src/services/HttpService.ts @@ -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/, ''); diff --git a/src/services/UserService.ts b/src/services/UserService.ts index 53717b8d5..23266c3e1 100644 --- a/src/services/UserService.ts +++ b/src/services/UserService.ts @@ -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, diff --git a/src/themes/carbon/ArrayFieldItemTemplate/ArrayFieldItemTemplate.tsx b/src/themes/carbon/ArrayFieldItemTemplate/ArrayFieldItemTemplate.tsx index b09acfb9d..cf596f982 100644 --- a/src/themes/carbon/ArrayFieldItemTemplate/ArrayFieldItemTemplate.tsx +++ b/src/themes/carbon/ArrayFieldItemTemplate/ArrayFieldItemTemplate.tsx @@ -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) { 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 ( - - - - - {children} - - - +
+
{children}
{hasToolbar && ( - - {(hasMoveUp || hasMoveDown) && ( - - )} - {(hasMoveUp || hasMoveDown) && ( - - )} - {hasRemove && ( - - )} - +
+
+ {(hasMoveUp || hasMoveDown) && ( + + )} + {(hasMoveUp || hasMoveDown) && ( + + )} + {hasRemove && ( + + )} +
+
)} - +
); } - -export default ArrayFieldItemTemplate; diff --git a/src/themes/carbon/ArrayFieldTemplate/ArrayFieldTemplate.tsx b/src/themes/carbon/ArrayFieldTemplate/ArrayFieldTemplate.tsx index 03c88951e..0c588813b 100644 --- a/src/themes/carbon/ArrayFieldTemplate/ArrayFieldTemplate.tsx +++ b/src/themes/carbon/ArrayFieldTemplate/ArrayFieldTemplate.tsx @@ -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) { 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(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 ( - - - - - - {items && - items.map(({ key, ...itemProps }: ArrayFieldTemplateItemType) => ( +
+ + +
+ {items && + items.map( + ({ key, ...itemProps }: ArrayFieldTemplateItemType) => ( - ))} - {canAdd && ( - - - - - - - + ) )} - - - +
+ {canAdd && ( + + )} +
); } - -export default ArrayFieldTemplate; diff --git a/src/themes/carbon/BaseInputTemplate/BaseInputTemplate.tsx b/src/themes/carbon/BaseInputTemplate/BaseInputTemplate.tsx index 9e71b921b..7954a0ace 100644 --- a/src/themes/carbon/BaseInputTemplate/BaseInputTemplate.tsx +++ b/src/themes/carbon/BaseInputTemplate/BaseInputTemplate.tsx @@ -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< (props: DescriptionFieldProps) { + const { id, description } = props; + if (!description) { + return null; + } + if (typeof description === 'string') { return ( - +

{description} - +

); } - - return null; + return ( +
+ {description} +
+ ); } - -export default DescriptionField; diff --git a/src/themes/carbon/FieldTemplate/Label.tsx b/src/themes/carbon/FieldTemplate/Label.tsx new file mode 100644 index 000000000..4a936e972 --- /dev/null +++ b/src/themes/carbon/FieldTemplate/Label.tsx @@ -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 ( + + ); +} diff --git a/src/themes/carbon/ObjectFieldTemplate/ObjectFieldTemplate.tsx b/src/themes/carbon/ObjectFieldTemplate/ObjectFieldTemplate.tsx index 7d9557e81..e7564f1a3 100644 --- a/src/themes/carbon/ObjectFieldTemplate/ObjectFieldTemplate.tsx +++ b/src/themes/carbon/ObjectFieldTemplate/ObjectFieldTemplate.tsx @@ -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) { + const { + description, + disabled, + formData, + idSchema, + onAddClick, + properties, + readonly, + registry, + required, + schema, + title, + uiSchema, + } = props; + const options = getUiOptions(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) && ( +
+ {(options.title || title) && ( )} - {(uiOptions.description || description) && ( + {(options.description || description) && ( )} - - {properties.map((element, index) => - // Remove the if the inner element is hidden as the - // itself would otherwise still take up space. - element.hidden ? ( - element.content - ) : ( - - {element.content} - - ) - )} - {canExpand(schema, uiSchema, formData) && ( - - - - - - )} - - + {properties.map((prop: ObjectFieldTemplatePropertyType) => prop.content)} + {canExpand(schema, uiSchema, formData) && ( + + )} +
); } - -export default ObjectFieldTemplate; diff --git a/src/themes/carbon/README.md b/src/themes/carbon/README.md new file mode 100644 index 000000000..de9169d25 --- /dev/null +++ b/src/themes/carbon/README.md @@ -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. diff --git a/src/themes/carbon/TitleField/TitleField.tsx b/src/themes/carbon/TitleField/TitleField.tsx index 8f0d3ae0b..f5254a672 100644 --- a/src/themes/carbon/TitleField/TitleField.tsx +++ b/src/themes/carbon/TitleField/TitleField.tsx @@ -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) { + const { id, title, required } = props; return ( - - {title} - - + + {title} + {required && {REQUIRED_FIELD_SYMBOL}} + ); } - -export default TitleField; diff --git a/src/themes/carbon/WrapIfAdditionalTemplate/WrapIfAdditionalTemplate.tsx b/src/themes/carbon/WrapIfAdditionalTemplate/WrapIfAdditionalTemplate.tsx index 954d82c45..ef15f9dcc 100644 --- a/src/themes/carbon/WrapIfAdditionalTemplate/WrapIfAdditionalTemplate.tsx +++ b/src/themes/carbon/WrapIfAdditionalTemplate/WrapIfAdditionalTemplate.tsx @@ -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) { + 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
{children}
; } - const handleBlur = ({ target }: React.FocusEvent) => - onKeyChange(target.value); - return ( - - - - {keyLabel} - +
+
+
+
+
+
{children}
+
+ - - - - {children} - - - - - +
+
+ ); } - -export default WrapIfAdditionalTemplate; diff --git a/src/themes/carbon/index.css b/src/themes/carbon/index.css index d44ae7e5d..329bc33fd 100644 --- a/src/themes/carbon/index.css +++ b/src/themes/carbon/index.css @@ -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; +}
{rowToUse.id}{rowToUse.message}{rowToUse.bpmn_task_name} + {rowToUse.bpmn_task_name || + (rowToUse.bpmn_task_type === 'Default Start Event' + ? 'Process Started' + : '') || + (rowToUse.bpmn_task_type === 'End Event' ? 'Process Ended' : '')} + {rowToUse.message} {rowToUse.bpmn_task_identifier} {rowToUse.bpmn_task_type}{rowToUse.bpmn_process_identifier}{rowToUse.bpmn_process_identifier} {rowToUse.username}
IdMessage Task NameMessage Task Identifier Task TypeBpmn Process IdentifierBpmn Process Identifier User Timestamp