Squashed 'spiffworkflow-frontend/' changes from c096ca3f9..326040b3c

326040b3c Frontend label changes (#54)
b4785077a added some more widget types to the form builder. w/ burnettk
27333e941 added basic form builder. w/ burnettk
f2e9ad2d6 pass the the correct model to id on create
92f6366a0 removed tabs from taskshow page w/ burnettk cullerton
bb146410f fixed some of the form widgets w/ burnettk cullerton
00db85342 Merge branch 'main' of github.com:sartography/spiff-arena
084069c72 added create new instance page and moved model run to a component w/ burnettk cullerton
4ba973b55 Merge branch 'main' into update-workflow-json
83b795a4e fixed eslint issues w/ burnettk
44920037c ran some pyl w/ burnettk cullerton
3c8ba0e89 load diagram page even if processes are not set w/ burnettk cullerton
4cdea6bc8 moved model delete button to show page as an icon w/ burnettk
aa2b5b9ff moved group list tiles to component and use on group show page w/ burnettk
61366c3d6 display groups as tiles on list page w/ burnettk
0282dde54 add simple refresh capability
482e6f425 upgrades
ec3019706 hide messages and configuration if not authorized w/ burnettk
ef3a5ff21 default time to midnight if it is null when filtering w/ burnettk
697c069c1 Merge pull request #48 from sartography/feature/add_times_to_instance_filter
c4daaeb1c added back the useEffect for report filtering w/ burnettk
a01f9db11 times on instance list table are working now w/ burnettk
38f1b58fc attempting to use date objects as the date states w/ burnettk
f430fa274 oops, just for one column
6ee14c93c just use the word id in the header and add tooltip
5dbcda2d3 a little cleanup to forms w/ burnettk
50429bc3d remove br since it felt like too much margin
f18435dd0 revert to working mui radio
6c3e59d65 notes about validation issue
abf6caafd add inline errors
7bf2b0673 Merge pull request #45 from sartography/feature/form_carbon_theme
6b9e77711 fixed error messaging a little bit for forms w/ burnettk cullerton
e3ba944f0 Merge pull request #43 from sartography/send_filters
eda32f7a8 Searching for call activities seems to be working now. I had the clear_caches in the wrong place previously - fixing.
cc52a5577 Pre-pr cleanup
e0af333d3 lint
c7e4feab1 Clear/remove filter works
2346a5d34 some updates for the carbon form theme w/ burnettk cullerton
8e4bedef3 added eslintignore file to ignore carbon theme for now w/ burnettk cullerton
1a63f312a added radio buttons w/ burnettk cullerton
5cf910d55 Minor tweak, in the hopes of getting a text box to update correctly.
035ab5600 Add flag to indicate if user filtered
9449bfce7 specify onRequestClose on modals w/ burnettk cullerton
951c53efd updated a couple form components to work with carbon w/ burnettk cullerton
341731487 support ts and js for lint and format commands w/ burnettk cullerton
ab089da90 copied mui theme to use as base for carbon theme w/ burnettk cullerton
c338e35f1 theme working with mui from the internet w/ burnettk cullerton
925790131 Merge branch 'main' of github.com:sartography/spiff-arena into send_filters
f390f82be Set process model from filter
d76bb34df attempting to add a theme w/ burnettk cullerton
18ccdbbcf added lint fix for frontend to pyl w/ burnettk cullerton
20b3c0c7b added development permission for test user w/ burnettk cullerton
8714a0ae2 Set status from filters
a684a71fc added some permissions for tasks
f6c526ca7 Set date filters from response
0d198c73a docs
35cdb2f33 add cypress grep
c57ae7772 added some permissions to the process model show page w/ burnettk
b5d2109b8 use id_for_file_path when using the process model id as a path for windows and added some more permission stuff to the frontend w/ burnettk
2ce445fa3 added permission service to frontend to allow checking for permissions w/ burnettk
225755493 fixed linting issues w/ burnettk
4d6fc0a82 added configuration nav item to help reduce nav items w/ burnettk
f0a5b4142 Navigate to my task (#35)
fc2a3d451 Merge pull request #36 from sartography/feature/call_activity_selection
ed9ec1547 Merge remote-tracking branch 'origin/main' into feature/call_activity_selection
56adc8118 some minor updates to model show page w/ burnettk
31d3b947d more refactoring for process instance list w/ burnettk
a4108a25e turned the table list route into a table component w/ burnettk
75a198995 refactored pagination table to allow prefixing page options w/ burnettk
14414079a Mostly a name change from BpmnProcessIdLookup to SpecReferenceCache.  I landed on this unfortunate name because:
902c3b7e3 added remaining task tables w/ burnettk
c4e2cf460 added message correlations to message instance list api call w/ burnettk
8866d6d00 i think this is not always truthy, eslint
fc5d8bc8e open accordion by default per feedback
19942b878 dedup
c822ecea6 fix a couple tests
3d2578b7a Merge remote-tracking branch 'origin/main' into feature/home_page_redesign
af8f245ce lint
43dabb65f add the username to the task list w/ burnettk
6d598978b added more task tables w/ burnettk
f4edf3754 added tasks for my open processes page w/ burnettk
1e063bf06 some more task tab play
d0e2e3b64 Merge remote-tracking branch 'origin/main' into feature/home_page_redesign
968acbc99 added home page routes and some tab stuff w/ burnettk

git-subtree-dir: spiffworkflow-frontend
git-subtree-split: 326040b3cb9cd16faeea545a25a91ed41f4340cf
This commit is contained in:
burnettk 2022-11-20 19:57:17 -05:00
parent e10bc47851
commit b198341b00
113 changed files with 6261 additions and 1416 deletions

1
.eslintignore Normal file
View File

@ -0,0 +1 @@
/src/themes/carbon

View File

@ -36,6 +36,7 @@ module.exports = {
], ],
'react/react-in-jsx-scope': 'off', 'react/react-in-jsx-scope': 'off',
'react/require-default-props': 'off', 'react/require-default-props': 'off',
'import/prefer-default-export': 'off',
'no-unused-vars': [ 'no-unused-vars': [
'error', 'error',
{ {

View File

@ -7,6 +7,9 @@ function error_handler() {
trap 'error_handler ${LINENO} $?' ERR trap 'error_handler ${LINENO} $?' ERR
set -o errtrace -o errexit -o nounset -o pipefail set -o errtrace -o errexit -o nounset -o pipefail
# see also: npx cypress run --env grep="can filter",grepFilterSpecs=true
# https://github.com/cypress-io/cypress/tree/develop/npm/grep#pre-filter-specs-grepfilterspecs
command="${1:-}" command="${1:-}"
if [[ -z "$command" ]]; then if [[ -z "$command" ]]; then
command=open command=open

View File

@ -6,8 +6,9 @@ module.exports = defineConfig({
chromeWebSecurity: false, chromeWebSecurity: false,
e2e: { e2e: {
baseUrl: 'http://localhost:7001', baseUrl: 'http://localhost:7001',
setupNodeEvents(_on, _config) { setupNodeEvents(_on, config) {
// implement node event listeners here require('@cypress/grep/src/plugin')(config);
return config;
}, },
}, },

View File

@ -169,14 +169,13 @@ describe('process-instances', () => {
cy.getBySel('process-instance-list-link').click(); cy.getBySel('process-instance-list-link').click();
cy.assertAtLeastOneItemInPaginatedResults(); cy.assertAtLeastOneItemInPaginatedResults();
const statusSelect = '#process-instance-status-select';
PROCESS_STATUSES.forEach((processStatus) => { PROCESS_STATUSES.forEach((processStatus) => {
if (!['all', 'waiting'].includes(processStatus)) { if (!['all', 'waiting'].includes(processStatus)) {
cy.get('#process-instance-status-select').click(); cy.get(statusSelect).click();
cy.get('#process-instance-status-select') cy.get(statusSelect).contains(processStatus).click();
.contains(processStatus)
.click();
// close the dropdown again // close the dropdown again
cy.get('#process-instance-status-select').click(); cy.get(statusSelect).click();
cy.getBySel('filter-button').click(); cy.getBySel('filter-button').click();
cy.assertAtLeastOneItemInPaginatedResults(); cy.assertAtLeastOneItemInPaginatedResults();
cy.getBySel(`process-instance-status-${processStatus}`).contains( cy.getBySel(`process-instance-status-${processStatus}`).contains(

View File

@ -144,10 +144,11 @@ describe('process-models', () => {
cy.getBySel('process-instance-list-link').click(); cy.getBySel('process-instance-list-link').click();
cy.getBySel('process-instance-show-link').click(); cy.getBySel('process-instance-show-link').click();
cy.contains('Delete').click(); cy.getBySel('process-instance-delete').click();
cy.contains('Are you sure'); cy.contains('Are you sure');
cy.getBySel('modal-confirmation-dialog').find('.cds--btn--danger').click(); cy.getBySel('modal-confirmation-dialog').find('.cds--btn--danger').click();
cy.contains(`Process Instances for: ${groupId}/${modelId}`);
// in breadcrumb
cy.contains(modelId).click(); cy.contains(modelId).click();
cy.contains('Edit process model').click(); cy.contains('Edit process model').click();

View File

@ -16,5 +16,9 @@
// Import commands.js using ES2015 syntax: // Import commands.js using ES2015 syntax:
import './commands'; import './commands';
import registerCypressGrep from '@cypress/grep';
registerCypressGrep();
// Alternatively you can use CommonJS syntax: // Alternatively you can use CommonJS syntax:
// require('./commands') // require('./commands')

2215
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -9,9 +9,16 @@
"@carbon/icons-react": "^11.10.0", "@carbon/icons-react": "^11.10.0",
"@carbon/react": "^1.16.0", "@carbon/react": "^1.16.0",
"@carbon/styles": "^1.16.0", "@carbon/styles": "^1.16.0",
"@casl/ability": "^6.3.2",
"@casl/react": "^3.1.0",
"@ginkgo-bioworks/react-json-schema-form-builder": "^2.9.0", "@ginkgo-bioworks/react-json-schema-form-builder": "^2.9.0",
"@monaco-editor/react": "^4.4.5", "@monaco-editor/react": "^4.4.5",
"@rjsf/core": "^4.2.0", "@mui/material": "^5.10.14",
"@react-icons/all-files": "^4.1.0",
"@rjsf/core": "*",
"@rjsf/mui": "^5.0.0-beta.13",
"@rjsf/utils": "^5.0.0-beta.13",
"@rjsf/validator-ajv6": "^5.0.0-beta.13",
"@tanstack/react-table": "^8.2.2", "@tanstack/react-table": "^8.2.2",
"@testing-library/jest-dom": "^5.16.4", "@testing-library/jest-dom": "^5.16.4",
"@testing-library/react": "^13.3.0", "@testing-library/react": "^13.3.0",
@ -71,9 +78,9 @@
"test": "react-scripts test --coverage", "test": "react-scripts test --coverage",
"t": "npm test -- --watchAll=false", "t": "npm test -- --watchAll=false",
"eject": "craco eject", "eject": "craco eject",
"format": "prettier --write src/**/*.js{,x}", "format": "prettier --write src/**/*.[tj]s{,x}",
"lint": "./node_modules/.bin/eslint src *.js", "lint": "./node_modules/.bin/eslint src",
"lint:fix": "./node_modules/.bin/eslint --fix src *.js" "lint:fix": "./node_modules/.bin/eslint --fix src"
}, },
"eslintConfig": { "eslintConfig": {
"extends": [ "extends": [
@ -94,6 +101,7 @@
] ]
}, },
"devDependencies": { "devDependencies": {
"@cypress/grep": "^3.1.0",
"@typescript-eslint/eslint-plugin": "^5.30.5", "@typescript-eslint/eslint-plugin": "^5.30.5",
"@typescript-eslint/parser": "^5.30.6", "@typescript-eslint/parser": "^5.30.6",
"cypress": "^10.8.0", "cypress": "^10.8.0",

View File

@ -3,15 +3,17 @@ import { useMemo, useState } from 'react';
import { Content } from '@carbon/react'; import { Content } from '@carbon/react';
import { BrowserRouter, Routes, Route } from 'react-router-dom'; import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { defineAbility } from '@casl/ability';
import ErrorContext from './contexts/ErrorContext'; import ErrorContext from './contexts/ErrorContext';
import NavigationBar from './components/NavigationBar'; import NavigationBar from './components/NavigationBar';
import HomePage from './routes/HomePage'; import HomePageRoutes from './routes/HomePageRoutes';
import TaskShow from './routes/TaskShow';
import ErrorBoundary from './components/ErrorBoundary'; import ErrorBoundary from './components/ErrorBoundary';
import AdminRoutes from './routes/AdminRoutes'; import AdminRoutes from './routes/AdminRoutes';
import { ErrorForDisplay } from './interfaces'; import { ErrorForDisplay } from './interfaces';
import { AbilityContext } from './contexts/Can';
export default function App() { export default function App() {
const [errorMessage, setErrorMessage] = useState<ErrorForDisplay | null>( const [errorMessage, setErrorMessage] = useState<ErrorForDisplay | null>(
null null
@ -22,6 +24,8 @@ export default function App() {
[errorMessage] [errorMessage]
); );
const ability = defineAbility(() => {});
let errorTag = null; let errorTag = null;
if (errorMessage) { if (errorMessage) {
let sentryLinkTag = null; let sentryLinkTag = null;
@ -47,29 +51,24 @@ export default function App() {
return ( return (
<div className="cds--white"> <div className="cds--white">
<ErrorContext.Provider value={errorContextValueArray}> {/* @ts-ignore */}
<BrowserRouter> <AbilityContext.Provider value={ability}>
<NavigationBar /> <ErrorContext.Provider value={errorContextValueArray}>
<Content> <BrowserRouter>
{errorTag} <NavigationBar />
<ErrorBoundary> <Content>
<Routes> {errorTag}
<Route path="/" element={<HomePage />} /> <ErrorBoundary>
<Route path="/tasks" element={<HomePage />} /> <Routes>
<Route path="/admin/*" element={<AdminRoutes />} /> <Route path="/*" element={<HomePageRoutes />} />
<Route <Route path="/tasks/*" element={<HomePageRoutes />} />
path="/tasks/:process_instance_id/:task_id" <Route path="/admin/*" element={<AdminRoutes />} />
element={<TaskShow />} </Routes>
/> </ErrorBoundary>
<Route </Content>
path="/tasks/:process_instance_id/:task_id" </BrowserRouter>
element={<TaskShow />} </ErrorContext.Provider>
/> </AbilityContext.Provider>
</Routes>
</ErrorBoundary>
</Content>
</BrowserRouter>
</ErrorContext.Provider>
</div> </div>
); );
} }

View File

@ -3,6 +3,7 @@ import { useState } from 'react';
import { Button, Modal } from '@carbon/react'; import { Button, Modal } from '@carbon/react';
type OwnProps = { type OwnProps = {
'data-qa'?: string;
description?: string; description?: string;
buttonLabel?: string; buttonLabel?: string;
onConfirmation: (..._args: any[]) => any; onConfirmation: (..._args: any[]) => any;
@ -18,6 +19,7 @@ export default function ButtonWithConfirmation({
description, description,
buttonLabel, buttonLabel,
onConfirmation, onConfirmation,
'data-qa': dataQa,
title = 'Are you sure?', title = 'Are you sure?',
confirmButtonLabel = 'OK', confirmButtonLabel = 'OK',
kind = 'danger', kind = 'danger',
@ -51,6 +53,7 @@ export default function ButtonWithConfirmation({
secondaryButtonText="Cancel" secondaryButtonText="Cancel"
onSecondarySubmit={handleConfirmationPromptCancel} onSecondarySubmit={handleConfirmationPromptCancel}
onRequestSubmit={handleConfirmation} onRequestSubmit={handleConfirmation}
onRequestClose={handleConfirmationPromptCancel}
/> />
); );
}; };
@ -58,6 +61,7 @@ export default function ButtonWithConfirmation({
return ( return (
<> <>
<Button <Button
data-qa={dataQa}
onClick={handleShowConfirmationPrompt} onClick={handleShowConfirmationPrompt}
kind={kind} kind={kind}
renderIcon={renderIcon} renderIcon={renderIcon}

View File

@ -1,57 +0,0 @@
import React from 'react';
import HttpService from '../services/HttpService';
import { modifyProcessModelPath } from '../helpers';
type Props = {
processGroupId: string;
processModelId: string;
onUploadedCallback?: (..._args: any[]) => any;
};
export default class FileInput extends React.Component<Props> {
fileInput: any;
processGroupId: any;
processModelId: any;
onUploadedCallback: any;
constructor({ processGroupId, processModelId, onUploadedCallback }: Props) {
super({ processGroupId, processModelId, onUploadedCallback });
this.handleSubmit = this.handleSubmit.bind(this);
this.fileInput = React.createRef();
this.processGroupId = processGroupId;
this.processModelId = processModelId;
this.onUploadedCallback = onUploadedCallback;
}
handleSubmit(event: any) {
event.preventDefault();
const modifiedProcessModelId = modifyProcessModelPath(
`${this.processGroupId}/${this.processModelId}`
);
const url = `/process-models/${modifiedProcessModelId}/files`;
const formData = new FormData();
formData.append('file', this.fileInput.current.files[0]);
formData.append('fileName', this.fileInput.current.files[0].name);
HttpService.makeCallToBackend({
path: url,
successCallback: this.onUploadedCallback,
httpMethod: 'POST',
postBody: formData,
});
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<label>
Upload file:
<input type="file" ref={this.fileInput} />
</label>
<button type="submit">Submit</button>
</form>
);
}
}

View File

@ -0,0 +1,13 @@
import ProcessInstanceListTable from './ProcessInstanceListTable';
const paginationQueryParamPrefix = 'my_completed_instances';
export default function MyCompletedInstances() {
return (
<ProcessInstanceListTable
filtersEnabled={false}
paginationQueryParamPrefix={paginationQueryParamPrefix}
perPageOptions={[2, 5, 25]}
/>
);
}

View File

@ -17,9 +17,13 @@ import {
import { Logout, Login } from '@carbon/icons-react'; import { Logout, Login } from '@carbon/icons-react';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useLocation } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
import { Can } from '@casl/react';
// @ts-expect-error TS(2307) FIXME: Cannot find module '../logo.svg' or its correspond... Remove this comment to see the full error message // @ts-expect-error TS(2307) FIXME: Cannot find module '../logo.svg' or its correspond... Remove this comment to see the full error message
import logo from '../logo.svg'; import logo from '../logo.svg';
import UserService from '../services/UserService'; import UserService from '../services/UserService';
import { useUriListForPermissions } from '../hooks/UriListForPermissions';
import { PermissionsToCheck } from '../interfaces';
import { usePermissionFetcher } from '../hooks/PermissionService';
// for ref: https://react-bootstrap.github.io/components/navbar/ // for ref: https://react-bootstrap.github.io/components/navbar/
export default function NavigationBar() { export default function NavigationBar() {
@ -34,6 +38,14 @@ export default function NavigationBar() {
const location = useLocation(); const location = useLocation();
const [activeKey, setActiveKey] = useState<string>(''); const [activeKey, setActiveKey] = useState<string>('');
const { targetUris } = useUriListForPermissions();
const permissionRequestData: PermissionsToCheck = {
[targetUris.authenticationListPath]: ['GET'],
[targetUris.messageInstanceListPath]: ['GET'],
[targetUris.secretListPath]: ['GET'],
};
const { ability } = usePermissionFetcher(permissionRequestData);
useEffect(() => { useEffect(() => {
let newActiveKey = '/admin/process-groups'; let newActiveKey = '/admin/process-groups';
if (location.pathname.match(/^\/admin\/messages\b/)) { if (location.pathname.match(/^\/admin\/messages\b/)) {
@ -44,10 +56,8 @@ export default function NavigationBar() {
newActiveKey = '/admin/process-instances/reports'; newActiveKey = '/admin/process-instances/reports';
} else if (location.pathname.match(/^\/admin\/process-instances\b/)) { } else if (location.pathname.match(/^\/admin\/process-instances\b/)) {
newActiveKey = '/admin/process-instances'; newActiveKey = '/admin/process-instances';
} else if (location.pathname.match(/^\/admin\/secrets\b/)) { } else if (location.pathname.match(/^\/admin\/configuration\b/)) {
newActiveKey = '/admin/secrets'; newActiveKey = '/admin/configuration';
} else if (location.pathname.match(/^\/admin\/authentications\b/)) {
newActiveKey = '/admin/authentications';
} else if (location.pathname === '/') { } else if (location.pathname === '/') {
newActiveKey = '/'; newActiveKey = '/';
} else if (location.pathname.match(/^\/tasks\b/)) { } else if (location.pathname.match(/^\/tasks\b/)) {
@ -86,6 +96,42 @@ export default function NavigationBar() {
); );
}; };
const configurationElement = () => {
return (
<Can
I="GET"
a={targetUris.authenticationListPath}
ability={ability}
passThrough
>
{(authenticationAllowed: boolean) => {
return (
<Can
I="GET"
a={targetUris.secretListPath}
ability={ability}
passThrough
>
{(secretAllowed: boolean) => {
if (secretAllowed || authenticationAllowed) {
return (
<HeaderMenuItem
href="/admin/configuration"
isCurrentPage={isActivePage('/admin/configuration')}
>
Configuration
</HeaderMenuItem>
);
}
return null;
}}
</Can>
);
}}
</Can>
);
};
const headerMenuItems = () => { const headerMenuItems = () => {
return ( return (
<> <>
@ -105,35 +151,26 @@ export default function NavigationBar() {
> >
Process Instances Process Instances
</HeaderMenuItem> </HeaderMenuItem>
<HeaderMenuItem <Can I="GET" a={targetUris.messageInstanceListPath} ability={ability}>
href="/admin/messages" <HeaderMenuItem
isCurrentPage={isActivePage('/admin/messages')} href="/admin/messages"
> isCurrentPage={isActivePage('/admin/messages')}
Messages >
</HeaderMenuItem> Messages
<HeaderMenuItem </HeaderMenuItem>
href="/admin/secrets" </Can>
isCurrentPage={isActivePage('/admin/secrets')} {configurationElement()}
>
Secrets
</HeaderMenuItem>
<HeaderMenuItem
href="/admin/authentications"
isCurrentPage={isActivePage('/admin/authentications')}
>
Authentications
</HeaderMenuItem>
<HeaderMenuItem <HeaderMenuItem
href="/admin/process-instances/reports" href="/admin/process-instances/reports"
isCurrentPage={isActivePage('/admin/process-instances/reports')} isCurrentPage={isActivePage('/admin/process-instances/reports')}
> >
Reports Perspectives
</HeaderMenuItem> </HeaderMenuItem>
</> </>
); );
}; };
if (activeKey) { if (activeKey && ability) {
return ( return (
<HeaderContainer <HeaderContainer
render={({ isSideNavExpanded, onClickSideNavExpand }: any) => ( render={({ isSideNavExpanded, onClickSideNavExpand }: any) => (

View File

@ -1,4 +1,4 @@
import { useNavigate } from 'react-router-dom'; import { useSearchParams } from 'react-router-dom';
// @ts-ignore // @ts-ignore
import { Pagination } from '@carbon/react'; import { Pagination } from '@carbon/react';
@ -13,8 +13,7 @@ type OwnProps = {
perPageOptions?: number[]; perPageOptions?: number[];
pagination: PaginationObject | null; pagination: PaginationObject | null;
tableToDisplay: any; tableToDisplay: any;
queryParamString?: string; paginationQueryParamPrefix?: string;
path: string;
}; };
export default function PaginationForTable({ export default function PaginationForTable({
@ -23,16 +22,21 @@ export default function PaginationForTable({
perPageOptions, perPageOptions,
pagination, pagination,
tableToDisplay, tableToDisplay,
queryParamString = '', paginationQueryParamPrefix,
path,
}: OwnProps) { }: OwnProps) {
const PER_PAGE_OPTIONS = [2, 10, 50, 100]; const PER_PAGE_OPTIONS = [2, 10, 50, 100];
const navigate = useNavigate(); const [searchParams, setSearchParams] = useSearchParams();
const paginationQueryParamPrefixToUse = paginationQueryParamPrefix
? `${paginationQueryParamPrefix}_`
: '';
const updateRows = (args: any) => { const updateRows = (args: any) => {
const newPage = args.page; const newPage = args.page;
const { pageSize } = args; const { pageSize } = args;
navigate(`${path}?page=${newPage}&per_page=${pageSize}${queryParamString}`);
searchParams.set(`${paginationQueryParamPrefixToUse}page`, newPage);
searchParams.set(`${paginationQueryParamPrefixToUse}per_page`, pageSize);
setSearchParams(searchParams);
}; };
if (pagination) { if (pagination) {

View File

@ -165,11 +165,7 @@ export default function ProcessGroupForm({
}; };
const formButtons = () => { const formButtons = () => {
const buttons = [ const buttons = [<Button type="submit">Submit</Button>];
<Button kind="secondary" type="submit">
Submit
</Button>,
];
if (mode === 'edit') { if (mode === 'edit') {
buttons.push( buttons.push(
<ButtonWithConfirmation <ButtonWithConfirmation

View File

@ -0,0 +1,95 @@
import { ReactElement, useEffect, useState } from 'react';
import { useSearchParams } from 'react-router-dom';
import {
ArrowRight,
// @ts-ignore
} from '@carbon/icons-react';
import {
ClickableTile,
// @ts-ignore
} from '@carbon/react';
import HttpService from '../services/HttpService';
import { ProcessGroup } from '../interfaces';
import { modifyProcessModelPath, truncateString } from '../helpers';
type OwnProps = {
processGroup?: ProcessGroup;
headerElement?: ReactElement;
};
export default function ProcessGroupListTiles({
processGroup,
headerElement,
}: OwnProps) {
const [searchParams] = useSearchParams();
const [processGroups, setProcessGroups] = useState<ProcessGroup[] | null>(
null
);
useEffect(() => {
const setProcessGroupsFromResult = (result: any) => {
setProcessGroups(result.results);
};
let queryParams = '?per_page=1000';
if (processGroup) {
queryParams = `${queryParams}&process_group_identifier=${processGroup.id}`;
}
HttpService.makeCallToBackend({
path: `/process-groups${queryParams}`,
successCallback: setProcessGroupsFromResult,
});
}, [searchParams, processGroup]);
const processGroupDirectChildrenCount = (pg: ProcessGroup) => {
return (pg.process_models || []).length + (pg.process_groups || []).length;
};
const processGroupsDisplayArea = () => {
let displayText = null;
if (processGroups && processGroups.length > 0) {
displayText = (processGroups || []).map((row: ProcessGroup) => {
return (
<ClickableTile
id="tile-1"
className="tile-process-group"
href={`/admin/process-groups/${modifyProcessModelPath(row.id)}`}
>
<div className="tile-process-group-content-container">
<ArrowRight />
<div className="tile-process-group-display-name">
{row.display_name}
</div>
<p className="tile-description">
{truncateString(row.description || '', 25)}
</p>
<p className="tile-process-group-children-count tile-pin-bottom">
Total Sub Items: {processGroupDirectChildrenCount(row)}
</p>
</div>
</ClickableTile>
);
});
} else {
displayText = <p>No Groups To Display</p>;
}
return displayText;
};
const processGroupArea = () => {
if (processGroups && (!processGroup || processGroups.length > 0)) {
return (
<>
{headerElement}
{processGroupsDisplayArea()}
</>
);
}
return null;
};
if (processGroups) {
return <>{processGroupArea()}</>;
}
return null;
}

View File

@ -0,0 +1,723 @@
import { useContext, useEffect, useMemo, useState } from 'react';
import {
Link,
useNavigate,
useParams,
useSearchParams,
} from 'react-router-dom';
// @ts-ignore
import { Filter } from '@carbon/icons-react';
import {
Button,
ButtonSet,
DatePicker,
DatePickerInput,
Table,
Grid,
Column,
MultiSelect,
TableHeader,
TableHead,
TableRow,
TimePicker,
// @ts-ignore
} from '@carbon/react';
import { PROCESS_STATUSES, DATE_FORMAT, DATE_FORMAT_CARBON } from '../config';
import {
convertDateAndTimeStringsToSeconds,
convertDateObjectToFormattedHoursMinutes,
convertSecondsToFormattedDateString,
convertSecondsToFormattedDateTime,
convertSecondsToFormattedTimeHoursMinutes,
getPageInfoFromSearchParams,
getProcessModelFullIdentifierFromSearchParams,
modifyProcessModelPath,
} 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';
import 'react-bootstrap-typeahead/css/Typeahead.bs5.css';
import { PaginationObject, ProcessModel } from '../interfaces';
import ProcessModelSearch from './ProcessModelSearch';
type OwnProps = {
filtersEnabled?: boolean;
processModelFullIdentifier?: string;
paginationQueryParamPrefix?: string;
perPageOptions?: number[];
};
interface dateParameters {
[key: string]: ((..._args: any[]) => any)[];
}
export default function ProcessInstanceListTable({
filtersEnabled = true,
processModelFullIdentifier,
paginationQueryParamPrefix,
perPageOptions,
}: OwnProps) {
const params = useParams();
const [searchParams] = useSearchParams();
const navigate = useNavigate();
const [processInstances, setProcessInstances] = useState([]);
const [reportMetadata, setReportMetadata] = useState({});
const [pagination, setPagination] = useState<PaginationObject | null>(null);
const [processInstanceFilters, setProcessInstanceFilters] = useState({});
const oneHourInSeconds = 3600;
const oneMonthInSeconds = oneHourInSeconds * 24 * 30;
const [startFromDate, setStartFromDate] = useState<string>('');
const [startToDate, setStartToDate] = useState<string>('');
const [endFromDate, setEndFromDate] = useState<string>('');
const [endToDate, setEndToDate] = useState<string>('');
const [startFromTime, setStartFromTime] = useState<string>('');
const [startToTime, setStartToTime] = useState<string>('');
const [endFromTime, setEndFromTime] = useState<string>('');
const [endToTime, setEndToTime] = useState<string>('');
const [showFilterOptions, setShowFilterOptions] = useState<boolean>(false);
const [startFromTimeInvalid, setStartFromTimeInvalid] =
useState<boolean>(false);
const [startToTimeInvalid, setStartToTimeInvalid] = useState<boolean>(false);
const [endFromTimeInvalid, setEndFromTimeInvalid] = useState<boolean>(false);
const [endToTimeInvalid, setEndToTimeInvalid] = useState<boolean>(false);
const setErrorMessage = (useContext as any)(ErrorContext)[1];
const [processStatusAllOptions, setProcessStatusAllOptions] = useState<any[]>(
[]
);
const [processStatusSelection, setProcessStatusSelection] = useState<
string[]
>([]);
const [processModelAvailableItems, setProcessModelAvailableItems] = useState<
ProcessModel[]
>([]);
const [processModelSelection, setProcessModelSelection] =
useState<ProcessModel | null>(null);
const dateParametersToAlwaysFilterBy: dateParameters = useMemo(() => {
return {
start_from: [setStartFromDate, setStartFromTime],
start_to: [setStartToDate, setStartToTime],
end_from: [setEndFromDate, setEndFromTime],
end_to: [setEndToDate, setEndToTime],
};
}, [
setStartFromDate,
setStartFromTime,
setStartToDate,
setStartToTime,
setEndFromDate,
setEndFromTime,
setEndToDate,
setEndToTime,
]);
const parametersToGetFromSearchParams = useMemo(() => {
return {
process_model_identifier: null,
process_status: null,
};
}, []);
// eslint-disable-next-line sonarjs/cognitive-complexity
useEffect(() => {
function setProcessInstancesFromResult(result: any) {
const processInstancesFromApi = result.results;
setProcessInstances(processInstancesFromApi);
setReportMetadata(result.report_metadata);
setPagination(result.pagination);
setProcessInstanceFilters(result.filters);
}
function getProcessInstances() {
// eslint-disable-next-line prefer-const
let { page, perPage } = getPageInfoFromSearchParams(
searchParams,
undefined,
undefined,
paginationQueryParamPrefix
);
if (perPageOptions && !perPageOptions.includes(perPage)) {
// eslint-disable-next-line prefer-destructuring
perPage = perPageOptions[1];
}
let queryParamString = `per_page=${perPage}&page=${page}`;
const userAppliedFilter = searchParams.get('user_filter');
if (userAppliedFilter) {
queryParamString += `&user_filter=${userAppliedFilter}`;
}
Object.keys(dateParametersToAlwaysFilterBy).forEach(
(paramName: string) => {
const dateFunctionToCall =
dateParametersToAlwaysFilterBy[paramName][0];
const timeFunctionToCall =
dateParametersToAlwaysFilterBy[paramName][1];
const searchParamValue = searchParams.get(paramName);
if (searchParamValue) {
queryParamString += `&${paramName}=${searchParamValue}`;
const dateString = convertSecondsToFormattedDateString(
searchParamValue as any
);
dateFunctionToCall(dateString);
const timeString = convertSecondsToFormattedTimeHoursMinutes(
searchParamValue as any
);
timeFunctionToCall(timeString);
setShowFilterOptions(true);
}
}
);
Object.keys(parametersToGetFromSearchParams).forEach(
(paramName: string) => {
if (
paramName === 'process_model_identifier' &&
processModelFullIdentifier
) {
queryParamString += `&process_model_identifier=${processModelFullIdentifier}`;
} else if (searchParams.get(paramName)) {
// @ts-expect-error TS(7053) FIXME:
const functionToCall = parametersToGetFromSearchParams[paramName];
queryParamString += `&${paramName}=${searchParams.get(paramName)}`;
if (functionToCall !== null) {
functionToCall(searchParams.get(paramName) || '');
}
setShowFilterOptions(true);
}
}
);
HttpService.makeCallToBackend({
path: `/process-instances?${queryParamString}`,
successCallback: setProcessInstancesFromResult,
});
}
function processResultForProcessModels(result: any) {
const processModelFullIdentifierFromSearchParams =
getProcessModelFullIdentifierFromSearchParams(searchParams);
const selectionArray = result.results.map((item: any) => {
const label = `${item.id}`;
Object.assign(item, { label });
if (label === processModelFullIdentifierFromSearchParams) {
setProcessModelSelection(item);
}
return item;
});
setProcessModelAvailableItems(selectionArray);
const processStatusSelectedArray: string[] = [];
const processStatusAllOptionsArray = PROCESS_STATUSES.map(
(processStatusOption: any) => {
const regex = new RegExp(`\\b${processStatusOption}\\b`);
if ((searchParams.get('process_status') || '').match(regex)) {
processStatusSelectedArray.push(processStatusOption);
}
return processStatusOption;
}
);
setProcessStatusSelection(processStatusSelectedArray);
setProcessStatusAllOptions(processStatusAllOptionsArray);
getProcessInstances();
}
if (filtersEnabled) {
// populate process model selection
HttpService.makeCallToBackend({
path: `/process-models?per_page=1000`,
successCallback: processResultForProcessModels,
});
} else {
getProcessInstances();
}
}, [
searchParams,
params,
oneMonthInSeconds,
oneHourInSeconds,
dateParametersToAlwaysFilterBy,
parametersToGetFromSearchParams,
filtersEnabled,
paginationQueryParamPrefix,
processModelFullIdentifier,
perPageOptions,
]);
// This sets the filter data using the saved reports returned from the initial instance_list query.
// This could probably be merged into the main useEffect but it works here now.
useEffect(() => {
const filters = processInstanceFilters as any;
Object.keys(dateParametersToAlwaysFilterBy).forEach((paramName: string) => {
const dateFunctionToCall = dateParametersToAlwaysFilterBy[paramName][0];
const timeFunctionToCall = dateParametersToAlwaysFilterBy[paramName][1];
const paramValue = filters[paramName];
dateFunctionToCall('');
timeFunctionToCall('');
if (paramValue) {
const dateString = convertSecondsToFormattedDateString(
paramValue as any
);
dateFunctionToCall(dateString);
const timeString = convertSecondsToFormattedTimeHoursMinutes(
paramValue as any
);
timeFunctionToCall(timeString);
setShowFilterOptions(true);
}
});
setProcessModelSelection(null);
processModelAvailableItems.forEach((item: any) => {
if (item.id === filters.process_model_identifier) {
setProcessModelSelection(item);
}
});
const processStatusSelectedArray: string[] = [];
if (filters.process_status) {
PROCESS_STATUSES.forEach((processStatusOption: any) => {
const regex = new RegExp(`\\b${processStatusOption}\\b`);
if (filters.process_status.match(regex)) {
processStatusSelectedArray.push(processStatusOption);
}
});
setShowFilterOptions(true);
}
setProcessStatusSelection(processStatusSelectedArray);
}, [
processInstanceFilters,
dateParametersToAlwaysFilterBy,
parametersToGetFromSearchParams,
processModelAvailableItems,
]);
// does the comparison, but also returns false if either argument
// is not truthy and therefore not comparable.
const isTrueComparison = (param1: any, operation: any, param2: any) => {
if (param1 && param2) {
switch (operation) {
case '<':
return param1 < param2;
case '>':
return param1 > param2;
default:
return false;
}
} else {
return false;
}
};
const applyFilter = (event: any) => {
event.preventDefault();
const { page, perPage } = getPageInfoFromSearchParams(
searchParams,
undefined,
undefined,
paginationQueryParamPrefix
);
let queryParamString = `per_page=${perPage}&page=${page}&user_filter=true`;
const startFromSeconds = convertDateAndTimeStringsToSeconds(
startFromDate,
startFromTime || '00:00:00'
);
const startToSeconds = convertDateAndTimeStringsToSeconds(
startToDate,
startToTime || '00:00:00'
);
const endFromSeconds = convertDateAndTimeStringsToSeconds(
endFromDate,
endFromTime || '00:00:00'
);
const endToSeconds = convertDateAndTimeStringsToSeconds(
endToDate,
endToTime || '00:00:00'
);
if (isTrueComparison(startFromSeconds, '>', startToSeconds)) {
setErrorMessage({
message: '"Start date from" cannot be after "start date to"',
});
return;
}
if (isTrueComparison(endFromSeconds, '>', endToSeconds)) {
setErrorMessage({
message: '"End date from" cannot be after "end date to"',
});
return;
}
if (isTrueComparison(startFromSeconds, '>', endFromSeconds)) {
setErrorMessage({
message: '"Start date from" cannot be after "end date from"',
});
return;
}
if (isTrueComparison(startToSeconds, '>', endToSeconds)) {
setErrorMessage({
message: '"Start date to" cannot be after "end date to"',
});
return;
}
if (startFromSeconds) {
queryParamString += `&start_from=${startFromSeconds}`;
}
if (startToSeconds) {
queryParamString += `&start_to=${startToSeconds}`;
}
if (endFromSeconds) {
queryParamString += `&end_from=${endFromSeconds}`;
}
if (endToSeconds) {
queryParamString += `&end_to=${endToSeconds}`;
}
if (processStatusSelection.length > 0) {
queryParamString += `&process_status=${processStatusSelection}`;
}
if (processModelSelection) {
queryParamString += `&process_model_identifier=${processModelSelection.id}`;
}
setErrorMessage(null);
navigate(`/admin/process-instances?${queryParamString}`);
};
const dateComponent = (
labelString: any,
name: any,
initialDate: any,
initialTime: string,
onChangeDateFunction: any,
onChangeTimeFunction: any,
timeInvalid: boolean,
setTimeInvalid: any
) => {
return (
<>
<DatePicker dateFormat={DATE_FORMAT_CARBON} datePickerType="single">
<DatePickerInput
id={`date-picker-${name}`}
placeholder={DATE_FORMAT}
labelText={labelString}
type="text"
size="md"
autocomplete="off"
allowInput={false}
onChange={(dateChangeEvent: any) => {
if (!initialDate && !initialTime) {
onChangeTimeFunction(
convertDateObjectToFormattedHoursMinutes(new Date())
);
}
onChangeDateFunction(dateChangeEvent.srcElement.value);
}}
value={initialDate}
/>
</DatePicker>
<TimePicker
invalid={timeInvalid}
id="time-picker"
labelText="Select a time"
pattern="^([01]\d|2[0-3]):?([0-5]\d)$"
value={initialTime}
onChange={(event: any) => {
if (event.srcElement.validity.valid) {
setTimeInvalid(false);
} else {
setTimeInvalid(true);
}
onChangeTimeFunction(event.srcElement.value);
}}
/>
</>
);
};
const processStatusSearch = () => {
return (
<MultiSelect
label="Choose Status"
className="our-class"
id="process-instance-status-select"
titleText="Status"
items={processStatusAllOptions}
onChange={(selection: any) => {
setProcessStatusSelection(selection.selectedItems);
}}
itemToString={(item: any) => {
return item || '';
}}
selectionFeedback="top-after-reopen"
selectedItems={processStatusSelection}
/>
);
};
const clearFilters = () => {
setProcessModelSelection(null);
setProcessStatusSelection([]);
setStartFromDate('');
setStartFromTime('');
setStartToDate('');
setStartToTime('');
setEndFromDate('');
setEndFromTime('');
setEndToDate('');
setEndToTime('');
};
const filterOptions = () => {
if (!showFilterOptions) {
return null;
}
return (
<>
<Grid fullWidth className="with-bottom-margin">
<Column md={8}>
<ProcessModelSearch
onChange={(selection: any) =>
setProcessModelSelection(selection.selectedItem)
}
processModels={processModelAvailableItems}
selectedItem={processModelSelection}
/>
</Column>
<Column md={8}>{processStatusSearch()}</Column>
</Grid>
<Grid fullWidth className="with-bottom-margin">
<Column md={4}>
{dateComponent(
'Start date from',
'start-from',
startFromDate,
startFromTime,
setStartFromDate,
setStartFromTime,
startFromTimeInvalid,
setStartFromTimeInvalid
)}
</Column>
<Column md={4}>
{dateComponent(
'Start date to',
'start-to',
startToDate,
startToTime,
setStartToDate,
setStartToTime,
startToTimeInvalid,
setStartToTimeInvalid
)}
</Column>
<Column md={4}>
{dateComponent(
'End date from',
'end-from',
endFromDate,
endFromTime,
setEndFromDate,
setEndFromTime,
endFromTimeInvalid,
setEndFromTimeInvalid
)}
</Column>
<Column md={4}>
{dateComponent(
'End date to',
'end-to',
endToDate,
endToTime,
setEndToDate,
setEndToTime,
endToTimeInvalid,
setEndToTimeInvalid
)}
</Column>
</Grid>
<Grid fullWidth className="with-bottom-margin">
<Column md={4}>
<ButtonSet>
<Button
kind=""
className="button-white-background"
onClick={clearFilters}
>
Clear
</Button>
<Button
kind="secondary"
onClick={applyFilter}
data-qa="filter-button"
>
Filter
</Button>
</ButtonSet>
</Column>
</Grid>
</>
);
};
const buildTable = () => {
const headerLabels: Record<string, string> = {
id: 'Id',
process_model_identifier: 'Process Model',
start_in_seconds: 'Start Time',
end_in_seconds: 'End Time',
status: 'Status',
spiff_step: 'SpiffWorkflow Step',
};
const getHeaderLabel = (header: string) => {
return headerLabels[header] ?? header;
};
const headers = (reportMetadata as any).columns.map((column: any) => {
// return <th>{getHeaderLabel((column as any).Header)}</th>;
return getHeaderLabel((column as any).Header);
});
const formatProcessInstanceId = (row: any, id: any) => {
const modifiedProcessModelId: String = modifyProcessModelPath(
row.process_model_identifier
);
return (
<Link
data-qa="process-instance-show-link"
to={`/admin/process-models/${modifiedProcessModelId}/process-instances/${row.id}`}
>
{id}
</Link>
);
};
const formatProcessModelIdentifier = (_row: any, identifier: any) => {
return (
<Link
to={`/admin/process-models/${modifyProcessModelPath(identifier)}`}
>
{identifier}
</Link>
);
};
const formatSecondsForDisplay = (_row: any, seconds: any) => {
return convertSecondsToFormattedDateTime(seconds) || '-';
};
const defaultFormatter = (_row: any, value: any) => {
return value;
};
const columnFormatters: Record<string, any> = {
id: formatProcessInstanceId,
process_model_identifier: formatProcessModelIdentifier,
start_in_seconds: formatSecondsForDisplay,
end_in_seconds: formatSecondsForDisplay,
};
const formattedColumn = (row: any, column: any) => {
const formatter = columnFormatters[column.accessor] ?? defaultFormatter;
const value = row[column.accessor];
if (column.accessor === 'status') {
return (
<td data-qa={`process-instance-status-${value}`}>
{formatter(row, value)}
</td>
);
}
return <td>{formatter(row, value)}</td>;
};
const rows = processInstances.map((row: any) => {
const currentRow = (reportMetadata as any).columns.map((column: any) => {
return formattedColumn(row, column);
});
return <tr key={row.id}>{currentRow}</tr>;
});
return (
<Table size="lg">
<TableHead>
<TableRow>
{headers.map((header: any) => (
<TableHeader
key={header}
title={header === 'Id' ? 'Process Instance Id' : null}
>
{header}
</TableHeader>
))}
</TableRow>
</TableHead>
<tbody>{rows}</tbody>
</Table>
);
};
const toggleShowFilterOptions = () => {
setShowFilterOptions(!showFilterOptions);
};
const filterComponent = () => {
if (!filtersEnabled) {
return null;
}
return (
<>
<Grid fullWidth>
<Column
sm={{ span: 1, offset: 3 }}
md={{ span: 1, offset: 7 }}
lg={{ span: 1, offset: 15 }}
>
<Button
data-qa="filter-section-expand-toggle"
kind="ghost"
renderIcon={Filter}
iconDescription="Filter Options"
hasIconOnly
size="lg"
onClick={toggleShowFilterOptions}
/>
</Column>
</Grid>
{filterOptions()}
</>
);
};
if (pagination) {
// eslint-disable-next-line prefer-const
let { page, perPage } = getPageInfoFromSearchParams(
searchParams,
undefined,
undefined,
paginationQueryParamPrefix
);
if (perPageOptions && !perPageOptions.includes(perPage)) {
// eslint-disable-next-line prefer-destructuring
perPage = perPageOptions[1];
}
return (
<>
{filterComponent()}
<PaginationForTable
page={page}
perPage={perPage}
pagination={pagination}
tableToDisplay={buildTable()}
paginationQueryParamPrefix={paginationQueryParamPrefix}
perPageOptions={perPageOptions}
/>
</>
);
}
return null;
}

View File

@ -0,0 +1,64 @@
import { useContext } from 'react';
import { useNavigate } from 'react-router-dom';
import {
Button,
// @ts-ignore
} from '@carbon/react';
import { ProcessModel } from '../interfaces';
import HttpService from '../services/HttpService';
import ErrorContext from '../contexts/ErrorContext';
import { modifyProcessModelPath } from '../helpers';
type OwnProps = {
processModel: ProcessModel;
onSuccessCallback: Function;
className?: string;
};
export default function ProcessInstanceRun({
processModel,
onSuccessCallback,
className,
}: OwnProps) {
const navigate = useNavigate();
const setErrorMessage = (useContext as any)(ErrorContext)[1];
const modifiedProcessModelId = modifyProcessModelPath(processModel.id);
const onProcessInstanceRun = (processInstance: any) => {
// FIXME: ensure that the task is actually for the current user as well
const processInstanceId = (processInstance as any).id;
const nextTask = (processInstance as any).next_task;
if (nextTask && nextTask.state === 'READY') {
navigate(`/tasks/${processInstanceId}/${nextTask.id}`);
}
onSuccessCallback(processInstance);
};
const processModelRun = (processInstance: any) => {
setErrorMessage(null);
HttpService.makeCallToBackend({
path: `/process-instances/${processInstance.id}/run`,
successCallback: onProcessInstanceRun,
failureCallback: setErrorMessage,
httpMethod: 'POST',
});
};
const processInstanceCreateAndRun = () => {
HttpService.makeCallToBackend({
path: `/process-models/${modifiedProcessModelId}/process-instances`,
successCallback: processModelRun,
httpMethod: 'POST',
});
};
return (
<Button
onClick={processInstanceCreateAndRun}
variant="primary"
className={className}
>
Run
</Button>
);
}

View File

@ -2,14 +2,9 @@ import { useState } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
// @ts-ignore // @ts-ignore
import { Button, ButtonSet, Form, Stack, TextInput } from '@carbon/react'; import { Button, ButtonSet, Form, Stack, TextInput } from '@carbon/react';
import { import { modifyProcessModelPath, slugifyString } from '../helpers';
getGroupFromModifiedModelId,
modifyProcessModelPath,
slugifyString,
} from '../helpers';
import HttpService from '../services/HttpService'; import HttpService from '../services/HttpService';
import { ProcessModel } from '../interfaces'; import { ProcessModel } from '../interfaces';
import ButtonWithConfirmation from './ButtonWithConfirmation';
type OwnProps = { type OwnProps = {
mode: string; mode: string;
@ -29,7 +24,6 @@ export default function ProcessModelForm({
useState<boolean>(false); useState<boolean>(false);
const [displayNameInvalid, setDisplayNameInvalid] = useState<boolean>(false); const [displayNameInvalid, setDisplayNameInvalid] = useState<boolean>(false);
const navigate = useNavigate(); const navigate = useNavigate();
const modifiedProcessModelPath = modifyProcessModelPath(processModel.id);
const navigateToProcessModel = (result: ProcessModel) => { const navigateToProcessModel = (result: ProcessModel) => {
if ('id' in result) { if ('id' in result) {
@ -40,30 +34,14 @@ export default function ProcessModelForm({
} }
}; };
const navigateToProcessModels = (_result: any) => {
navigate(
`/admin/process-groups/${getGroupFromModifiedModelId(
modifiedProcessModelPath
)}`
);
};
const hasValidIdentifier = (identifierToCheck: string) => { 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 deleteProcessModel = () => {
HttpService.makeCallToBackend({
path: `/process-models/${modifiedProcessModelPath}`,
successCallback: navigateToProcessModels,
httpMethod: 'DELETE',
});
};
const handleFormSubmission = (event: any) => { const handleFormSubmission = (event: any) => {
event.preventDefault(); event.preventDefault();
let hasErrors = false; let hasErrors = false;
if (!hasValidIdentifier(processModel.id)) { if (mode === 'new' && !hasValidIdentifier(processModel.id)) {
setIdentifierInvalid(true); setIdentifierInvalid(true);
hasErrors = true; hasErrors = true;
} }
@ -74,10 +52,7 @@ export default function ProcessModelForm({
if (hasErrors) { if (hasErrors) {
return; return;
} }
let path = `/process-models`; const path = `/process-models/${processGroupId}`;
if (mode === 'edit') {
path = `/process-models/${modifiedProcessModelPath}`;
}
let httpMethod = 'POST'; let httpMethod = 'POST';
if (mode === 'edit') { if (mode === 'edit') {
httpMethod = 'PUT'; httpMethod = 'PUT';
@ -88,7 +63,7 @@ export default function ProcessModelForm({
}; };
if (mode === 'new') { if (mode === 'new') {
Object.assign(postBody, { Object.assign(postBody, {
id: `${processGroupId}:${processModel.id}`, id: `${processGroupId}/${processModel.id}`,
}); });
} }
@ -175,16 +150,6 @@ export default function ProcessModelForm({
Submit Submit
</Button>, </Button>,
]; ];
if (mode === 'edit') {
buttons.push(
<ButtonWithConfirmation
description={`Delete Process Model ${processModel.id}?`}
onConfirmation={deleteProcessModel}
buttonLabel="Delete"
confirmButtonLabel="Delete"
/>
);
}
return <ButtonSet>{buttons}</ButtonSet>; return <ButtonSet>{buttons}</ButtonSet>;
}; };
return ( return (

View File

@ -0,0 +1,105 @@
import { ReactElement, useEffect, useState } from 'react';
import { Link, useSearchParams } from 'react-router-dom';
import {
Tile,
// @ts-ignore
} from '@carbon/react';
import HttpService from '../services/HttpService';
import { ProcessModel, ProcessInstance } from '../interfaces';
import { modifyProcessModelPath, truncateString } from '../helpers';
import ProcessInstanceRun from './ProcessInstanceRun';
type OwnProps = {
headerElement?: ReactElement;
};
export default function ProcessModelListTiles({ headerElement }: OwnProps) {
const [searchParams] = useSearchParams();
const [processModels, setProcessModels] = useState<ProcessModel[] | null>(
null
);
const [processInstance, setProcessInstance] =
useState<ProcessInstance | null>(null);
useEffect(() => {
const setProcessModelsFromResult = (result: any) => {
setProcessModels(result.results);
};
// only allow 10 for now until we get the backend only returnin certain models for user execution
const queryParams = '?per_page=10';
HttpService.makeCallToBackend({
path: `/process-models${queryParams}`,
successCallback: setProcessModelsFromResult,
});
}, [searchParams]);
const processInstanceRunResultTag = () => {
if (processInstance) {
return (
<div className="alert alert-success" role="alert">
<p>
Process Instance {processInstance.id} kicked off (
<Link
to={`/admin/process-models/${modifyProcessModelPath(
processInstance.process_model_identifier
)}/process-instances/${processInstance.id}`}
data-qa="process-instance-show-link"
>
view
</Link>
).
</p>
</div>
);
}
return null;
};
const processModelsDisplayArea = () => {
let displayText = null;
if (processModels && processModels.length > 0) {
displayText = (processModels || []).map((row: ProcessModel) => {
return (
<Tile
id="tile-1"
className="tile-process-group"
href={`/admin/process-models/${modifyProcessModelPath(row.id)}`}
>
<div className="tile-process-group-content-container">
<div className="tile-title-top">{row.display_name}</div>
<p className="tile-description">
{truncateString(row.description || '', 25)}
</p>
<ProcessInstanceRun
processModel={row}
onSuccessCallback={setProcessInstance}
className="tile-pin-bottom"
/>
</div>
</Tile>
);
});
} else {
displayText = <p>No Models To Display</p>;
}
return displayText;
};
const processModelArea = () => {
if (processModels && processModels.length > 0) {
return (
<>
{headerElement}
{processInstanceRunResultTag()}
{processModelsDisplayArea()}
</>
);
}
return null;
};
if (processModels) {
return <>{processModelArea()}</>;
}
return null;
}

View File

@ -0,0 +1,56 @@
import {
ComboBox,
// @ts-ignore
} from '@carbon/react';
import { truncateString } from '../helpers';
import { ProcessReference } from '../interfaces';
type OwnProps = {
onChange: (..._args: any[]) => any;
processes: ProcessReference[];
selectedItem?: ProcessReference | null;
titleText?: string;
height?: string;
};
export default function ProcessSearch({
processes,
selectedItem,
onChange,
titleText = 'Process Search',
height = '50px',
}: OwnProps) {
const shouldFilter = (options: any) => {
const process: ProcessReference = options.item;
const { inputValue } = options;
return (
inputValue === null ||
`${process.display_name} (${process.identifier})`
.toLowerCase()
.includes(inputValue.toLowerCase())
);
};
return (
<div style={{ width: '100%', height }}>
<ComboBox
onChange={onChange}
id="process-model-select"
data-qa="process-model-selection"
items={processes}
itemToString={(process: ProcessReference) => {
if (process) {
return `${process.display_name} (${truncateString(
process.identifier,
20
)})`;
}
return null;
}}
shouldFilterItem={shouldFilter}
placeholder="Choose a process"
titleText={titleText}
selectedItem={selectedItem}
/>
</div>
);
}

View File

@ -52,10 +52,14 @@ import TouchModule from 'diagram-js/lib/navigation/touch';
// @ts-expect-error TS(7016) FIXME // @ts-expect-error TS(7016) FIXME
import ZoomScrollModule from 'diagram-js/lib/navigation/zoomscroll'; import ZoomScrollModule from 'diagram-js/lib/navigation/zoomscroll';
import { Can } from '@casl/react';
import HttpService from '../services/HttpService'; import HttpService from '../services/HttpService';
import ButtonWithConfirmation from './ButtonWithConfirmation'; import ButtonWithConfirmation from './ButtonWithConfirmation';
import { makeid } from '../helpers'; import { makeid } from '../helpers';
import { useUriListForPermissions } from '../hooks/UriListForPermissions';
import { PermissionsToCheck } from '../interfaces';
import { usePermissionFetcher } from '../hooks/PermissionService';
type OwnProps = { type OwnProps = {
processModelId: string; processModelId: string;
@ -76,6 +80,7 @@ type OwnProps = {
onServiceTasksRequested?: (..._args: any[]) => any; onServiceTasksRequested?: (..._args: any[]) => any;
onJsonFilesRequested?: (..._args: any[]) => any; onJsonFilesRequested?: (..._args: any[]) => any;
onDmnFilesRequested?: (..._args: any[]) => any; onDmnFilesRequested?: (..._args: any[]) => any;
onSearchProcessModels?: (..._args: any[]) => any;
url?: string; url?: string;
}; };
@ -99,6 +104,7 @@ export default function ReactDiagramEditor({
onServiceTasksRequested, onServiceTasksRequested,
onJsonFilesRequested, onJsonFilesRequested,
onDmnFilesRequested, onDmnFilesRequested,
onSearchProcessModels,
url, url,
}: OwnProps) { }: OwnProps) {
const [diagramXMLString, setDiagramXMLString] = useState(''); const [diagramXMLString, setDiagramXMLString] = useState('');
@ -107,6 +113,13 @@ export default function ReactDiagramEditor({
const alreadyImportedXmlRef = useRef(false); const alreadyImportedXmlRef = useRef(false);
const { targetUris } = useUriListForPermissions();
const permissionRequestData: PermissionsToCheck = {
[targetUris.processModelShowPath]: ['PUT'],
[targetUris.processModelFileShowPath]: ['POST', 'GET', 'PUT', 'DELETE'],
};
const { ability } = usePermissionFetcher(permissionRequestData);
useEffect(() => { useEffect(() => {
if (diagramModelerState) { if (diagramModelerState) {
return; return;
@ -292,6 +305,12 @@ export default function ReactDiagramEditor({
diagramModeler.on('spiff.json_files.requested', (event: any) => { diagramModeler.on('spiff.json_files.requested', (event: any) => {
handleServiceTasksRequested(event); handleServiceTasksRequested(event);
}); });
diagramModeler.on('spiff.callactivity.search', (event: any) => {
if (onSearchProcessModels) {
onSearchProcessModels(event.value, event.eventBus, event.element);
}
});
}, [ }, [
diagramModelerState, diagramModelerState,
diagramType, diagramType,
@ -304,6 +323,7 @@ export default function ReactDiagramEditor({
onServiceTasksRequested, onServiceTasksRequested,
onJsonFilesRequested, onJsonFilesRequested,
onDmnFilesRequested, onDmnFilesRequested,
onSearchProcessModels,
]); ]);
useEffect(() => { useEffect(() => {
@ -517,20 +537,40 @@ export default function ReactDiagramEditor({
if (diagramType !== 'readonly') { if (diagramType !== 'readonly') {
return ( return (
<> <>
<Button onClick={handleSave} variant="danger"> <Can
Save I="PUT"
</Button> a={targetUris.processModelFileShowPath}
{fileName && ( ability={ability}
<ButtonWithConfirmation >
description={`Delete file ${fileName}?`} <Button onClick={handleSave}>Save</Button>
onConfirmation={handleDelete} </Can>
buttonLabel="Delete" <Can
/> I="DELETE"
)} a={targetUris.processModelFileShowPath}
{onSetPrimaryFile && ( ability={ability}
<Button onClick={handleSetPrimaryFile}>Set as primary file</Button> >
)} {fileName && (
<Button onClick={downloadXmlFile}>Download xml</Button> <ButtonWithConfirmation
description={`Delete file ${fileName}?`}
onConfirmation={handleDelete}
buttonLabel="Delete"
/>
)}
</Can>
<Can I="PUT" a={targetUris.processModelShowPath} ability={ability}>
{onSetPrimaryFile && (
<Button onClick={handleSetPrimaryFile}>
Set as primary file
</Button>
)}
</Can>
<Can
I="GET"
a={targetUris.processModelFileShowPath}
ability={ability}
>
<Button onClick={downloadXmlFile}>Download xml</Button>
</Can>
</> </>
); );
} }

View File

@ -1,59 +0,0 @@
import { useEffect, useState } from 'react';
import Nav from 'react-bootstrap/Nav';
import { useLocation } from 'react-router-dom';
export default function SubNavigation() {
const location = useLocation();
const [activeKey, setActiveKey] = useState('');
useEffect(() => {
let newActiveKey = '/admin/process-groups';
if (location.pathname.match(/^\/admin\/messages\b/)) {
newActiveKey = '/admin/messages';
} else if (
location.pathname.match(/^\/admin\/process-instances\/reports\b/)
) {
newActiveKey = '/admin/process-instances/reports';
} else if (location.pathname.match(/^\/admin\/process-instances\b/)) {
newActiveKey = '/admin/process-instances';
} else if (location.pathname.match(/^\/admin\/secrets\b/)) {
newActiveKey = '/admin/secrets';
} else if (location.pathname.match(/^\/admin\/authentications\b/)) {
newActiveKey = '/admin/authentications';
} else if (location.pathname === '/') {
newActiveKey = '/';
} else if (location.pathname.match(/^\/tasks\b/)) {
newActiveKey = '/';
}
setActiveKey(newActiveKey);
}, [location]);
if (activeKey) {
return (
<Nav variant="tabs" activeKey={activeKey}>
<Nav.Item data-qa="nav-home">
<Nav.Link href="/">Home</Nav.Link>
</Nav.Item>
<Nav.Item>
<Nav.Link href="/admin/process-groups">Process Models</Nav.Link>
</Nav.Item>
<Nav.Item>
<Nav.Link href="/admin/process-instances">Process Instances</Nav.Link>
</Nav.Item>
<Nav.Item>
<Nav.Link href="/admin/messages">Messages</Nav.Link>
</Nav.Item>
<Nav.Item>
<Nav.Link href="/admin/secrets">Secrets</Nav.Link>
</Nav.Item>
<Nav.Item>
<Nav.Link href="/admin/authentications">Authentications</Nav.Link>
</Nav.Item>
<Nav.Item>
<Nav.Link href="/admin/process-instances/reports">Reports</Nav.Link>
</Nav.Item>
</Nav>
);
}
return null;
}

View File

@ -0,0 +1,142 @@
import { useEffect, useState } from 'react';
// @ts-ignore
import { Button, Table } from '@carbon/react';
import { Link, useSearchParams } from 'react-router-dom';
import PaginationForTable from './PaginationForTable';
import {
convertSecondsToFormattedDateTime,
getPageInfoFromSearchParams,
modifyProcessModelPath,
} from '../helpers';
import HttpService from '../services/HttpService';
import { PaginationObject } from '../interfaces';
const PER_PAGE_FOR_TASKS_ON_HOME_PAGE = 5;
const paginationQueryParamPrefix = 'tasks_for_my_open_processes';
export default function MyOpenProcesses() {
const [searchParams] = useSearchParams();
const [tasks, setTasks] = useState([]);
const [pagination, setPagination] = useState<PaginationObject | null>(null);
useEffect(() => {
const { page, perPage } = getPageInfoFromSearchParams(
searchParams,
PER_PAGE_FOR_TASKS_ON_HOME_PAGE,
undefined,
paginationQueryParamPrefix
);
const setTasksFromResult = (result: any) => {
setTasks(result.results);
setPagination(result.pagination);
};
HttpService.makeCallToBackend({
path: `/tasks/for-my-open-processes?per_page=${perPage}&page=${page}`,
successCallback: setTasksFromResult,
});
}, [searchParams]);
const buildTable = () => {
const rows = tasks.map((row) => {
const rowToUse = row as any;
const taskUrl = `/tasks/${rowToUse.process_instance_id}/${rowToUse.task_id}`;
const modifiedProcessModelIdentifier = modifyProcessModelPath(
rowToUse.process_model_identifier
);
return (
<tr key={rowToUse.id}>
<td>
<Link
data-qa="process-model-show-link"
to={`/admin/process-models/${modifiedProcessModelIdentifier}`}
>
{rowToUse.process_model_display_name}
</Link>
</td>
<td>
<Link
data-qa="process-instance-show-link"
to={`/admin/process-models/${modifiedProcessModelIdentifier}/process-instances/${rowToUse.process_instance_id}`}
>
View {rowToUse.process_instance_id}
</Link>
</td>
<td
title={`task id: ${rowToUse.name}, spiffworkflow task guid: ${rowToUse.id}`}
>
{rowToUse.task_title}
</td>
<td>{rowToUse.process_instance_status}</td>
<td>{rowToUse.group_identifier || '-'}</td>
<td>
{convertSecondsToFormattedDateTime(
rowToUse.created_at_in_seconds
) || '-'}
</td>
<td>
{convertSecondsToFormattedDateTime(
rowToUse.updated_at_in_seconds
) || '-'}
</td>
<td>
<Button
variant="primary"
href={taskUrl}
hidden={rowToUse.process_instance_status === 'suspended'}
disabled={!rowToUse.current_user_is_potential_owner}
>
Go
</Button>
</td>
</tr>
);
});
return (
<Table striped bordered>
<thead>
<tr>
<th>Process Model</th>
<th>Process Instance</th>
<th>Task Name</th>
<th>Process Instance Status</th>
<th>Assigned Group</th>
<th>Process Started</th>
<th>Process Updated</th>
<th>Actions</th>
</tr>
</thead>
<tbody>{rows}</tbody>
</Table>
);
};
const tasksComponent = () => {
if (pagination && pagination.total < 1) {
return null;
}
const { page, perPage } = getPageInfoFromSearchParams(
searchParams,
PER_PAGE_FOR_TASKS_ON_HOME_PAGE,
undefined,
paginationQueryParamPrefix
);
return (
<>
<h1>Tasks for my open processes</h1>
<PaginationForTable
page={page}
perPage={perPage}
perPageOptions={[2, PER_PAGE_FOR_TASKS_ON_HOME_PAGE, 25]}
pagination={pagination}
tableToDisplay={buildTable()}
paginationQueryParamPrefix={paginationQueryParamPrefix}
/>
</>
);
};
if (pagination) {
return tasksComponent();
}
return null;
}

View File

@ -0,0 +1,143 @@
import { useEffect, useState } from 'react';
// @ts-ignore
import { Button, Table } from '@carbon/react';
import { Link, useSearchParams } from 'react-router-dom';
import PaginationForTable from './PaginationForTable';
import {
convertSecondsToFormattedDateTime,
getPageInfoFromSearchParams,
modifyProcessModelPath,
} from '../helpers';
import HttpService from '../services/HttpService';
import { PaginationObject } from '../interfaces';
const PER_PAGE_FOR_TASKS_ON_HOME_PAGE = 5;
export default function TasksWaitingForMe() {
const [searchParams] = useSearchParams();
const [tasks, setTasks] = useState([]);
const [pagination, setPagination] = useState<PaginationObject | null>(null);
useEffect(() => {
const { page, perPage } = getPageInfoFromSearchParams(
searchParams,
PER_PAGE_FOR_TASKS_ON_HOME_PAGE,
undefined,
'tasks_waiting_for_me'
);
const setTasksFromResult = (result: any) => {
setTasks(result.results);
setPagination(result.pagination);
};
HttpService.makeCallToBackend({
path: `/tasks/for-me?per_page=${perPage}&page=${page}`,
successCallback: setTasksFromResult,
});
}, [searchParams]);
const buildTable = () => {
const rows = tasks.map((row) => {
const rowToUse = row as any;
const taskUrl = `/tasks/${rowToUse.process_instance_id}/${rowToUse.task_id}`;
const modifiedProcessModelIdentifier = modifyProcessModelPath(
rowToUse.process_model_identifier
);
return (
<tr key={rowToUse.id}>
<td>
<Link
data-qa="process-model-show-link"
to={`/admin/process-models/${modifiedProcessModelIdentifier}`}
>
{rowToUse.process_model_display_name}
</Link>
</td>
<td>
<Link
data-qa="process-instance-show-link"
to={`/admin/process-models/${modifiedProcessModelIdentifier}/process-instances/${rowToUse.process_instance_id}`}
>
View {rowToUse.process_instance_id}
</Link>
</td>
<td
title={`task id: ${rowToUse.name}, spiffworkflow task guid: ${rowToUse.id}`}
>
{rowToUse.task_title}
</td>
<td>{rowToUse.username}</td>
<td>{rowToUse.process_instance_status}</td>
<td>{rowToUse.group_identifier || '-'}</td>
<td>
{convertSecondsToFormattedDateTime(
rowToUse.created_at_in_seconds
) || '-'}
</td>
<td>
{convertSecondsToFormattedDateTime(
rowToUse.updated_at_in_seconds
) || '-'}
</td>
<td>
<Button
variant="primary"
href={taskUrl}
hidden={rowToUse.process_instance_status === 'suspended'}
disabled={!rowToUse.current_user_is_potential_owner}
>
Go
</Button>
</td>
</tr>
);
});
return (
<Table striped bordered>
<thead>
<tr>
<th>Process Model</th>
<th>Process Instance</th>
<th>Task Name</th>
<th>Process Started By</th>
<th>Process Instance Status</th>
<th>Assigned Group</th>
<th>Process Started</th>
<th>Process Updated</th>
<th>Actions</th>
</tr>
</thead>
<tbody>{rows}</tbody>
</Table>
);
};
const tasksComponent = () => {
if (pagination && pagination.total < 1) {
return null;
}
const { page, perPage } = getPageInfoFromSearchParams(
searchParams,
PER_PAGE_FOR_TASKS_ON_HOME_PAGE,
undefined,
'tasks_waiting_for_me'
);
return (
<>
<h1>Tasks waiting for me</h1>
<PaginationForTable
page={page}
perPage={perPage}
perPageOptions={[2, PER_PAGE_FOR_TASKS_ON_HOME_PAGE, 25]}
pagination={pagination}
tableToDisplay={buildTable()}
paginationQueryParamPrefix="tasks_waiting_for_me"
/>
</>
);
};
if (pagination) {
return tasksComponent();
}
return null;
}

View File

@ -0,0 +1,144 @@
import { useEffect, useState } from 'react';
// @ts-ignore
import { Button, Table } from '@carbon/react';
import { Link, useSearchParams } from 'react-router-dom';
import PaginationForTable from './PaginationForTable';
import {
convertSecondsToFormattedDateTime,
getPageInfoFromSearchParams,
modifyProcessModelPath,
} from '../helpers';
import HttpService from '../services/HttpService';
import { PaginationObject } from '../interfaces';
const PER_PAGE_FOR_TASKS_ON_HOME_PAGE = 5;
const paginationQueryParamPrefix = 'tasks_waiting_for_my_groups';
export default function TasksForWaitingForMyGroups() {
const [searchParams] = useSearchParams();
const [tasks, setTasks] = useState([]);
const [pagination, setPagination] = useState<PaginationObject | null>(null);
useEffect(() => {
const { page, perPage } = getPageInfoFromSearchParams(
searchParams,
PER_PAGE_FOR_TASKS_ON_HOME_PAGE,
undefined,
paginationQueryParamPrefix
);
const setTasksFromResult = (result: any) => {
setTasks(result.results);
setPagination(result.pagination);
};
HttpService.makeCallToBackend({
path: `/tasks/for-my-groups?per_page=${perPage}&page=${page}`,
successCallback: setTasksFromResult,
});
}, [searchParams]);
const buildTable = () => {
const rows = tasks.map((row) => {
const rowToUse = row as any;
const taskUrl = `/tasks/${rowToUse.process_instance_id}/${rowToUse.task_id}`;
const modifiedProcessModelIdentifier = modifyProcessModelPath(
rowToUse.process_model_identifier
);
return (
<tr key={rowToUse.id}>
<td>
<Link
data-qa="process-model-show-link"
to={`/admin/process-models/${modifiedProcessModelIdentifier}`}
>
{rowToUse.process_model_display_name}
</Link>
</td>
<td>
<Link
data-qa="process-instance-show-link"
to={`/admin/process-models/${modifiedProcessModelIdentifier}/process-instances/${rowToUse.process_instance_id}`}
>
View {rowToUse.process_instance_id}
</Link>
</td>
<td
title={`task id: ${rowToUse.name}, spiffworkflow task guid: ${rowToUse.id}`}
>
{rowToUse.task_title}
</td>
<td>{rowToUse.username}</td>
<td>{rowToUse.process_instance_status}</td>
<td>{rowToUse.group_identifier || '-'}</td>
<td>
{convertSecondsToFormattedDateTime(
rowToUse.created_at_in_seconds
) || '-'}
</td>
<td>
{convertSecondsToFormattedDateTime(
rowToUse.updated_at_in_seconds
) || '-'}
</td>
<td>
<Button
variant="primary"
href={taskUrl}
hidden={rowToUse.process_instance_status === 'suspended'}
disabled={!rowToUse.current_user_is_potential_owner}
>
Go
</Button>
</td>
</tr>
);
});
return (
<Table striped bordered>
<thead>
<tr>
<th>Process Model</th>
<th>Process Instance</th>
<th>Task Name</th>
<th>Process Started By</th>
<th>Process Instance Status</th>
<th>Assigned Group</th>
<th>Process Started</th>
<th>Process Updated</th>
<th>Actions</th>
</tr>
</thead>
<tbody>{rows}</tbody>
</Table>
);
};
const tasksComponent = () => {
if (pagination && pagination.total < 1) {
return null;
}
const { page, perPage } = getPageInfoFromSearchParams(
searchParams,
PER_PAGE_FOR_TASKS_ON_HOME_PAGE,
undefined,
paginationQueryParamPrefix
);
return (
<>
<h1>Tasks waiting for my groups</h1>
<PaginationForTable
page={page}
perPage={perPage}
perPageOptions={[2, PER_PAGE_FOR_TASKS_ON_HOME_PAGE, 25]}
pagination={pagination}
tableToDisplay={buildTable()}
paginationQueryParamPrefix={paginationQueryParamPrefix}
/>
</>
);
};
if (pagination) {
return tasksComponent();
}
return null;
}

View File

@ -18,5 +18,6 @@ export const PROCESS_STATUSES = [
// with time: yyyy-MM-dd HH:mm:ss // with time: yyyy-MM-dd HH:mm:ss
export const DATE_TIME_FORMAT = 'yyyy-MM-dd HH:mm:ss'; 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 = 'yyyy-MM-dd';
export const DATE_FORMAT_CARBON = 'Y-m-d'; export const DATE_FORMAT_CARBON = 'Y-m-d';

6
src/contexts/Can.tsx Normal file
View File

@ -0,0 +1,6 @@
import { createContext } from 'react';
import { Ability } from '@casl/ability';
import { createContextualCan } from '@casl/react';
export const AbilityContext = createContext(new Ability());
export const Can = createContextualCan(AbilityContext.Consumer);

View File

@ -1,4 +1,4 @@
import { convertSecondsToFormattedDate, slugifyString } from './helpers'; import { convertSecondsToFormattedDateString, slugifyString } from './helpers';
test('it can slugify a string', () => { test('it can slugify a string', () => {
expect(slugifyString('hello---world_ and then Some such-')).toEqual( expect(slugifyString('hello---world_ and then Some such-')).toEqual(
@ -7,6 +7,6 @@ test('it can slugify a string', () => {
}); });
test('it can keep the correct date when converting seconds to date', () => { test('it can keep the correct date when converting seconds to date', () => {
const dateString = convertSecondsToFormattedDate(1666325400); const dateString = convertSecondsToFormattedDateString(1666325400);
expect(dateString).toEqual('2022-10-21'); expect(dateString).toEqual('2022-10-21');
}); });

View File

@ -1,5 +1,9 @@
import { format } from 'date-fns'; import { format } from 'date-fns';
import { DATE_TIME_FORMAT, DATE_FORMAT } from './config'; import {
DATE_TIME_FORMAT,
DATE_FORMAT,
TIME_FORMAT_HOURS_MINUTES,
} from './config';
import { import {
DEFAULT_PER_PAGE, DEFAULT_PER_PAGE,
DEFAULT_PAGE, DEFAULT_PAGE,
@ -42,27 +46,72 @@ export const convertDateToSeconds = (
return null; return null;
}; };
export const convertDateObjectToFormattedString = (dateObject: Date) => {
if (dateObject) {
return format(dateObject, DATE_FORMAT);
}
return null;
};
export const convertDateAndTimeStringsToDate = (
dateString: string,
timeString: string
) => {
if (dateString && timeString) {
return new Date(`${dateString}T${timeString}`);
}
return null;
};
export const convertDateAndTimeStringsToSeconds = (
dateString: string,
timeString: string
) => {
const dateObject = convertDateAndTimeStringsToDate(dateString, timeString);
if (dateObject) {
return convertDateToSeconds(dateObject);
}
return null;
};
export const convertStringToDate = (dateString: string) => { export const convertStringToDate = (dateString: string) => {
if (dateString) { return convertDateAndTimeStringsToSeconds(dateString, '00:10:00');
// add midnight time to the date so it c uses the correct date };
// after converting to timezone
return new Date(`${dateString}T00:10:00`); export const convertSecondsToDateObject = (seconds: number) => {
if (seconds) {
return new Date(seconds * 1000);
} }
return null; return null;
}; };
export const convertSecondsToFormattedDateTime = (seconds: number) => { export const convertSecondsToFormattedDateTime = (seconds: number) => {
if (seconds) { const dateObject = convertSecondsToDateObject(seconds);
const dateObject = new Date(seconds * 1000); if (dateObject) {
return format(dateObject, DATE_TIME_FORMAT); return format(dateObject, DATE_TIME_FORMAT);
} }
return null; return null;
}; };
export const convertSecondsToFormattedDate = (seconds: number) => { export const convertDateObjectToFormattedHoursMinutes = (dateObject: Date) => {
if (seconds) { if (dateObject) {
const dateObject = new Date(seconds * 1000); return format(dateObject, TIME_FORMAT_HOURS_MINUTES);
return format(dateObject, DATE_FORMAT); }
return null;
};
export const convertSecondsToFormattedTimeHoursMinutes = (seconds: number) => {
const dateObject = convertSecondsToDateObject(seconds);
if (dateObject) {
return convertDateObjectToFormattedHoursMinutes(dateObject);
}
return null;
};
export const convertSecondsToFormattedDateString = (seconds: number) => {
const dateObject = convertSecondsToDateObject(seconds);
if (dateObject) {
return convertDateObjectToFormattedString(dateObject);
} }
return null; return null;
}; };
@ -79,11 +128,20 @@ export const objectIsEmpty = (obj: object) => {
export const getPageInfoFromSearchParams = ( export const getPageInfoFromSearchParams = (
searchParams: any, searchParams: any,
defaultPerPage: string | number = DEFAULT_PER_PAGE, defaultPerPage: string | number = DEFAULT_PER_PAGE,
defaultPage: string | number = DEFAULT_PAGE defaultPage: string | number = DEFAULT_PAGE,
paginationQueryParamPrefix: string | null = null
) => { ) => {
const page = parseInt(searchParams.get('page') || defaultPage.toString(), 10); const paginationQueryParamPrefixToUse = paginationQueryParamPrefix
? `${paginationQueryParamPrefix}_`
: '';
const page = parseInt(
searchParams.get(`${paginationQueryParamPrefixToUse}page`) ||
defaultPage.toString(),
10
);
const perPage = parseInt( const perPage = parseInt(
searchParams.get('per_page') || defaultPerPage.toString(), searchParams.get(`${paginationQueryParamPrefixToUse}per_page`) ||
defaultPerPage.toString(),
10 10
); );
@ -139,3 +197,16 @@ export const getGroupFromModifiedModelId = (modifiedId: string) => {
export const splitProcessModelId = (processModelId: string) => { export const splitProcessModelId = (processModelId: string) => {
return processModelId.split('/'); return processModelId.split('/');
}; };
export const refreshAtInterval = (
interval: number,
timeout: number,
func: Function
) => {
const intervalRef = setInterval(() => func(), interval * 1000);
const timeoutRef = setTimeout(
() => clearInterval(intervalRef),
timeout * 1000
);
return [intervalRef, timeoutRef];
};

View File

@ -0,0 +1,48 @@
// We may need to update usage of Ability when we update.
// They say they are going to rename PureAbility to Ability and remove the old class.
import { AbilityBuilder, Ability } from '@casl/ability';
import { useContext, useEffect } from 'react';
import { AbilityContext } from '../contexts/Can';
import { PermissionCheckResponseBody, PermissionsToCheck } from '../interfaces';
import HttpService from '../services/HttpService';
export const usePermissionFetcher = (
permissionsToCheck: PermissionsToCheck
) => {
const ability = useContext(AbilityContext);
useEffect(() => {
const processPermissionResult = (result: PermissionCheckResponseBody) => {
const oldRules = ability.rules;
const { can, cannot, rules } = new AbilityBuilder(Ability);
Object.keys(result.results).forEach((url: string) => {
const permissionVerbResults = result.results[url];
Object.keys(permissionVerbResults).forEach((permissionVerb: string) => {
const hasPermission = permissionVerbResults[permissionVerb];
if (hasPermission) {
can(permissionVerb, url);
} else {
cannot(permissionVerb, url);
}
});
});
oldRules.forEach((oldRule: any) => {
if (oldRule.inverted) {
cannot(oldRule.action, oldRule.subject);
} else {
can(oldRule.action, oldRule.subject);
}
});
ability.update(rules);
};
HttpService.makeCallToBackend({
path: `/permissions-check`,
httpMethod: 'POST',
successCallback: processPermissionResult,
postBody: { requests_to_check: permissionsToCheck },
});
});
return { ability };
};

View File

@ -0,0 +1,20 @@
import { useParams } from 'react-router-dom';
export const useUriListForPermissions = () => {
const params = useParams();
const targetUris = {
authenticationListPath: `/v1.0/authentications`,
messageInstanceListPath: '/v1.0/messages',
processGroupListPath: '/v1.0/process-groups',
processGroupShowPath: `/v1.0/process-groups/${params.process_group_id}`,
processInstanceActionPath: `/v1.0/process-models/${params.process_model_id}/process-instances`,
processInstanceListPath: '/v1.0/process-instances',
processModelCreatePath: `/v1.0/process-models/${params.process_group_id}`,
processModelFileCreatePath: `/v1.0/process-models/${params.process_model_id}/files`,
processModelFileShowPath: `/v1.0/process-models/${params.process_model_id}/files/${params.file_name}`,
processModelShowPath: `/v1.0/process-models/${params.process_model_id}`,
secretListPath: `/v1.0/secrets`,
};
return { targetUris };
};

View File

@ -5,21 +5,21 @@
color: white; color: white;
} }
h1{ h1 {
height: 36px;
font-family: 'IBM Plex Sans';
font-style: normal;
font-weight: 400; font-weight: 400;
font-size: 28px; font-size: 28px;
line-height: 36px; line-height: 36px;
color: #161616; color: #161616;
flex: none;
order: 0;
align-self: stretch;
flex-grow: 0;
margin-bottom: 1em margin-bottom: 1em
} }
h2 {
font-weight: 400;
font-size: 20px;
line-height: 28px;
color: #161616;
}
.span-tag { .span-tag {
color: black; color: black;
} }
@ -31,7 +31,7 @@ h1{
border: 1px solid #393939; border: 1px solid #393939;
} }
.cds--btn.button-white-background:hover { .cds--btn.button-white-background:hover {
background: #525252; background: lightgrey;
} }
.cds--breadcrumb-item a.cds--link:hover { .cds--breadcrumb-item a.cds--link:hover {
@ -71,7 +71,7 @@ h1{
} }
code { code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace; monospace;
} }
.app-logo { .app-logo {
@ -182,9 +182,64 @@ h1.with-icons {
} }
/* Json Web Form CSS Fix - Bootstrap now requries that each li have a "list-inline-item." Also have a PR /* Json Web Form CSS Fix - Bootstrap now requries that each li have a "list-inline-item." Also have a PR
in on this with the react-jsonschema-form repo. This is just a patch fix to allow date inputs to layout a little more cleanly */ in on this with the react-jsonschema-form repo. This is just a patch fix to allow date inputs to layout a little more cleanly */
.list-inline>li { .list-inline>li {
display: inline-block; display: inline-block;
padding-right: 5px; padding-right: 5px;
padding-left: 5px; padding-left: 5px;
} }
.cds--tile.tile-process-group {
padding: 0px;
margin: 16px;
width: 354px;
height: 264px;
background: #F4F4F4;
order: 1;
float: left;
}
.tile-process-group-content-container {
width: 354px;
height: 264px;
padding: 1em;
position: relative;
}
.tile-process-group-display-name {
margin-top: 2em;
margin-bottom: 1em;
font-size: 20px;
line-height: 28px;
color: #161616;
order: 0;
}
.tile-title-top {
margin-bottom: 2em;
font-size: 20px;
line-height: 28px;
color: #161616;
order: 0;
}
.tile-description {
font-size: 14px;
line-height: 20px;
letter-spacing: 0.16px;
color: #161616;
order: 1;
}
.tile-process-group-children-count {
font-size: 14px;
line-height: 20px;
letter-spacing: 0.16px;
color: #161616;
order: 1;
}
.tile-pin-bottom {
position: absolute;
bottom: 1em;
}

View File

@ -11,16 +11,18 @@ export interface RecentProcessModel {
processModelDisplayName: string; processModelDisplayName: string;
} }
export interface ProcessGroup { export interface ProcessReference {
id: string;
display_name: string;
description?: string | null;
}
export interface ProcessFileReference {
id: string; // The unique id of the process or decision table. id: string; // The unique id of the process or decision table.
name: string; // The process or decision table name. name: string; // The process or decision Display name.
identifier: string;
display_name: string;
process_group_id: string;
process_model_id: string;
type: string; // either "decision" or "process" type: string; // either "decision" or "process"
file_name: string;
has_lanes: boolean;
is_executable: boolean;
is_primary: boolean;
} }
export interface ProcessFile { export interface ProcessFile {
@ -28,12 +30,17 @@ export interface ProcessFile {
last_modified: string; last_modified: string;
name: string; name: string;
process_model_id: string; process_model_id: string;
references: ProcessFileReference[]; references: ProcessReference[];
size: number; size: number;
type: string; type: string;
file_contents?: string; file_contents?: string;
} }
export interface ProcessInstance {
id: number;
process_model_identifier: string;
}
export interface ProcessModel { export interface ProcessModel {
id: string; id: string;
description: string; description: string;
@ -42,6 +49,14 @@ export interface ProcessModel {
files: ProcessFile[]; files: ProcessFile[];
} }
export interface ProcessGroup {
id: string;
display_name: string;
description?: string | null;
process_models?: ProcessModel[];
process_groups?: ProcessGroup[];
}
// tuple of display value and URL // tuple of display value and URL
export type HotCrumbItem = [displayValue: string, url?: string]; export type HotCrumbItem = [displayValue: string, url?: string];
@ -70,3 +85,28 @@ export interface PaginationObject {
export interface CarbonComboBoxSelection { export interface CarbonComboBoxSelection {
selectedItem: ProcessModel; selectedItem: ProcessModel;
} }
export interface CarbonComboBoxProcessSelection {
selectedItem: ProcessReference;
}
export interface PermissionsToCheck {
[key: string]: string[];
}
export interface PermissionVerbResults {
[key: string]: boolean;
}
export interface PermissionCheckResult {
[key: string]: PermissionVerbResults;
}
export interface PermissionCheckResponseBody {
results: PermissionCheckResult;
}
export interface FormField {
id: string;
title: string;
required: boolean;
type: string;
enum: string[];
}

View File

@ -20,10 +20,8 @@ import ReactFormEditor from './ReactFormEditor';
import ErrorContext from '../contexts/ErrorContext'; import ErrorContext from '../contexts/ErrorContext';
import ProcessInstanceLogList from './ProcessInstanceLogList'; import ProcessInstanceLogList from './ProcessInstanceLogList';
import MessageInstanceList from './MessageInstanceList'; import MessageInstanceList from './MessageInstanceList';
import SecretList from './SecretList'; import Configuration from './Configuration';
import SecretNew from './SecretNew'; import JsonSchemaFormBuilder from './JsonSchemaFormBuilder';
import SecretShow from './SecretShow';
import AuthenticationList from './AuthenticationList';
export default function AdminRoutes() { export default function AdminRoutes() {
const location = useLocation(); const location = useLocation();
@ -110,10 +108,11 @@ export default function AdminRoutes() {
/> />
<Route path="process-instances" element={<ProcessInstanceList />} /> <Route path="process-instances" element={<ProcessInstanceList />} />
<Route path="messages" element={<MessageInstanceList />} /> <Route path="messages" element={<MessageInstanceList />} />
<Route path="secrets" element={<SecretList />} /> <Route path="configuration/*" element={<Configuration />} />
<Route path="secrets/new" element={<SecretNew />} /> <Route
<Route path="secrets/:key" element={<SecretShow />} /> path="process-models/:process_model_id/form-builder"
<Route path="authentications" element={<AuthenticationList />} /> element={<JsonSchemaFormBuilder />}
/>
</Routes> </Routes>
); );
} }

View File

@ -0,0 +1,5 @@
import MyCompletedInstances from '../components/MyCompletedInstances';
export default function CompletedInstances() {
return <MyCompletedInstances />;
}

View File

@ -0,0 +1,65 @@
import { useContext, 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 SecretList from './SecretList';
import SecretNew from './SecretNew';
import SecretShow from './SecretShow';
import AuthenticationList from './AuthenticationList';
import { useUriListForPermissions } from '../hooks/UriListForPermissions';
import { PermissionsToCheck } from '../interfaces';
import { usePermissionFetcher } from '../hooks/PermissionService';
export default function Configuration() {
const location = useLocation();
const setErrorMessage = (useContext as any)(ErrorContext)[1];
const [selectedTabIndex, setSelectedTabIndex] = useState<number>(0);
const navigate = useNavigate();
const { targetUris } = useUriListForPermissions();
const permissionRequestData: PermissionsToCheck = {
[targetUris.authenticationListPath]: ['GET'],
[targetUris.secretListPath]: ['GET'],
};
const { ability } = usePermissionFetcher(permissionRequestData);
useEffect(() => {
setErrorMessage(null);
let newSelectedTabIndex = 0;
if (location.pathname.match(/^\/admin\/configuration\/authentications\b/)) {
newSelectedTabIndex = 1;
}
setSelectedTabIndex(newSelectedTabIndex);
}, [location, setErrorMessage]);
return (
<>
<Tabs selectedIndex={selectedTabIndex}>
<TabList aria-label="List of tabs">
<Can I="GET" a={targetUris.secretListPath} ability={ability}>
<Tab onClick={() => navigate('/admin/configuration/secrets')}>
Secrets
</Tab>
</Can>
<Can I="GET" a={targetUris.authenticationListPath} ability={ability}>
<Tab
onClick={() => navigate('/admin/configuration/authentications')}
>
Authentications
</Tab>
</Can>
</TabList>
</Tabs>
<br />
<Routes>
<Route path="/" element={<SecretList />} />
<Route path="secrets" element={<SecretList />} />
<Route path="secrets/new" element={<SecretNew />} />
<Route path="secrets/:key" element={<SecretShow />} />
<Route path="authentications" element={<AuthenticationList />} />
</Routes>
</>
);
}

View File

@ -0,0 +1,9 @@
import ProcessModelListTiles from '../components/ProcessModelListTiles';
export default function CreateNewInstance() {
return (
<ProcessModelListTiles
headerElement={<h1>Process models available to you</h1>}
/>
);
}

View File

@ -0,0 +1,15 @@
import TasksForMyOpenProcesses from '../components/TasksForMyOpenProcesses';
import TasksWaitingForMe from '../components/TasksWaitingForMe';
import TasksForWaitingForMyGroups from '../components/TasksWaitingForMyGroups';
export default function GroupedTasks() {
return (
<>
<TasksForMyOpenProcesses />
<br />
<TasksWaitingForMe />
<br />
<TasksForWaitingForMyGroups />
</>
);
}

View File

@ -0,0 +1,67 @@
import { useContext, 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';
import CreateNewInstance from './CreateNewInstance';
export default function HomePageRoutes() {
const location = useLocation();
const setErrorMessage = (useContext as any)(ErrorContext)[1];
const [selectedTabIndex, setSelectedTabIndex] = useState<number>(0);
const navigate = useNavigate();
useEffect(() => {
setErrorMessage(null);
let newSelectedTabIndex = 0;
if (location.pathname.match(/^\/tasks\/grouped\b/)) {
newSelectedTabIndex = 1;
} else if (location.pathname.match(/^\/tasks\/completed-instances\b/)) {
newSelectedTabIndex = 2;
} else if (location.pathname.match(/^\/tasks\/create-new-instance\b/)) {
newSelectedTabIndex = 3;
}
setSelectedTabIndex(newSelectedTabIndex);
}, [location, setErrorMessage]);
const renderTabs = () => {
if (location.pathname.match(/^\/tasks\/\d+\/\b/)) {
return null;
}
return (
<>
<Tabs selectedIndex={selectedTabIndex}>
<TabList aria-label="List of tabs">
<Tab onClick={() => navigate('/tasks/my-tasks')}>My Tasks</Tab>
<Tab onClick={() => navigate('/tasks/grouped')}>Grouped Tasks</Tab>
<Tab onClick={() => navigate('/tasks/completed-instances')}>
Completed Instances
</Tab>
<Tab onClick={() => navigate('/tasks/create-new-instance')}>
Create New Instance +
</Tab>
</TabList>
</Tabs>
<br />
</>
);
};
return (
<>
{renderTabs()}
<Routes>
<Route path="/" element={<MyTasks />} />
<Route path="my-tasks" element={<MyTasks />} />
<Route path=":process_instance_id/:task_id" element={<TaskShow />} />
<Route path="grouped" element={<GroupedTasks />} />
<Route path="completed-instances" element={<CompletedInstances />} />
<Route path="create-new-instance" element={<CreateNewInstance />} />
</Routes>
</>
);
}

View File

@ -0,0 +1,250 @@
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 { modifyProcessModelPath, slugifyString } from '../helpers';
import HttpService from '../services/HttpService';
export default function JsonSchemaFormBuilder() {
const params = useParams();
const formFieldTypes = ['textbox', 'checkbox', 'select'];
const [formTitle, setFormTitle] = useState<string>('');
const [formDescription, setFormDescription] = useState<string>('');
const [formId, setFormId] = useState<string>('');
const [formFields, setFormFields] = useState<FormField[]>([]);
const [showNewFormField, setShowNewFormField] = useState<boolean>(false);
const [formFieldSelectOptions, setFormFieldSelectOptions] =
useState<string>('');
const [formIdHasBeenUpdatedByUser, setFormIdHasBeenUpdatedByUser] =
useState<boolean>(false);
const [formFieldIdHasBeenUpdatedByUser, setFormFieldIdHasBeenUpdatedByUser] =
useState<boolean>(false);
const [showFormFieldSelectTextField, setShowFormFieldSelectTextField] =
useState<boolean>(false);
const [formFieldId, setFormFieldId] = useState<string>('');
const [formFieldTitle, setFormFieldTitle] = useState<string>('');
const [formFieldType, setFormFieldType] = useState<string>('');
const modifiedProcessModelId = modifyProcessModelPath(
`${params.process_model_id}`
);
useEffect(() => {}, []);
const renderFormJson = () => {
const formJson = {
title: formTitle,
description: formDescription,
properties: {},
required: [],
};
formFields.forEach((formField: FormField) => {
let jsonSchemaFieldType = 'string';
if (formField.type === 'checkbox') {
jsonSchemaFieldType = 'boolean';
}
const formJsonObject: any = {
type: jsonSchemaFieldType,
title: formField.title,
};
if (formField.type === 'select') {
formJsonObject.enum = formField.enum;
}
(formJson.properties as any)[formField.id] = formJsonObject;
});
return JSON.stringify(formJson, null, 2);
};
const renderFormUiJson = () => {
const uiOrder = formFields.map((formField: FormField) => {
return formField.id;
});
return JSON.stringify({ 'ui:order': uiOrder }, null, 2);
};
const onFormFieldTitleChange = (newFormFieldTitle: string) => {
console.log('newFormFieldTitle', newFormFieldTitle);
console.log(
'setFormFieldIdHasBeenUpdatedByUser',
formFieldIdHasBeenUpdatedByUser
);
if (!formFieldIdHasBeenUpdatedByUser) {
setFormFieldId(slugifyString(newFormFieldTitle));
}
setFormFieldTitle(newFormFieldTitle);
};
const onFormTitleChange = (newFormTitle: string) => {
if (!formIdHasBeenUpdatedByUser) {
setFormId(slugifyString(newFormTitle));
}
setFormTitle(newFormTitle);
};
const addFormField = () => {
const newFormField: FormField = {
id: formFieldId,
title: formFieldTitle,
required: false,
type: formFieldType,
enum: formFieldSelectOptions.split(','),
};
setFormFieldIdHasBeenUpdatedByUser(false);
setShowNewFormField(false);
setFormFields([...formFields, newFormField]);
};
const handleFormFieldTypeChange = (event: any) => {
setFormFieldType(event.srcElement.value);
if (event.srcElement.value === 'select') {
setShowFormFieldSelectTextField(true);
} else {
setShowFormFieldSelectTextField(false);
}
};
const newFormFieldComponent = () => {
if (showNewFormField) {
return (
<>
<TextInput
id="form-field-title"
name="title"
labelText="Title"
value={formFieldTitle}
onChange={(event: any) => {
onFormFieldTitleChange(event.srcElement.value);
}}
/>
<TextInput
id="json-form-field-id"
name="id"
labelText="ID"
value={formFieldId}
onChange={(event: any) => {
setFormFieldIdHasBeenUpdatedByUser(true);
setFormFieldId(event.srcElement.value);
}}
/>
<Select
id="form-field-type"
labelText="Type"
onChange={handleFormFieldTypeChange}
>
{formFieldTypes.map((fft: string) => {
return <SelectItem text={fft} value={fft} />;
})}
</Select>
{showFormFieldSelectTextField ? (
<TextInput
id="json-form-field-select-options"
name="select-options"
labelText="Select Options"
onChange={(event: any) => {
setFormFieldSelectOptions(event.srcElement.value);
}}
/>
) : null}
<Button onClick={addFormField}>Add Field</Button>
</>
);
}
return null;
};
const formFieldArea = () => {
if (formFields.length > 0) {
return formFields.map((formField: FormField) => {
return <p>Form Field: {formField.id}</p>;
});
}
return null;
};
const handleSaveCallback = (result: any) => {
console.log('result', result);
};
const uploadFile = (file: File) => {
const url = `/process-models/${modifiedProcessModelId}/files`;
const httpMethod = 'POST';
const formData = new FormData();
formData.append('file', file);
formData.append('fileName', file.name);
HttpService.makeCallToBackend({
path: url,
successCallback: handleSaveCallback,
httpMethod,
postBody: formData,
});
};
const saveFile = () => {
const formJsonFileName = `${formId}-schema.json`;
const formUiJsonFileName = `${formId}-uischema.json`;
uploadFile(new File([renderFormJson()], formJsonFileName));
uploadFile(new File([renderFormUiJson()], formUiJsonFileName));
};
const jsonFormArea = () => {
return (
<>
<Button onClick={saveFile}>Save</Button>
<TextInput
id="json-form-title"
name="title"
labelText="Title"
value={formTitle}
onChange={(event: any) => {
onFormTitleChange(event.srcElement.value);
}}
/>
<TextInput
id="json-form-id"
name="id"
labelText="ID"
value={formId}
onChange={(event: any) => {
setFormIdHasBeenUpdatedByUser(true);
setFormId(event.srcElement.value);
}}
/>
<TextInput
id="form-description"
name="description"
labelText="Description"
value={formDescription}
onChange={(event: any) => {
setFormDescription(event.srcElement.value);
}}
/>
<Button
onClick={() => {
setFormFieldId('');
setFormFieldTitle('');
setFormFieldType('');
setFormFieldSelectOptions('');
setShowFormFieldSelectTextField(false);
setShowNewFormField(true);
}}
>
New Field
</Button>
{formFieldArea()}
{newFormFieldComponent()}
</>
);
};
return <>{jsonFormArea()}</>;
}

View File

@ -5,7 +5,7 @@ import { Link, useParams, useSearchParams } from 'react-router-dom';
import PaginationForTable from '../components/PaginationForTable'; import PaginationForTable from '../components/PaginationForTable';
import ProcessBreadcrumb from '../components/ProcessBreadcrumb'; import ProcessBreadcrumb from '../components/ProcessBreadcrumb';
import { import {
convertSecondsToFormattedDate, convertSecondsToFormattedDateString,
getPageInfoFromSearchParams, getPageInfoFromSearchParams,
modifyProcessModelPath, modifyProcessModelPath,
unModifyProcessModelPath, unModifyProcessModelPath,
@ -65,10 +65,12 @@ export default function MessageInstanceList() {
</td> </td>
<td>{rowToUse.message_identifier}</td> <td>{rowToUse.message_identifier}</td>
<td>{rowToUse.message_type}</td> <td>{rowToUse.message_type}</td>
<td>{rowToUse.failure_cause}</td> <td>{rowToUse.failure_cause || '-'}</td>
<td>{rowToUse.status}</td> <td>{rowToUse.status}</td>
<td> <td>
{convertSecondsToFormattedDate(rowToUse.created_at_in_seconds)} {convertSecondsToFormattedDateString(
rowToUse.created_at_in_seconds
)}
</td> </td>
</tr> </tr>
); );
@ -94,14 +96,8 @@ export default function MessageInstanceList() {
if (pagination) { if (pagination) {
const { page, perPage } = getPageInfoFromSearchParams(searchParams); const { page, perPage } = getPageInfoFromSearchParams(searchParams);
let queryParamString = '';
let breadcrumbElement = null; let breadcrumbElement = null;
if (searchParams.get('process_instance_id')) { if (searchParams.get('process_instance_id')) {
queryParamString += `&process_group_id=${searchParams.get(
'process_group_id'
)}&process_model_id=${searchParams.get(
'process_model_id'
)}&process_instance_id=${searchParams.get('process_instance_id')}`;
breadcrumbElement = ( breadcrumbElement = (
<ProcessBreadcrumb <ProcessBreadcrumb
hotCrumbs={[ hotCrumbs={[
@ -132,8 +128,6 @@ export default function MessageInstanceList() {
perPage={perPage} perPage={perPage}
pagination={pagination} pagination={pagination}
tableToDisplay={buildTable()} tableToDisplay={buildTable()}
queryParamString={queryParamString}
path="/admin/messages"
/> />
</> </>
); );

View File

@ -6,30 +6,38 @@ import PaginationForTable from '../components/PaginationForTable';
import { import {
getPageInfoFromSearchParams, getPageInfoFromSearchParams,
modifyProcessModelPath, modifyProcessModelPath,
refreshAtInterval,
} from '../helpers'; } from '../helpers';
import HttpService from '../services/HttpService'; import HttpService from '../services/HttpService';
import { PaginationObject, RecentProcessModel } from '../interfaces'; import { PaginationObject, RecentProcessModel } from '../interfaces';
const PER_PAGE_FOR_TASKS_ON_HOME_PAGE = 5; const PER_PAGE_FOR_TASKS_ON_HOME_PAGE = 5;
const REFRESH_INTERVAL = 10;
const REFRESH_TIMEOUT = 600;
export default function HomePage() { export default function MyTasks() {
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const [tasks, setTasks] = useState([]); const [tasks, setTasks] = useState([]);
const [pagination, setPagination] = useState<PaginationObject | null>(null); const [pagination, setPagination] = useState<PaginationObject | null>(null);
useEffect(() => { useEffect(() => {
const { page, perPage } = getPageInfoFromSearchParams( const getTasks = () => {
searchParams, const { page, perPage } = getPageInfoFromSearchParams(
PER_PAGE_FOR_TASKS_ON_HOME_PAGE searchParams,
); PER_PAGE_FOR_TASKS_ON_HOME_PAGE
const setTasksFromResult = (result: any) => { );
setTasks(result.results); const setTasksFromResult = (result: any) => {
setPagination(result.pagination); setTasks(result.results);
setPagination(result.pagination);
};
HttpService.makeCallToBackend({
path: `/tasks?per_page=${perPage}&page=${page}`,
successCallback: setTasksFromResult,
});
}; };
HttpService.makeCallToBackend({
path: `/tasks?per_page=${perPage}&page=${page}`, getTasks();
successCallback: setTasksFromResult, refreshAtInterval(REFRESH_INTERVAL, REFRESH_TIMEOUT, getTasks);
});
}, [searchParams]); }, [searchParams]);
let recentProcessModels: RecentProcessModel[] = []; let recentProcessModels: RecentProcessModel[] = [];
@ -122,7 +130,7 @@ export default function HomePage() {
}); });
return ( return (
<> <>
<h1>Processes I can start</h1> <h1>Recently viewed process models</h1>
<Table striped bordered> <Table striped bordered>
<thead> <thead>
<tr> <tr>
@ -152,7 +160,6 @@ export default function HomePage() {
perPageOptions={[2, PER_PAGE_FOR_TASKS_ON_HOME_PAGE, 25]} perPageOptions={[2, PER_PAGE_FOR_TASKS_ON_HOME_PAGE, 25]}
pagination={pagination} pagination={pagination}
tableToDisplay={buildTable()} tableToDisplay={buildTable()}
path="/tasks"
/> />
</> </>
); );
@ -170,6 +177,7 @@ export default function HomePage() {
return ( return (
<> <>
{tasksWaitingForMe} {tasksWaitingForMe}
<br />
{relevantProcessModelSection} {relevantProcessModelSection}
</> </>
); );

View File

@ -1,42 +1,34 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { Link, useNavigate, useSearchParams } from 'react-router-dom'; import { useNavigate, useSearchParams } from 'react-router-dom';
import { import {
Button, Button,
Table,
// ExpandableTile,
// TileAboveTheFoldContent,
// TileBelowTheFoldContent,
// TextInput,
// ClickableTile,
// @ts-ignore // @ts-ignore
} from '@carbon/react'; } from '@carbon/react';
import { Can } from '@casl/react';
import ProcessBreadcrumb from '../components/ProcessBreadcrumb'; import ProcessBreadcrumb from '../components/ProcessBreadcrumb';
import PaginationForTable from '../components/PaginationForTable';
import HttpService from '../services/HttpService'; import HttpService from '../services/HttpService';
import { import { modifyProcessModelPath } from '../helpers';
getPageInfoFromSearchParams, import { CarbonComboBoxSelection, PermissionsToCheck } from '../interfaces';
modifyProcessModelPath, import { useUriListForPermissions } from '../hooks/UriListForPermissions';
} from '../helpers'; import { usePermissionFetcher } from '../hooks/PermissionService';
import { CarbonComboBoxSelection, ProcessGroup } from '../interfaces';
import ProcessModelSearch from '../components/ProcessModelSearch'; import ProcessModelSearch from '../components/ProcessModelSearch';
import ProcessGroupListTiles from '../components/ProcessGroupListTiles';
// Example process group json
// {'process_group_id': 'sure', 'display_name': 'Test Workflows', 'id': 'test_process_group'}
export default function ProcessGroupList() { export default function ProcessGroupList() {
const navigate = useNavigate();
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const navigate = useNavigate();
const [processGroups, setProcessGroups] = useState([]);
const [pagination, setPagination] = useState(null);
const [processModelAvailableItems, setProcessModelAvailableItems] = useState( const [processModelAvailableItems, setProcessModelAvailableItems] = useState(
[] []
); );
const { targetUris } = useUriListForPermissions();
const permissionRequestData: PermissionsToCheck = {
[targetUris.processGroupListPath]: ['POST'],
};
const { ability } = usePermissionFetcher(permissionRequestData);
useEffect(() => { useEffect(() => {
const setProcessGroupsFromResult = (result: any) => {
setProcessGroups(result.results);
setPagination(result.pagination);
};
const processResultForProcessModels = (result: any) => { const processResultForProcessModels = (result: any) => {
const selectionArray = result.results.map((item: any) => { const selectionArray = result.results.map((item: any) => {
const label = `${item.id}`; const label = `${item.id}`;
@ -45,13 +37,6 @@ export default function ProcessGroupList() {
}); });
setProcessModelAvailableItems(selectionArray); setProcessModelAvailableItems(selectionArray);
}; };
const { page, perPage } = getPageInfoFromSearchParams(searchParams);
// for browsing
HttpService.makeCallToBackend({
path: `/process-groups?per_page=${perPage}&page=${page}`,
successCallback: setProcessGroupsFromResult,
});
// for search box // for search box
HttpService.makeCallToBackend({ HttpService.makeCallToBackend({
path: `/process-models?per_page=1000`, path: `/process-models?per_page=1000`,
@ -59,66 +44,6 @@ export default function ProcessGroupList() {
}); });
}, [searchParams]); }, [searchParams]);
const buildTable = () => {
const rows = processGroups.map((row: ProcessGroup) => {
return (
<tr key={(row as any).id}>
<td>
<Link
to={`/admin/process-groups/${(row as any).id}`}
title={(row as any).id}
>
{(row as any).display_name}
</Link>
</td>
</tr>
);
});
return (
<Table striped bordered>
<thead>
<tr>
<th>Process Group</th>
</tr>
</thead>
<tbody>{rows}</tbody>
</Table>
);
// const rows = processGroups.map((row: ProcessGroup) => {
// return (
// <span>
// <ClickableTile href={`/admin/process-groups/${row.id}`}>
// {row.display_name}
// </ClickableTile>
// </span>
// );
// });
//
// return <div style={{ width: '400px' }}>{rows}</div>;
};
const processGroupsDisplayArea = () => {
const { page, perPage } = getPageInfoFromSearchParams(searchParams);
let displayText = null;
if (processGroups?.length > 0) {
displayText = (
<>
<h3>Browse</h3>
<PaginationForTable
page={page}
perPage={perPage}
pagination={pagination as any}
tableToDisplay={buildTable()}
path="/admin/process-groups"
/>
</>
);
} else {
displayText = <p>No Groups To Display</p>;
}
return displayText;
};
const processModelSearchArea = () => { const processModelSearchArea = () => {
const processModelSearchOnChange = (selection: CarbonComboBoxSelection) => { const processModelSearchOnChange = (selection: CarbonComboBoxSelection) => {
const processModel = selection.selectedItem; const processModel = selection.selectedItem;
@ -135,18 +60,21 @@ export default function ProcessGroupList() {
); );
}; };
if (pagination) { if (processModelAvailableItems) {
return ( return (
<> <>
<ProcessBreadcrumb hotCrumbs={[['Process Groups']]} /> <ProcessBreadcrumb hotCrumbs={[['Process Groups']]} />
<Button kind="secondary" href="/admin/process-groups/new"> <Can I="POST" a={targetUris.processGroupListPath} ability={ability}>
Add a process group <Button kind="secondary" href="/admin/process-groups/new">
</Button> Add a process group
<br /> </Button>
<br />
<br />
</Can>
<br /> <br />
{processModelSearchArea()} {processModelSearchArea()}
<br /> <br />
{processGroupsDisplayArea()} <ProcessGroupListTiles />
</> </>
); );
} }

View File

@ -2,6 +2,7 @@ import { useEffect, useState } from 'react';
import { Link, useSearchParams, useParams } from 'react-router-dom'; import { Link, useSearchParams, useParams } from 'react-router-dom';
// @ts-ignore // @ts-ignore
import { Button, Table, Stack } from '@carbon/react'; import { Button, Table, Stack } from '@carbon/react';
import { Can } from '@casl/react';
import ProcessBreadcrumb from '../components/ProcessBreadcrumb'; import ProcessBreadcrumb from '../components/ProcessBreadcrumb';
import PaginationForTable from '../components/PaginationForTable'; import PaginationForTable from '../components/PaginationForTable';
import HttpService from '../services/HttpService'; import HttpService from '../services/HttpService';
@ -10,7 +11,15 @@ import {
modifyProcessModelPath, modifyProcessModelPath,
unModifyProcessModelPath, unModifyProcessModelPath,
} from '../helpers'; } from '../helpers';
import { ProcessGroup, ProcessModel } from '../interfaces'; import {
PaginationObject,
PermissionsToCheck,
ProcessGroup,
ProcessModel,
} from '../interfaces';
import { useUriListForPermissions } from '../hooks/UriListForPermissions';
import { usePermissionFetcher } from '../hooks/PermissionService';
import ProcessGroupListTiles from '../components/ProcessGroupListTiles';
export default function ProcessGroupShow() { export default function ProcessGroupShow() {
const params = useParams(); const params = useParams();
@ -18,9 +27,16 @@ export default function ProcessGroupShow() {
const [processGroup, setProcessGroup] = useState<ProcessGroup | null>(null); const [processGroup, setProcessGroup] = useState<ProcessGroup | null>(null);
const [processModels, setProcessModels] = useState([]); const [processModels, setProcessModels] = useState([]);
const [processGroups, setProcessGroups] = useState([]); const [modelPagination, setModelPagination] =
const [modelPagination, setModelPagination] = useState(null); useState<PaginationObject | null>(null);
const [groupPagination, setGroupPagination] = useState(null);
const { targetUris } = useUriListForPermissions();
const permissionRequestData: PermissionsToCheck = {
[targetUris.processGroupListPath]: ['POST'],
[targetUris.processGroupShowPath]: ['PUT'],
[targetUris.processModelCreatePath]: ['POST'],
};
const { ability } = usePermissionFetcher(permissionRequestData);
useEffect(() => { useEffect(() => {
const { page, perPage } = getPageInfoFromSearchParams(searchParams); const { page, perPage } = getPageInfoFromSearchParams(searchParams);
@ -29,10 +45,6 @@ export default function ProcessGroupShow() {
setProcessModels(result.results); setProcessModels(result.results);
setModelPagination(result.pagination); setModelPagination(result.pagination);
}; };
const setProcessGroupFromResult = (result: any) => {
setProcessGroups(result.results);
setGroupPagination(result.pagination);
};
const processResult = (result: any) => { const processResult = (result: any) => {
setProcessGroup(result); setProcessGroup(result);
const unmodifiedProcessGroupId = unModifyProcessModelPath( const unmodifiedProcessGroupId = unModifyProcessModelPath(
@ -42,10 +54,6 @@ export default function ProcessGroupShow() {
path: `/process-models?process_group_identifier=${unmodifiedProcessGroupId}&per_page=${perPage}&page=${page}`, path: `/process-models?process_group_identifier=${unmodifiedProcessGroupId}&per_page=${perPage}&page=${page}`,
successCallback: setProcessModelFromResult, successCallback: setProcessModelFromResult,
}); });
HttpService.makeCallToBackend({
path: `/process-groups?process_group_identifier=${unmodifiedProcessGroupId}&per_page=${perPage}&page=${page}`,
successCallback: setProcessGroupFromResult,
});
}; };
HttpService.makeCallToBackend({ HttpService.makeCallToBackend({
path: `/process-groups/${params.process_group_id}`, path: `/process-groups/${params.process_group_id}`,
@ -58,7 +66,9 @@ export default function ProcessGroupShow() {
return null; return null;
} }
const rows = processModels.map((row: ProcessModel) => { const rows = processModels.map((row: ProcessModel) => {
const modifiedProcessModelId: String = modifyProcessModelPath((row as any).id); const modifiedProcessModelId: String = modifyProcessModelPath(
(row as any).id
);
return ( return (
<tr key={row.id}> <tr key={row.id}>
<td> <td>
@ -75,7 +85,7 @@ export default function ProcessGroupShow() {
}); });
return ( return (
<div> <div>
<h3>Process Models</h3> <h2>Process Models</h2>
<Table striped bordered> <Table striped bordered>
<thead> <thead>
<tr> <tr>
@ -89,43 +99,7 @@ export default function ProcessGroupShow() {
); );
}; };
const buildGroupTable = () => { if (processGroup && modelPagination) {
if (processGroup === null) {
return null;
}
const rows = processGroups.map((row: ProcessGroup) => {
const modifiedProcessGroupId: String = modifyProcessModelPath(row.id);
return (
<tr key={row.id}>
<td>
<Link
to={`/admin/process-groups/${modifiedProcessGroupId}`}
data-qa="process-model-show-link"
>
{row.id}
</Link>
</td>
<td>{row.display_name}</td>
</tr>
);
});
return (
<div>
<h3>Process Groups</h3>
<Table striped bordered>
<thead>
<tr>
<th>Process Group Id</th>
<th>Display Name</th>
</tr>
</thead>
<tbody>{rows}</tbody>
</Table>
</div>
);
};
if (processGroup && groupPagination && modelPagination) {
const { page, perPage } = getPageInfoFromSearchParams(searchParams); const { page, perPage } = getPageInfoFromSearchParams(searchParams);
const modifiedProcessGroupId = modifyProcessModelPath(processGroup.id); const modifiedProcessGroupId = modifyProcessModelPath(processGroup.id);
return ( return (
@ -136,43 +110,51 @@ export default function ProcessGroupShow() {
['', `process_group:${processGroup.id}`], ['', `process_group:${processGroup.id}`],
]} ]}
/> />
<h1>Process Group: {processGroup.display_name}</h1>
<ul> <ul>
<Stack orientation="horizontal" gap={3}> <Stack orientation="horizontal" gap={3}>
<Button <Can I="POST" a={targetUris.processGroupListPath} ability={ability}>
kind="secondary" <Button
href={`/admin/process-groups/new?parentGroupId=${processGroup.id}`} href={`/admin/process-groups/new?parentGroupId=${processGroup.id}`}
>
Add a process group
</Button>
</Can>
<Can
I="POST"
a={targetUris.processModelCreatePath}
ability={ability}
> >
Add a process group <Button
</Button> href={`/admin/process-models/${modifiedProcessGroupId}/new`}
<Button >
href={`/admin/process-models/${modifiedProcessGroupId}/new`} Add a process model
> </Button>
Add a process model </Can>
</Button> <Can I="PUT" a={targetUris.processGroupShowPath} ability={ability}>
<Button <Button
href={`/admin/process-groups/${modifiedProcessGroupId}/edit`} href={`/admin/process-groups/${modifiedProcessGroupId}/edit`}
variant="secondary" >
> Edit process group
Edit process group </Button>
</Button> </Can>
</Stack> </Stack>
<br /> <br />
<br /> <br />
<PaginationForTable {/* eslint-disable-next-line sonarjs/no-gratuitous-expressions */}
page={page} {modelPagination && modelPagination.total > 0 && (
perPage={perPage} <PaginationForTable
pagination={modelPagination} page={page}
tableToDisplay={buildModelTable()} perPage={perPage}
path={`/admin/process-groups/${processGroup.id}`} pagination={modelPagination}
/> tableToDisplay={buildModelTable()}
/>
)}
<br /> <br />
<br /> <br />
<PaginationForTable <ProcessGroupListTiles
page={page} processGroup={processGroup}
perPage={perPage} headerElement={<h2>Process Groups</h2>}
pagination={groupPagination}
tableToDisplay={buildGroupTable()}
path={`/admin/process-groups/${processGroup.id}`}
/> />
</ul> </ul>
</> </>

View File

@ -1,477 +1,15 @@
import { useContext, useEffect, useMemo, useState } from 'react'; import { useSearchParams } from 'react-router-dom';
import {
Link,
useNavigate,
useParams,
useSearchParams,
} from 'react-router-dom';
// @ts-ignore
import { Filter } from '@carbon/icons-react';
import {
Button,
ButtonSet,
DatePicker,
DatePickerInput,
Table,
Grid,
Column,
MultiSelect,
TableHeader,
TableHead,
TableRow,
// @ts-ignore
} from '@carbon/react';
import { PROCESS_STATUSES, DATE_FORMAT, DATE_FORMAT_CARBON } from '../config';
import {
convertDateStringToSeconds,
convertSecondsToFormattedDate,
getPageInfoFromSearchParams,
getProcessModelFullIdentifierFromSearchParams,
modifyProcessModelPath,
} from '../helpers';
import PaginationForTable from '../components/PaginationForTable';
import 'react-datepicker/dist/react-datepicker.css'; import 'react-datepicker/dist/react-datepicker.css';
import ErrorContext from '../contexts/ErrorContext';
import HttpService from '../services/HttpService';
import 'react-bootstrap-typeahead/css/Typeahead.css'; import 'react-bootstrap-typeahead/css/Typeahead.css';
import 'react-bootstrap-typeahead/css/Typeahead.bs5.css'; import 'react-bootstrap-typeahead/css/Typeahead.bs5.css';
import { PaginationObject, ProcessModel } from '../interfaces';
import ProcessModelSearch from '../components/ProcessModelSearch';
import ProcessBreadcrumb from '../components/ProcessBreadcrumb'; import ProcessBreadcrumb from '../components/ProcessBreadcrumb';
import ProcessInstanceListTable from '../components/ProcessInstanceListTable';
import { getProcessModelFullIdentifierFromSearchParams } from '../helpers';
export default function ProcessInstanceList() { export default function ProcessInstanceList() {
const params = useParams();
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const navigate = useNavigate();
const [processInstances, setProcessInstances] = useState([]);
const [reportMetadata, setReportMetadata] = useState({});
const [pagination, setPagination] = useState<PaginationObject | null>(null);
const oneHourInSeconds = 3600;
const oneMonthInSeconds = oneHourInSeconds * 24 * 30;
const [startFrom, setStartFrom] = useState<string>('');
const [startTo, setStartTo] = useState<string>('');
const [endFrom, setEndFrom] = useState<string>('');
const [endTo, setEndTo] = useState<string>('');
const [showFilterOptions, setShowFilterOptions] = useState<boolean>(false);
const setErrorMessage = (useContext as any)(ErrorContext)[1];
const [processStatusAllOptions, setProcessStatusAllOptions] = useState<any[]>(
[]
);
const [processStatusSelection, setProcessStatusSelection] = useState<
string[]
>([]);
const [processModelAvailableItems, setProcessModelAvailableItems] = useState<
ProcessModel[]
>([]);
const [processModelSelection, setProcessModelSelection] =
useState<ProcessModel | null>(null);
const parametersToAlwaysFilterBy = useMemo(() => {
return {
start_from: setStartFrom,
start_to: setStartTo,
end_from: setEndFrom,
end_to: setEndTo,
};
}, [setStartFrom, setStartTo, setEndFrom, setEndTo]);
const parametersToGetFromSearchParams = useMemo(() => {
return {
process_model_identifier: null,
process_status: null,
};
}, []);
// eslint-disable-next-line sonarjs/cognitive-complexity
useEffect(() => {
function setProcessInstancesFromResult(result: any) {
const processInstancesFromApi = result.results;
setProcessInstances(processInstancesFromApi);
setReportMetadata(result.report_metadata);
setPagination(result.pagination);
}
function getProcessInstances() {
const { page, perPage } = getPageInfoFromSearchParams(searchParams);
let queryParamString = `per_page=${perPage}&page=${page}`;
Object.keys(parametersToAlwaysFilterBy).forEach((paramName: string) => {
// @ts-expect-error TS(7053) FIXME:
const functionToCall = parametersToAlwaysFilterBy[paramName];
const searchParamValue = searchParams.get(paramName);
if (searchParamValue) {
queryParamString += `&${paramName}=${searchParamValue}`;
const dateString = convertSecondsToFormattedDate(
searchParamValue as any
);
functionToCall(dateString);
setShowFilterOptions(true);
}
});
Object.keys(parametersToGetFromSearchParams).forEach(
(paramName: string) => {
if (searchParams.get(paramName)) {
// @ts-expect-error TS(7053) FIXME:
const functionToCall = parametersToGetFromSearchParams[paramName];
queryParamString += `&${paramName}=${searchParams.get(paramName)}`;
if (functionToCall !== null) {
functionToCall(searchParams.get(paramName) || '');
}
setShowFilterOptions(true);
}
}
);
HttpService.makeCallToBackend({
path: `/process-instances?${queryParamString}`,
successCallback: setProcessInstancesFromResult,
});
}
function processResultForProcessModels(result: any) {
const processModelFullIdentifier =
getProcessModelFullIdentifierFromSearchParams(searchParams);
const selectionArray = result.results.map((item: any) => {
const label = `${item.id}`;
Object.assign(item, { label });
if (label === processModelFullIdentifier) {
setProcessModelSelection(item);
}
return item;
});
setProcessModelAvailableItems(selectionArray);
const processStatusSelectedArray: string[] = [];
const processStatusAllOptionsArray = PROCESS_STATUSES.map(
(processStatusOption: any) => {
const regex = new RegExp(`\\b${processStatusOption}\\b`);
if ((searchParams.get('process_status') || '').match(regex)) {
processStatusSelectedArray.push(processStatusOption);
}
return processStatusOption;
}
);
setProcessStatusSelection(processStatusSelectedArray);
setProcessStatusAllOptions(processStatusAllOptionsArray);
getProcessInstances();
}
// populate process model selection
HttpService.makeCallToBackend({
path: `/process-models?per_page=1000`,
successCallback: processResultForProcessModels,
});
}, [
searchParams,
params,
oneMonthInSeconds,
oneHourInSeconds,
parametersToAlwaysFilterBy,
parametersToGetFromSearchParams,
]);
// does the comparison, but also returns false if either argument
// is not truthy and therefore not comparable.
const isTrueComparison = (param1: any, operation: any, param2: any) => {
if (param1 && param2) {
switch (operation) {
case '<':
return param1 < param2;
case '>':
return param1 > param2;
default:
return false;
}
} else {
return false;
}
};
const applyFilter = (event: any) => {
event.preventDefault();
const { page, perPage } = getPageInfoFromSearchParams(searchParams);
let queryParamString = `per_page=${perPage}&page=${page}`;
const startFromSeconds = convertDateStringToSeconds(startFrom);
const endFromSeconds = convertDateStringToSeconds(endFrom);
const startToSeconds = convertDateStringToSeconds(startTo);
const endToSeconds = convertDateStringToSeconds(endTo);
if (isTrueComparison(startFromSeconds, '>', startToSeconds)) {
setErrorMessage({
message: '"Start date from" cannot be after "start date to"',
});
return;
}
if (isTrueComparison(endFromSeconds, '>', endToSeconds)) {
setErrorMessage({
message: '"End date from" cannot be after "end date to"',
});
return;
}
if (isTrueComparison(startFromSeconds, '>', endFromSeconds)) {
setErrorMessage({
message: '"Start date from" cannot be after "end date from"',
});
return;
}
if (isTrueComparison(startToSeconds, '>', endToSeconds)) {
setErrorMessage({
message: '"Start date to" cannot be after "end date to"',
});
return;
}
if (startFromSeconds) {
queryParamString += `&start_from=${startFromSeconds}`;
}
if (startToSeconds) {
queryParamString += `&start_to=${startToSeconds}`;
}
if (endFromSeconds) {
queryParamString += `&end_from=${endFromSeconds}`;
}
if (endToSeconds) {
queryParamString += `&end_to=${endToSeconds}`;
}
if (processStatusSelection.length > 0) {
queryParamString += `&process_status=${processStatusSelection}`;
}
if (processModelSelection) {
queryParamString += `&process_model_identifier=${processModelSelection.id}`;
}
setErrorMessage(null);
navigate(`/admin/process-instances?${queryParamString}`);
};
const dateComponent = (
labelString: any,
name: any,
initialDate: any,
onChangeFunction: any
) => {
return (
<DatePicker dateFormat={DATE_FORMAT_CARBON} datePickerType="single">
<DatePickerInput
id={`date-picker-${name}`}
placeholder={DATE_FORMAT}
labelText={labelString}
type="text"
size="md"
autocomplete="off"
allowInput={false}
onChange={(dateChangeEvent: any) => {
onChangeFunction(dateChangeEvent.srcElement.value);
}}
value={initialDate}
/>
</DatePicker>
);
};
const getSearchParamsAsQueryString = () => {
let queryParamString = '';
Object.keys(parametersToAlwaysFilterBy).forEach((paramName) => {
const searchParamValue = searchParams.get(paramName);
if (searchParamValue) {
queryParamString += `&${paramName}=${searchParamValue}`;
}
});
Object.keys(parametersToGetFromSearchParams).forEach(
(paramName: string) => {
if (searchParams.get(paramName)) {
queryParamString += `&${paramName}=${searchParams.get(paramName)}`;
}
}
);
return queryParamString;
};
const processStatusSearch = () => {
return (
<MultiSelect
label="Choose Status"
className="our-class"
id="process-instance-status-select"
titleText="Status"
items={processStatusAllOptions}
onChange={(selection: any) => {
setProcessStatusSelection(selection.selectedItems);
}}
itemToString={(item: any) => {
return item || '';
}}
selectionFeedback="top-after-reopen"
selectedItems={processStatusSelection}
/>
);
};
const clearFilters = () => {
setProcessModelSelection(null);
setProcessStatusSelection([]);
setStartFrom('');
setStartTo('');
setEndFrom('');
setEndTo('');
};
const filterOptions = () => {
if (!showFilterOptions) {
return null;
}
return (
<>
<Grid fullWidth className="with-bottom-margin">
<Column md={8}>
<ProcessModelSearch
onChange={(selection: any) =>
setProcessModelSelection(selection.selectedItem)
}
processModels={processModelAvailableItems}
selectedItem={processModelSelection}
/>
</Column>
<Column md={8}>{processStatusSearch()}</Column>
</Grid>
<Grid fullWidth className="with-bottom-margin">
<Column md={4}>
{dateComponent(
'Start date from',
'start-from',
startFrom,
setStartFrom
)}
</Column>
<Column md={4}>
{dateComponent('Start date to', 'start-to', startTo, setStartTo)}
</Column>
<Column md={4}>
{dateComponent('End date from', 'end-from', endFrom, setEndFrom)}
</Column>
<Column md={4}>
{dateComponent('End date to', 'end-to', endTo, setEndTo)}
</Column>
</Grid>
<Grid fullWidth className="with-bottom-margin">
<Column md={4}>
<ButtonSet>
<Button
kind=""
className="button-white-background"
onClick={clearFilters}
>
Clear
</Button>
<Button
kind="secondary"
onClick={applyFilter}
data-qa="filter-button"
>
Filter
</Button>
</ButtonSet>
</Column>
</Grid>
</>
);
};
const buildTable = () => {
const headerLabels: Record<string, string> = {
id: 'Process Instance Id',
process_model_identifier: 'Process Model',
start_in_seconds: 'Start Time',
end_in_seconds: 'End Time',
status: 'Status',
spiff_step: 'SpiffWorkflow Step',
};
const getHeaderLabel = (header: string) => {
return headerLabels[header] ?? header;
};
const headers = (reportMetadata as any).columns.map((column: any) => {
// return <th>{getHeaderLabel((column as any).Header)}</th>;
return getHeaderLabel((column as any).Header);
});
const formatProcessInstanceId = (row: any, id: any) => {
const modifiedProcessModelId: String = modifyProcessModelPath(
row.process_model_identifier
);
return (
<Link
data-qa="process-instance-show-link"
to={`/admin/process-models/${modifiedProcessModelId}/process-instances/${row.id}`}
>
{id}
</Link>
);
};
const formatProcessModelIdentifier = (_row: any, identifier: any) => {
return (
<Link
to={`/admin/process-models/${modifyProcessModelPath(identifier)}`}
>
{identifier}
</Link>
);
};
const formatSecondsForDisplay = (_row: any, seconds: any) => {
return convertSecondsToFormattedDate(seconds) || '-';
};
const defaultFormatter = (_row: any, value: any) => {
return value;
};
const columnFormatters: Record<string, any> = {
id: formatProcessInstanceId,
process_model_identifier: formatProcessModelIdentifier,
start_in_seconds: formatSecondsForDisplay,
end_in_seconds: formatSecondsForDisplay,
};
const formattedColumn = (row: any, column: any) => {
const formatter = columnFormatters[column.accessor] ?? defaultFormatter;
const value = row[column.accessor];
if (column.accessor === 'status') {
return (
<td data-qa={`process-instance-status-${value}`}>
{formatter(row, value)}
</td>
);
}
return <td>{formatter(row, value)}</td>;
};
const rows = processInstances.map((row: any) => {
const currentRow = (reportMetadata as any).columns.map((column: any) => {
return formattedColumn(row, column);
});
return <tr key={row.id}>{currentRow}</tr>;
});
return (
<Table size="lg">
<TableHead>
<TableRow>
{headers.map((header: any) => (
<TableHeader key={header}>{header}</TableHeader>
))}
</TableRow>
</TableHead>
<tbody>{rows}</tbody>
</Table>
);
};
const processInstanceBreadcrumbElement = () => { const processInstanceBreadcrumbElement = () => {
const processModelFullIdentifier = const processModelFullIdentifier =
getProcessModelFullIdentifierFromSearchParams(searchParams); getProcessModelFullIdentifierFromSearchParams(searchParams);
@ -496,47 +34,11 @@ export default function ProcessInstanceList() {
const processInstanceTitleElement = () => { const processInstanceTitleElement = () => {
return <h1>Process Instances</h1>; return <h1>Process Instances</h1>;
}; };
return (
const toggleShowFilterOptions = () => { <>
setShowFilterOptions(!showFilterOptions); {processInstanceBreadcrumbElement()}
}; {processInstanceTitleElement()}
<ProcessInstanceListTable />
if (pagination) { </>
const { page, perPage } = getPageInfoFromSearchParams(searchParams); );
return (
<>
{processInstanceBreadcrumbElement()}
{processInstanceTitleElement()}
<Grid fullWidth>
<Column
sm={{ span: 1, offset: 3 }}
md={{ span: 1, offset: 7 }}
lg={{ span: 1, offset: 15 }}
>
<Button
data-qa="filter-section-expand-toggle"
kind="ghost"
renderIcon={Filter}
iconDescription="Filter Options"
hasIconOnly
size="lg"
onClick={toggleShowFilterOptions}
/>
</Column>
</Grid>
{filterOptions()}
<br />
<PaginationForTable
page={page}
perPage={perPage}
pagination={pagination}
tableToDisplay={buildTable()}
queryParamString={getSearchParamsAsQueryString()}
path="/admin/process-instances"
/>
</>
);
}
return null;
} }

View File

@ -6,9 +6,9 @@ import PaginationForTable from '../components/PaginationForTable';
import ProcessBreadcrumb from '../components/ProcessBreadcrumb'; import ProcessBreadcrumb from '../components/ProcessBreadcrumb';
import { import {
getPageInfoFromSearchParams, getPageInfoFromSearchParams,
convertSecondsToFormattedDate,
modifyProcessModelPath, modifyProcessModelPath,
unModifyProcessModelPath, unModifyProcessModelPath,
convertSecondsToFormattedDateTime,
} from '../helpers'; } from '../helpers';
import HttpService from '../services/HttpService'; import HttpService from '../services/HttpService';
@ -49,7 +49,7 @@ export default function ProcessInstanceLogList() {
data-qa="process-instance-show-link" data-qa="process-instance-show-link"
to={`/admin/process-models/${modifiedProcessModelId}/process-instances/${rowToUse.process_instance_id}/${rowToUse.spiff_step}`} to={`/admin/process-models/${modifiedProcessModelId}/process-instances/${rowToUse.process_instance_id}/${rowToUse.spiff_step}`}
> >
{convertSecondsToFormattedDate(rowToUse.timestamp)} {convertSecondsToFormattedDateTime(rowToUse.timestamp)}
</Link> </Link>
</td> </td>
</tr> </tr>
@ -74,7 +74,6 @@ export default function ProcessInstanceLogList() {
}; };
if (pagination) { if (pagination) {
console.log('params.process_model_id', params.process_model_id);
const { page, perPage } = getPageInfoFromSearchParams(searchParams); const { page, perPage } = getPageInfoFromSearchParams(searchParams);
return ( return (
<main> <main>
@ -99,7 +98,6 @@ export default function ProcessInstanceLogList() {
perPage={perPage} perPage={perPage}
pagination={pagination} pagination={pagination}
tableToDisplay={buildTable()} tableToDisplay={buildTable()}
path={`/admin/process-models/${modifiedProcessModelId}/process-instances/${params.process_instance_id}/logs`}
/> />
</main> </main>
); );

View File

@ -44,9 +44,9 @@ export default function ProcessInstanceReportList() {
const headerStuff = ( const headerStuff = (
<> <>
<h1>Process Instance Reports</h1> <h1>Process Instance Perspectives</h1>
<Button href="/admin/process-instances/reports/new"> <Button href="/admin/process-instances/reports/new">
Add a process instance report Add a process instance perspective
</Button> </Button>
</> </>
); );
@ -61,7 +61,7 @@ export default function ProcessInstanceReportList() {
return ( return (
<main> <main>
{headerStuff} {headerStuff}
<p>No reports found</p> <p>No perspectives found</p>
</main> </main>
); );
} }

View File

@ -56,7 +56,7 @@ export default function ProcessInstanceReportNew() {
return ( return (
<> <>
<ProcessBreadcrumb /> <ProcessBreadcrumb />
<h1>Add Process Model</h1> <h1>Add Process Instance Perspective</h1>
<form onSubmit={addProcessInstanceReport}> <form onSubmit={addProcessInstanceReport}>
<label htmlFor="identifier"> <label htmlFor="identifier">
identifier: identifier:

View File

@ -80,18 +80,17 @@ export default function ProcessInstanceReport() {
processGroupId={params.process_group_id} processGroupId={params.process_group_id}
linkProcessModel linkProcessModel
/> />
<h1>Process Instance Report: {params.report_identifier}</h1> <h1>Process Instance Perspective: {params.report_identifier}</h1>
<Button <Button
href={`/admin/process-instances/reports/${params.report_identifier}/edit`} href={`/admin/process-instances/reports/${params.report_identifier}/edit`}
> >
Edit process instance report Edit process instance perspective
</Button> </Button>
<PaginationForTable <PaginationForTable
page={page} page={page}
perPage={perPage} perPage={perPage}
pagination={pagination} pagination={pagination}
tableToDisplay={buildTable()} tableToDisplay={buildTable()}
path={`/admin/process-models/${params.process_group_id}/${params.process_model_id}/process-instances/report`}
/> />
</main> </main>
); );

View File

@ -23,6 +23,7 @@ import {
Stack, Stack,
// @ts-ignore // @ts-ignore
} from '@carbon/react'; } from '@carbon/react';
import { Can } from '@casl/react';
import ProcessBreadcrumb from '../components/ProcessBreadcrumb'; import ProcessBreadcrumb from '../components/ProcessBreadcrumb';
import HttpService from '../services/HttpService'; import HttpService from '../services/HttpService';
import ReactDiagramEditor from '../components/ReactDiagramEditor'; import ReactDiagramEditor from '../components/ReactDiagramEditor';
@ -32,6 +33,9 @@ import {
} from '../helpers'; } from '../helpers';
import ButtonWithConfirmation from '../components/ButtonWithConfirmation'; import ButtonWithConfirmation from '../components/ButtonWithConfirmation';
import ErrorContext from '../contexts/ErrorContext'; import ErrorContext from '../contexts/ErrorContext';
import { useUriListForPermissions } from '../hooks/UriListForPermissions';
import { PermissionsToCheck } from '../interfaces';
import { usePermissionFetcher } from '../hooks/PermissionService';
export default function ProcessInstanceShow() { export default function ProcessInstanceShow() {
const navigate = useNavigate(); const navigate = useNavigate();
@ -50,6 +54,12 @@ export default function ProcessInstanceShow() {
); );
const modifiedProcessModelId = params.process_model_id; const modifiedProcessModelId = params.process_model_id;
const { targetUris } = useUriListForPermissions();
const permissionRequestData: PermissionsToCheck = {
[targetUris.messageInstanceListPath]: ['GET'],
};
const { ability } = usePermissionFetcher(permissionRequestData);
const navigateToProcessInstances = (_result: any) => { const navigateToProcessInstances = (_result: any) => {
navigate( navigate(
`/admin/process-instances?process_model_identifier=${unModifiedProcessModelId}` `/admin/process-instances?process_model_identifier=${unModifiedProcessModelId}`
@ -63,12 +73,12 @@ export default function ProcessInstanceShow() {
}); });
if (typeof params.spiff_step === 'undefined') if (typeof params.spiff_step === 'undefined')
HttpService.makeCallToBackend({ HttpService.makeCallToBackend({
path: `/process-instance/${params.process_instance_id}/tasks?all_tasks=true`, path: `/process-instances/${modifiedProcessModelId}/${params.process_instance_id}/tasks?all_tasks=true`,
successCallback: setTasks, successCallback: setTasks,
}); });
else else
HttpService.makeCallToBackend({ HttpService.makeCallToBackend({
path: `/process-instance/${params.process_instance_id}/tasks?all_tasks=true&spiff_step=${params.spiff_step}`, path: `/process-instances/${modifiedProcessModelId}/${params.process_instance_id}/tasks?all_tasks=true&spiff_step=${params.spiff_step}`,
successCallback: setTasks, successCallback: setTasks,
}); });
}, [params, modifiedProcessModelId]); }, [params, modifiedProcessModelId]);
@ -245,14 +255,20 @@ export default function ProcessInstanceShow() {
> >
Logs Logs
</Button> </Button>
<Button <Can
size="sm" I="GET"
className="button-white-background" a={targetUris.messageInstanceListPath}
data-qa="process-instance-message-instance-list-link" ability={ability}
href={`/admin/messages?process_model_id=${params.process_model_id}&process_instance_id=${params.process_instance_id}`}
> >
Messages <Button
</Button> size="sm"
className="button-white-background"
data-qa="process-instance-message-instance-list-link"
href={`/admin/messages?process_model_id=${params.process_model_id}&process_instance_id=${params.process_instance_id}`}
>
Messages
</Button>
</Can>
</ButtonSet> </ButtonSet>
</Column> </Column>
</Grid> </Grid>
@ -521,6 +537,7 @@ export default function ProcessInstanceShow() {
elements.push(resumeButton(processInstanceToUse)); elements.push(resumeButton(processInstanceToUse));
elements.push( elements.push(
<ButtonWithConfirmation <ButtonWithConfirmation
data-qa="process-instance-delete"
kind="ghost" kind="ghost"
renderIcon={TrashCan} renderIcon={TrashCan}
iconDescription="Delete" iconDescription="Delete"

View File

@ -18,7 +18,13 @@ import ProcessBreadcrumb from '../components/ProcessBreadcrumb';
import HttpService from '../services/HttpService'; import HttpService from '../services/HttpService';
import ErrorContext from '../contexts/ErrorContext'; import ErrorContext from '../contexts/ErrorContext';
import { makeid, modifyProcessModelPath } from '../helpers'; import { makeid, modifyProcessModelPath } from '../helpers';
import { ProcessFile, ProcessModel } from '../interfaces'; import {
CarbonComboBoxProcessSelection,
ProcessFile,
ProcessModel,
ProcessReference,
} from '../interfaces';
import ProcessSearch from '../components/ProcessSearch';
export default function ProcessModelEditDiagram() { export default function ProcessModelEditDiagram() {
const [showFileNameEditor, setShowFileNameEditor] = useState(false); const [showFileNameEditor, setShowFileNameEditor] = useState(false);
@ -36,6 +42,10 @@ export default function ProcessModelEditDiagram() {
const [markdownText, setMarkdownText] = useState<string | undefined>(''); const [markdownText, setMarkdownText] = useState<string | undefined>('');
const [markdownEventBus, setMarkdownEventBus] = useState<any>(null); const [markdownEventBus, setMarkdownEventBus] = useState<any>(null);
const [showMarkdownEditor, setShowMarkdownEditor] = useState(false); const [showMarkdownEditor, setShowMarkdownEditor] = useState(false);
const [showProcessSearch, setShowProcessSearch] = useState(false);
const [processSearchEventBus, setProcessSearchEventBus] = useState<any>(null);
const [processSearchElement, setProcessSearchElement] = useState<any>(null);
const [processes, setProcesses] = useState<ProcessReference[]>([]);
const handleShowMarkdownEditor = () => setShowMarkdownEditor(true); const handleShowMarkdownEditor = () => setShowMarkdownEditor(true);
@ -90,6 +100,23 @@ export default function ProcessModelEditDiagram() {
const processModelPath = `process-models/${modifiedProcessModelId}`; const processModelPath = `process-models/${modifiedProcessModelId}`;
useEffect(() => {
// Grab all available process models in case we need to search for them.
// Taken from the Process Group List
const processResults = (result: any) => {
const selectionArray = result.map((item: any) => {
const label = `${item.display_name} (${item.identifier})`;
Object.assign(item, { label });
return item;
});
setProcesses(selectionArray);
};
HttpService.makeCallToBackend({
path: `/processes`,
successCallback: processResults,
});
}, [processModel]);
useEffect(() => { useEffect(() => {
const processResult = (result: ProcessModel) => { const processResult = (result: ProcessModel) => {
setProcessModel(result); setProcessModel(result);
@ -215,6 +242,7 @@ export default function ProcessModelEditDiagram() {
secondaryButtonText="Cancel" secondaryButtonText="Cancel"
onSecondarySubmit={handleFileNameCancel} onSecondarySubmit={handleFileNameCancel}
onRequestSubmit={handleFileNameSave} onRequestSubmit={handleFileNameSave}
onRequestClose={handleFileNameCancel}
> >
<label>File Name:</label> <label>File Name:</label>
<span> <span>
@ -278,7 +306,7 @@ export default function ProcessModelEditDiagram() {
const options: any[] = []; const options: any[] = [];
dmnFiles.forEach((file) => { dmnFiles.forEach((file) => {
file.references.forEach((ref) => { file.references.forEach((ref) => {
options.push({ label: ref.name, value: ref.id }); options.push({ label: ref.display_name, value: ref.identifier });
}); });
}); });
event.eventBus.fire('spiff.dmn_files.returned', { options }); event.eventBus.fire('spiff.dmn_files.returned', { options });
@ -607,6 +635,7 @@ export default function ProcessModelEditDiagram() {
primaryButtonText="Close" primaryButtonText="Close"
onRequestSubmit={handleScriptEditorClose} onRequestSubmit={handleScriptEditorClose}
size="lg" size="lg"
onRequestClose={handleScriptEditorClose}
> >
<Editor <Editor
height={500} height={500}
@ -644,6 +673,7 @@ export default function ProcessModelEditDiagram() {
modalHeading="Edit Markdown" modalHeading="Edit Markdown"
primaryButtonText="Close" primaryButtonText="Close"
onRequestSubmit={handleMarkdownEditorClose} onRequestSubmit={handleMarkdownEditorClose}
onRequestClose={handleMarkdownEditorClose}
size="lg" size="lg"
> >
<MDEditor <MDEditor
@ -656,6 +686,45 @@ export default function ProcessModelEditDiagram() {
); );
}; };
const onSearchProcessModels = (
processId: string,
eventBus: any,
element: any
) => {
setProcessSearchEventBus(eventBus);
setProcessSearchElement(element);
setShowProcessSearch(true);
};
const processSearchOnClose = (selection: CarbonComboBoxProcessSelection) => {
const selectedProcessModel = selection.selectedItem;
if (selectedProcessModel) {
processSearchEventBus.fire('spiff.callactivity.update', {
element: processSearchElement,
value: selectedProcessModel.identifier,
});
}
setShowProcessSearch(false);
};
const processModelSelector = () => {
return (
<Modal
open={showProcessSearch}
modalHeading="Select Process Model"
primaryButtonText="Close"
onRequestSubmit={processSearchOnClose}
size="lg"
>
<ProcessSearch
height="500px"
onChange={processSearchOnClose}
processes={processes}
titleText="Process model search"
/>
</Modal>
);
};
const findFileNameForReferenceId = ( const findFileNameForReferenceId = (
id: string, id: string,
type: string type: string
@ -676,19 +745,18 @@ export default function ProcessModelEditDiagram() {
return matchFile; return matchFile;
}; };
/**
* fixme: Not currently in use. This would only work for bpmn files within the process model. Which is right for DMN and json, but not right here. Need to merge in work on the nested process groups before tackling this.
* @param processId
*/
const onLaunchBpmnEditor = (processId: string) => { const onLaunchBpmnEditor = (processId: string) => {
const file = findFileNameForReferenceId(processId, 'bpmn'); const processRef = processes.find((p) => {
if (file) { return p.identifier === processId;
});
if (processRef) {
const path = generatePath( const path = generatePath(
'/admin/process-models/:process_model_id/files/:file_name', '/admin/process-models/:process_model_path/files/:file_name',
{ {
process_model_id: params.process_model_id, process_model_path: modifyProcessModelPath(
file_name: file.name, processRef.process_model_id
),
file_name: processRef.file_name,
} }
); );
window.open(path); window.open(path);
@ -763,6 +831,7 @@ export default function ProcessModelEditDiagram() {
onJsonFilesRequested={onJsonFilesRequested} onJsonFilesRequested={onJsonFilesRequested}
onLaunchDmnEditor={onLaunchDmnEditor} onLaunchDmnEditor={onLaunchDmnEditor}
onDmnFilesRequested={onDmnFilesRequested} onDmnFilesRequested={onDmnFilesRequested}
onSearchProcessModels={onSearchProcessModels}
/> />
); );
}; };
@ -790,7 +859,7 @@ export default function ProcessModelEditDiagram() {
{newFileNameBox()} {newFileNameBox()}
{scriptEditor()} {scriptEditor()}
{markdownEditor()} {markdownEditor()}
{processModelSelector()}
<div id="diagram-container" /> <div id="diagram-container" />
</> </>
); );

View File

@ -27,12 +27,26 @@ import {
TableBody, TableBody,
// @ts-ignore // @ts-ignore
} from '@carbon/react'; } from '@carbon/react';
import { Can } from '@casl/react';
import ProcessBreadcrumb from '../components/ProcessBreadcrumb'; import ProcessBreadcrumb from '../components/ProcessBreadcrumb';
import HttpService from '../services/HttpService'; import HttpService from '../services/HttpService';
import ErrorContext from '../contexts/ErrorContext'; import ErrorContext from '../contexts/ErrorContext';
import { modifyProcessModelPath, unModifyProcessModelPath } from '../helpers'; import {
import { ProcessFile, ProcessModel, RecentProcessModel } from '../interfaces'; getGroupFromModifiedModelId,
modifyProcessModelPath,
} from '../helpers';
import {
PermissionsToCheck,
ProcessFile,
ProcessInstance,
ProcessModel,
RecentProcessModel,
} from '../interfaces';
import ButtonWithConfirmation from '../components/ButtonWithConfirmation'; import ButtonWithConfirmation from '../components/ButtonWithConfirmation';
import ProcessInstanceListTable from '../components/ProcessInstanceListTable';
import { usePermissionFetcher } from '../hooks/PermissionService';
import { useUriListForPermissions } from '../hooks/UriListForPermissions';
import ProcessInstanceRun from '../components/ProcessInstanceRun';
const storeRecentProcessModelInLocalStorage = ( const storeRecentProcessModelInLocalStorage = (
processModelForStorage: ProcessModel processModelForStorage: ProcessModel
@ -88,13 +102,23 @@ export default function ProcessModelShow() {
const setErrorMessage = (useContext as any)(ErrorContext)[1]; const setErrorMessage = (useContext as any)(ErrorContext)[1];
const [processModel, setProcessModel] = useState<ProcessModel | null>(null); const [processModel, setProcessModel] = useState<ProcessModel | null>(null);
const [processInstanceResult, setProcessInstanceResult] = useState(null); const [processInstance, setProcessInstance] =
useState<ProcessInstance | null>(null);
const [reloadModel, setReloadModel] = useState<boolean>(false); const [reloadModel, setReloadModel] = useState<boolean>(false);
const [filesToUpload, setFilesToUpload] = useState<any>(null); const [filesToUpload, setFilesToUpload] = useState<any>(null);
const [showFileUploadModal, setShowFileUploadModal] = const [showFileUploadModal, setShowFileUploadModal] =
useState<boolean>(false); useState<boolean>(false);
const navigate = useNavigate(); const navigate = useNavigate();
const { targetUris } = useUriListForPermissions();
const permissionRequestData: PermissionsToCheck = {
[targetUris.processModelShowPath]: ['PUT', 'DELETE'],
[targetUris.processInstanceListPath]: ['GET'],
[targetUris.processInstanceActionPath]: ['POST'],
[targetUris.processModelFileCreatePath]: ['POST', 'GET', 'DELETE'],
};
const { ability } = usePermissionFetcher(permissionRequestData);
const modifiedProcessModelId = modifyProcessModelPath( const modifiedProcessModelId = modifyProcessModelPath(
`${params.process_model_id}` `${params.process_model_id}`
); );
@ -111,52 +135,19 @@ export default function ProcessModelShow() {
}); });
}, [reloadModel, modifiedProcessModelId]); }, [reloadModel, modifiedProcessModelId]);
const processModelRun = (processInstance: any) => { const processInstanceRunResultTag = () => {
setErrorMessage(null); if (processInstance) {
HttpService.makeCallToBackend({
path: `/process-instances/${processInstance.id}/run`,
successCallback: setProcessInstanceResult,
failureCallback: setErrorMessage,
httpMethod: 'POST',
});
};
const processInstanceCreateAndRun = () => {
HttpService.makeCallToBackend({
path: `/process-models/${modifiedProcessModelId}/process-instances`,
successCallback: processModelRun,
httpMethod: 'POST',
});
};
const processInstanceResultTag = () => {
if (processModel && processInstanceResult) {
let takeMeToMyTaskBlurb = null;
// FIXME: ensure that the task is actually for the current user as well
const processInstanceId = (processInstanceResult as any).id;
const nextTask = (processInstanceResult as any).next_task;
if (nextTask && nextTask.state === 'READY') {
takeMeToMyTaskBlurb = (
<span>
You have a task to complete. Go to{' '}
<Link to={`/tasks/${processInstanceId}/${nextTask.id}`}>
my task
</Link>
.
</span>
);
}
return ( return (
<div className="alert alert-success" role="alert"> <div className="alert alert-success" role="alert">
<p> <p>
Process Instance {processInstanceId} kicked off ( Process Instance {processInstance.id} kicked off (
<Link <Link
to={`/admin/process-models/${modifiedProcessModelId}/process-instances/${processInstanceId}`} to={`/admin/process-models/${modifiedProcessModelId}/process-instances/${processInstance.id}`}
data-qa="process-instance-show-link" data-qa="process-instance-show-link"
> >
view view
</Link> </Link>
). {takeMeToMyTaskBlurb} ).
</p> </p>
</div> </div>
); );
@ -242,6 +233,22 @@ export default function ProcessModelShow() {
return null; return null;
}; };
const navigateToProcessModels = (_result: any) => {
navigate(
`/admin/process-groups/${getGroupFromModifiedModelId(
modifiedProcessModelId
)}`
);
};
const deleteProcessModel = () => {
HttpService.makeCallToBackend({
path: `/process-models/${modifiedProcessModelId}`,
successCallback: navigateToProcessModels,
httpMethod: 'DELETE',
});
};
const navigateToFileEdit = (processModelFile: ProcessFile) => { const navigateToFileEdit = (processModelFile: ProcessFile) => {
const url = profileModelFileEditUrl(processModelFile); const url = profileModelFileEditUrl(processModelFile);
if (url) { if (url) {
@ -255,50 +262,62 @@ export default function ProcessModelShow() {
) => { ) => {
const elements = []; const elements = [];
elements.push( elements.push(
<Button <Can I="GET" a={targetUris.processModelFileCreatePath} ability={ability}>
kind="ghost" <Button
renderIcon={Edit} kind="ghost"
iconDescription="Edit File" renderIcon={Edit}
hasIconOnly iconDescription="Edit File"
size="lg" hasIconOnly
data-qa={`edit-file-${processModelFile.name.replace('.', '-')}`} size="lg"
onClick={() => navigateToFileEdit(processModelFile)} data-qa={`edit-file-${processModelFile.name.replace('.', '-')}`}
/> onClick={() => navigateToFileEdit(processModelFile)}
/>
</Can>
); );
elements.push( elements.push(
<Button <Can I="GET" a={targetUris.processModelFileCreatePath} ability={ability}>
kind="ghost" <Button
renderIcon={Download} kind="ghost"
iconDescription="Download File" renderIcon={Download}
hasIconOnly iconDescription="Download File"
size="lg" hasIconOnly
onClick={() => downloadFile(processModelFile.name)} size="lg"
/> onClick={() => downloadFile(processModelFile.name)}
/>
</Can>
); );
elements.push( elements.push(
<ButtonWithConfirmation <Can
kind="ghost" I="DELETE"
renderIcon={TrashCan} a={targetUris.processModelFileCreatePath}
iconDescription="Delete File" ability={ability}
hasIconOnly >
description={`Delete file: ${processModelFile.name}`} <ButtonWithConfirmation
onConfirmation={() => { kind="ghost"
onDeleteFile(processModelFile.name); renderIcon={TrashCan}
}} iconDescription="Delete File"
confirmButtonLabel="Delete" hasIconOnly
/> description={`Delete file: ${processModelFile.name}`}
onConfirmation={() => {
onDeleteFile(processModelFile.name);
}}
confirmButtonLabel="Delete"
/>
</Can>
); );
if (processModelFile.name.match(/\.bpmn$/) && !isPrimaryBpmnFile) { if (processModelFile.name.match(/\.bpmn$/) && !isPrimaryBpmnFile) {
elements.push( elements.push(
<Button <Can I="PUT" a={targetUris.processModelShowPath} ability={ability}>
kind="ghost" <Button
renderIcon={Favorite} kind="ghost"
iconDescription="Set As Primary File" renderIcon={Favorite}
hasIconOnly iconDescription="Set As Primary File"
size="lg" hasIconOnly
onClick={() => onSetPrimaryFile(processModelFile.name)} size="lg"
/> onClick={() => onSetPrimaryFile(processModelFile.name)}
/>
</Can>
); );
} }
return elements; return elements;
@ -329,7 +348,11 @@ export default function ProcessModelShow() {
let fileLink = null; let fileLink = null;
const fileUrl = profileModelFileEditUrl(processModelFile); const fileUrl = profileModelFileEditUrl(processModelFile);
if (fileUrl) { if (fileUrl) {
fileLink = <Link to={fileUrl}>{processModelFile.name}</Link>; if (ability.can('GET', targetUris.processModelFileCreatePath)) {
fileLink = <Link to={fileUrl}>{processModelFile.name}</Link>;
} else {
fileLink = <span>{processModelFile.name}</span>;
}
} }
constructedTag = ( constructedTag = (
<TableRow key={processModelFile.name}> <TableRow key={processModelFile.name}>
@ -343,7 +366,6 @@ export default function ProcessModelShow() {
return constructedTag; return constructedTag;
}); });
// return <ul>{tags}</ul>;
const headers = ['Name', 'Actions']; const headers = ['Name', 'Actions'];
return ( return (
<Table size="lg" useZebraStyles={false}> <Table size="lg" useZebraStyles={false}>
@ -361,29 +383,9 @@ export default function ProcessModelShow() {
); );
}; };
const processInstancesUl = () => {
const unmodifiedProcessModelId: String = unModifyProcessModelPath(
`${params.process_model_id}`
);
if (!processModel) {
return null;
}
return (
<ul>
<li>
<Link
to={`/admin/process-instances?process_model_identifier=${unmodifiedProcessModelId}`}
data-qa="process-instance-list-link"
>
List
</Link>
</li>
</ul>
);
};
const handleFileUploadCancel = () => { const handleFileUploadCancel = () => {
setShowFileUploadModal(false); setShowFileUploadModal(false);
setFilesToUpload(null);
}; };
const handleFileUpload = (event: any) => { const handleFileUpload = (event: any) => {
@ -401,6 +403,7 @@ export default function ProcessModelShow() {
}); });
} }
setShowFileUploadModal(false); setShowFileUploadModal(false);
setFilesToUpload(null);
}; };
const fileUploadModal = () => { const fileUploadModal = () => {
@ -428,6 +431,7 @@ export default function ProcessModelShow() {
iconDescription="Delete file" iconDescription="Delete file"
name="" name=""
multiple={false} multiple={false}
onDelete={() => setFilesToUpload(null)}
onChange={(event: any) => setFilesToUpload(event.target.files)} onChange={(event: any) => setFilesToUpload(event.target.files)}
/> />
</Modal> </Modal>
@ -439,10 +443,11 @@ export default function ProcessModelShow() {
return null; return null;
} }
return ( return (
<Grid fullWidth> <Grid condensed fullWidth>
<Column md={5} lg={9} sm={3}> <Column md={5} lg={9} sm={3}>
<Accordion align="end"> <Accordion align="end" open>
<AccordionItem <AccordionItem
open
data-qa="files-accordion" data-qa="files-accordion"
title={ title={
<Stack orientation="horizontal"> <Stack orientation="horizontal">
@ -454,47 +459,53 @@ export default function ProcessModelShow() {
</Stack> </Stack>
} }
> >
<ButtonSet> <Can
<Button I="POST"
renderIcon={Upload} a={targetUris.processModelFileCreatePath}
data-qa="upload-file-button" ability={ability}
onClick={() => setShowFileUploadModal(true)} >
size="sm" <ButtonSet>
kind="" <Button
className="button-white-background" renderIcon={Upload}
> data-qa="upload-file-button"
Upload File onClick={() => setShowFileUploadModal(true)}
</Button> size="sm"
<Button kind=""
renderIcon={Add} className="button-white-background"
href={`/admin/process-models/${modifiedProcessModelId}/files?file_type=bpmn`} >
size="sm" Upload File
> </Button>
New BPMN File <Button
</Button> renderIcon={Add}
<Button href={`/admin/process-models/${modifiedProcessModelId}/files?file_type=bpmn`}
renderIcon={Add} size="sm"
href={`/admin/process-models/${modifiedProcessModelId}/files?file_type=dmn`} >
size="sm" New BPMN File
> </Button>
New DMN File <Button
</Button> renderIcon={Add}
<Button href={`/admin/process-models/${modifiedProcessModelId}/files?file_type=dmn`}
renderIcon={Add} size="sm"
href={`/admin/process-models/${modifiedProcessModelId}/form?file_ext=json`} >
size="sm" New DMN File
> </Button>
New JSON File <Button
</Button> renderIcon={Add}
<Button href={`/admin/process-models/${modifiedProcessModelId}/form?file_ext=json`}
renderIcon={Add} size="sm"
href={`/admin/process-models/${modifiedProcessModelId}/form?file_ext=md`} >
size="sm" New JSON File
> </Button>
New Markdown File <Button
</Button> renderIcon={Add}
</ButtonSet> href={`/admin/process-models/${modifiedProcessModelId}/form?file_ext=md`}
<br /> size="sm"
>
New Markdown File
</Button>
</ButtonSet>
<br />
</Can>
{processModelFileList()} {processModelFileList()}
</AccordionItem> </AccordionItem>
</Accordion> </Accordion>
@ -516,27 +527,57 @@ export default function ProcessModelShow() {
], ],
]} ]}
/> />
<h1>Process Model: {processModel.display_name}</h1> <Stack orientation="horizontal" gap={1}>
<h1 className="with-icons">
Process Model: {processModel.display_name}
</h1>
<Can I="DELETE" a={targetUris.processModelShowPath} ability={ability}>
<ButtonWithConfirmation
kind="ghost"
renderIcon={TrashCan}
iconDescription="Delete Process Model"
hasIconOnly
description={`Delete process model: ${processModel.display_name}`}
onConfirmation={deleteProcessModel}
confirmButtonLabel="Delete"
/>
</Can>
</Stack>
<p className="process-description">{processModel.description}</p> <p className="process-description">{processModel.description}</p>
<Stack orientation="horizontal" gap={3}> <Stack orientation="horizontal" gap={3}>
<Button onClick={processInstanceCreateAndRun} variant="primary"> <Can
Run I="POST"
</Button> a={targetUris.processInstanceActionPath}
<Button ability={ability}
href={`/admin/process-models/${modifiedProcessModelId}/edit`}
variant="secondary"
> >
Edit process model <ProcessInstanceRun
</Button> processModel={processModel}
onSuccessCallback={setProcessInstance}
/>
</Can>
<Can I="PUT" a={targetUris.processModelShowPath} ability={ability}>
<Button
href={`/admin/process-models/${modifiedProcessModelId}/edit`}
variant="secondary"
>
Edit process model
</Button>
</Can>
</Stack> </Stack>
<br /> <br />
<br /> <br />
{processInstanceResultTag()} {processInstanceRunResultTag()}
<br />
<Can I="GET" a={targetUris.processInstanceListPath} ability={ability}>
<ProcessInstanceListTable
filtersEnabled={false}
processModelFullIdentifier={processModel.id}
perPageOptions={[2, 5, 25]}
/>
<br />
</Can>
{processModelButtons()} {processModelButtons()}
<br />
<br />
<h3>Process Instances</h3>
{processInstancesUl()}
</> </>
); );
} }

View File

@ -132,6 +132,7 @@ export default function ReactFormEditor() {
secondaryButtonText="Cancel" secondaryButtonText="Cancel"
onSecondarySubmit={handleFileNameCancel} onSecondarySubmit={handleFileNameCancel}
onRequestSubmit={handleFileNameSave} onRequestSubmit={handleFileNameSave}
onRequestClose={handleFileNameCancel}
> >
<label>File Name:</label> <label>File Name:</label>
<span> <span>
@ -174,6 +175,19 @@ export default function ReactFormEditor() {
<Button onClick={saveFile} variant="danger" data-qa="file-save-button"> <Button onClick={saveFile} variant="danger" data-qa="file-save-button">
Save Save
</Button> </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 ? ( {params.file_name ? (
<ButtonWithConfirmation <ButtonWithConfirmation
description={`Delete file ${params.file_name}?`} description={`Delete file ${params.file_name}?`}

View File

@ -42,12 +42,12 @@ export default function SecretList() {
return ( return (
<tr key={(row as any).key}> <tr key={(row as any).key}>
<td> <td>
<Link to={`/admin/secrets/${(row as any).key}`}> <Link to={`/admin/configuration/secrets/${(row as any).key}`}>
{(row as any).id} {(row as any).id}
</Link> </Link>
</td> </td>
<td> <td>
<Link to={`/admin/secrets/${(row as any).key}`}> <Link to={`/admin/configuration/secrets/${(row as any).key}`}>
{(row as any).key} {(row as any).key}
</Link> </Link>
</td> </td>
@ -83,7 +83,6 @@ export default function SecretList() {
perPage={perPage} perPage={perPage}
pagination={pagination as any} pagination={pagination as any}
tableToDisplay={buildTable()} tableToDisplay={buildTable()}
path="/admin/secrets"
/> />
); );
} else { } else {
@ -97,7 +96,7 @@ export default function SecretList() {
<div> <div>
<h1>Secrets</h1> <h1>Secrets</h1>
{SecretsDisplayArea()} {SecretsDisplayArea()}
<Button href="/admin/secrets/new">Add a secret</Button> <Button href="/admin/configuration/secrets/new">Add a secret</Button>
</div> </div>
); );
} }

View File

@ -12,11 +12,11 @@ export default function SecretNew() {
const navigate = useNavigate(); const navigate = useNavigate();
const navigateToSecret = (_result: any) => { const navigateToSecret = (_result: any) => {
navigate(`/admin/secrets/${key}`); navigate(`/admin/configuration/secrets/${key}`);
}; };
const navigateToSecrets = () => { const navigateToSecrets = () => {
navigate(`/admin/secrets`); navigate(`/admin/configuration/secrets`);
}; };
const changeSpacesToDash = (someString: string) => { const changeSpacesToDash = (someString: string) => {

View File

@ -26,10 +26,6 @@ export default function SecretShow() {
} }
}; };
// const reloadSecret = (_result: any) => {
// window.location.reload();
// };
const updateSecretValue = () => { const updateSecretValue = () => {
if (secret && secretValue) { if (secret && secretValue) {
secret.value = secretValue; secret.value = secretValue;
@ -48,7 +44,7 @@ export default function SecretShow() {
}; };
const navigateToSecrets = (_result: any) => { const navigateToSecrets = (_result: any) => {
navigate(`/admin/secrets`); navigate(`/admin/configuration/secrets`);
}; };
const deleteSecret = () => { const deleteSecret = () => {

View File

@ -1,13 +1,30 @@
import { useContext, useEffect, useState } from 'react'; import { useContext, useEffect, useState } from 'react';
import { Link, useNavigate, useParams } from 'react-router-dom'; import { useNavigate, useParams } from 'react-router-dom';
import Form from '@rjsf/core';
// @ts-ignore // FIXME: npm install @rjsf/validator-ajv8 and use it as soon as
import { Button, Stack } from '@carbon/react'; // rawErrors is fixed.
// https://react-jsonschema-form.readthedocs.io/en/latest/usage/validation/
// https://github.com/rjsf-team/react-jsonschema-form/issues/2309 links to a codesandbox that might be useful to fork
// if we wanted to file a defect against rjsf to show the difference between validator-ajv6 and validator-ajv8.
// https://github.com/rjsf-team/react-jsonschema-form/blob/main/docs/api-reference/uiSchema.md talks about rawErrors
import validator from '@rjsf/validator-ajv6';
import {
TabList,
Tab,
Tabs,
Grid,
Column,
// @ts-ignore
} from '@carbon/react';
import ReactMarkdown from 'react-markdown'; import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm'; import remarkGfm from 'remark-gfm';
// eslint-disable-next-line import/no-named-as-default
import Form from '../themes/carbon';
import HttpService from '../services/HttpService'; import HttpService from '../services/HttpService';
import ErrorContext from '../contexts/ErrorContext'; import ErrorContext from '../contexts/ErrorContext';
import { modifyProcessModelPath } from '../helpers';
export default function TaskShow() { export default function TaskShow() {
const [task, setTask] = useState(null); const [task, setTask] = useState(null);
@ -18,16 +35,22 @@ export default function TaskShow() {
const setErrorMessage = (useContext as any)(ErrorContext)[1]; const setErrorMessage = (useContext as any)(ErrorContext)[1];
useEffect(() => { useEffect(() => {
const processResult = (result: any) => {
setTask(result);
HttpService.makeCallToBackend({
path: `/process-instances/${modifyProcessModelPath(
result.process_model_identifier
)}/${params.process_instance_id}/tasks`,
successCallback: setUserTasks,
});
};
HttpService.makeCallToBackend({ HttpService.makeCallToBackend({
path: `/tasks/${params.process_instance_id}/${params.task_id}`, path: `/tasks/${params.process_instance_id}/${params.task_id}`,
successCallback: setTask, successCallback: processResult,
// This causes the page to continuously reload // This causes the page to continuously reload
// failureCallback: setErrorMessage, // failureCallback: setErrorMessage,
}); });
HttpService.makeCallToBackend({
path: `/process-instance/${params.process_instance_id}/tasks`,
successCallback: setUserTasks,
});
}, [params]); }, [params]);
const processSubmitResult = (result: any) => { const processSubmitResult = (result: any) => {
@ -56,39 +79,52 @@ export default function TaskShow() {
const buildTaskNavigation = () => { const buildTaskNavigation = () => {
let userTasksElement; let userTasksElement;
let selectedTabIndex = 0;
if (userTasks) { if (userTasks) {
userTasksElement = (userTasks as any).map(function getUserTasksElement( userTasksElement = (userTasks as any).map(function getUserTasksElement(
userTask: any userTask: any,
index: number
) { ) {
const taskUrl = `/tasks/${params.process_instance_id}/${userTask.id}`; const taskUrl = `/tasks/${params.process_instance_id}/${userTask.id}`;
if (userTask.id === params.task_id) { if (userTask.id === params.task_id) {
return <span>{userTask.name}</span>; selectedTabIndex = index;
return <Tab selected>{userTask.title}</Tab>;
} }
if (userTask.state === 'COMPLETED') { if (userTask.state === 'COMPLETED') {
return ( return (
<Link to={taskUrl} data-qa={`form-nav-${userTask.name}`}> <Tab
{userTask.name} onClick={() => navigate(taskUrl)}
</Link> data-qa={`form-nav-${userTask.name}`}
>
{userTask.title}
</Tab>
); );
} }
if (userTask.state === 'FUTURE') { if (userTask.state === 'FUTURE') {
return <span style={{ color: 'red' }}>{userTask.name}</span>; return <Tab disabled>{userTask.title}</Tab>;
} }
if (userTask.state === 'READY') { if (userTask.state === 'READY') {
return ( return (
<Link to={taskUrl} data-qa={`form-nav-${userTask.name}`}> <Tab
{userTask.name} - Current onClick={() => navigate(taskUrl)}
</Link> data-qa={`form-nav-${userTask.name}`}
>
{userTask.title}
</Tab>
); );
} }
return null; return null;
}); });
} }
return ( return (
<Stack orientation="horizontal" gap={3}> <Tabs
<Button href="/tasks">Go Back To List</Button> title="Steps in this process instance involving people"
{userTasksElement} selectedIndex={selectedTabIndex}
</Stack> >
<TabList aria-label="List of tabs" contained>
{userTasksElement}
</TabList>
</Tabs>
); );
}; };
@ -132,14 +168,19 @@ export default function TaskShow() {
} }
return ( return (
<Form <Grid fullWidth condensed>
formData={taskData} <Column md={5} lg={8} sm={4}>
onSubmit={handleFormSubmit} <Form
schema={jsonSchema} formData={taskData}
uiSchema={formUiSchema} onSubmit={handleFormSubmit}
> schema={jsonSchema}
{reactFragmentToHideSubmitButton} uiSchema={formUiSchema}
</Form> validator={validator}
>
{reactFragmentToHideSubmitButton}
</Form>
</Column>
</Grid>
); );
}; };
@ -157,7 +198,7 @@ export default function TaskShow() {
); );
}; };
if (task) { if (task && userTasks) {
const taskToUse = task as any; const taskToUse = task as any;
let statusString = ''; let statusString = '';
if (taskToUse.state !== 'READY') { if (taskToUse.state !== 'READY') {

View File

@ -0,0 +1,17 @@
import React from 'react';
import AddIcon from '@mui/icons-material/Add';
import IconButton from '@mui/material/IconButton';
import { IconButtonProps } from '@rjsf/utils';
const AddButton: React.ComponentType<IconButtonProps> = ({
uiSchema,
...props
}) => {
return (
<IconButton title="Add Item" {...props} color="primary">
<AddIcon />
</IconButton>
);
};
export default AddButton;

View File

@ -0,0 +1,2 @@
export { default } from './AddButton';
export * from './AddButton';

View File

@ -0,0 +1,72 @@
import React, { CSSProperties } from 'react';
import Box from '@mui/material/Box';
import Grid from '@mui/material/Grid';
import Paper from '@mui/material/Paper';
import { ArrayFieldTemplateItemType } from '@rjsf/utils';
function ArrayFieldItemTemplate(props: ArrayFieldTemplateItemType) {
const {
children,
disabled,
hasToolbar,
hasMoveDown,
hasMoveUp,
hasRemove,
index,
onDropIndexClick,
onReorderClick,
readonly,
uiSchema,
registry,
} = props;
const { MoveDownButton, MoveUpButton, RemoveButton } =
registry.templates.ButtonTemplates;
const btnStyle: CSSProperties = {
flex: 1,
paddingLeft: 6,
paddingRight: 6,
fontWeight: 'bold',
minWidth: 0,
};
return (
<Grid container alignItems="center">
<Grid item xs style={{ overflow: 'auto' }}>
<Box mb={2}>
<Paper elevation={2}>
<Box p={2}>{children}</Box>
</Paper>
</Box>
</Grid>
{hasToolbar && (
<Grid item>
{(hasMoveUp || hasMoveDown) && (
<MoveUpButton
style={btnStyle}
disabled={disabled || readonly || !hasMoveUp}
onClick={onReorderClick(index, index - 1)}
uiSchema={uiSchema}
/>
)}
{(hasMoveUp || hasMoveDown) && (
<MoveDownButton
style={btnStyle}
disabled={disabled || readonly || !hasMoveDown}
onClick={onReorderClick(index, index + 1)}
uiSchema={uiSchema}
/>
)}
{hasRemove && (
<RemoveButton
style={btnStyle}
disabled={disabled || readonly}
onClick={onDropIndexClick(index)}
uiSchema={uiSchema}
/>
)}
</Grid>
)}
</Grid>
);
}
export default ArrayFieldItemTemplate;

View File

@ -0,0 +1,2 @@
export { default } from './ArrayFieldItemTemplate';
export * from './ArrayFieldItemTemplate';

View File

@ -0,0 +1,90 @@
import React from 'react';
import Box from '@mui/material/Box';
import Grid from '@mui/material/Grid';
import Paper from '@mui/material/Paper';
import {
ArrayFieldTemplateItemType,
ArrayFieldTemplateProps,
getTemplate,
getUiOptions,
} from '@rjsf/utils';
function ArrayFieldTemplate(props: ArrayFieldTemplateProps) {
const {
canAdd,
disabled,
idSchema,
uiSchema,
items,
onAddClick,
readonly,
registry,
required,
schema,
title,
} = props;
const uiOptions = getUiOptions(uiSchema);
const ArrayFieldDescriptionTemplate =
getTemplate<'ArrayFieldDescriptionTemplate'>(
'ArrayFieldDescriptionTemplate',
registry,
uiOptions
);
const ArrayFieldItemTemplate = getTemplate<'ArrayFieldItemTemplate'>(
'ArrayFieldItemTemplate',
registry,
uiOptions
);
const ArrayFieldTitleTemplate = getTemplate<'ArrayFieldTitleTemplate'>(
'ArrayFieldTitleTemplate',
registry,
uiOptions
);
// Button templates are not overridden in the uiSchema
const {
ButtonTemplates: { AddButton },
} = registry.templates;
return (
<Paper elevation={2}>
<Box p={2}>
<ArrayFieldTitleTemplate
idSchema={idSchema}
title={uiOptions.title || title}
schema={schema}
uiSchema={uiSchema}
required={required}
registry={registry}
/>
<ArrayFieldDescriptionTemplate
idSchema={idSchema}
description={uiOptions.description || schema.description}
schema={schema}
uiSchema={uiSchema}
registry={registry}
/>
<Grid container key={`array-item-list-${idSchema.$id}`}>
{items &&
items.map(({ key, ...itemProps }: ArrayFieldTemplateItemType) => (
<ArrayFieldItemTemplate key={key} {...itemProps} />
))}
{canAdd && (
<Grid container justifyContent="flex-end">
<Grid item>
<Box mt={2}>
<AddButton
className="array-item-add"
onClick={onAddClick}
disabled={disabled || readonly}
uiSchema={uiSchema}
/>
</Box>
</Grid>
</Grid>
)}
</Grid>
</Box>
</Paper>
);
}
export default ArrayFieldTemplate;

View File

@ -0,0 +1,2 @@
export { default } from './ArrayFieldTemplate';
export * from './ArrayFieldTemplate';

View File

@ -0,0 +1,126 @@
// @ts-ignore
import { TextInput } from '@carbon/react';
import {
getInputProps,
FormContextType,
RJSFSchema,
StrictRJSFSchema,
WidgetProps,
} from '@rjsf/utils';
import { useCallback } from 'react';
/** The `BaseInputTemplate` is the template to use to render the basic `<input>` component for the `core` theme.
* It is used as the template for rendering many of the <input> based widgets that differ by `type` and callbacks only.
* It can be customized/overridden for other themes or individual implementations as needed.
*
* @param props - The `WidgetProps` for this template
*/
export default function BaseInputTemplate<
T = any,
S extends StrictRJSFSchema = RJSFSchema,
F extends FormContextType = any
>(props: WidgetProps<T, S, F>) {
const {
id,
value,
readonly,
disabled,
autofocus,
label,
onBlur,
onFocus,
onChange,
required,
options,
schema,
uiSchema,
formContext,
registry,
rawErrors,
type,
...rest
} = props;
// Note: since React 15.2.0 we can't forward unknown element attributes, so we
// exclude the "options" and "schema" ones here.
if (!id) {
console.log('No id for', props);
throw new Error(`no id for props ${JSON.stringify(props)}`);
}
const inputProps = {
...rest,
...getInputProps<T, S, F>(schema, type, options),
};
let inputValue;
if (inputProps.type === 'number' || inputProps.type === 'integer') {
inputValue = value || value === 0 ? value : '';
} else {
inputValue = value == null ? '' : value;
}
const _onChange = useCallback(
({ target: { value } }: React.ChangeEvent<HTMLInputElement>) =>
onChange(value === '' ? options.emptyValue : value),
[onChange, options]
);
const _onBlur = useCallback(
({ target: { value } }: React.FocusEvent<HTMLInputElement>) =>
onBlur(id, value),
[onBlur, id]
);
const _onFocus = useCallback(
({ target: { value } }: React.FocusEvent<HTMLInputElement>) =>
onFocus(id, value),
[onFocus, id]
);
let labelToUse = label;
if (uiSchema && uiSchema['ui:title']) {
labelToUse = uiSchema['ui:title'];
} else if (schema && schema.title) {
labelToUse = schema.title;
}
if (required) {
labelToUse = `${labelToUse}*`;
}
let invalid = false;
let errorMessageForField = null;
if (rawErrors && rawErrors.length > 0) {
invalid = true;
errorMessageForField = `${labelToUse.replace(/\*$/, '')} ${rawErrors[0]}`;
}
return (
<>
<TextInput
id={id}
name={id}
labelText={labelToUse}
invalid={invalid}
invalidText={errorMessageForField}
autoFocus={autofocus}
disabled={disabled || readonly}
value={value || value === 0 ? value : ''}
onChange={_onChange}
onBlur={_onBlur}
onFocus={_onFocus}
// eslint-disable-next-line react/jsx-props-no-spreading
{...inputProps}
/>
{Array.isArray(schema.examples) && (
<datalist key={`datalist_${id}`} id={`examples_${id}`}>
{[
...new Set(
schema.examples.concat(schema.default ? [schema.default] : [])
),
].map((example: any) => (
<option key={example} value={example} />
))}
</datalist>
)}
</>
);
}

View File

@ -0,0 +1,2 @@
export { default } from './BaseInputTemplate';
export * from './BaseInputTemplate';

View File

@ -0,0 +1,8 @@
import { ComponentType } from 'react';
import { withTheme, FormProps } from '@rjsf/core';
import Theme from '../Theme';
const CarbonForm: ComponentType<FormProps> = withTheme(Theme);
export default CarbonForm;

View File

@ -0,0 +1,2 @@
export { default } from './CarbonForm';
export * from './CarbonForm';

View File

@ -0,0 +1,52 @@
import React from 'react';
import Checkbox from '@mui/material/Checkbox';
import FormControlLabel from '@mui/material/FormControlLabel';
import { schemaRequiresTrueValue, WidgetProps } from '@rjsf/utils';
function CheckboxWidget(props: WidgetProps) {
const {
schema,
id,
value,
disabled,
readonly,
label,
autofocus,
onChange,
onBlur,
onFocus,
} = props;
// Because an unchecked checkbox will cause html5 validation to fail, only add
// the "required" attribute if the field value must be "true", due to the
// "const" or "enum" keywords
const required = schemaRequiresTrueValue(schema);
const _onChange = (_: any, checked: boolean) => onChange(checked);
const _onBlur = ({
target: { value },
}: React.FocusEvent<HTMLButtonElement>) => onBlur(id, value);
const _onFocus = ({
target: { value },
}: React.FocusEvent<HTMLButtonElement>) => onFocus(id, value);
return (
<FormControlLabel
control={
<Checkbox
id={id}
name={id}
checked={typeof value === 'undefined' ? false : Boolean(value)}
required={required}
disabled={disabled || readonly}
autoFocus={autofocus}
onChange={_onChange}
onBlur={_onBlur}
onFocus={_onFocus}
/>
}
label={label || ''}
/>
);
}
export default CheckboxWidget;

View File

@ -0,0 +1,2 @@
export { default } from './CheckboxWidget';
export * from './CheckboxWidget';

View File

@ -0,0 +1,93 @@
import React from 'react';
import Checkbox from '@mui/material/Checkbox';
import FormControlLabel from '@mui/material/FormControlLabel';
import FormGroup from '@mui/material/FormGroup';
import FormLabel from '@mui/material/FormLabel';
import { WidgetProps } from '@rjsf/utils';
const selectValue = (value: any, selected: any, all: any) => {
const at = all.indexOf(value);
const updated = selected.slice(0, at).concat(value, selected.slice(at));
// As inserting values at predefined index positions doesn't work with empty
// arrays, we need to reorder the updated selection to match the initial order
return updated.sort((a: any, b: any) => all.indexOf(a) > all.indexOf(b));
};
const deselectValue = (value: any, selected: any) => {
return selected.filter((v: any) => v !== value);
};
function CheckboxesWidget({
schema,
label,
id,
disabled,
options,
value,
autofocus,
readonly,
required,
onChange,
onBlur,
onFocus,
}: WidgetProps) {
const { enumOptions, enumDisabled, inline } = options;
const _onChange =
(option: any) =>
({ target: { checked } }: React.ChangeEvent<HTMLInputElement>) => {
const all = (enumOptions as any).map(({ value }: any) => value);
if (checked) {
onChange(selectValue(option.value, value, all));
} else {
onChange(deselectValue(option.value, value));
}
};
const _onBlur = ({
target: { value },
}: React.FocusEvent<HTMLButtonElement>) => onBlur(id, value);
const _onFocus = ({
target: { value },
}: React.FocusEvent<HTMLButtonElement>) => onFocus(id, value);
return (
<>
<FormLabel required={required} htmlFor={id}>
{label || schema.title}
</FormLabel>
<FormGroup id={id} row={!!inline}>
{Array.isArray(enumOptions) &&
enumOptions.map((option, index: number) => {
const checked = value.indexOf(option.value) !== -1;
const itemDisabled =
Array.isArray(enumDisabled) &&
enumDisabled.indexOf(option.value) !== -1;
const checkbox = (
<Checkbox
id={`${id}-${option.value}`}
name={id}
checked={checked}
disabled={disabled || itemDisabled || readonly}
autoFocus={autofocus && index === 0}
onChange={_onChange(option)}
onBlur={_onBlur}
onFocus={_onFocus}
/>
);
return (
<FormControlLabel
control={checkbox}
key={option.value}
label={option.label}
/>
);
})}
</FormGroup>
</>
);
}
export default CheckboxesWidget;

View File

@ -0,0 +1,2 @@
export { default } from './CheckboxesWidget';
export * from './CheckboxesWidget';

View File

@ -0,0 +1,29 @@
import React from 'react';
import { getTemplate, localToUTC, utcToLocal, WidgetProps } from '@rjsf/utils';
function DateTimeWidget(props: WidgetProps) {
const { options, registry } = props;
const BaseInputTemplate = getTemplate<'BaseInputTemplate'>(
'BaseInputTemplate',
registry,
options
);
const value = utcToLocal(props.value);
const onChange = (value: any) => {
props.onChange(localToUTC(value));
};
return (
<BaseInputTemplate
type="datetime-local"
InputLabelProps={{
shrink: true,
}}
{...props}
value={value}
onChange={onChange}
/>
);
}
export default DateTimeWidget;

View File

@ -0,0 +1,2 @@
export { default } from './DateTimeWidget';
export * from './DateTimeWidget';

View File

@ -0,0 +1,22 @@
import React from 'react';
import { getTemplate, WidgetProps } from '@rjsf/utils';
function DateWidget(props: WidgetProps) {
const { options, registry } = props;
const BaseInputTemplate = getTemplate<'BaseInputTemplate'>(
'BaseInputTemplate',
registry,
options
);
return (
<BaseInputTemplate
type="date"
InputLabelProps={{
shrink: true,
}}
{...props}
/>
);
}
export default DateWidget;

View File

@ -0,0 +1,2 @@
export { default } from './DateWidget';
export * from './DateWidget';

View File

@ -0,0 +1,17 @@
import React from 'react';
import Typography from '@mui/material/Typography';
import { DescriptionFieldProps } from '@rjsf/utils';
function DescriptionField({ description, id }: DescriptionFieldProps) {
if (description) {
return (
<Typography id={id} variant="subtitle2" style={{ marginTop: '5px' }}>
{description}
</Typography>
);
}
return null;
}
export default DescriptionField;

View File

@ -0,0 +1,2 @@
export { default } from './DescriptionField';
export * from './DescriptionField';

View File

@ -0,0 +1,16 @@
import { ErrorListProps } from '@rjsf/utils';
// @ts-ignore
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>
);
}
return null;
}
export default ErrorList;

View File

@ -0,0 +1,2 @@
export { default } from './ErrorList';
export * from './ErrorList';

View File

@ -0,0 +1,9 @@
import { FieldErrorProps } from '@rjsf/utils';
/** The `FieldErrorTemplate` component renders the errors local to the particular field
*
* @param props - The `FieldErrorProps` for the errors being rendered
*/
export default function FieldErrorTemplate(_props: FieldErrorProps) {
return null;
}

View File

@ -0,0 +1,2 @@
export { default } from './FieldErrorTemplate';
export * from './FieldErrorTemplate';

View File

@ -0,0 +1,16 @@
import React from 'react';
import { FieldHelpProps } from '@rjsf/utils';
import FormHelperText from '@mui/material/FormHelperText';
/** The `FieldHelpTemplate` component renders any help desired for a field
*
* @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>;
}

View File

@ -0,0 +1,2 @@
export { default } from './FieldHelpTemplate';
export * from './FieldHelpTemplate';

View File

@ -0,0 +1,64 @@
import React from 'react';
import FormControl from '@mui/material/FormControl';
import Typography from '@mui/material/Typography';
import { FieldTemplateProps, 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',
registry,
uiOptions
);
if (hidden) {
return <div style={{ display: 'none' }}>{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}>
{children}
{displayLabel && rawDescription ? (
<Typography variant="caption" color="textSecondary">
{rawDescription}
</Typography>
) : null}
{errors}
{help}
</FormControl>
</WrapIfAdditionalTemplate>
);
}
export default FieldTemplate;

View File

@ -0,0 +1,2 @@
export { default } from './FieldTemplate';
export * from './FieldTemplate';

View File

@ -0,0 +1,55 @@
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';
export default function MuiIconButton(props: IconButtonProps) {
const { icon, color, uiSchema, ...otherProps } = props;
return (
<IconButton
{...otherProps}
size="small"
color={color as MuiIconButtonProps['color']}
>
{icon}
</IconButton>
);
}
export function MoveDownButton(props: IconButtonProps) {
return (
<MuiIconButton
title="Move down"
{...props}
icon={<ArrowDownwardIcon fontSize="small" />}
/>
);
}
export function MoveUpButton(props: IconButtonProps) {
return (
<MuiIconButton
title="Move up"
{...props}
icon={<ArrowUpwardIcon fontSize="small" />}
/>
);
}
export function RemoveButton(props: IconButtonProps) {
const { iconType, ...otherProps } = props;
return (
<MuiIconButton
title="Remove"
{...otherProps}
color="error"
icon={
<RemoveIcon fontSize={iconType === 'default' ? undefined : 'small'} />
}
/>
);
}

View File

@ -0,0 +1,2 @@
export { default } from './IconButton';
export * from './IconButton';

View File

@ -0,0 +1,89 @@
import React from 'react';
import Grid from '@mui/material/Grid';
import {
ObjectFieldTemplateProps,
canExpand,
getTemplate,
getUiOptions,
} from '@rjsf/utils';
function ObjectFieldTemplate({
description,
title,
properties,
required,
disabled,
readonly,
uiSchema,
idSchema,
schema,
formData,
onAddClick,
registry,
}: ObjectFieldTemplateProps) {
const uiOptions = getUiOptions(uiSchema);
const TitleFieldTemplate = getTemplate<'TitleFieldTemplate'>(
'TitleFieldTemplate',
registry,
uiOptions
);
const DescriptionFieldTemplate = getTemplate<'DescriptionFieldTemplate'>(
'DescriptionFieldTemplate',
registry,
uiOptions
);
// Button templates are not overridden in the uiSchema
const {
ButtonTemplates: { AddButton },
} = registry.templates;
return (
<>
{(uiOptions.title || title) && (
<TitleFieldTemplate
id={`${idSchema.$id}-title`}
title={title}
required={required}
schema={schema}
uiSchema={uiSchema}
registry={registry}
/>
)}
{(uiOptions.description || description) && (
<DescriptionFieldTemplate
id={`${idSchema.$id}-description`}
description={uiOptions.description || description!}
schema={schema}
uiSchema={uiSchema}
registry={registry}
/>
)}
<Grid container spacing={2} style={{ marginTop: '10px' }}>
{properties.map((element, index) =>
// Remove the <Grid> if the inner element is hidden as the <Grid>
// itself would otherwise still take up space.
element.hidden ? (
element.content
) : (
<Grid item xs={12} key={index} style={{ marginBottom: '10px' }}>
{element.content}
</Grid>
)
)}
{canExpand(schema, uiSchema, formData) && (
<Grid container justifyContent="flex-end">
<Grid item>
<AddButton
className="object-property-expand"
onClick={onAddClick(schema)}
disabled={disabled || readonly}
uiSchema={uiSchema}
/>
</Grid>
</Grid>
)}
</Grid>
</>
);
}
export default ObjectFieldTemplate;

View File

@ -0,0 +1,2 @@
export { default } from './ObjectFieldTemplate';
export * from './ObjectFieldTemplate';

View File

@ -0,0 +1,75 @@
import React from "react";
import FormControlLabel from "@mui/material/FormControlLabel";
import FormLabel from "@mui/material/FormLabel";
import Radio from "@mui/material/Radio";
import RadioGroup from "@mui/material/RadioGroup";
import { WidgetProps } from "@rjsf/utils";
const RadioWidget = ({
id,
schema,
options,
value,
required,
disabled,
readonly,
label,
onChange,
onBlur,
onFocus,
}: WidgetProps) => {
const { enumOptions, enumDisabled } = options;
const _onChange = (_: any, value: any) =>
onChange(schema.type == "boolean" ? value !== "false" : value);
const _onBlur = ({ target: { value } }: React.FocusEvent<HTMLInputElement>) =>
onBlur(id, value);
const _onFocus = ({
target: { value },
}: React.FocusEvent<HTMLInputElement>) => onFocus(id, value);
const row = options ? options.inline : false;
return (
<>
<FormLabel required={required} htmlFor={id}>
{label || schema.title}
</FormLabel>
<RadioGroup
id={id}
name={id}
value={`${value}`}
row={row as boolean}
onChange={_onChange}
onBlur={_onBlur}
onFocus={_onFocus}
>
{Array.isArray(enumOptions) &&
enumOptions.map((option) => {
const itemDisabled =
Array.isArray(enumDisabled) &&
enumDisabled.indexOf(option.value) !== -1;
const radio = (
<FormControlLabel
control={
<Radio
name={id}
id={`${id}-${option.value}`}
color="primary"
/>
}
label={`${option.label}`}
value={`${option.value}`}
key={option.value}
disabled={disabled || itemDisabled || readonly}
/>
);
return radio;
})}
</RadioGroup>
</>
);
};
export default RadioWidget;

View File

@ -0,0 +1,2 @@
export { default } from './RadioWidget';
export * from './RadioWidget';

View File

@ -0,0 +1,47 @@
import React from 'react';
import FormLabel from '@mui/material/FormLabel';
import Slider from '@mui/material/Slider';
import { WidgetProps, rangeSpec } from '@rjsf/utils';
function RangeWidget({
value,
readonly,
disabled,
onBlur,
onFocus,
options,
schema,
onChange,
required,
label,
id,
}: WidgetProps) {
const sliderProps = { value, label, id, name: id, ...rangeSpec(schema) };
const _onChange = (_: any, value?: number | number[]) => {
onChange(value ? options.emptyValue : value);
};
const _onBlur = ({ target: { value } }: React.FocusEvent<HTMLInputElement>) =>
onBlur(id, value);
const _onFocus = ({
target: { value },
}: React.FocusEvent<HTMLInputElement>) => onFocus(id, value);
return (
<>
<FormLabel required={required} id={id}>
{label}
</FormLabel>
<Slider
disabled={disabled || readonly}
onChange={_onChange}
onBlur={_onBlur}
onFocus={_onFocus}
valueLabelDisplay="auto"
{...sliderProps}
/>
</>
);
}
export default RangeWidget;

View File

@ -0,0 +1,2 @@
export { default } from './RangeWidget';
export * from './RangeWidget';

View File

@ -0,0 +1,87 @@
// @ts-ignore
import { Select, SelectItem } from '@carbon/react';
import { WidgetProps, processSelectValue } from '@rjsf/utils';
function SelectWidget({
schema,
id,
options,
label,
required,
disabled,
readonly,
value,
multiple,
autofocus,
onChange,
onBlur,
onFocus,
uiSchema,
placeholder,
rawErrors = [],
}: WidgetProps) {
const { enumOptions, enumDisabled } = options;
const emptyValue = multiple ? [] : '';
const _onChange = ({
target: { value },
}: React.ChangeEvent<{ name?: string; value: unknown }>) =>
onChange(processSelectValue(schema, value, options));
const _onBlur = ({ target: { value } }: React.FocusEvent<HTMLInputElement>) =>
onBlur(id, processSelectValue(schema, value, options));
const _onFocus = ({
target: { value },
}: React.FocusEvent<HTMLInputElement>) =>
onFocus(id, processSelectValue(schema, value, options));
let labelToUse = label;
if (uiSchema && uiSchema['ui:title']) {
labelToUse = uiSchema['ui:title'];
} else if (schema && schema.title) {
labelToUse = schema.title;
}
if (required) {
labelToUse = `${labelToUse}*`;
}
let invalid = false;
let errorMessageForField = null;
if (rawErrors && rawErrors.length > 0) {
invalid = true;
errorMessageForField = `${labelToUse.replace(/\*$/, '')} ${rawErrors[0]}`;
}
return (
<Select
id={id}
name={id}
labelText={labelToUse}
select
helperText={placeholder}
value={typeof value === 'undefined' ? emptyValue : value}
disabled={disabled || readonly}
autoFocus={autofocus}
error={rawErrors.length > 0}
onChange={_onChange}
onBlur={_onBlur}
onFocus={_onFocus}
invalid={invalid}
invalidText={errorMessageForField}
InputLabelProps={{
shrink: true,
}}
SelectProps={{
multiple: typeof multiple === 'undefined' ? false : multiple,
}}
>
{(enumOptions as any).map(({ value, label }: any, i: number) => {
const disabled: any =
enumDisabled && (enumDisabled as any).indexOf(value) != -1;
return <SelectItem text={label} value={value} disabled={disabled} />;
})}
</Select>
);
}
export default SelectWidget;

View File

@ -0,0 +1,2 @@
export { default } from './SelectWidget';
export * from './SelectWidget';

View File

@ -0,0 +1,30 @@
import React from 'react';
// import Box from '@mui/material/Box';
// @ts-ignore
import { Button } from '@carbon/react';
import { SubmitButtonProps, getSubmitButtonOptions } from '@rjsf/utils';
// const SubmitButton: React.ComponentType<SubmitButtonProps> = (props) => {
function SubmitButton(props: SubmitButtonProps) {
const { uiSchema } = props;
const {
submitText,
norender,
props: submitButtonProps,
} = getSubmitButtonOptions(uiSchema);
if (norender) {
return null;
}
return (
<Button
className="react-json-schema-form-submit-button"
type="submit"
// eslint-disable-next-line react/jsx-props-no-spreading
{...submitButtonProps}
>
{submitText}
</Button>
);
}
export default SubmitButton;

View File

@ -0,0 +1,2 @@
export { default } from './SubmitButton';
export * from './SubmitButton';

View File

@ -0,0 +1,35 @@
import AddButton from '../AddButton';
import ArrayFieldItemTemplate from '../ArrayFieldItemTemplate';
import ArrayFieldTemplate from '../ArrayFieldTemplate';
import BaseInputTemplate from '../BaseInputTemplate';
import DescriptionField from '../DescriptionField';
import ErrorList from '../ErrorList';
import { MoveDownButton, MoveUpButton, RemoveButton } from '../IconButton';
import FieldErrorTemplate from '../FieldErrorTemplate';
import FieldHelpTemplate from '../FieldHelpTemplate';
import FieldTemplate from '../FieldTemplate';
import ObjectFieldTemplate from '../ObjectFieldTemplate';
import SubmitButton from '../SubmitButton';
import TitleField from '../TitleField';
import WrapIfAdditionalTemplate from '../WrapIfAdditionalTemplate';
export default {
ArrayFieldItemTemplate,
ArrayFieldTemplate,
BaseInputTemplate,
ButtonTemplates: {
AddButton,
MoveDownButton,
MoveUpButton,
RemoveButton,
SubmitButton,
},
DescriptionFieldTemplate: DescriptionField,
ErrorListTemplate: ErrorList,
FieldErrorTemplate,
FieldHelpTemplate,
FieldTemplate,
ObjectFieldTemplate,
TitleFieldTemplate: TitleField,
WrapIfAdditionalTemplate,
};

Some files were not shown because too many files have changed in this diff Show More