Squashed 'spiffworkflow-frontend/' changes from f099c181..c22d22ba
c22d22ba logs list page should respect the for-me variant dcc3e3b1 show help text for textareas as well w/ burnettk 35b8bf35 default to the for-me path on process instance show page links da646e54 added support to validate custom errors in nested properties in json schema forms 17412e9d make it so the message at the top of form is never strictly wrong 7dea59b7 adding some padding to form text inputs w/ burnettk 3f92f492 added ability to display the environment in the frontend header bar w/ burnettk 85246120 put the env vars in the env section of the github action configs w/ burnettk 58ff9075 added test for quickstart guide w/ burnettk 03e725b0 corrected cypress env var db755731 added some support for using the backend openid server for cypress tests w/ burnettk e76b6429 anything in the Tasks waiting for me table can now be completed by the current user 56403b4c users can always complete tasks on process instance show page and on task group table on home page w/ burnettk e2ab6eff avoid endless redirects on error on authentication list page w/ burnettk 66608837 bug fix 4781d5fc link to the spiff step from a task on the frontend and use the correct db in ci 69eb426c pyl w/ burnettk 3cf036b0 call activities are also working w/ burnettk 40b101a7 add assertions so this fails fast if anything changes in the future 74d2d3fa Update README.md 24c3c431 fix formatting ed977079 document runtime config e62427b9 do not fail if SPIFFWORKFLOW_FRONTEND_RUNTIME_CONFIG is missing 551bdf17 allow setting configs for the frontend through env vars w/ burnettk bc91c62c Merge remote-tracking branch 'origin/main' into frontend/use-api-subpath eeab0d0f Use the same markdown library for displaying as for editing - could enable a security plugin, but doing so would prevent BPMN developers from using the Sub and Sup markdown supported by GitHub. 28d6f75c When searching for human tasks to determine if the current user can complete it, filter on the "completed" flag. 34b36fe2 put setDisabled back in the awkward place since i was seeing the subsequent form stay disabled 4794a8cd Merge branch 'main' of github.com:sartography/spiff-arena into main 78b9ef73 Fix that dreadful unknown "KeyError" exception that was cropping up. Adding a bit of detail to the spiffworkflow exceptions when a duplicate process model is found. Disable the submit button on tasks after you click submit (avoid the double click and give users a better experience) d854a122 Merge pull request #125 from sartography/feature/dynamically-hide-fields-w-task-data eda73854 make form schema and form ui schema both dicts, add support for hiding fields based on task data bda549f3 Prevent double click on submit of forms. 3f02fa80 try to improve exception handling by avoiding raising ApiError from services 06835267 remove duplicate label on radio buttons 5d1e3517 less annoying file name for autocomplete c569acde frontend: use /api subpath instead of subdomain d3dda548 add deps for serve e6173efe Revert "revert Dockerfile until we get it working" 74103b97 revert Dockerfile until we get it working 8e9badd3 get bin as well for script df7a97e6 Merge pull request #116 from sartography/frontend/improve-dockerfile 7a17bc6f remove unneeded divs 2cf58c24 IBM says you can't have more columns than your parents, even if you try to start another grid, with kburnett abc85ee3 make task show wide, and make repeating form icons match site styles. w/ dfunk 6400827e even textareas need to have blank labels since labels are in FieldTemplate 462852d6 replace fieldTemplate with unthemed core version and remove labels since that is handled in there a50dd004 wrap field template so we can style with margin bottom e6a1ca97 new mechanism to handle help more in line with how carbon works cb6ce6b4 frontend: avoid redundant steps in Dockerfile b6b7ceeb we were expecting an object when doing this check, so codify it cfcfe292 use the 403 response to tell if a user has access to task data on the task show page w/ burnettk 355406e2 do not reset error state from the adminroutes component since this causes the error to be removed right away and then cannot be displayed w/ burnettk 3e7962cf if we get a result back it will be a task with a model identifier 0f043f72 use the ProcessInstanceTask interface where we can and move some stuff around better for useEffect 4ee4e5e0 run_pyl had various recommendations that I find a bit of a pain in the butt, but that I did anyway. 84e7d3fa Merge remote-tracking branch 'origin/main' into feature/jinja_errors acbc0a07 Added useMemo to error context No longer clear errors in the task bar, as that will constantly remove them as soon as they are shown. 19c3ab0e TaskShow had a useEffect that depended on params, that dependency caused an infinite request cycle when an error occured. The same issue was happening on the ProcessInstanceListTable, and there it was being managed by a "SafelySetErrorMessage" function in one case, but would not be addressed in all possible cases. c37224ec added the process model identifier for the diagram if it is not the top level w/ burnettk befefb6d Merge pull request #107 from sartography/feature/metadata_on_instance_show 65e42110 use a modal for metadata instead w/ burnettk 4a2f2a00 put process instance show page to match main w/ burnettk 0f1ab490 do not allow deleting primary bpmn file and do not allow instantiating models without a primary bpmn file w/ burnettk 3718f1a3 show metadata on instance show page but for some reason it reorders elements w/ burnettk 400a1469 add process model file name validation for new files w/ burnettk 132c5814 added locking system for process instances so hopefully background jobs will not take instances currently being run by the user w/ burnettk 3fa86949 make sure that all new form field elements are not dropdowns f69d3416 expanded functionality of the form builder 2453938f use the correct place for keycloak w/ burnettk 3d85a5f6 Merges 50d0a8e3 Lots of adjustments from running pyl Main change is in the ErrorDisplay.tsx to assure all error information is provided. and index.css to make it "pretty" git-subtree-dir: spiffworkflow-frontend git-subtree-split: c22d22ba5d8a96dca66a405d9c230284c81b4740
This commit is contained in:
parent
f612f26dd2
commit
ce1e54dced
|
@ -0,0 +1 @@
|
|||
/node_modules
|
|
@ -68,7 +68,7 @@ jobs:
|
|||
path: sample-process-models
|
||||
- name: start_keycloak
|
||||
working-directory: ./spiffworkflow-backend
|
||||
run: ./bin/start_keycloak
|
||||
run: ./keycloak/bin/start_keycloak
|
||||
- name: start_backend
|
||||
working-directory: ./spiffworkflow-backend
|
||||
run: ./bin/build_and_run_with_docker_compose
|
||||
|
|
31
Dockerfile
31
Dockerfile
|
@ -1,5 +1,5 @@
|
|||
### STAGE 1: Build ###
|
||||
FROM quay.io/sartography/node:latest
|
||||
# Base image to share ENV vars that activate VENV.
|
||||
FROM quay.io/sartography/node:latest AS base
|
||||
|
||||
RUN mkdir /app
|
||||
WORKDIR /app
|
||||
|
@ -7,8 +7,16 @@ WORKDIR /app
|
|||
# this matches total memory on spiffworkflow-demo
|
||||
ENV NODE_OPTIONS=--max_old_space_size=2048
|
||||
|
||||
ADD package.json /app/
|
||||
ADD package-lock.json /app/
|
||||
# Setup image for installing JS dependencies.
|
||||
FROM base AS setup
|
||||
|
||||
COPY . /app/
|
||||
|
||||
RUN cp /app/package.json /app/package.json.bak
|
||||
ADD justservewebserver.package.json /app/package.json
|
||||
RUN npm ci --ignore-scripts
|
||||
RUN cp -r /app/node_modules /app/node_modules.justserve
|
||||
RUN cp /app/package.json.bak /app/package.json
|
||||
|
||||
# npm ci because it respects the lock file.
|
||||
# --ignore-scripts because authors can do bad things in postinstall scripts.
|
||||
|
@ -16,8 +24,19 @@ ADD package-lock.json /app/
|
|||
# npx can-i-ignore-scripts can check that it's safe to ignore scripts.
|
||||
RUN npm ci --ignore-scripts
|
||||
|
||||
COPY . /app/
|
||||
|
||||
RUN npm run build
|
||||
|
||||
# Final image without setup dependencies.
|
||||
FROM base AS final
|
||||
|
||||
LABEL source="https://github.com/sartography/spiff-arena"
|
||||
LABEL description="Software development platform for building, running, and monitoring executable diagrams"
|
||||
|
||||
# WARNING: On localhost frontend assumes backend is one port lowe.
|
||||
ENV PORT0=7001
|
||||
|
||||
COPY --from=setup /app/build /app/build
|
||||
COPY --from=setup /app/bin /app/bin
|
||||
COPY --from=setup /app/node_modules.justserve /app/node_modules
|
||||
|
||||
ENTRYPOINT ["/app/bin/boot_server_in_docker"]
|
||||
|
|
11
README.md
11
README.md
|
@ -63,6 +63,17 @@ Fix lint in code.
|
|||
|
||||
Probably just stick with lint:fix which also runs prettier.
|
||||
|
||||
## Runtime configuration options
|
||||
|
||||
The frontend docker image respects the following environment variables.
|
||||
|
||||
SPIFFWORKFLOW_FRONTEND_RUNTIME_CONFIG_APP_ROUTING_STRATEGY=subdomain_based
|
||||
SPIFFWORKFLOW_FRONTEND_RUNTIME_CONFIG_APP_ROUTING_STRATEGY=path_based
|
||||
|
||||
subdomain_based example: api.spiffworkflow.org goes to backend
|
||||
|
||||
path_based example: spiffworkflow.org/api goes to backend
|
||||
|
||||
## Learn More
|
||||
|
||||
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
|
||||
|
|
|
@ -7,4 +7,41 @@ function error_handler() {
|
|||
trap 'error_handler ${LINENO} $?' ERR
|
||||
set -o errtrace -o errexit -o nounset -o pipefail
|
||||
|
||||
# sort of like https://lithic.tech/blog/2020-05/react-dynamic-config, but without golang
|
||||
react_configs=$(env | grep -E "^SPIFFWORKFLOW_FRONTEND_RUNTIME_CONFIG_" || echo '')
|
||||
if [[ -n "$react_configs" ]]; then
|
||||
index_html_file="./build/index.html"
|
||||
if [[ ! -f "$index_html_file" ]]; then
|
||||
>&2 echo "ERROR: Could not find '${index_html_file}'. Cannot use SPIFFWORKFLOW_FRONTEND_RUNTIME_CONFIG values without it."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v sed >/dev/null ; then
|
||||
>&2 echo "ERROR: sed command not found. Cannot use SPIFFWORKFLOW_FRONTEND_RUNTIME_CONFIG values without it."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v perl >/dev/null ; then
|
||||
>&2 echo "ERROR: perl command not found. Cannot use SPIFFWORKFLOW_FRONTEND_RUNTIME_CONFIG values without it."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
for react_config in $react_configs; do
|
||||
react_config_without_prefix=$(sed -E 's/^SPIFFWORKFLOW_FRONTEND_RUNTIME_CONFIG_([^=]*)=(.*)/\1=\\"\2\\"/' <<<"${react_config}")
|
||||
|
||||
if [[ -z "$react_config_without_prefix" ]]; then
|
||||
>&2 echo "ERROR: Could not parse react config line: '${react_config}'."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# actually do the search and replace to add the js config to the html page
|
||||
perl -pi -e "s/(window.spiffworkflowFrontendJsenv=\{\})/\1;window.spiffworkflowFrontendJsenv.${react_config_without_prefix}/" "$index_html_file"
|
||||
|
||||
if ! grep -Eq "${react_config_without_prefix}" "$index_html_file"; then
|
||||
>&2 echo "ERROR: Could not find '${react_config_without_prefix}' in '${index_html_file}' after search and replace. It is likely that the assumptions in boot_server_in_docker about the contents of the html page have changed. Fix the glitch in boot_server_in_docker."
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
exec ./node_modules/.bin/serve -s build -l "$PORT0"
|
||||
|
|
|
@ -7,14 +7,12 @@ function error_handler() {
|
|||
trap 'error_handler ${LINENO} $?' ERR
|
||||
set -o errtrace -o errexit -o nounset -o pipefail
|
||||
|
||||
max_attempts="${1:-}"
|
||||
if [[ -z "$max_attempts" ]]; then
|
||||
max_attempts=100
|
||||
fi
|
||||
max_attempts="${1:-100}"
|
||||
port="${2:-7001}"
|
||||
|
||||
echo "waiting for backend to come up..."
|
||||
echo "waiting for frontend to come up..."
|
||||
attempts=0
|
||||
while [[ "$(curl -s -o /dev/null -w '%{http_code}' http://localhost:7001)" != "200" ]]; do
|
||||
while [[ "$(curl -s -o /dev/null -w '%{http_code}' "http://localhost:${port}")" != "200" ]]; do
|
||||
if [[ "$attempts" -gt "$max_attempts" ]]; then
|
||||
>&2 echo "ERROR: Server not up after $max_attempts attempts. There is probably a problem"
|
||||
exit 1
|
||||
|
@ -22,3 +20,4 @@ while [[ "$(curl -s -o /dev/null -w '%{http_code}' http://localhost:7001)" != "2
|
|||
attempts=$(( attempts + 1 ))
|
||||
sleep 1
|
||||
done
|
||||
echo "frontend up"
|
||||
|
|
|
@ -30,7 +30,7 @@ const cypressConfig = {
|
|||
videoUploadOnPasses: false,
|
||||
chromeWebSecurity: false,
|
||||
e2e: {
|
||||
baseUrl: 'http://localhost:7001',
|
||||
baseUrl: `http://localhost:${process.env.SPIFFWORKFLOW_FRONTEND_PORT || 7001}`,
|
||||
setupNodeEvents(on, config) {
|
||||
deleteVideosOnSuccess(on)
|
||||
require('@cypress/grep/src/plugin')(config);
|
||||
|
|
|
@ -33,7 +33,7 @@ describe('process-groups', () => {
|
|||
cy.contains(newGroupDisplayName).should('not.exist');
|
||||
|
||||
// meaning the process group list page is loaded, so we can sign out safely without worrying about ajax requests failing
|
||||
cy.get('.tile-process-group-content-container').should('exist');
|
||||
cy.getBySel('process-groups-loaded').should('exist');
|
||||
});
|
||||
|
||||
// process groups no longer has pagination post-tiles
|
||||
|
|
|
@ -43,17 +43,27 @@ Cypress.Commands.add('navigateToAdmin', () => {
|
|||
|
||||
Cypress.Commands.add('login', (selector, ...args) => {
|
||||
cy.visit('/admin');
|
||||
cy.get('#username').type('ciadmin1');
|
||||
cy.get('#password').type('ciadmin1');
|
||||
cy.get('#kc-login').click();
|
||||
const username = Cypress.env('SPIFFWORKFLOW_FRONTEND_USERNAME') || 'ciadmin1';
|
||||
const password = Cypress.env('SPIFFWORKFLOW_FRONTEND_PASSWORD') || 'ciadmin1';
|
||||
cy.get('#username').type(username);
|
||||
cy.get('#password').type(password);
|
||||
if (Cypress.env('SPIFFWORKFLOW_FRONTEND_AUTH_WITH_KEYCLOAK') === true) {
|
||||
cy.get('#kc-login').click();
|
||||
} else {
|
||||
cy.get('#spiff-login-button').click();
|
||||
}
|
||||
});
|
||||
|
||||
Cypress.Commands.add('logout', (selector, ...args) => {
|
||||
cy.getBySel('logout-button').click();
|
||||
|
||||
// otherwise we can click logout, quickly load the next page, and the javascript
|
||||
// doesn't have time to actually sign you out
|
||||
cy.contains('Sign in to your account');
|
||||
if (Cypress.env('SPIFFWORKFLOW_FRONTEND_AUTH_WITH_KEYCLOAK') === true) {
|
||||
// otherwise we can click logout, quickly load the next page, and the javascript
|
||||
// doesn't have time to actually sign you out
|
||||
cy.contains('Sign in to your account');
|
||||
} else {
|
||||
cy.get('#spiff-login-button').should('exist');
|
||||
}
|
||||
});
|
||||
|
||||
Cypress.Commands.add('createGroup', (groupId, groupDisplayName) => {
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
{
|
||||
"name": "spiffworkflow-frontend",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"serve": "^14.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "ESLINT_NO_DEV_ERRORS=true PORT=7001 craco start",
|
||||
"build": "craco build",
|
||||
"test": "react-scripts test --coverage",
|
||||
"t": "npm test -- --watchAll=false",
|
||||
"eject": "craco eject",
|
||||
"format": "prettier --write src/**/*.[tj]s{,x}",
|
||||
"lint": "./node_modules/.bin/eslint src",
|
||||
"lint:fix": "./node_modules/.bin/eslint --fix src"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -27,7 +27,7 @@
|
|||
"@types/node": "^18.6.5",
|
||||
"@types/react": "^18.0.17",
|
||||
"@types/react-dom": "^18.0.6",
|
||||
"@uiw/react-md-editor": "^3.19.5",
|
||||
"@uiw/react-md-editor": "^3.20.2",
|
||||
"autoprefixer": "10.4.8",
|
||||
"axios": "^0.27.2",
|
||||
"bootstrap": "^5.2.0",
|
||||
|
@ -49,14 +49,13 @@
|
|||
"react-bootstrap": "^2.5.0",
|
||||
"react-bootstrap-typeahead": "^6.0.0",
|
||||
"react-datepicker": "^4.8.0",
|
||||
"react-devtools": "^4.27.1",
|
||||
"react-dom": "^18.2.0",
|
||||
"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",
|
||||
"serve": "^14.0.0",
|
||||
"timepicker": "^1.13.18",
|
||||
"typescript": "^4.7.4",
|
||||
|
|
|
@ -25,6 +25,10 @@
|
|||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>SpiffWorkflow</title>
|
||||
<script>
|
||||
window.spiffworkflowFrontendJsenv = {};
|
||||
</script>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
|
|
15
src/App.tsx
15
src/App.tsx
|
@ -1,29 +1,20 @@
|
|||
import { useMemo, useState } from 'react';
|
||||
// @ts-ignore
|
||||
import { Content } from '@carbon/react';
|
||||
|
||||
import { BrowserRouter, Routes, Route } from 'react-router-dom';
|
||||
import { defineAbility } from '@casl/ability';
|
||||
import ErrorContext from './contexts/ErrorContext';
|
||||
import NavigationBar from './components/NavigationBar';
|
||||
|
||||
import HomePageRoutes from './routes/HomePageRoutes';
|
||||
import ErrorBoundary from './components/ErrorBoundary';
|
||||
import AdminRoutes from './routes/AdminRoutes';
|
||||
import { ErrorForDisplay } from './interfaces';
|
||||
|
||||
import { AbilityContext } from './contexts/Can';
|
||||
import UserService from './services/UserService';
|
||||
import ErrorDisplay from './components/ErrorDisplay';
|
||||
import APIErrorProvider from './contexts/APIErrorContext';
|
||||
|
||||
export default function App() {
|
||||
const [errorObject, setErrorObject] = useState<ErrorForDisplay | null>(null);
|
||||
|
||||
const errorContextValueArray = useMemo(
|
||||
() => [errorObject, setErrorObject],
|
||||
[errorObject]
|
||||
);
|
||||
|
||||
if (!UserService.isLoggedIn()) {
|
||||
UserService.doLogin();
|
||||
return null;
|
||||
|
@ -35,7 +26,7 @@ export default function App() {
|
|||
<div className="cds--white">
|
||||
{/* @ts-ignore */}
|
||||
<AbilityContext.Provider value={ability}>
|
||||
<ErrorContext.Provider value={errorContextValueArray}>
|
||||
<APIErrorProvider>
|
||||
<BrowserRouter>
|
||||
<NavigationBar />
|
||||
<Content>
|
||||
|
@ -49,7 +40,7 @@ export default function App() {
|
|||
</ErrorBoundary>
|
||||
</Content>
|
||||
</BrowserRouter>
|
||||
</ErrorContext.Provider>
|
||||
</APIErrorProvider>
|
||||
</AbilityContext.Provider>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -1,10 +1,26 @@
|
|||
import { useContext } from 'react';
|
||||
import ErrorContext from '../contexts/ErrorContext';
|
||||
import { Notification } from './Notification';
|
||||
import useAPIError from '../hooks/UseApiError';
|
||||
|
||||
function errorDetailDisplay(
|
||||
errorObject: any,
|
||||
propertyName: string,
|
||||
title: string
|
||||
) {
|
||||
// Creates a bit of html for displaying a single error property if it exists.
|
||||
if (propertyName in errorObject && errorObject[propertyName]) {
|
||||
return (
|
||||
<div className="error_info">
|
||||
<span className="error_title">{title}:</span>
|
||||
{errorObject[propertyName]}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export default function ErrorDisplay() {
|
||||
const [errorObject, setErrorObject] = (useContext as any)(ErrorContext);
|
||||
|
||||
const errorObject = useAPIError().error;
|
||||
const { removeError } = useAPIError();
|
||||
let errorTag = null;
|
||||
if (errorObject) {
|
||||
let sentryLinkTag = null;
|
||||
|
@ -21,32 +37,38 @@ export default function ErrorDisplay() {
|
|||
);
|
||||
}
|
||||
|
||||
let message = <div>{errorObject.message}</div>;
|
||||
let title = 'Error:';
|
||||
if ('task_name' in errorObject && errorObject.task_name) {
|
||||
title = 'Error in python script:';
|
||||
message = (
|
||||
<>
|
||||
<br />
|
||||
<div>
|
||||
Task: {errorObject.task_name} ({errorObject.task_id})
|
||||
</div>
|
||||
<div>File name: {errorObject.file_name}</div>
|
||||
<div>Line number in script task: {errorObject.line_number}</div>
|
||||
<br />
|
||||
<div>{errorObject.message}</div>
|
||||
</>
|
||||
const message = <div>{errorObject.message}</div>;
|
||||
const title = 'Error:';
|
||||
const taskName = errorDetailDisplay(errorObject, 'task_name', 'Task Name');
|
||||
const taskId = errorDetailDisplay(errorObject, 'task_id', 'Task ID');
|
||||
const fileName = errorDetailDisplay(errorObject, 'file_name', 'File Name');
|
||||
const lineNumber = errorDetailDisplay(
|
||||
errorObject,
|
||||
'line_number',
|
||||
'Line Number'
|
||||
);
|
||||
const errorLine = errorDetailDisplay(errorObject, 'error_line', 'Context');
|
||||
let taskTrace = null;
|
||||
if (errorObject.task_trace && errorObject.task_trace.length > 1) {
|
||||
taskTrace = (
|
||||
<div className="error_info">
|
||||
<span className="error_title">Call Activity Trace:</span>
|
||||
{errorObject.task_trace.reverse().join(' -> ')}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
errorTag = (
|
||||
<Notification
|
||||
title={title}
|
||||
onClose={() => setErrorObject(null)}
|
||||
type="error"
|
||||
>
|
||||
<Notification title={title} onClose={() => removeError()} type="error">
|
||||
{message}
|
||||
<br />
|
||||
{sentryLinkTag}
|
||||
{taskName}
|
||||
{taskId}
|
||||
{fileName}
|
||||
{lineNumber}
|
||||
{errorLine}
|
||||
{taskTrace}
|
||||
</Notification>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -25,6 +25,7 @@ import { useUriListForPermissions } from '../hooks/UriListForPermissions';
|
|||
import { PermissionsToCheck } from '../interfaces';
|
||||
import { usePermissionFetcher } from '../hooks/PermissionService';
|
||||
import { UnauthenticatedError } from '../services/HttpService';
|
||||
import { SPIFF_ENVIRONMENT } from '../config';
|
||||
|
||||
// for ref: https://react-bootstrap.github.io/components/navbar/
|
||||
export default function NavigationBar() {
|
||||
|
@ -80,7 +81,12 @@ export default function NavigationBar() {
|
|||
if (UserService.isLoggedIn()) {
|
||||
return (
|
||||
<>
|
||||
<HeaderGlobalAction className="username-header-text">
|
||||
{SPIFF_ENVIRONMENT ? (
|
||||
<HeaderGlobalAction className="spiff-environment-header-text unclickable-text">
|
||||
{SPIFF_ENVIRONMENT}
|
||||
</HeaderGlobalAction>
|
||||
) : null}
|
||||
<HeaderGlobalAction className="username-header-text unclickable-text">
|
||||
{UserService.getPreferredUsername()}
|
||||
</HeaderGlobalAction>
|
||||
<HeaderGlobalAction
|
||||
|
|
|
@ -35,7 +35,7 @@ export default function ProcessGroupForm({
|
|||
};
|
||||
|
||||
const hasValidIdentifier = (identifierToCheck: string) => {
|
||||
return identifierToCheck.match(/^[a-z0-9][0-9a-z-]+[a-z0-9]$/);
|
||||
return identifierToCheck.match(/^[a-z0-9][0-9a-z-]*[a-z0-9]$/);
|
||||
};
|
||||
|
||||
const handleFormSubmission = (event: any) => {
|
||||
|
|
|
@ -94,7 +94,13 @@ export default function ProcessGroupListTiles({
|
|||
};
|
||||
|
||||
if (processGroups) {
|
||||
return <>{processGroupArea()}</>;
|
||||
return (
|
||||
<>
|
||||
{/* so we can check if the groups have loaded in cypress tests */}
|
||||
<div data-qa="process-groups-loaded" hidden />
|
||||
{processGroupArea()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { useContext, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import {
|
||||
Link,
|
||||
useNavigate,
|
||||
|
@ -40,13 +40,11 @@ import {
|
|||
getProcessModelFullIdentifierFromSearchParams,
|
||||
modifyProcessIdentifierForPathParam,
|
||||
refreshAtInterval,
|
||||
setErrorMessageSafely,
|
||||
} from '../helpers';
|
||||
|
||||
import PaginationForTable from './PaginationForTable';
|
||||
import 'react-datepicker/dist/react-datepicker.css';
|
||||
|
||||
import ErrorContext from '../contexts/ErrorContext';
|
||||
import HttpService from '../services/HttpService';
|
||||
|
||||
import 'react-bootstrap-typeahead/css/Typeahead.css';
|
||||
|
@ -61,6 +59,7 @@ import {
|
|||
ReportMetadata,
|
||||
ReportFilter,
|
||||
User,
|
||||
ErrorForDisplay,
|
||||
} from '../interfaces';
|
||||
import ProcessModelSearch from './ProcessModelSearch';
|
||||
import ProcessInstanceReportSearch from './ProcessInstanceReportSearch';
|
||||
|
@ -68,6 +67,7 @@ import ProcessInstanceListDeleteReport from './ProcessInstanceListDeleteReport';
|
|||
import ProcessInstanceListSaveAsReport from './ProcessInstanceListSaveAsReport';
|
||||
import { FormatProcessModelDisplayName } from './MiniComponents';
|
||||
import { Notification } from './Notification';
|
||||
import useAPIError from '../hooks/UseApiError';
|
||||
|
||||
const REFRESH_INTERVAL = 5;
|
||||
const REFRESH_TIMEOUT = 600;
|
||||
|
@ -110,6 +110,7 @@ export default function ProcessInstanceListTable({
|
|||
const params = useParams();
|
||||
const [searchParams] = useSearchParams();
|
||||
const navigate = useNavigate();
|
||||
const { addError, removeError } = useAPIError();
|
||||
|
||||
const [processInstances, setProcessInstances] = useState([]);
|
||||
const [reportMetadata, setReportMetadata] = useState<ReportMetadata | null>();
|
||||
|
@ -133,8 +134,6 @@ export default function ProcessInstanceListTable({
|
|||
const [endFromTimeInvalid, setEndFromTimeInvalid] = useState<boolean>(false);
|
||||
const [endToTimeInvalid, setEndToTimeInvalid] = useState<boolean>(false);
|
||||
|
||||
const [errorObject, setErrorObject] = (useContext as any)(ErrorContext);
|
||||
|
||||
const processInstanceListPathPrefix =
|
||||
variant === 'all'
|
||||
? '/admin/process-instances/all'
|
||||
|
@ -517,7 +516,7 @@ export default function ProcessInstanceListTable({
|
|||
}
|
||||
if (message !== '') {
|
||||
valid = false;
|
||||
setErrorMessageSafely(message, errorObject, setErrorObject);
|
||||
addError({ message } as ErrorForDisplay);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -579,7 +578,7 @@ export default function ProcessInstanceListTable({
|
|||
queryParamString += `&process_initiator_username=${processInitiatorSelection.username}`;
|
||||
}
|
||||
|
||||
setErrorObject(null);
|
||||
removeError();
|
||||
setProcessInstanceReportJustSaved(null);
|
||||
setProcessInstanceFilters({});
|
||||
navigate(`${processInstanceListPathPrefix}?${queryParamString}`);
|
||||
|
@ -679,7 +678,7 @@ export default function ProcessInstanceListTable({
|
|||
queryParamString = `?report_id=${selectedReport.id}`;
|
||||
}
|
||||
|
||||
setErrorObject(null);
|
||||
removeError();
|
||||
setProcessInstanceReportJustSaved(mode || null);
|
||||
navigate(`${processInstanceListPathPrefix}${queryParamString}`);
|
||||
};
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { useContext } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
Button,
|
||||
|
@ -11,9 +10,9 @@ import {
|
|||
RecentProcessModel,
|
||||
} from '../interfaces';
|
||||
import HttpService from '../services/HttpService';
|
||||
import ErrorContext from '../contexts/ErrorContext';
|
||||
import { modifyProcessIdentifierForPathParam } from '../helpers';
|
||||
import { usePermissionFetcher } from '../hooks/PermissionService';
|
||||
import useAPIError from '../hooks/UseApiError';
|
||||
|
||||
const storeRecentProcessModelInLocalStorage = (
|
||||
processModelForStorage: ProcessModel
|
||||
|
@ -78,7 +77,7 @@ export default function ProcessInstanceRun({
|
|||
checkPermissions = true,
|
||||
}: OwnProps) {
|
||||
const navigate = useNavigate();
|
||||
const setErrorObject = (useContext as any)(ErrorContext)[1];
|
||||
const { addError, removeError } = useAPIError();
|
||||
const modifiedProcessModelId = modifyProcessIdentifierForPathParam(
|
||||
processModel.id
|
||||
);
|
||||
|
@ -105,20 +104,22 @@ export default function ProcessInstanceRun({
|
|||
};
|
||||
|
||||
const processModelRun = (processInstance: any) => {
|
||||
setErrorObject(null);
|
||||
removeError();
|
||||
storeRecentProcessModelInLocalStorage(processModel);
|
||||
HttpService.makeCallToBackend({
|
||||
path: `/process-instances/${modifiedProcessModelId}/${processInstance.id}/run`,
|
||||
successCallback: onProcessInstanceRun,
|
||||
failureCallback: setErrorObject,
|
||||
failureCallback: addError,
|
||||
httpMethod: 'POST',
|
||||
});
|
||||
};
|
||||
|
||||
const processInstanceCreateAndRun = () => {
|
||||
removeError();
|
||||
HttpService.makeCallToBackend({
|
||||
path: processInstanceCreatePath,
|
||||
successCallback: processModelRun,
|
||||
failureCallback: addError,
|
||||
httpMethod: 'POST',
|
||||
});
|
||||
};
|
||||
|
|
|
@ -6,7 +6,7 @@ import BpmnViewer from 'bpmn-js/lib/Viewer';
|
|||
import {
|
||||
BpmnPropertiesPanelModule,
|
||||
BpmnPropertiesProviderModule,
|
||||
// @ts-expect-error TS(7016) FIXME: Could not find a declaration file for module 'bpmn... Remove this comment to see the full error message
|
||||
// @ts-expect-error TS(7016) FIXME: Could not find a declaration file for module 'bpmn... RemoFve this comment to see the full error message
|
||||
} from 'bpmn-js-properties-panel';
|
||||
|
||||
// @ts-expect-error TS(7016) FIXME: Could not find a declaration file for module 'dmn-... Remove this comment to see the full error message
|
||||
|
|
|
@ -36,6 +36,7 @@ type OwnProps = {
|
|||
showDateStarted?: boolean;
|
||||
showLastUpdated?: boolean;
|
||||
hideIfNoTasks?: boolean;
|
||||
canCompleteAllTasks?: boolean;
|
||||
};
|
||||
|
||||
export default function TaskListTable({
|
||||
|
@ -56,6 +57,7 @@ export default function TaskListTable({
|
|||
showDateStarted = true,
|
||||
showLastUpdated = true,
|
||||
hideIfNoTasks = false,
|
||||
canCompleteAllTasks = false,
|
||||
}: OwnProps) {
|
||||
const [searchParams] = useSearchParams();
|
||||
const [tasks, setTasks] = useState<ProcessInstanceTask[] | null>(null);
|
||||
|
@ -128,7 +130,10 @@ export default function TaskListTable({
|
|||
|
||||
const regex = new RegExp(`\\b(${preferredUsername}|${userEmail})\\b`);
|
||||
let hasAccessToCompleteTask = false;
|
||||
if ((processInstanceTask.potential_owner_usernames || '').match(regex)) {
|
||||
if (
|
||||
canCompleteAllTasks ||
|
||||
(processInstanceTask.potential_owner_usernames || '').match(regex)
|
||||
) {
|
||||
hasAccessToCompleteTask = true;
|
||||
}
|
||||
const rowElements = [];
|
||||
|
|
|
@ -11,6 +11,7 @@ export default function TasksWaitingForMe() {
|
|||
textToShowIfEmpty="No tasks are waiting for you."
|
||||
autoReload
|
||||
showWaitingOn={false}
|
||||
canCompleteAllTasks
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -29,6 +29,7 @@ export default function TasksWaitingForMyGroups() {
|
|||
textToShowIfEmpty="This group has no task assignments at this time."
|
||||
autoReload
|
||||
showWaitingOn={false}
|
||||
canCompleteAllTasks
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -1,17 +1,63 @@
|
|||
const { port, hostname } = window.location;
|
||||
let hostAndPort = `api.${hostname}`;
|
||||
let protocol = 'https';
|
||||
|
||||
declare global {
|
||||
interface SpiffworkflowFrontendJsenvObject {
|
||||
[key: string]: string;
|
||||
}
|
||||
interface Window {
|
||||
spiffworkflowFrontendJsenv: SpiffworkflowFrontendJsenvObject;
|
||||
}
|
||||
}
|
||||
|
||||
let spiffEnvironment = '';
|
||||
let appRoutingStrategy = 'subdomain_based';
|
||||
if ('spiffworkflowFrontendJsenv' in window) {
|
||||
if ('APP_ROUTING_STRATEGY' in window.spiffworkflowFrontendJsenv) {
|
||||
appRoutingStrategy = window.spiffworkflowFrontendJsenv.APP_ROUTING_STRATEGY;
|
||||
}
|
||||
if ('ENVIRONMENT_IDENTIFIER' in window.spiffworkflowFrontendJsenv) {
|
||||
spiffEnvironment = window.spiffworkflowFrontendJsenv.ENVIRONMENT_IDENTIFIER;
|
||||
}
|
||||
}
|
||||
|
||||
let hostAndPortAndPathPrefix;
|
||||
if (appRoutingStrategy === 'subdomain_based') {
|
||||
hostAndPortAndPathPrefix = `api.${hostname}`;
|
||||
} else if (appRoutingStrategy === 'path_based') {
|
||||
hostAndPortAndPathPrefix = `${hostname}/api`;
|
||||
} else {
|
||||
throw new Error(`Invalid app routing strategy: ${appRoutingStrategy}`);
|
||||
}
|
||||
|
||||
if (/^\d+\./.test(hostname) || hostname === 'localhost') {
|
||||
let serverPort = 7000;
|
||||
if (!Number.isNaN(Number(port))) {
|
||||
serverPort = Number(port) - 1;
|
||||
}
|
||||
hostAndPort = `${hostname}:${serverPort}`;
|
||||
hostAndPortAndPathPrefix = `${hostname}:${serverPort}`;
|
||||
protocol = 'http';
|
||||
|
||||
if (spiffEnvironment === '') {
|
||||
// using destructuring on an array where we only want the first element
|
||||
// seems super confusing for non-javascript devs to read so let's NOT do that.
|
||||
// eslint-disable-next-line prefer-destructuring
|
||||
spiffEnvironment = hostname.split('.')[0];
|
||||
}
|
||||
}
|
||||
|
||||
let url = `${protocol}://${hostAndPort}/v1.0`;
|
||||
if (
|
||||
'spiffworkflowFrontendJsenv' in window &&
|
||||
'APP_ROUTING_STRATEGY' in window.spiffworkflowFrontendJsenv
|
||||
) {
|
||||
appRoutingStrategy = window.spiffworkflowFrontendJsenv.APP_ROUTING_STRATEGY;
|
||||
}
|
||||
|
||||
let url = `${protocol}://${hostAndPortAndPathPrefix}/v1.0`;
|
||||
|
||||
// this can only ever work locally since this is a static site.
|
||||
// use spiffworkflowFrontendJsenv if you want to inject env vars
|
||||
// that can be read by the static site.
|
||||
if (process.env.REACT_APP_BACKEND_BASE_URL) {
|
||||
url = process.env.REACT_APP_BACKEND_BASE_URL;
|
||||
}
|
||||
|
@ -33,3 +79,5 @@ export const DATE_TIME_FORMAT = 'yyyy-MM-dd HH:mm:ss';
|
|||
export const TIME_FORMAT_HOURS_MINUTES = 'HH:mm';
|
||||
export const DATE_FORMAT = 'yyyy-MM-dd';
|
||||
export const DATE_FORMAT_CARBON = 'Y-m-d';
|
||||
|
||||
export const SPIFF_ENVIRONMENT = spiffEnvironment;
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
import React, { createContext, useState } from 'react';
|
||||
import { ErrorForDisplay } from '../interfaces';
|
||||
|
||||
type ErrorContextType = {
|
||||
error: null | ErrorForDisplay;
|
||||
addError: Function;
|
||||
removeError: Function;
|
||||
};
|
||||
export const APIErrorContext = createContext<ErrorContextType>({
|
||||
error: null,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
addError: () => {},
|
||||
removeError: () => {},
|
||||
});
|
||||
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line react/prop-types
|
||||
export default function APIErrorProvider({ children }) {
|
||||
const [error, setError] = useState<ErrorForDisplay | null>(null);
|
||||
const addError = (errorForDisplay: ErrorForDisplay | null) => {
|
||||
setError(errorForDisplay);
|
||||
};
|
||||
const removeError = () => setError(null);
|
||||
|
||||
const contextValue = React.useMemo(
|
||||
() => ({
|
||||
error,
|
||||
addError: (newError: ErrorForDisplay | null) => {
|
||||
addError(newError);
|
||||
},
|
||||
removeError: () => {
|
||||
removeError();
|
||||
},
|
||||
}),
|
||||
[error]
|
||||
);
|
||||
|
||||
return (
|
||||
<APIErrorContext.Provider value={contextValue}>
|
||||
{children}
|
||||
</APIErrorContext.Provider>
|
||||
);
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
import { createContext } from 'react';
|
||||
|
||||
// @ts-expect-error TS(2554) FIXME: Expected 1 arguments, but got 0.
|
||||
const ErrorContext = createContext();
|
||||
export default ErrorContext;
|
|
@ -8,7 +8,6 @@ import {
|
|||
DEFAULT_PER_PAGE,
|
||||
DEFAULT_PAGE,
|
||||
} from './components/PaginationForTable';
|
||||
import { ErrorForDisplay } from './interfaces';
|
||||
|
||||
// https://www.30secondsofcode.org/js/s/slugify
|
||||
export const slugifyString = (str: any) => {
|
||||
|
@ -258,20 +257,6 @@ export const getBpmnProcessIdentifiers = (rootBpmnElement: any) => {
|
|||
return childProcesses;
|
||||
};
|
||||
|
||||
// Setting the error message state to the same string is still considered a change
|
||||
// and re-renders the page so check the message first to avoid that.
|
||||
export const setErrorMessageSafely = (
|
||||
newErrorMessageString: string,
|
||||
oldErrorMessage: ErrorForDisplay,
|
||||
errorMessageSetter: any
|
||||
) => {
|
||||
if (oldErrorMessage && oldErrorMessage.message === newErrorMessageString) {
|
||||
return null;
|
||||
}
|
||||
errorMessageSetter({ message: newErrorMessageString });
|
||||
return null;
|
||||
};
|
||||
|
||||
export const isInteger = (str: string | number) => {
|
||||
return /^\d+$/.test(str.toString());
|
||||
};
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
// src/common/hooks/useAPIError/index.js
|
||||
import { useContext } from 'react';
|
||||
import { APIErrorContext } from '../contexts/APIErrorContext';
|
||||
|
||||
function useAPIError() {
|
||||
const { error, addError, removeError } = useContext(APIErrorContext);
|
||||
return { error, addError, removeError };
|
||||
}
|
||||
|
||||
export default useAPIError;
|
|
@ -14,6 +14,21 @@
|
|||
width: 5rem;
|
||||
}
|
||||
|
||||
.cds--header__action.spiff-environment-header-text {
|
||||
width: 5rem;
|
||||
color: #126d82;
|
||||
}
|
||||
|
||||
.cds--header__action.unclickable-text:hover {
|
||||
background-color: #161616;
|
||||
cursor: default;
|
||||
}
|
||||
.cds--header__action.unclickable-text:focus {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
border-color: none;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-weight: 400;
|
||||
font-size: 28px;
|
||||
|
@ -173,6 +188,10 @@ h1.with-icons {
|
|||
margin-top: 1.3em;
|
||||
}
|
||||
|
||||
.with-top-margin-for-label-next-to-text-input {
|
||||
margin-top: 2.3em;
|
||||
}
|
||||
|
||||
.with-tiny-top-margin {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
@ -374,3 +393,12 @@ svg.notification-icon {
|
|||
.tag-type-green:hover {
|
||||
background-color: #80ee90;
|
||||
}
|
||||
|
||||
/* Errors and notifications */
|
||||
.error_info .error_title {
|
||||
display: inline-block;
|
||||
font-weight: lighter;
|
||||
width: 100px;
|
||||
text-align: right;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
|
|
@ -24,20 +24,29 @@ export interface RecentProcessModel {
|
|||
export interface ProcessInstanceTask {
|
||||
id: number;
|
||||
task_id: string;
|
||||
|
||||
calling_subprocess_task_id: string;
|
||||
created_at_in_seconds: number;
|
||||
current_user_is_potential_owner: number;
|
||||
data: any;
|
||||
form_schema: any;
|
||||
form_ui_schema: any;
|
||||
lane_assignment_id: string;
|
||||
name: string;
|
||||
process_identifier: string;
|
||||
process_initiator_username: string;
|
||||
process_instance_id: number;
|
||||
process_instance_status: string;
|
||||
process_model_display_name: string;
|
||||
process_model_identifier: string;
|
||||
task_title: string;
|
||||
lane_assignment_id: string;
|
||||
process_instance_status: string;
|
||||
properties: any;
|
||||
state: string;
|
||||
process_identifier: string;
|
||||
name: string;
|
||||
process_initiator_username: string;
|
||||
created_at_in_seconds: number;
|
||||
task_title: string;
|
||||
title: string;
|
||||
type: string;
|
||||
updated_at_in_seconds: number;
|
||||
current_user_is_potential_owner: number;
|
||||
calling_subprocess_task_id: string;
|
||||
|
||||
task_spiff_step?: number;
|
||||
potential_owner_usernames?: string;
|
||||
assigned_user_group_identifier?: string;
|
||||
}
|
||||
|
@ -66,6 +75,12 @@ export interface ProcessFile {
|
|||
file_contents?: string;
|
||||
}
|
||||
|
||||
export interface ProcessInstanceMetadata {
|
||||
id: number;
|
||||
key: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface ProcessInstance {
|
||||
id: number;
|
||||
process_model_identifier: string;
|
||||
|
@ -80,6 +95,8 @@ export interface ProcessInstance {
|
|||
updated_at_in_seconds: number;
|
||||
bpmn_version_control_identifier: string;
|
||||
bpmn_version_control_type: string;
|
||||
process_metadata?: ProcessInstanceMetadata[];
|
||||
process_model_with_diagram_identifier?: string;
|
||||
}
|
||||
|
||||
export interface MessageCorrelationProperties {
|
||||
|
@ -182,6 +199,7 @@ export interface ErrorForDisplay {
|
|||
task_id?: string;
|
||||
line_number?: number;
|
||||
file_name?: string;
|
||||
task_trace?: [string];
|
||||
}
|
||||
|
||||
export interface AuthenticationParam {
|
||||
|
@ -225,7 +243,16 @@ export interface PermissionCheckResponseBody {
|
|||
export interface FormField {
|
||||
id: string;
|
||||
title: string;
|
||||
required: boolean;
|
||||
required?: boolean;
|
||||
type: string;
|
||||
enum: string[];
|
||||
enum?: string[];
|
||||
default?: any;
|
||||
pattern?: string;
|
||||
}
|
||||
|
||||
export interface JsonSchemaForm {
|
||||
file_contents: string;
|
||||
name: string;
|
||||
process_model_id: string;
|
||||
required: string[];
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Routes, Route, useLocation } from 'react-router-dom';
|
||||
|
||||
import { useContext, useEffect } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import ProcessGroupList from './ProcessGroupList';
|
||||
import ProcessGroupShow from './ProcessGroupShow';
|
||||
import ProcessGroupNew from './ProcessGroupNew';
|
||||
|
@ -17,7 +17,6 @@ import ProcessInstanceReportList from './ProcessInstanceReportList';
|
|||
import ProcessInstanceReportNew from './ProcessInstanceReportNew';
|
||||
import ProcessInstanceReportEdit from './ProcessInstanceReportEdit';
|
||||
import ReactFormEditor from './ReactFormEditor';
|
||||
import ErrorContext from '../contexts/ErrorContext';
|
||||
import ProcessInstanceLogList from './ProcessInstanceLogList';
|
||||
import MessageInstanceList from './MessageInstanceList';
|
||||
import Configuration from './Configuration';
|
||||
|
@ -27,11 +26,8 @@ import ProcessInstanceFindById from './ProcessInstanceFindById';
|
|||
|
||||
export default function AdminRoutes() {
|
||||
const location = useLocation();
|
||||
const setErrorObject = (useContext as any)(ErrorContext)[1];
|
||||
|
||||
useEffect(() => {
|
||||
setErrorObject(null);
|
||||
}, [location, setErrorObject]);
|
||||
useEffect(() => {}, [location]);
|
||||
|
||||
if (UserService.hasRole(['admin'])) {
|
||||
return (
|
||||
|
@ -114,7 +110,11 @@ export default function AdminRoutes() {
|
|||
/>
|
||||
<Route
|
||||
path="logs/:process_model_id/:process_instance_id"
|
||||
element={<ProcessInstanceLogList />}
|
||||
element={<ProcessInstanceLogList variant="all" />}
|
||||
/>
|
||||
<Route
|
||||
path="logs/for-me/:process_model_id/:process_instance_id"
|
||||
element={<ProcessInstanceLogList variant="for-me" />}
|
||||
/>
|
||||
<Route
|
||||
path="process-instances"
|
||||
|
|
|
@ -1,14 +1,11 @@
|
|||
import { useContext, useEffect, useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
// @ts-ignore
|
||||
import { Table } from '@carbon/react';
|
||||
import ErrorContext from '../contexts/ErrorContext';
|
||||
import { AuthenticationItem } from '../interfaces';
|
||||
import HttpService from '../services/HttpService';
|
||||
import UserService from '../services/UserService';
|
||||
|
||||
export default function AuthenticationList() {
|
||||
const setErrorObject = (useContext as any)(ErrorContext)[1];
|
||||
|
||||
const [authenticationList, setAuthenticationList] = useState<
|
||||
AuthenticationItem[] | null
|
||||
>(null);
|
||||
|
@ -26,9 +23,8 @@ export default function AuthenticationList() {
|
|||
HttpService.makeCallToBackend({
|
||||
path: `/authentications`,
|
||||
successCallback: processResult,
|
||||
failureCallback: setErrorObject,
|
||||
});
|
||||
}, [setErrorObject]);
|
||||
}, []);
|
||||
|
||||
const buildTable = () => {
|
||||
if (authenticationList) {
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { useContext, useEffect, useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Route, Routes, useLocation, useNavigate } from 'react-router-dom';
|
||||
// @ts-ignore
|
||||
import { Tabs, TabList, Tab } from '@carbon/react';
|
||||
import { Can } from '@casl/react';
|
||||
import ErrorContext from '../contexts/ErrorContext';
|
||||
import useAPIError from '../hooks/UseApiError';
|
||||
import SecretList from './SecretList';
|
||||
import SecretNew from './SecretNew';
|
||||
import SecretShow from './SecretShow';
|
||||
|
@ -14,7 +14,7 @@ import { usePermissionFetcher } from '../hooks/PermissionService';
|
|||
|
||||
export default function Configuration() {
|
||||
const location = useLocation();
|
||||
const setErrorObject = (useContext as any)(ErrorContext)[1];
|
||||
const { removeError } = useAPIError();
|
||||
const [selectedTabIndex, setSelectedTabIndex] = useState<number>(0);
|
||||
const navigate = useNavigate();
|
||||
|
||||
|
@ -26,13 +26,14 @@ export default function Configuration() {
|
|||
const { ability } = usePermissionFetcher(permissionRequestData);
|
||||
|
||||
useEffect(() => {
|
||||
setErrorObject(null);
|
||||
console.log('Configuration remove error');
|
||||
removeError();
|
||||
let newSelectedTabIndex = 0;
|
||||
if (location.pathname.match(/^\/admin\/configuration\/authentications\b/)) {
|
||||
newSelectedTabIndex = 1;
|
||||
}
|
||||
setSelectedTabIndex(newSelectedTabIndex);
|
||||
}, [location, setErrorObject]);
|
||||
}, [location, removeError]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
import { useContext, useEffect, useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Route, Routes, useLocation, useNavigate } from 'react-router-dom';
|
||||
// @ts-ignore
|
||||
import { Tabs, TabList, Tab } from '@carbon/react';
|
||||
import TaskShow from './TaskShow';
|
||||
import ErrorContext from '../contexts/ErrorContext';
|
||||
import MyTasks from './MyTasks';
|
||||
import GroupedTasks from './GroupedTasks';
|
||||
import CompletedInstances from './CompletedInstances';
|
||||
|
@ -11,12 +10,11 @@ import CreateNewInstance from './CreateNewInstance';
|
|||
|
||||
export default function HomePageRoutes() {
|
||||
const location = useLocation();
|
||||
const setErrorObject = (useContext as any)(ErrorContext)[1];
|
||||
const [selectedTabIndex, setSelectedTabIndex] = useState<number>(0);
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
setErrorObject(null);
|
||||
// Do not remove errors here, or they always get removed.
|
||||
let newSelectedTabIndex = 0;
|
||||
if (location.pathname.match(/^\/tasks\/completed-instances\b/)) {
|
||||
newSelectedTabIndex = 1;
|
||||
|
@ -24,7 +22,7 @@ export default function HomePageRoutes() {
|
|||
newSelectedTabIndex = 2;
|
||||
}
|
||||
setSelectedTabIndex(newSelectedTabIndex);
|
||||
}, [location, setErrorObject]);
|
||||
}, [location]);
|
||||
|
||||
const renderTabs = () => {
|
||||
if (location.pathname.match(/^\/tasks\/\d+\/\b/)) {
|
||||
|
|
|
@ -1,17 +1,32 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
// @ts-ignore
|
||||
import { Button, Select, SelectItem, TextInput } from '@carbon/react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { FormField } from '../interfaces';
|
||||
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
|
||||
import {
|
||||
Button,
|
||||
Select,
|
||||
SelectItem,
|
||||
TextInput,
|
||||
Grid,
|
||||
Column,
|
||||
// @ts-ignore
|
||||
} from '@carbon/react';
|
||||
import validator from '@rjsf/validator-ajv8';
|
||||
import { FormField, JsonSchemaForm } from '../interfaces';
|
||||
import { Form } from '../themes/carbon';
|
||||
import {
|
||||
modifyProcessIdentifierForPathParam,
|
||||
slugifyString,
|
||||
underscorizeString,
|
||||
} from '../helpers';
|
||||
import HttpService from '../services/HttpService';
|
||||
import { Notification } from '../components/Notification';
|
||||
import ProcessBreadcrumb from '../components/ProcessBreadcrumb';
|
||||
import ButtonWithConfirmation from '../components/ButtonWithConfirmation';
|
||||
|
||||
export default function JsonSchemaFormBuilder() {
|
||||
const params = useParams();
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
const formFieldTypes = ['textbox', 'checkbox', 'select'];
|
||||
|
||||
const [formTitle, setFormTitle] = useState<string>('');
|
||||
|
@ -31,34 +46,90 @@ export default function JsonSchemaFormBuilder() {
|
|||
const [formFieldId, setFormFieldId] = useState<string>('');
|
||||
const [formFieldTitle, setFormFieldTitle] = useState<string>('');
|
||||
const [formFieldType, setFormFieldType] = useState<string>('');
|
||||
const [requiredFields, setRequiredFields] = useState<string[]>([]);
|
||||
const [savedJsonSchema, setSavedJsonSchema] = useState<boolean>(false);
|
||||
|
||||
const modifiedProcessModelId = modifyProcessIdentifierForPathParam(
|
||||
`${params.process_model_id}`
|
||||
);
|
||||
|
||||
useEffect(() => {}, []);
|
||||
useEffect(() => {
|
||||
const processResult = (result: JsonSchemaForm) => {
|
||||
const jsonForm = JSON.parse(result.file_contents);
|
||||
setFormTitle(jsonForm.title);
|
||||
setFormDescription(jsonForm.description);
|
||||
setRequiredFields(jsonForm.required);
|
||||
const newFormId = (searchParams.get('file_name') || '').replace(
|
||||
'-schema.json',
|
||||
''
|
||||
);
|
||||
setFormId(newFormId);
|
||||
const newFormFields: FormField[] = [];
|
||||
Object.keys(jsonForm.properties).forEach((propertyId: string) => {
|
||||
const propertyDetails = jsonForm.properties[propertyId];
|
||||
newFormFields.push({
|
||||
id: propertyId,
|
||||
title: propertyDetails.title,
|
||||
required: propertyDetails.required,
|
||||
type: propertyDetails.type,
|
||||
enum: propertyDetails.enum,
|
||||
default: propertyDetails.default,
|
||||
pattern: propertyDetails.pattern,
|
||||
});
|
||||
});
|
||||
setFormFields(newFormFields);
|
||||
};
|
||||
if (searchParams.get('file_name')) {
|
||||
HttpService.makeCallToBackend({
|
||||
path: `/process-models/${modifiedProcessModelId}/files/${searchParams.get(
|
||||
'file_name'
|
||||
)}`,
|
||||
successCallback: processResult,
|
||||
});
|
||||
}
|
||||
}, [modifiedProcessModelId, searchParams]);
|
||||
|
||||
const formSubmitResultElement = () => {
|
||||
if (savedJsonSchema) {
|
||||
return (
|
||||
<Notification
|
||||
title="Form Saved"
|
||||
onClose={() => setSavedJsonSchema(false)}
|
||||
>
|
||||
It saved
|
||||
</Notification>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const renderFormJson = () => {
|
||||
const formJson = {
|
||||
title: formTitle,
|
||||
description: formDescription,
|
||||
properties: {},
|
||||
required: [],
|
||||
required: requiredFields,
|
||||
};
|
||||
|
||||
formFields.forEach((formField: FormField) => {
|
||||
let jsonSchemaFieldType = 'string';
|
||||
if (formField.type === 'checkbox') {
|
||||
let jsonSchemaFieldType = formField.type;
|
||||
if (['checkbox'].includes(formField.type)) {
|
||||
jsonSchemaFieldType = 'boolean';
|
||||
}
|
||||
const formJsonObject: any = {
|
||||
type: jsonSchemaFieldType,
|
||||
type: jsonSchemaFieldType || 'string',
|
||||
title: formField.title,
|
||||
};
|
||||
|
||||
if (formField.type === 'select') {
|
||||
if (formField.enum) {
|
||||
formJsonObject.enum = formField.enum;
|
||||
}
|
||||
if (formField.default !== undefined) {
|
||||
formJsonObject.default = formField.default;
|
||||
}
|
||||
if (formField.pattern) {
|
||||
formJsonObject.pattern = formField.pattern;
|
||||
}
|
||||
(formJson.properties as any)[formField.id] = formJsonObject;
|
||||
});
|
||||
|
||||
|
@ -86,13 +157,23 @@ export default function JsonSchemaFormBuilder() {
|
|||
setFormTitle(newFormTitle);
|
||||
};
|
||||
|
||||
const getFormFieldType = (indicatedType: string) => {
|
||||
if (indicatedType === 'checkbox') {
|
||||
return 'boolean';
|
||||
}
|
||||
// undefined or 'select' or 'textbox'
|
||||
return 'string';
|
||||
};
|
||||
|
||||
const addFormField = () => {
|
||||
const newFormField: FormField = {
|
||||
id: formFieldId,
|
||||
title: formFieldTitle,
|
||||
required: false,
|
||||
type: formFieldType,
|
||||
enum: formFieldSelectOptions.split(','),
|
||||
type: getFormFieldType(formFieldType),
|
||||
enum: showFormFieldSelectTextField
|
||||
? formFieldSelectOptions.split(',')
|
||||
: undefined,
|
||||
};
|
||||
|
||||
setFormFieldIdHasBeenUpdatedByUser(false);
|
||||
|
@ -168,8 +249,8 @@ export default function JsonSchemaFormBuilder() {
|
|||
return null;
|
||||
};
|
||||
|
||||
const handleSaveCallback = (result: any) => {
|
||||
console.log('result', result);
|
||||
const handleSaveCallback = () => {
|
||||
setSavedJsonSchema(true);
|
||||
};
|
||||
|
||||
const uploadFile = (file: File) => {
|
||||
|
@ -188,17 +269,118 @@ export default function JsonSchemaFormBuilder() {
|
|||
};
|
||||
|
||||
const saveFile = () => {
|
||||
const formJsonFileName = `${formId}-schema.json`;
|
||||
const formUiJsonFileName = `${formId}-uischema.json`;
|
||||
setSavedJsonSchema(false);
|
||||
let formJsonFileName = `${formId}-schema.json`;
|
||||
let formUiJsonFileName: string | null = `${formId}-uischema.json`;
|
||||
if (searchParams.get('file_name')) {
|
||||
formJsonFileName = searchParams.get('file_name') as any;
|
||||
if (formJsonFileName.match(/-schema\.json$/)) {
|
||||
formUiJsonFileName = (searchParams.get('file_name') as any).replace(
|
||||
'-schema.json',
|
||||
'-uischema.json'
|
||||
);
|
||||
} else {
|
||||
formUiJsonFileName = null;
|
||||
}
|
||||
}
|
||||
|
||||
uploadFile(new File([renderFormJson()], formJsonFileName));
|
||||
uploadFile(new File([renderFormUiJson()], formUiJsonFileName));
|
||||
if (formUiJsonFileName) {
|
||||
uploadFile(new File([renderFormUiJson()], formUiJsonFileName));
|
||||
}
|
||||
};
|
||||
|
||||
const deleteFile = () => {
|
||||
const url = `/process-models/${modifiedProcessModelId}/files/${params.file_name}`;
|
||||
const httpMethod = 'DELETE';
|
||||
|
||||
const navigateToProcessModelShow = (_httpResult: any) => {
|
||||
navigate(`/admin/process-models/${modifiedProcessModelId}`);
|
||||
};
|
||||
|
||||
HttpService.makeCallToBackend({
|
||||
path: url,
|
||||
successCallback: navigateToProcessModelShow,
|
||||
httpMethod,
|
||||
});
|
||||
};
|
||||
|
||||
const formIdTextField = () => {
|
||||
if (searchParams.get('file_name')) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<TextInput
|
||||
id="json-form-id"
|
||||
name="id"
|
||||
labelText="ID"
|
||||
value={formId}
|
||||
onChange={(event: any) => {
|
||||
setFormIdHasBeenUpdatedByUser(true);
|
||||
setFormId(event.srcElement.value);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const jsonFormButton = () => {
|
||||
if (!searchParams.get('file_name')) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<ButtonWithConfirmation
|
||||
data-qa="delete-process-model-file"
|
||||
description={`Delete file ${searchParams.get('file_name')}?`}
|
||||
onConfirmation={deleteFile}
|
||||
buttonLabel="Delete"
|
||||
/>
|
||||
<Button
|
||||
onClick={() =>
|
||||
navigate(
|
||||
`/admin/process-models/${
|
||||
params.process_model_id
|
||||
}/form/${searchParams.get('file_name')}`
|
||||
)
|
||||
}
|
||||
variant="danger"
|
||||
data-qa="form-builder-button"
|
||||
>
|
||||
View Json
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
const processModelFileName = searchParams.get('file_name') || '';
|
||||
|
||||
const topOfPageElements = () => {
|
||||
return (
|
||||
<>
|
||||
<ProcessBreadcrumb
|
||||
hotCrumbs={[
|
||||
['Process Groups', '/admin'],
|
||||
{
|
||||
entityToExplode: params.process_model_id || '',
|
||||
entityType: 'process-model-id',
|
||||
linkLastItem: true,
|
||||
},
|
||||
[processModelFileName],
|
||||
]}
|
||||
/>
|
||||
<h1>
|
||||
Process Model File{processModelFileName ? ': ' : ''}
|
||||
{processModelFileName}
|
||||
</h1>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const jsonFormArea = () => {
|
||||
return (
|
||||
<>
|
||||
{formSubmitResultElement()}
|
||||
<Button onClick={saveFile}>Save</Button>
|
||||
{jsonFormButton()}
|
||||
<TextInput
|
||||
id="json-form-title"
|
||||
name="title"
|
||||
|
@ -208,16 +390,7 @@ export default function JsonSchemaFormBuilder() {
|
|||
onFormTitleChange(event.srcElement.value);
|
||||
}}
|
||||
/>
|
||||
<TextInput
|
||||
id="json-form-id"
|
||||
name="id"
|
||||
labelText="ID"
|
||||
value={formId}
|
||||
onChange={(event: any) => {
|
||||
setFormIdHasBeenUpdatedByUser(true);
|
||||
setFormId(event.srcElement.value);
|
||||
}}
|
||||
/>
|
||||
{formIdTextField()}
|
||||
<TextInput
|
||||
id="form-description"
|
||||
name="description"
|
||||
|
@ -244,6 +417,28 @@ export default function JsonSchemaFormBuilder() {
|
|||
</>
|
||||
);
|
||||
};
|
||||
const schemaString = renderFormJson();
|
||||
const uiSchemaString = renderFormUiJson();
|
||||
const schema = JSON.parse(schemaString);
|
||||
const uiSchema = JSON.parse(uiSchemaString);
|
||||
|
||||
return <>{jsonFormArea()}</>;
|
||||
return (
|
||||
<>
|
||||
{topOfPageElements()}
|
||||
<Grid fullWidth>
|
||||
<Column md={5} lg={8} sm={4}>
|
||||
{jsonFormArea()}
|
||||
</Column>
|
||||
<Column md={5} lg={8} sm={4}>
|
||||
<h2>Form Preview</h2>
|
||||
<Form
|
||||
formData={{}}
|
||||
schema={schema}
|
||||
uiSchema={uiSchema}
|
||||
validator={validator}
|
||||
/>
|
||||
</Column>
|
||||
</Grid>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -12,17 +12,23 @@ import {
|
|||
import HttpService from '../services/HttpService';
|
||||
import { useUriListForPermissions } from '../hooks/UriListForPermissions';
|
||||
|
||||
export default function ProcessInstanceLogList() {
|
||||
type OwnProps = {
|
||||
variant: string;
|
||||
};
|
||||
|
||||
export default function ProcessInstanceLogList({ variant }: OwnProps) {
|
||||
const params = useParams();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const [processInstanceLogs, setProcessInstanceLogs] = useState([]);
|
||||
const [pagination, setPagination] = useState(null);
|
||||
const modifiedProcessModelId = modifyProcessIdentifierForPathParam(
|
||||
`${params.process_model_id}`
|
||||
);
|
||||
const { targetUris } = useUriListForPermissions();
|
||||
const isDetailedView = searchParams.get('detailed') === 'true';
|
||||
|
||||
let processInstanceShowPageBaseUrl = `/admin/process-instances/for-me/${params.process_model_id}`;
|
||||
if (variant === 'all') {
|
||||
processInstanceShowPageBaseUrl = `/admin/process-instances/${params.process_model_id}`;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const setProcessInstanceLogListFromResult = (result: any) => {
|
||||
setProcessInstanceLogs(result.results);
|
||||
|
@ -65,7 +71,7 @@ export default function ProcessInstanceLogList() {
|
|||
<td>
|
||||
<Link
|
||||
data-qa="process-instance-show-link"
|
||||
to={`/admin/process-instances/${modifiedProcessModelId}/${rowToUse.process_instance_id}/${rowToUse.spiff_step}`}
|
||||
to={`${processInstanceShowPageBaseUrl}/${rowToUse.process_instance_id}/${rowToUse.spiff_step}`}
|
||||
>
|
||||
{convertSecondsToFormattedDateTime(rowToUse.timestamp)}
|
||||
</Link>
|
||||
|
@ -111,7 +117,7 @@ export default function ProcessInstanceLogList() {
|
|||
},
|
||||
[
|
||||
`Process Instance: ${params.process_instance_id}`,
|
||||
`/admin/process-instances/${params.process_model_id}/${params.process_instance_id}`,
|
||||
`${processInstanceShowPageBaseUrl}/${params.process_instance_id}`,
|
||||
],
|
||||
['Logs'],
|
||||
]}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { useContext, useEffect, useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import Editor from '@monaco-editor/react';
|
||||
import {
|
||||
useParams,
|
||||
|
@ -35,20 +35,22 @@ import HttpService from '../services/HttpService';
|
|||
import ReactDiagramEditor from '../components/ReactDiagramEditor';
|
||||
import {
|
||||
convertSecondsToFormattedDateTime,
|
||||
modifyProcessIdentifierForPathParam,
|
||||
unModifyProcessIdentifierForPathParam,
|
||||
} from '../helpers';
|
||||
import ButtonWithConfirmation from '../components/ButtonWithConfirmation';
|
||||
import ErrorContext from '../contexts/ErrorContext';
|
||||
import { useUriListForPermissions } from '../hooks/UriListForPermissions';
|
||||
import {
|
||||
PermissionsToCheck,
|
||||
ProcessData,
|
||||
ProcessInstance,
|
||||
ProcessInstanceMetadata,
|
||||
ProcessInstanceTask,
|
||||
} from '../interfaces';
|
||||
import { usePermissionFetcher } from '../hooks/PermissionService';
|
||||
import ProcessInstanceClass from '../classes/ProcessInstanceClass';
|
||||
import TaskListTable from '../components/TaskListTable';
|
||||
import useAPIError from '../hooks/UseApiError';
|
||||
|
||||
type OwnProps = {
|
||||
variant: string;
|
||||
|
@ -74,9 +76,10 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
|
|||
const [eventTextEditorEnabled, setEventTextEditorEnabled] =
|
||||
useState<boolean>(false);
|
||||
const [displayDetails, setDisplayDetails] = useState<boolean>(false);
|
||||
const [showProcessInstanceMetadata, setShowProcessInstanceMetadata] =
|
||||
useState<boolean>(false);
|
||||
|
||||
const setErrorObject = (useContext as any)(ErrorContext)[1];
|
||||
|
||||
const { addError, removeError } = useAPIError();
|
||||
const unModifiedProcessModelId = unModifyProcessIdentifierForPathParam(
|
||||
`${params.process_model_id}`
|
||||
);
|
||||
|
@ -112,6 +115,13 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
|
|||
);
|
||||
};
|
||||
|
||||
let processInstanceShowPageBaseUrl = `/admin/process-instances/for-me/${params.process_model_id}/${params.process_instance_id}`;
|
||||
let processInstanceLogListPageBaseUrl = `/admin/logs/for-me/${params.process_model_id}/${params.process_instance_id}`;
|
||||
if (variant === 'all') {
|
||||
processInstanceShowPageBaseUrl = `/admin/process-instances/${params.process_model_id}/${params.process_instance_id}`;
|
||||
processInstanceLogListPageBaseUrl = `/admin/logs/${params.process_model_id}/${params.process_instance_id}`;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (permissionsLoaded) {
|
||||
const processTaskFailure = () => {
|
||||
|
@ -151,11 +161,11 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
|
|||
}
|
||||
}
|
||||
}, [
|
||||
targetUris,
|
||||
params,
|
||||
modifiedProcessModelId,
|
||||
permissionsLoaded,
|
||||
ability,
|
||||
targetUris,
|
||||
searchParams,
|
||||
taskListPath,
|
||||
variant,
|
||||
|
@ -237,19 +247,26 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
|
|||
return processInstance && currentSpiffStep() === processInstance.spiff_step;
|
||||
};
|
||||
|
||||
const spiffStepLink = (label: any, distance: number) => {
|
||||
const spiffStepLink = (label: any, spiffStep: number) => {
|
||||
const processIdentifier = searchParams.get('process_identifier');
|
||||
let queryParams = '';
|
||||
const callActivityTaskId = searchParams.get('call_activity_task_id');
|
||||
const queryParamArray = [];
|
||||
if (processIdentifier) {
|
||||
queryParams = `?process_identifier=${processIdentifier}`;
|
||||
queryParamArray.push(`process_identifier=${processIdentifier}`);
|
||||
}
|
||||
if (callActivityTaskId) {
|
||||
queryParamArray.push(`call_activity_task_id=${callActivityTaskId}`);
|
||||
}
|
||||
let queryParams = '';
|
||||
if (queryParamArray.length > 0) {
|
||||
queryParams = `?${queryParamArray.join('&')}`;
|
||||
}
|
||||
|
||||
return (
|
||||
<Link
|
||||
reloadDocument
|
||||
data-qa="process-instance-step-link"
|
||||
to={`/admin/process-instances/${params.process_model_id}/${
|
||||
params.process_instance_id
|
||||
}/${currentSpiffStep() + distance}${queryParams}`}
|
||||
to={`${processInstanceShowPageBaseUrl}/${spiffStep}${queryParams}`}
|
||||
>
|
||||
{label}
|
||||
</Link>
|
||||
|
@ -261,7 +278,7 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
|
|||
return null;
|
||||
}
|
||||
|
||||
return spiffStepLink(<CaretLeft />, -1);
|
||||
return spiffStepLink(<CaretLeft />, currentSpiffStep() - 1);
|
||||
};
|
||||
|
||||
const nextStepLink = () => {
|
||||
|
@ -269,11 +286,11 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
|
|||
return null;
|
||||
}
|
||||
|
||||
return spiffStepLink(<CaretRight />, 1);
|
||||
return spiffStepLink(<CaretRight />, currentSpiffStep() + 1);
|
||||
};
|
||||
|
||||
const returnToLastSpiffStep = () => {
|
||||
window.location.href = `/admin/process-instances/${params.process_model_id}/${params.process_instance_id}`;
|
||||
window.location.href = processInstanceShowPageBaseUrl;
|
||||
};
|
||||
|
||||
const resetProcessInstance = () => {
|
||||
|
@ -392,6 +409,23 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
|
|||
{processInstance.process_initiator_username}
|
||||
</Column>
|
||||
</Grid>
|
||||
{processInstance.process_model_with_diagram_identifier ? (
|
||||
<Grid condensed fullWidth>
|
||||
<Column sm={1} md={1} lg={2} className="grid-list-title">
|
||||
Current Diagram:{' '}
|
||||
</Column>
|
||||
<Column sm={4} md={6} lg={8} className="grid-date">
|
||||
<Link
|
||||
data-qa="go-to-current-diagram-process-model"
|
||||
to={`/admin/process-models/${modifyProcessIdentifierForPathParam(
|
||||
processInstance.process_model_with_diagram_identifier || ''
|
||||
)}`}
|
||||
>
|
||||
{processInstance.process_model_with_diagram_identifier}
|
||||
</Link>
|
||||
</Column>
|
||||
</Grid>
|
||||
) : null}
|
||||
<Grid condensed fullWidth>
|
||||
<Column sm={1} md={1} lg={2} className="grid-list-title">
|
||||
Started:{' '}
|
||||
|
@ -427,7 +461,7 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
|
|||
size="sm"
|
||||
className="button-white-background"
|
||||
data-qa="process-instance-log-list-link"
|
||||
href={`/admin/logs/${modifiedProcessModelId}/${params.process_instance_id}`}
|
||||
href={`${processInstanceLogListPageBaseUrl}`}
|
||||
>
|
||||
Logs
|
||||
</Button>
|
||||
|
@ -446,6 +480,19 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
|
|||
Messages
|
||||
</Button>
|
||||
</Can>
|
||||
{processInstance.process_metadata &&
|
||||
processInstance.process_metadata.length > 0 ? (
|
||||
<Button
|
||||
size="sm"
|
||||
className="button-white-background"
|
||||
data-qa="process-instance-show-metadata"
|
||||
onClick={() => {
|
||||
setShowProcessInstanceMetadata(true);
|
||||
}}
|
||||
>
|
||||
Metadata
|
||||
</Button>
|
||||
) : null}
|
||||
</ButtonSet>
|
||||
</Column>
|
||||
</Grid>
|
||||
|
@ -684,7 +731,8 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
|
|||
setSelectingEvent(false);
|
||||
initializeTaskDataToDisplay(taskToDisplay);
|
||||
setEventPayload('{}');
|
||||
setErrorObject(null);
|
||||
console.log('cancel updating task');
|
||||
removeError();
|
||||
};
|
||||
|
||||
const taskDataStringToObject = (dataString: string) => {
|
||||
|
@ -699,16 +747,12 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
|
|||
refreshPage();
|
||||
};
|
||||
|
||||
const saveTaskDataFailure = (result: any) => {
|
||||
setErrorObject({ message: result.message });
|
||||
};
|
||||
|
||||
const saveTaskData = () => {
|
||||
if (!taskToDisplay) {
|
||||
return;
|
||||
}
|
||||
|
||||
setErrorObject(null);
|
||||
console.log('saveTaskData');
|
||||
removeError();
|
||||
|
||||
// taskToUse is copy of taskToDisplay, with taskDataToDisplay in data attribute
|
||||
const taskToUse: any = { ...taskToDisplay, data: taskDataToDisplay };
|
||||
|
@ -716,7 +760,7 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
|
|||
path: `${targetUris.processInstanceTaskListDataPath}/${taskToUse.id}`,
|
||||
httpMethod: 'PUT',
|
||||
successCallback: saveTaskDataResult,
|
||||
failureCallback: saveTaskDataFailure,
|
||||
failureCallback: addError,
|
||||
postBody: {
|
||||
new_task_data: taskToUse.data,
|
||||
},
|
||||
|
@ -730,7 +774,7 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
|
|||
path: `/send-event/${modifiedProcessModelId}/${params.process_instance_id}`,
|
||||
httpMethod: 'POST',
|
||||
successCallback: saveTaskDataResult,
|
||||
failureCallback: saveTaskDataFailure,
|
||||
failureCallback: addError,
|
||||
postBody: eventToSend,
|
||||
});
|
||||
};
|
||||
|
@ -903,6 +947,41 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
|
|||
);
|
||||
};
|
||||
|
||||
const processInstanceMetadataArea = () => {
|
||||
if (
|
||||
!processInstance ||
|
||||
(processInstance.process_metadata &&
|
||||
processInstance.process_metadata.length < 1)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
const metadataComponents: any[] = [];
|
||||
(processInstance.process_metadata || []).forEach(
|
||||
(processInstanceMetadata: ProcessInstanceMetadata) => {
|
||||
metadataComponents.push(
|
||||
<Grid condensed fullWidth>
|
||||
<Column sm={1} md={1} lg={2} className="grid-list-title">
|
||||
{processInstanceMetadata.key}
|
||||
</Column>
|
||||
<Column sm={3} md={3} lg={3} className="grid-date">
|
||||
{processInstanceMetadata.value}
|
||||
</Column>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
);
|
||||
return (
|
||||
<Modal
|
||||
open={showProcessInstanceMetadata}
|
||||
modalHeading="Metadata"
|
||||
passiveModal
|
||||
onRequestClose={() => setShowProcessInstanceMetadata(false)}
|
||||
>
|
||||
{metadataComponents}
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
const taskUpdateDisplayArea = () => {
|
||||
const taskToUse: any = { ...taskToDisplay, data: taskDataToDisplay };
|
||||
const candidateEvents: any = getEvents(taskToUse);
|
||||
|
@ -923,6 +1002,19 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
|
|||
): {taskToUse.state}
|
||||
{taskDisplayButtons(taskToUse)}
|
||||
</Stack>
|
||||
{taskToUse.task_spiff_step ? (
|
||||
<div>
|
||||
<Stack orientation="horizontal" gap={2}>
|
||||
Task completed at step:{' '}
|
||||
{spiffStepLink(
|
||||
`${taskToUse.task_spiff_step}`,
|
||||
taskToUse.task_spiff_step
|
||||
)}
|
||||
</Stack>
|
||||
<br />
|
||||
<br />
|
||||
</div>
|
||||
) : null}
|
||||
{selectingEvent
|
||||
? eventSelector(candidateEvents)
|
||||
: taskDataContainer()}
|
||||
|
@ -1027,6 +1119,7 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
|
|||
showDateStarted={false}
|
||||
showLastUpdated={false}
|
||||
hideIfNoTasks
|
||||
canCompleteAllTasks
|
||||
/>
|
||||
</Column>
|
||||
</Grid>
|
||||
|
@ -1034,6 +1127,7 @@ export default function ProcessInstanceShow({ variant }: OwnProps) {
|
|||
<br />
|
||||
{taskUpdateDisplayArea()}
|
||||
{processDataDisplayArea()}
|
||||
{processInstanceMetadataArea()}
|
||||
{stepsElement()}
|
||||
<br />
|
||||
<ReactDiagramEditor
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { useContext, useEffect, useRef, useState } from 'react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import {
|
||||
generatePath,
|
||||
useNavigate,
|
||||
|
@ -14,6 +14,9 @@ import {
|
|||
Tab,
|
||||
TabPanels,
|
||||
TabPanel,
|
||||
TextInput,
|
||||
Grid,
|
||||
Column,
|
||||
// @ts-ignore
|
||||
} from '@carbon/react';
|
||||
import Row from 'react-bootstrap/Row';
|
||||
|
@ -25,7 +28,7 @@ import MDEditor from '@uiw/react-md-editor';
|
|||
import ReactDiagramEditor from '../components/ReactDiagramEditor';
|
||||
import ProcessBreadcrumb from '../components/ProcessBreadcrumb';
|
||||
import HttpService from '../services/HttpService';
|
||||
import ErrorContext from '../contexts/ErrorContext';
|
||||
import useAPIError from '../hooks/UseApiError';
|
||||
import { makeid, modifyProcessIdentifierForPathParam } from '../helpers';
|
||||
import {
|
||||
CarbonComboBoxProcessSelection,
|
||||
|
@ -60,6 +63,8 @@ export default function ProcessModelEditDiagram() {
|
|||
const [processes, setProcesses] = useState<ProcessReference[]>([]);
|
||||
const [displaySaveFileMessage, setDisplaySaveFileMessage] =
|
||||
useState<boolean>(false);
|
||||
const [processModelFileInvalidText, setProcessModelFileInvalidText] =
|
||||
useState<string>('');
|
||||
|
||||
const handleShowMarkdownEditor = () => setShowMarkdownEditor(true);
|
||||
|
||||
|
@ -100,7 +105,7 @@ export default function ProcessModelEditDiagram() {
|
|||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
const setErrorObject = (useContext as any)(ErrorContext)[1];
|
||||
const { addError, removeError } = useAPIError();
|
||||
const [processModelFile, setProcessModelFile] = useState<ProcessFile | null>(
|
||||
null
|
||||
);
|
||||
|
@ -160,6 +165,7 @@ export default function ProcessModelEditDiagram() {
|
|||
const handleFileNameCancel = () => {
|
||||
setShowFileNameEditor(false);
|
||||
setNewFileName('');
|
||||
setProcessModelFileInvalidText('');
|
||||
};
|
||||
|
||||
const navigateToProcessModelFile = (_result: any) => {
|
||||
|
@ -176,7 +182,7 @@ export default function ProcessModelEditDiagram() {
|
|||
|
||||
const saveDiagram = (bpmnXML: any, fileName = params.file_name) => {
|
||||
setDisplaySaveFileMessage(false);
|
||||
setErrorObject(null);
|
||||
removeError();
|
||||
setBpmnXmlForDiagramRendering(bpmnXML);
|
||||
|
||||
let url = `/process-models/${modifiedProcessModelId}/files`;
|
||||
|
@ -202,7 +208,7 @@ export default function ProcessModelEditDiagram() {
|
|||
HttpService.makeCallToBackend({
|
||||
path: url,
|
||||
successCallback: navigateToProcessModelFile,
|
||||
failureCallback: setErrorObject,
|
||||
failureCallback: addError,
|
||||
httpMethod,
|
||||
postBody: formData,
|
||||
});
|
||||
|
@ -251,6 +257,11 @@ export default function ProcessModelEditDiagram() {
|
|||
|
||||
const handleFileNameSave = (event: any) => {
|
||||
event.preventDefault();
|
||||
if (!newFileName) {
|
||||
setProcessModelFileInvalidText('Process Model file name is required.');
|
||||
return;
|
||||
}
|
||||
setProcessModelFileInvalidText('');
|
||||
setShowFileNameEditor(false);
|
||||
saveDiagram(bpmnXmlForDiagramRendering);
|
||||
};
|
||||
|
@ -267,17 +278,32 @@ export default function ProcessModelEditDiagram() {
|
|||
onRequestSubmit={handleFileNameSave}
|
||||
onRequestClose={handleFileNameCancel}
|
||||
>
|
||||
<label>File Name:</label>
|
||||
<span>
|
||||
<input
|
||||
name="file_name"
|
||||
type="text"
|
||||
value={newFileName}
|
||||
onChange={(e) => setNewFileName(e.target.value)}
|
||||
autoFocus
|
||||
/>
|
||||
{fileExtension}
|
||||
</span>
|
||||
<Grid
|
||||
condensed
|
||||
fullWidth
|
||||
className="megacondensed process-model-files-section"
|
||||
>
|
||||
<Column md={4} lg={8} sm={4}>
|
||||
<TextInput
|
||||
id="process_model_file_name"
|
||||
labelText="File Name:"
|
||||
value={newFileName}
|
||||
onChange={(e: any) => setNewFileName(e.target.value)}
|
||||
invalidText={processModelFileInvalidText}
|
||||
invalid={!!processModelFileInvalidText}
|
||||
size="sm"
|
||||
autoFocus
|
||||
/>
|
||||
</Column>
|
||||
<Column
|
||||
md={4}
|
||||
lg={8}
|
||||
sm={4}
|
||||
className="with-top-margin-for-label-next-to-text-input"
|
||||
>
|
||||
{fileExtension}
|
||||
</Column>
|
||||
</Grid>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { useContext, useEffect, useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Link, useNavigate, useParams } from 'react-router-dom';
|
||||
import {
|
||||
Add,
|
||||
|
@ -32,7 +32,8 @@ import {
|
|||
import { Can } from '@casl/react';
|
||||
import ProcessBreadcrumb from '../components/ProcessBreadcrumb';
|
||||
import HttpService from '../services/HttpService';
|
||||
import ErrorContext from '../contexts/ErrorContext';
|
||||
import useAPIError from '../hooks/UseApiError';
|
||||
|
||||
import {
|
||||
getGroupFromModifiedModelId,
|
||||
modifyProcessIdentifierForPathParam,
|
||||
|
@ -52,7 +53,7 @@ import { Notification } from '../components/Notification';
|
|||
|
||||
export default function ProcessModelShow() {
|
||||
const params = useParams();
|
||||
const setErrorObject = (useContext as any)(ErrorContext)[1];
|
||||
const { addError, removeError } = useAPIError();
|
||||
|
||||
const [processModel, setProcessModel] = useState<ProcessModel | null>(null);
|
||||
const [processInstance, setProcessInstance] =
|
||||
|
@ -148,7 +149,7 @@ export default function ProcessModelShow() {
|
|||
!('file_contents' in processModelFile) ||
|
||||
processModelFile.file_contents === undefined
|
||||
) {
|
||||
setErrorObject({
|
||||
addError({
|
||||
message: `Could not file file contents for file: ${processModelFile.name}`,
|
||||
});
|
||||
return;
|
||||
|
@ -169,7 +170,7 @@ export default function ProcessModelShow() {
|
|||
};
|
||||
|
||||
const downloadFile = (fileName: string) => {
|
||||
setErrorObject(null);
|
||||
removeError();
|
||||
const processModelPath = `process-models/${modifiedProcessModelId}`;
|
||||
HttpService.makeCallToBackend({
|
||||
path: `/${processModelPath}/files/${fileName}`,
|
||||
|
@ -374,7 +375,7 @@ export default function ProcessModelShow() {
|
|||
|
||||
const doFileUpload = (event: any) => {
|
||||
event.preventDefault();
|
||||
setErrorObject(null);
|
||||
removeError();
|
||||
const url = `/process-models/${modifiedProcessModelId}/files`;
|
||||
const formData = new FormData();
|
||||
formData.append('file', filesToUpload[0]);
|
||||
|
@ -384,7 +385,7 @@ export default function ProcessModelShow() {
|
|||
successCallback: onUploadedCallback,
|
||||
httpMethod: 'POST',
|
||||
postBody: formData,
|
||||
failureCallback: setErrorObject,
|
||||
failureCallback: addError,
|
||||
});
|
||||
setFilesToUpload(null);
|
||||
};
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { useContext, useEffect, useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import Editor from '@monaco-editor/react';
|
||||
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
|
||||
// @ts-ignore
|
||||
|
@ -8,16 +8,14 @@ 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';
|
||||
|
||||
import useAPIError from '../hooks/UseApiError';
|
||||
// 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 { addError, removeError } = useAPIError();
|
||||
const [showFileNameEditor, setShowFileNameEditor] = useState(false);
|
||||
const [newFileName, setNewFileName] = useState('');
|
||||
const searchParams = useSearchParams()[0];
|
||||
|
@ -87,7 +85,7 @@ export default function ReactFormEditor() {
|
|||
};
|
||||
|
||||
const saveFile = () => {
|
||||
setErrorObject(null);
|
||||
removeError();
|
||||
setDisplaySaveFileMessage(false);
|
||||
|
||||
let url = `/process-models/${modifiedProcessModelId}/files`;
|
||||
|
@ -116,7 +114,7 @@ export default function ReactFormEditor() {
|
|||
HttpService.makeCallToBackend({
|
||||
path: url,
|
||||
successCallback: navigateToProcessModelFile,
|
||||
failureCallback: setErrorObject,
|
||||
failureCallback: addError,
|
||||
httpMethod,
|
||||
postBody: formData,
|
||||
});
|
||||
|
@ -190,6 +188,9 @@ export default function ReactFormEditor() {
|
|||
|
||||
if (processModelFile || !params.file_name) {
|
||||
const processModelFileName = processModelFile ? processModelFile.name : '';
|
||||
const formBuildFileParam = params.file_name
|
||||
? `?file_name=${params.file_name}`
|
||||
: '';
|
||||
return (
|
||||
<main>
|
||||
<ProcessBreadcrumb
|
||||
|
@ -212,19 +213,7 @@ export default function ReactFormEditor() {
|
|||
<Button onClick={saveFile} variant="danger" data-qa="file-save-button">
|
||||
Save
|
||||
</Button>
|
||||
{params.file_name ? null : (
|
||||
<Button
|
||||
onClick={() =>
|
||||
navigate(
|
||||
`/admin/process-models/${params.process_model_id}/form-builder`
|
||||
)
|
||||
}
|
||||
variant="danger"
|
||||
data-qa="form-builder-button"
|
||||
>
|
||||
Form Builder
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{params.file_name ? (
|
||||
<ButtonWithConfirmation
|
||||
data-qa="delete-process-model-file"
|
||||
|
@ -233,6 +222,17 @@ export default function ReactFormEditor() {
|
|||
buttonLabel="Delete"
|
||||
/>
|
||||
) : null}
|
||||
<Button
|
||||
onClick={() =>
|
||||
navigate(
|
||||
`/admin/process-models/${params.process_model_id}/form-builder${formBuildFileParam}`
|
||||
)
|
||||
}
|
||||
variant="danger"
|
||||
data-qa="form-builder-button"
|
||||
>
|
||||
Form Builder
|
||||
</Button>
|
||||
{hasDiagram ? (
|
||||
<Button
|
||||
onClick={() =>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { useContext, useEffect, useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import validator from '@rjsf/validator-ajv8';
|
||||
|
||||
|
@ -12,75 +12,81 @@ import {
|
|||
// @ts-ignore
|
||||
} from '@carbon/react';
|
||||
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import MDEditor from '@uiw/react-md-editor';
|
||||
// eslint-disable-next-line import/no-named-as-default
|
||||
import Form from '../themes/carbon';
|
||||
import HttpService from '../services/HttpService';
|
||||
import ErrorContext from '../contexts/ErrorContext';
|
||||
import useAPIError from '../hooks/UseApiError';
|
||||
import { modifyProcessIdentifierForPathParam } from '../helpers';
|
||||
import { useUriListForPermissions } from '../hooks/UriListForPermissions';
|
||||
import { PermissionsToCheck } from '../interfaces';
|
||||
import { usePermissionFetcher } from '../hooks/PermissionService';
|
||||
import { ProcessInstanceTask } from '../interfaces';
|
||||
|
||||
export default function TaskShow() {
|
||||
const [task, setTask] = useState(null);
|
||||
const [task, setTask] = useState<ProcessInstanceTask | null>(null);
|
||||
const [userTasks, setUserTasks] = useState(null);
|
||||
const params = useParams();
|
||||
const navigate = useNavigate();
|
||||
const [disabled, setDisabled] = useState(false);
|
||||
|
||||
const setErrorObject = (useContext as any)(ErrorContext)[1];
|
||||
|
||||
const { targetUris } = useUriListForPermissions();
|
||||
const permissionRequestData: PermissionsToCheck = {
|
||||
[targetUris.processInstanceTaskListDataPath]: ['GET'],
|
||||
};
|
||||
const { ability, permissionsLoaded } = usePermissionFetcher(
|
||||
permissionRequestData
|
||||
);
|
||||
const { addError, removeError } = useAPIError();
|
||||
|
||||
useEffect(() => {
|
||||
if (permissionsLoaded) {
|
||||
const processResult = (result: any) => {
|
||||
setTask(result);
|
||||
if (ability.can('GET', targetUris.processInstanceTaskListDataPath)) {
|
||||
HttpService.makeCallToBackend({
|
||||
path: `/task-data/${modifyProcessIdentifierForPathParam(
|
||||
result.process_model_identifier
|
||||
)}/${params.process_instance_id}`,
|
||||
successCallback: setUserTasks,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const processResult = (result: ProcessInstanceTask) => {
|
||||
setTask(result);
|
||||
const url = `/task-data/${modifyProcessIdentifierForPathParam(
|
||||
result.process_model_identifier
|
||||
)}/${params.process_instance_id}`;
|
||||
// if user is unauthorized to get task-data then don't do anything
|
||||
// Checking like this so we can dynamically create the url with the correct process model
|
||||
// instead of passing the process model identifier in through the params
|
||||
HttpService.makeCallToBackend({
|
||||
path: `/tasks/${params.process_instance_id}/${params.task_id}`,
|
||||
successCallback: processResult,
|
||||
// This causes the page to continuously reload
|
||||
// failureCallback: setErrorObject,
|
||||
path: url,
|
||||
successCallback: (tasks: any) => {
|
||||
setDisabled(false);
|
||||
setUserTasks(tasks);
|
||||
},
|
||||
onUnauthorized: () => {
|
||||
setDisabled(false);
|
||||
},
|
||||
failureCallback: (error: any) => {
|
||||
addError(error);
|
||||
},
|
||||
});
|
||||
}
|
||||
}, [params, permissionsLoaded, ability, targetUris]);
|
||||
};
|
||||
HttpService.makeCallToBackend({
|
||||
path: `/tasks/${params.process_instance_id}/${params.task_id}`,
|
||||
successCallback: processResult,
|
||||
failureCallback: addError,
|
||||
});
|
||||
// FIXME: not sure what to do about addError. adding it to this array causes the page to endlessly reload
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [params]);
|
||||
|
||||
const processSubmitResult = (result: any) => {
|
||||
setErrorObject(null);
|
||||
removeError();
|
||||
if (result.ok) {
|
||||
navigate(`/tasks`);
|
||||
} else if (result.process_instance_id) {
|
||||
navigate(`/tasks/${result.process_instance_id}/${result.id}`);
|
||||
} else {
|
||||
setErrorObject(`Received unexpected error: ${result.message}`);
|
||||
addError(result);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFormSubmit = (event: any) => {
|
||||
setErrorObject(null);
|
||||
if (disabled) {
|
||||
return;
|
||||
}
|
||||
setDisabled(true);
|
||||
removeError();
|
||||
const dataToSubmit = event.formData;
|
||||
delete dataToSubmit.isManualTask;
|
||||
HttpService.makeCallToBackend({
|
||||
path: `/tasks/${params.process_instance_id}/${params.task_id}`,
|
||||
successCallback: processSubmitResult,
|
||||
failureCallback: setErrorObject,
|
||||
failureCallback: (error: any) => {
|
||||
addError(error);
|
||||
setDisabled(false);
|
||||
},
|
||||
httpMethod: 'PUT',
|
||||
postBody: dataToSubmit,
|
||||
});
|
||||
|
@ -147,6 +153,7 @@ export default function TaskShow() {
|
|||
Object.keys(jsonSchema.properties).forEach((propertyKey: string) => {
|
||||
const propertyMetadata = jsonSchema.properties[propertyKey];
|
||||
if (
|
||||
typeof propertyMetadata === 'object' &&
|
||||
'minimumDate' in propertyMetadata &&
|
||||
propertyMetadata.minimumDate === 'today'
|
||||
) {
|
||||
|
@ -161,17 +168,28 @@ export default function TaskShow() {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// recurse through all nested properties as well
|
||||
getFieldsWithDateValidations(
|
||||
propertyMetadata,
|
||||
formData[propertyKey],
|
||||
errors[propertyKey]
|
||||
);
|
||||
});
|
||||
}
|
||||
return errors;
|
||||
};
|
||||
|
||||
const formElement = (taskToUse: any) => {
|
||||
const formElement = () => {
|
||||
if (!task) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let formUiSchema;
|
||||
let taskData = taskToUse.data;
|
||||
let jsonSchema = taskToUse.form_schema;
|
||||
let taskData = task.data;
|
||||
let jsonSchema = task.form_schema;
|
||||
let reactFragmentToHideSubmitButton = null;
|
||||
if (taskToUse.type === 'Manual Task') {
|
||||
if (task.type === 'Manual Task') {
|
||||
taskData = {};
|
||||
jsonSchema = {
|
||||
type: 'object',
|
||||
|
@ -189,10 +207,10 @@ export default function TaskShow() {
|
|||
'ui:widget': 'hidden',
|
||||
},
|
||||
};
|
||||
} else if (taskToUse.form_ui_schema) {
|
||||
formUiSchema = JSON.parse(taskToUse.form_ui_schema);
|
||||
} else if (task.form_ui_schema) {
|
||||
formUiSchema = task.form_ui_schema;
|
||||
}
|
||||
if (taskToUse.state !== 'READY') {
|
||||
if (task.state !== 'READY') {
|
||||
formUiSchema = Object.assign(formUiSchema || {}, {
|
||||
'ui:readonly': true,
|
||||
});
|
||||
|
@ -204,10 +222,16 @@ export default function TaskShow() {
|
|||
reactFragmentToHideSubmitButton = <div />;
|
||||
}
|
||||
|
||||
if (taskToUse.type === 'Manual Task' && taskToUse.state === 'READY') {
|
||||
if (task.state === 'READY') {
|
||||
let buttonText = 'Submit';
|
||||
if (task.type === 'Manual Task') {
|
||||
buttonText = 'Continue';
|
||||
}
|
||||
reactFragmentToHideSubmitButton = (
|
||||
<div>
|
||||
<Button type="submit">Continue</Button>
|
||||
<Button type="submit" disabled={disabled}>
|
||||
{buttonText}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -218,8 +242,9 @@ export default function TaskShow() {
|
|||
|
||||
return (
|
||||
<Grid fullWidth condensed>
|
||||
<Column md={5} lg={8} sm={4}>
|
||||
<Column sm={4} md={5} lg={8}>
|
||||
<Form
|
||||
disabled={disabled}
|
||||
formData={taskData}
|
||||
onSubmit={handleFormSubmit}
|
||||
schema={jsonSchema}
|
||||
|
@ -234,36 +259,35 @@ export default function TaskShow() {
|
|||
);
|
||||
};
|
||||
|
||||
const instructionsElement = (taskToUse: any) => {
|
||||
const instructionsElement = () => {
|
||||
if (!task) {
|
||||
return null;
|
||||
}
|
||||
let instructions = '';
|
||||
if (taskToUse.properties.instructionsForEndUser) {
|
||||
instructions = taskToUse.properties.instructionsForEndUser;
|
||||
if (task.properties.instructionsForEndUser) {
|
||||
instructions = task.properties.instructionsForEndUser;
|
||||
}
|
||||
return (
|
||||
<div className="markdown">
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]}>
|
||||
{instructions}
|
||||
</ReactMarkdown>
|
||||
<MDEditor.Markdown source={instructions} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
if (task) {
|
||||
const taskToUse = task as any;
|
||||
let statusString = '';
|
||||
if (taskToUse.state !== 'READY') {
|
||||
statusString = ` ${taskToUse.state}`;
|
||||
if (task.state !== 'READY') {
|
||||
statusString = ` ${task.state}`;
|
||||
}
|
||||
|
||||
return (
|
||||
<main>
|
||||
<div>{buildTaskNavigation()}</div>
|
||||
<h3>
|
||||
Task: {taskToUse.title} ({taskToUse.process_model_display_name})
|
||||
{statusString}
|
||||
Task: {task.title} ({task.process_model_display_name}){statusString}
|
||||
</h3>
|
||||
{instructionsElement(taskToUse)}
|
||||
{formElement(taskToUse)}
|
||||
{instructionsElement()}
|
||||
{formElement()}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ type backendCallProps = {
|
|||
path: string;
|
||||
successCallback: Function;
|
||||
failureCallback?: Function;
|
||||
onUnauthorized?: Function;
|
||||
httpMethod?: string;
|
||||
extraHeaders?: object;
|
||||
postBody?: any;
|
||||
|
@ -37,6 +38,7 @@ const makeCallToBackend = ({
|
|||
path,
|
||||
successCallback,
|
||||
failureCallback,
|
||||
onUnauthorized,
|
||||
httpMethod = 'GET',
|
||||
extraHeaders = {},
|
||||
postBody = {},
|
||||
|
@ -88,9 +90,13 @@ backendCallProps) => {
|
|||
if (isSuccessful) {
|
||||
successCallback(result);
|
||||
} else if (is403) {
|
||||
// Hopefully we can make this service a hook and use the error message context directly
|
||||
// eslint-disable-next-line no-alert
|
||||
alert(result.message);
|
||||
if (onUnauthorized) {
|
||||
onUnauthorized(result);
|
||||
} else {
|
||||
// Hopefully we can make this service a hook and use the error message context directly
|
||||
// eslint-disable-next-line no-alert
|
||||
alert(result.message);
|
||||
}
|
||||
} else {
|
||||
let message = 'A server error occurred.';
|
||||
if (result.message) {
|
||||
|
|
|
@ -1,17 +1,36 @@
|
|||
import React from 'react';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import { IconButtonProps } from '@rjsf/utils';
|
||||
import {
|
||||
FormContextType,
|
||||
IconButtonProps,
|
||||
RJSFSchema,
|
||||
StrictRJSFSchema,
|
||||
} from '@rjsf/utils';
|
||||
|
||||
const AddButton: React.ComponentType<IconButtonProps> = ({
|
||||
uiSchema,
|
||||
...props
|
||||
}) => {
|
||||
// @ts-ignore
|
||||
import { AddAlt } from '@carbon/icons-react';
|
||||
|
||||
import IconButton from '../IconButton/IconButton';
|
||||
|
||||
/** The `AddButton` renders a button that represent the `Add` action on a form
|
||||
*/
|
||||
export default function AddButton<
|
||||
T = any,
|
||||
S extends StrictRJSFSchema = RJSFSchema,
|
||||
F extends FormContextType = any
|
||||
>({ className, onClick, disabled, registry }: IconButtonProps<T, S, F>) {
|
||||
return (
|
||||
<IconButton title="Add Item" {...props} color="primary">
|
||||
<AddIcon />
|
||||
</IconButton>
|
||||
<div className="row">
|
||||
<p className={`col-xs-3 col-xs-offset-9 text-right ${className}`}>
|
||||
<IconButton
|
||||
iconType="info"
|
||||
icon="plus"
|
||||
className="btn-add col-xs-12"
|
||||
title="Add"
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
registry={registry}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddButton;
|
||||
}
|
||||
|
|
|
@ -5,6 +5,11 @@ import {
|
|||
RJSFSchema,
|
||||
StrictRJSFSchema,
|
||||
} from '@rjsf/utils';
|
||||
import {
|
||||
Grid,
|
||||
Column,
|
||||
// @ts-ignore
|
||||
} from '@carbon/react';
|
||||
|
||||
/** The `ArrayFieldItemTemplate` component is the template used to render an items of an array.
|
||||
*
|
||||
|
@ -33,53 +38,57 @@ export default function ArrayFieldItemTemplate<
|
|||
const { MoveDownButton, MoveUpButton, RemoveButton } =
|
||||
registry.templates.ButtonTemplates;
|
||||
const btnStyle: CSSProperties = {
|
||||
flex: 1,
|
||||
paddingLeft: 6,
|
||||
paddingRight: 6,
|
||||
fontWeight: 'bold',
|
||||
marginBottom: '0.5em',
|
||||
};
|
||||
const mainColumnWidthSmall = 3;
|
||||
const mainColumnWidthMedium = 4;
|
||||
const mainColumnWidthLarge = 7;
|
||||
return (
|
||||
<div className={className}>
|
||||
<div className={hasToolbar ? 'col-xs-9' : 'col-xs-12'}>{children}</div>
|
||||
{hasToolbar && (
|
||||
<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 condensed fullWidth>
|
||||
<Column
|
||||
sm={mainColumnWidthSmall}
|
||||
md={mainColumnWidthMedium}
|
||||
lg={mainColumnWidthLarge}
|
||||
>
|
||||
{children}
|
||||
</Column>
|
||||
{hasToolbar && (
|
||||
<Column sm={1} md={1} lg={1}>
|
||||
<div className="array-item-toolbox">
|
||||
<div className="NOT-btn-group">
|
||||
{(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>
|
||||
</Column>
|
||||
)}
|
||||
</Grid>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -85,6 +85,11 @@ export default function BaseInputTemplate<
|
|||
labelToUse = `${labelToUse}*`;
|
||||
}
|
||||
|
||||
let helperText = null;
|
||||
if (uiSchema && uiSchema['ui:help']) {
|
||||
helperText = uiSchema['ui:help'];
|
||||
}
|
||||
|
||||
let invalid = false;
|
||||
let errorMessageForField = null;
|
||||
if (rawErrors && rawErrors.length > 0) {
|
||||
|
@ -101,8 +106,8 @@ export default function BaseInputTemplate<
|
|||
<TextInput
|
||||
id={id}
|
||||
name={id}
|
||||
className="input"
|
||||
labelText={labelToUse}
|
||||
className="text-input"
|
||||
helperText={helperText}
|
||||
invalid={invalid}
|
||||
invalidText={errorMessageForField}
|
||||
autoFocus={autofocus}
|
||||
|
|
|
@ -5,8 +5,8 @@ import { Tag } from '@carbon/react';
|
|||
function ErrorList({ errors }: ErrorListProps) {
|
||||
if (errors) {
|
||||
return (
|
||||
<Tag type="red" size="md" title="Fill Required Fields">
|
||||
Please fill out required fields
|
||||
<Tag type="red" size="md" title="Fix validation issues">
|
||||
Some fields are invalid. Please correct them before submitting the form.
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -7,10 +7,8 @@ import FormHelperText from '@mui/material/FormHelperText';
|
|||
* @param props - The `FieldHelpProps` to be rendered
|
||||
*/
|
||||
export default function FieldHelpTemplate(props: FieldHelpProps) {
|
||||
const { idSchema, help } = props;
|
||||
if (!help) {
|
||||
return null;
|
||||
}
|
||||
const id = `${idSchema.$id}__help`;
|
||||
return <FormHelperText id={id}>{help}</FormHelperText>;
|
||||
// ui:help is handled by helperText in all carbon widgets.
|
||||
// see BaseInputTemplate/BaseInputTemplate.tsx and
|
||||
// SelectWidget/SelectWidget.tsx
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -1,64 +1,57 @@
|
|||
import React from 'react';
|
||||
import FormControl from '@mui/material/FormControl';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { FieldTemplateProps, getTemplate, getUiOptions } from '@rjsf/utils';
|
||||
import {
|
||||
FieldTemplateProps,
|
||||
FormContextType,
|
||||
RJSFSchema,
|
||||
StrictRJSFSchema,
|
||||
getTemplate,
|
||||
getUiOptions,
|
||||
} from '@rjsf/utils';
|
||||
|
||||
function FieldTemplate({
|
||||
id,
|
||||
children,
|
||||
classNames,
|
||||
disabled,
|
||||
displayLabel,
|
||||
hidden,
|
||||
label,
|
||||
onDropPropertyClick,
|
||||
onKeyChange,
|
||||
readonly,
|
||||
required,
|
||||
rawErrors = [],
|
||||
errors,
|
||||
help,
|
||||
rawDescription,
|
||||
schema,
|
||||
uiSchema,
|
||||
registry,
|
||||
}: FieldTemplateProps) {
|
||||
const uiOptions = getUiOptions(uiSchema);
|
||||
const WrapIfAdditionalTemplate = getTemplate<'WrapIfAdditionalTemplate'>(
|
||||
'WrapIfAdditionalTemplate',
|
||||
import Label from './Label';
|
||||
|
||||
/** The `FieldTemplate` component is the template used by `SchemaField` to render any field. It renders the field
|
||||
* content, (label, description, children, errors and help) inside of a `WrapIfAdditional` component.
|
||||
*
|
||||
* @param props - The `FieldTemplateProps` for this component
|
||||
*/
|
||||
export default function FieldTemplate<
|
||||
T = any,
|
||||
S extends StrictRJSFSchema = RJSFSchema,
|
||||
F extends FormContextType = any
|
||||
>(props: FieldTemplateProps<T, S, F>) {
|
||||
const {
|
||||
id,
|
||||
label,
|
||||
children,
|
||||
errors,
|
||||
help,
|
||||
description,
|
||||
hidden,
|
||||
required,
|
||||
displayLabel,
|
||||
registry,
|
||||
uiOptions
|
||||
);
|
||||
|
||||
uiSchema,
|
||||
} = props;
|
||||
const uiOptions = getUiOptions(uiSchema);
|
||||
const WrapIfAdditionalTemplate = getTemplate<
|
||||
'WrapIfAdditionalTemplate',
|
||||
T,
|
||||
S,
|
||||
F
|
||||
>('WrapIfAdditionalTemplate', registry, uiOptions);
|
||||
if (hidden) {
|
||||
return <div style={{ display: 'none' }}>{children}</div>;
|
||||
return <div className="hidden">{children}</div>;
|
||||
}
|
||||
return (
|
||||
<WrapIfAdditionalTemplate
|
||||
classNames={classNames}
|
||||
disabled={disabled}
|
||||
id={id}
|
||||
label={label}
|
||||
onDropPropertyClick={onDropPropertyClick}
|
||||
onKeyChange={onKeyChange}
|
||||
readonly={readonly}
|
||||
required={required}
|
||||
schema={schema}
|
||||
uiSchema={uiSchema}
|
||||
registry={registry}
|
||||
>
|
||||
<FormControl fullWidth error={!!rawErrors.length} required={required}>
|
||||
<div className="rjsf-field">
|
||||
<WrapIfAdditionalTemplate {...props}>
|
||||
{displayLabel && <Label label={label} required={required} id={id} />}
|
||||
{displayLabel && description ? description : null}
|
||||
{children}
|
||||
{displayLabel && rawDescription ? (
|
||||
<Typography variant="caption" color="textSecondary">
|
||||
{rawDescription}
|
||||
</Typography>
|
||||
) : null}
|
||||
{errors}
|
||||
{help}
|
||||
</FormControl>
|
||||
</WrapIfAdditionalTemplate>
|
||||
</WrapIfAdditionalTemplate>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default FieldTemplate;
|
||||
|
|
|
@ -1,55 +1,96 @@
|
|||
import React from 'react';
|
||||
import IconButton, {
|
||||
IconButtonProps as MuiIconButtonProps,
|
||||
} from '@mui/material/IconButton';
|
||||
import ArrowDownwardIcon from '@mui/icons-material/ArrowDownward';
|
||||
import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward';
|
||||
import RemoveIcon from '@mui/icons-material/Remove';
|
||||
import { IconButtonProps } from '@rjsf/utils';
|
||||
import {
|
||||
FormContextType,
|
||||
IconButtonProps,
|
||||
RJSFSchema,
|
||||
StrictRJSFSchema,
|
||||
} from '@rjsf/utils';
|
||||
|
||||
export default function MuiIconButton(props: IconButtonProps) {
|
||||
const { icon, color, uiSchema, ...otherProps } = props;
|
||||
// @ts-ignore
|
||||
import { Add, TrashCan, ArrowUp, ArrowDown } from '@carbon/icons-react';
|
||||
|
||||
export default function IconButton<
|
||||
T = any,
|
||||
S extends StrictRJSFSchema = RJSFSchema,
|
||||
F extends FormContextType = any
|
||||
>(props: IconButtonProps<T, S, F>) {
|
||||
const {
|
||||
iconType = 'default',
|
||||
icon,
|
||||
className,
|
||||
uiSchema,
|
||||
registry,
|
||||
...otherProps
|
||||
} = props;
|
||||
// icon string optios: plus, remove, arrow-up, arrow-down
|
||||
let carbonIcon = (
|
||||
<p>
|
||||
Add new <Add />
|
||||
</p>
|
||||
);
|
||||
if (icon === 'remove') {
|
||||
carbonIcon = <TrashCan />;
|
||||
}
|
||||
if (icon === 'arrow-up') {
|
||||
carbonIcon = <ArrowUp />;
|
||||
}
|
||||
if (icon === 'arrow-down') {
|
||||
carbonIcon = <ArrowDown />;
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={`btn btn-${iconType} ${className}`}
|
||||
{...otherProps}
|
||||
>
|
||||
{carbonIcon}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
export function MoveDownButton<
|
||||
T = any,
|
||||
S extends StrictRJSFSchema = RJSFSchema,
|
||||
F extends FormContextType = any
|
||||
>(props: IconButtonProps<T, S, F>) {
|
||||
return (
|
||||
<IconButton
|
||||
{...otherProps}
|
||||
size="small"
|
||||
color={color as MuiIconButtonProps['color']}
|
||||
>
|
||||
{icon}
|
||||
</IconButton>
|
||||
);
|
||||
}
|
||||
|
||||
export function MoveDownButton(props: IconButtonProps) {
|
||||
return (
|
||||
<MuiIconButton
|
||||
title="Move down"
|
||||
className="array-item-move-down"
|
||||
{...props}
|
||||
icon={<ArrowDownwardIcon fontSize="small" />}
|
||||
icon="arrow-down"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function MoveUpButton(props: IconButtonProps) {
|
||||
export function MoveUpButton<
|
||||
T = any,
|
||||
S extends StrictRJSFSchema = RJSFSchema,
|
||||
F extends FormContextType = any
|
||||
>(props: IconButtonProps<T, S, F>) {
|
||||
return (
|
||||
<MuiIconButton
|
||||
<IconButton
|
||||
title="Move up"
|
||||
className="array-item-move-up"
|
||||
{...props}
|
||||
icon={<ArrowUpwardIcon fontSize="small" />}
|
||||
icon="arrow-up"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function RemoveButton(props: IconButtonProps) {
|
||||
const { iconType, ...otherProps } = props;
|
||||
export function RemoveButton<
|
||||
T = any,
|
||||
S extends StrictRJSFSchema = RJSFSchema,
|
||||
F extends FormContextType = any
|
||||
>(props: IconButtonProps<T, S, F>) {
|
||||
return (
|
||||
<MuiIconButton
|
||||
<IconButton
|
||||
title="Remove"
|
||||
{...otherProps}
|
||||
color="error"
|
||||
icon={
|
||||
<RemoveIcon fontSize={iconType === 'default' ? undefined : 'small'} />
|
||||
}
|
||||
className="array-item-remove"
|
||||
{...props}
|
||||
iconType="danger"
|
||||
icon="remove"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -32,9 +32,6 @@ const RadioWidget = ({
|
|||
|
||||
return (
|
||||
<>
|
||||
<FormLabel required={required} htmlFor={id}>
|
||||
{label || schema.title}
|
||||
</FormLabel>
|
||||
<RadioGroup
|
||||
id={id}
|
||||
name={id}
|
||||
|
|
|
@ -41,6 +41,10 @@ function SelectWidget({
|
|||
} else if (schema && schema.title) {
|
||||
labelToUse = schema.title;
|
||||
}
|
||||
let helperText = null;
|
||||
if (uiSchema && uiSchema['ui:help']) {
|
||||
helperText = uiSchema['ui:help'];
|
||||
}
|
||||
if (required) {
|
||||
labelToUse = `${labelToUse}*`;
|
||||
}
|
||||
|
@ -49,16 +53,20 @@ function SelectWidget({
|
|||
let errorMessageForField = null;
|
||||
if (rawErrors && rawErrors.length > 0) {
|
||||
invalid = true;
|
||||
errorMessageForField = `${labelToUse.replace(/\*$/, '')} ${rawErrors[0]}`;
|
||||
// errorMessageForField = `${labelToUse.replace(/\*$/, '')} ${rawErrors[0]}`;
|
||||
errorMessageForField = rawErrors[0];
|
||||
}
|
||||
|
||||
// maybe use placeholder somehow. it was previously jammed into the helperText field,
|
||||
// but allowing ui:help to grab that spot seems much more appropriate.
|
||||
|
||||
return (
|
||||
<Select
|
||||
id={id}
|
||||
name={id}
|
||||
labelText={labelToUse}
|
||||
labelText=""
|
||||
select
|
||||
helperText={placeholder}
|
||||
helperText={helperText}
|
||||
value={typeof value === 'undefined' ? emptyValue : value}
|
||||
disabled={disabled || readonly}
|
||||
autoFocus={autofocus}
|
||||
|
|
|
@ -61,20 +61,26 @@ function TextareaWidget<
|
|||
labelToUse = `${labelToUse}*`;
|
||||
}
|
||||
|
||||
let helperText = null;
|
||||
if (uiSchema && uiSchema['ui:help']) {
|
||||
helperText = uiSchema['ui:help'];
|
||||
}
|
||||
|
||||
let invalid = false;
|
||||
let errorMessageForField = null;
|
||||
if (rawErrors && rawErrors.length > 0) {
|
||||
invalid = true;
|
||||
errorMessageForField = `${labelToUse.replace(/\*$/, '')} ${rawErrors[0]}`;
|
||||
errorMessageForField = rawErrors[0];
|
||||
}
|
||||
|
||||
return (
|
||||
<TextArea
|
||||
id={id}
|
||||
name={id}
|
||||
className="form-control"
|
||||
className="text-input"
|
||||
helperText={helperText}
|
||||
value={value || ''}
|
||||
labelText={labelToUse}
|
||||
labelText=""
|
||||
placeholder={placeholder}
|
||||
required={required}
|
||||
disabled={disabled}
|
||||
|
|
|
@ -1,7 +1,3 @@
|
|||
button.react-json-schema-form-submit-button {
|
||||
margin-top: 1.5em;
|
||||
}
|
||||
|
||||
.rjsf .header {
|
||||
font-weight: 400;
|
||||
font-size: 20px;
|
||||
|
@ -17,6 +13,15 @@ button.react-json-schema-form-submit-button {
|
|||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.rjsf .input {
|
||||
/* for some reason it wraps the entire form using FieldTemplate.jsx, which is where we added the rjsf-field thing (which is only intended for fields, not entire forms. hence the double rjsf-field reference, only for rjsf-fields inside rjsf-fields, so we don't get double margin after the last field */
|
||||
.rjsf .rjsf-field .rjsf-field {
|
||||
margin-bottom: 2em;
|
||||
}
|
||||
|
||||
.array-item-toolbox {
|
||||
margin-left: 2em;
|
||||
}
|
||||
|
||||
.rjsf .text-input {
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue