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:
parent
e10bc47851
commit
b198341b00
|
@ -0,0 +1 @@
|
|||
/src/themes/carbon
|
|
@ -36,6 +36,7 @@ module.exports = {
|
|||
],
|
||||
'react/react-in-jsx-scope': 'off',
|
||||
'react/require-default-props': 'off',
|
||||
'import/prefer-default-export': 'off',
|
||||
'no-unused-vars': [
|
||||
'error',
|
||||
{
|
||||
|
|
|
@ -7,6 +7,9 @@ function error_handler() {
|
|||
trap 'error_handler ${LINENO} $?' ERR
|
||||
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:-}"
|
||||
if [[ -z "$command" ]]; then
|
||||
command=open
|
||||
|
|
|
@ -6,8 +6,9 @@ module.exports = defineConfig({
|
|||
chromeWebSecurity: false,
|
||||
e2e: {
|
||||
baseUrl: 'http://localhost:7001',
|
||||
setupNodeEvents(_on, _config) {
|
||||
// implement node event listeners here
|
||||
setupNodeEvents(_on, config) {
|
||||
require('@cypress/grep/src/plugin')(config);
|
||||
return config;
|
||||
},
|
||||
},
|
||||
|
||||
|
|
|
@ -169,14 +169,13 @@ describe('process-instances', () => {
|
|||
cy.getBySel('process-instance-list-link').click();
|
||||
cy.assertAtLeastOneItemInPaginatedResults();
|
||||
|
||||
const statusSelect = '#process-instance-status-select';
|
||||
PROCESS_STATUSES.forEach((processStatus) => {
|
||||
if (!['all', 'waiting'].includes(processStatus)) {
|
||||
cy.get('#process-instance-status-select').click();
|
||||
cy.get('#process-instance-status-select')
|
||||
.contains(processStatus)
|
||||
.click();
|
||||
cy.get(statusSelect).click();
|
||||
cy.get(statusSelect).contains(processStatus).click();
|
||||
// close the dropdown again
|
||||
cy.get('#process-instance-status-select').click();
|
||||
cy.get(statusSelect).click();
|
||||
cy.getBySel('filter-button').click();
|
||||
cy.assertAtLeastOneItemInPaginatedResults();
|
||||
cy.getBySel(`process-instance-status-${processStatus}`).contains(
|
||||
|
|
|
@ -144,10 +144,11 @@ describe('process-models', () => {
|
|||
|
||||
cy.getBySel('process-instance-list-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.getBySel('modal-confirmation-dialog').find('.cds--btn--danger').click();
|
||||
cy.contains(`Process Instances for: ${groupId}/${modelId}`);
|
||||
|
||||
// in breadcrumb
|
||||
cy.contains(modelId).click();
|
||||
|
||||
cy.contains('Edit process model').click();
|
||||
|
|
|
@ -16,5 +16,9 @@
|
|||
// Import commands.js using ES2015 syntax:
|
||||
import './commands';
|
||||
|
||||
import registerCypressGrep from '@cypress/grep';
|
||||
|
||||
registerCypressGrep();
|
||||
|
||||
// Alternatively you can use CommonJS syntax:
|
||||
// require('./commands')
|
||||
|
|
File diff suppressed because it is too large
Load Diff
16
package.json
16
package.json
|
@ -9,9 +9,16 @@
|
|||
"@carbon/icons-react": "^11.10.0",
|
||||
"@carbon/react": "^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",
|
||||
"@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",
|
||||
"@testing-library/jest-dom": "^5.16.4",
|
||||
"@testing-library/react": "^13.3.0",
|
||||
|
@ -71,9 +78,9 @@
|
|||
"test": "react-scripts test --coverage",
|
||||
"t": "npm test -- --watchAll=false",
|
||||
"eject": "craco eject",
|
||||
"format": "prettier --write src/**/*.js{,x}",
|
||||
"lint": "./node_modules/.bin/eslint src *.js",
|
||||
"lint:fix": "./node_modules/.bin/eslint --fix src *.js"
|
||||
"format": "prettier --write src/**/*.[tj]s{,x}",
|
||||
"lint": "./node_modules/.bin/eslint src",
|
||||
"lint:fix": "./node_modules/.bin/eslint --fix src"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
|
@ -94,6 +101,7 @@
|
|||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cypress/grep": "^3.1.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.30.5",
|
||||
"@typescript-eslint/parser": "^5.30.6",
|
||||
"cypress": "^10.8.0",
|
||||
|
|
49
src/App.tsx
49
src/App.tsx
|
@ -3,15 +3,17 @@ import { useMemo, useState } from 'react';
|
|||
import { Content } from '@carbon/react';
|
||||
|
||||
import { BrowserRouter, Routes, Route } from 'react-router-dom';
|
||||
import { defineAbility } from '@casl/ability';
|
||||
import ErrorContext from './contexts/ErrorContext';
|
||||
import NavigationBar from './components/NavigationBar';
|
||||
|
||||
import HomePage from './routes/HomePage';
|
||||
import TaskShow from './routes/TaskShow';
|
||||
import HomePageRoutes from './routes/HomePageRoutes';
|
||||
import ErrorBoundary from './components/ErrorBoundary';
|
||||
import AdminRoutes from './routes/AdminRoutes';
|
||||
import { ErrorForDisplay } from './interfaces';
|
||||
|
||||
import { AbilityContext } from './contexts/Can';
|
||||
|
||||
export default function App() {
|
||||
const [errorMessage, setErrorMessage] = useState<ErrorForDisplay | null>(
|
||||
null
|
||||
|
@ -22,6 +24,8 @@ export default function App() {
|
|||
[errorMessage]
|
||||
);
|
||||
|
||||
const ability = defineAbility(() => {});
|
||||
|
||||
let errorTag = null;
|
||||
if (errorMessage) {
|
||||
let sentryLinkTag = null;
|
||||
|
@ -47,29 +51,24 @@ export default function App() {
|
|||
|
||||
return (
|
||||
<div className="cds--white">
|
||||
<ErrorContext.Provider value={errorContextValueArray}>
|
||||
<BrowserRouter>
|
||||
<NavigationBar />
|
||||
<Content>
|
||||
{errorTag}
|
||||
<ErrorBoundary>
|
||||
<Routes>
|
||||
<Route path="/" element={<HomePage />} />
|
||||
<Route path="/tasks" element={<HomePage />} />
|
||||
<Route path="/admin/*" element={<AdminRoutes />} />
|
||||
<Route
|
||||
path="/tasks/:process_instance_id/:task_id"
|
||||
element={<TaskShow />}
|
||||
/>
|
||||
<Route
|
||||
path="/tasks/:process_instance_id/:task_id"
|
||||
element={<TaskShow />}
|
||||
/>
|
||||
</Routes>
|
||||
</ErrorBoundary>
|
||||
</Content>
|
||||
</BrowserRouter>
|
||||
</ErrorContext.Provider>
|
||||
{/* @ts-ignore */}
|
||||
<AbilityContext.Provider value={ability}>
|
||||
<ErrorContext.Provider value={errorContextValueArray}>
|
||||
<BrowserRouter>
|
||||
<NavigationBar />
|
||||
<Content>
|
||||
{errorTag}
|
||||
<ErrorBoundary>
|
||||
<Routes>
|
||||
<Route path="/*" element={<HomePageRoutes />} />
|
||||
<Route path="/tasks/*" element={<HomePageRoutes />} />
|
||||
<Route path="/admin/*" element={<AdminRoutes />} />
|
||||
</Routes>
|
||||
</ErrorBoundary>
|
||||
</Content>
|
||||
</BrowserRouter>
|
||||
</ErrorContext.Provider>
|
||||
</AbilityContext.Provider>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ import { useState } from 'react';
|
|||
import { Button, Modal } from '@carbon/react';
|
||||
|
||||
type OwnProps = {
|
||||
'data-qa'?: string;
|
||||
description?: string;
|
||||
buttonLabel?: string;
|
||||
onConfirmation: (..._args: any[]) => any;
|
||||
|
@ -18,6 +19,7 @@ export default function ButtonWithConfirmation({
|
|||
description,
|
||||
buttonLabel,
|
||||
onConfirmation,
|
||||
'data-qa': dataQa,
|
||||
title = 'Are you sure?',
|
||||
confirmButtonLabel = 'OK',
|
||||
kind = 'danger',
|
||||
|
@ -51,6 +53,7 @@ export default function ButtonWithConfirmation({
|
|||
secondaryButtonText="Cancel"
|
||||
onSecondarySubmit={handleConfirmationPromptCancel}
|
||||
onRequestSubmit={handleConfirmation}
|
||||
onRequestClose={handleConfirmationPromptCancel}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -58,6 +61,7 @@ export default function ButtonWithConfirmation({
|
|||
return (
|
||||
<>
|
||||
<Button
|
||||
data-qa={dataQa}
|
||||
onClick={handleShowConfirmationPrompt}
|
||||
kind={kind}
|
||||
renderIcon={renderIcon}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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]}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -17,9 +17,13 @@ import {
|
|||
import { Logout, Login } from '@carbon/icons-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
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
|
||||
import logo from '../logo.svg';
|
||||
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/
|
||||
export default function NavigationBar() {
|
||||
|
@ -34,6 +38,14 @@ export default function NavigationBar() {
|
|||
const location = useLocation();
|
||||
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(() => {
|
||||
let newActiveKey = '/admin/process-groups';
|
||||
if (location.pathname.match(/^\/admin\/messages\b/)) {
|
||||
|
@ -44,10 +56,8 @@ export default function NavigationBar() {
|
|||
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.match(/^\/admin\/configuration\b/)) {
|
||||
newActiveKey = '/admin/configuration';
|
||||
} else if (location.pathname === '/') {
|
||||
newActiveKey = '/';
|
||||
} 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 = () => {
|
||||
return (
|
||||
<>
|
||||
|
@ -105,35 +151,26 @@ export default function NavigationBar() {
|
|||
>
|
||||
Process Instances
|
||||
</HeaderMenuItem>
|
||||
<HeaderMenuItem
|
||||
href="/admin/messages"
|
||||
isCurrentPage={isActivePage('/admin/messages')}
|
||||
>
|
||||
Messages
|
||||
</HeaderMenuItem>
|
||||
<HeaderMenuItem
|
||||
href="/admin/secrets"
|
||||
isCurrentPage={isActivePage('/admin/secrets')}
|
||||
>
|
||||
Secrets
|
||||
</HeaderMenuItem>
|
||||
<HeaderMenuItem
|
||||
href="/admin/authentications"
|
||||
isCurrentPage={isActivePage('/admin/authentications')}
|
||||
>
|
||||
Authentications
|
||||
</HeaderMenuItem>
|
||||
<Can I="GET" a={targetUris.messageInstanceListPath} ability={ability}>
|
||||
<HeaderMenuItem
|
||||
href="/admin/messages"
|
||||
isCurrentPage={isActivePage('/admin/messages')}
|
||||
>
|
||||
Messages
|
||||
</HeaderMenuItem>
|
||||
</Can>
|
||||
{configurationElement()}
|
||||
<HeaderMenuItem
|
||||
href="/admin/process-instances/reports"
|
||||
isCurrentPage={isActivePage('/admin/process-instances/reports')}
|
||||
>
|
||||
Reports
|
||||
Perspectives
|
||||
</HeaderMenuItem>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
if (activeKey) {
|
||||
if (activeKey && ability) {
|
||||
return (
|
||||
<HeaderContainer
|
||||
render={({ isSideNavExpanded, onClickSideNavExpand }: any) => (
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { useNavigate } from 'react-router-dom';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
|
||||
// @ts-ignore
|
||||
import { Pagination } from '@carbon/react';
|
||||
|
@ -13,8 +13,7 @@ type OwnProps = {
|
|||
perPageOptions?: number[];
|
||||
pagination: PaginationObject | null;
|
||||
tableToDisplay: any;
|
||||
queryParamString?: string;
|
||||
path: string;
|
||||
paginationQueryParamPrefix?: string;
|
||||
};
|
||||
|
||||
export default function PaginationForTable({
|
||||
|
@ -23,16 +22,21 @@ export default function PaginationForTable({
|
|||
perPageOptions,
|
||||
pagination,
|
||||
tableToDisplay,
|
||||
queryParamString = '',
|
||||
path,
|
||||
paginationQueryParamPrefix,
|
||||
}: OwnProps) {
|
||||
const PER_PAGE_OPTIONS = [2, 10, 50, 100];
|
||||
const navigate = useNavigate();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const paginationQueryParamPrefixToUse = paginationQueryParamPrefix
|
||||
? `${paginationQueryParamPrefix}_`
|
||||
: '';
|
||||
|
||||
const updateRows = (args: any) => {
|
||||
const newPage = args.page;
|
||||
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) {
|
||||
|
|
|
@ -165,11 +165,7 @@ export default function ProcessGroupForm({
|
|||
};
|
||||
|
||||
const formButtons = () => {
|
||||
const buttons = [
|
||||
<Button kind="secondary" type="submit">
|
||||
Submit
|
||||
</Button>,
|
||||
];
|
||||
const buttons = [<Button type="submit">Submit</Button>];
|
||||
if (mode === 'edit') {
|
||||
buttons.push(
|
||||
<ButtonWithConfirmation
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -2,14 +2,9 @@ import { useState } from 'react';
|
|||
import { useNavigate } from 'react-router-dom';
|
||||
// @ts-ignore
|
||||
import { Button, ButtonSet, Form, Stack, TextInput } from '@carbon/react';
|
||||
import {
|
||||
getGroupFromModifiedModelId,
|
||||
modifyProcessModelPath,
|
||||
slugifyString,
|
||||
} from '../helpers';
|
||||
import { modifyProcessModelPath, slugifyString } from '../helpers';
|
||||
import HttpService from '../services/HttpService';
|
||||
import { ProcessModel } from '../interfaces';
|
||||
import ButtonWithConfirmation from './ButtonWithConfirmation';
|
||||
|
||||
type OwnProps = {
|
||||
mode: string;
|
||||
|
@ -29,7 +24,6 @@ export default function ProcessModelForm({
|
|||
useState<boolean>(false);
|
||||
const [displayNameInvalid, setDisplayNameInvalid] = useState<boolean>(false);
|
||||
const navigate = useNavigate();
|
||||
const modifiedProcessModelPath = modifyProcessModelPath(processModel.id);
|
||||
|
||||
const navigateToProcessModel = (result: ProcessModel) => {
|
||||
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) => {
|
||||
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) => {
|
||||
event.preventDefault();
|
||||
let hasErrors = false;
|
||||
if (!hasValidIdentifier(processModel.id)) {
|
||||
if (mode === 'new' && !hasValidIdentifier(processModel.id)) {
|
||||
setIdentifierInvalid(true);
|
||||
hasErrors = true;
|
||||
}
|
||||
|
@ -74,10 +52,7 @@ export default function ProcessModelForm({
|
|||
if (hasErrors) {
|
||||
return;
|
||||
}
|
||||
let path = `/process-models`;
|
||||
if (mode === 'edit') {
|
||||
path = `/process-models/${modifiedProcessModelPath}`;
|
||||
}
|
||||
const path = `/process-models/${processGroupId}`;
|
||||
let httpMethod = 'POST';
|
||||
if (mode === 'edit') {
|
||||
httpMethod = 'PUT';
|
||||
|
@ -88,7 +63,7 @@ export default function ProcessModelForm({
|
|||
};
|
||||
if (mode === 'new') {
|
||||
Object.assign(postBody, {
|
||||
id: `${processGroupId}:${processModel.id}`,
|
||||
id: `${processGroupId}/${processModel.id}`,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -175,16 +150,6 @@ export default function ProcessModelForm({
|
|||
Submit
|
||||
</Button>,
|
||||
];
|
||||
if (mode === 'edit') {
|
||||
buttons.push(
|
||||
<ButtonWithConfirmation
|
||||
description={`Delete Process Model ${processModel.id}?`}
|
||||
onConfirmation={deleteProcessModel}
|
||||
buttonLabel="Delete"
|
||||
confirmButtonLabel="Delete"
|
||||
/>
|
||||
);
|
||||
}
|
||||
return <ButtonSet>{buttons}</ButtonSet>;
|
||||
};
|
||||
return (
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -52,10 +52,14 @@ import TouchModule from 'diagram-js/lib/navigation/touch';
|
|||
// @ts-expect-error TS(7016) FIXME
|
||||
import ZoomScrollModule from 'diagram-js/lib/navigation/zoomscroll';
|
||||
|
||||
import { Can } from '@casl/react';
|
||||
import HttpService from '../services/HttpService';
|
||||
|
||||
import ButtonWithConfirmation from './ButtonWithConfirmation';
|
||||
import { makeid } from '../helpers';
|
||||
import { useUriListForPermissions } from '../hooks/UriListForPermissions';
|
||||
import { PermissionsToCheck } from '../interfaces';
|
||||
import { usePermissionFetcher } from '../hooks/PermissionService';
|
||||
|
||||
type OwnProps = {
|
||||
processModelId: string;
|
||||
|
@ -76,6 +80,7 @@ type OwnProps = {
|
|||
onServiceTasksRequested?: (..._args: any[]) => any;
|
||||
onJsonFilesRequested?: (..._args: any[]) => any;
|
||||
onDmnFilesRequested?: (..._args: any[]) => any;
|
||||
onSearchProcessModels?: (..._args: any[]) => any;
|
||||
url?: string;
|
||||
};
|
||||
|
||||
|
@ -99,6 +104,7 @@ export default function ReactDiagramEditor({
|
|||
onServiceTasksRequested,
|
||||
onJsonFilesRequested,
|
||||
onDmnFilesRequested,
|
||||
onSearchProcessModels,
|
||||
url,
|
||||
}: OwnProps) {
|
||||
const [diagramXMLString, setDiagramXMLString] = useState('');
|
||||
|
@ -107,6 +113,13 @@ export default function ReactDiagramEditor({
|
|||
|
||||
const alreadyImportedXmlRef = useRef(false);
|
||||
|
||||
const { targetUris } = useUriListForPermissions();
|
||||
const permissionRequestData: PermissionsToCheck = {
|
||||
[targetUris.processModelShowPath]: ['PUT'],
|
||||
[targetUris.processModelFileShowPath]: ['POST', 'GET', 'PUT', 'DELETE'],
|
||||
};
|
||||
const { ability } = usePermissionFetcher(permissionRequestData);
|
||||
|
||||
useEffect(() => {
|
||||
if (diagramModelerState) {
|
||||
return;
|
||||
|
@ -292,6 +305,12 @@ export default function ReactDiagramEditor({
|
|||
diagramModeler.on('spiff.json_files.requested', (event: any) => {
|
||||
handleServiceTasksRequested(event);
|
||||
});
|
||||
|
||||
diagramModeler.on('spiff.callactivity.search', (event: any) => {
|
||||
if (onSearchProcessModels) {
|
||||
onSearchProcessModels(event.value, event.eventBus, event.element);
|
||||
}
|
||||
});
|
||||
}, [
|
||||
diagramModelerState,
|
||||
diagramType,
|
||||
|
@ -304,6 +323,7 @@ export default function ReactDiagramEditor({
|
|||
onServiceTasksRequested,
|
||||
onJsonFilesRequested,
|
||||
onDmnFilesRequested,
|
||||
onSearchProcessModels,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -517,20 +537,40 @@ export default function ReactDiagramEditor({
|
|||
if (diagramType !== 'readonly') {
|
||||
return (
|
||||
<>
|
||||
<Button onClick={handleSave} variant="danger">
|
||||
Save
|
||||
</Button>
|
||||
{fileName && (
|
||||
<ButtonWithConfirmation
|
||||
description={`Delete file ${fileName}?`}
|
||||
onConfirmation={handleDelete}
|
||||
buttonLabel="Delete"
|
||||
/>
|
||||
)}
|
||||
{onSetPrimaryFile && (
|
||||
<Button onClick={handleSetPrimaryFile}>Set as primary file</Button>
|
||||
)}
|
||||
<Button onClick={downloadXmlFile}>Download xml</Button>
|
||||
<Can
|
||||
I="PUT"
|
||||
a={targetUris.processModelFileShowPath}
|
||||
ability={ability}
|
||||
>
|
||||
<Button onClick={handleSave}>Save</Button>
|
||||
</Can>
|
||||
<Can
|
||||
I="DELETE"
|
||||
a={targetUris.processModelFileShowPath}
|
||||
ability={ability}
|
||||
>
|
||||
{fileName && (
|
||||
<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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -18,5 +18,6 @@ export const PROCESS_STATUSES = [
|
|||
|
||||
// with time: 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_CARBON = 'Y-m-d';
|
||||
|
|
|
@ -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);
|
|
@ -1,4 +1,4 @@
|
|||
import { convertSecondsToFormattedDate, slugifyString } from './helpers';
|
||||
import { convertSecondsToFormattedDateString, slugifyString } from './helpers';
|
||||
|
||||
test('it can slugify a string', () => {
|
||||
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', () => {
|
||||
const dateString = convertSecondsToFormattedDate(1666325400);
|
||||
const dateString = convertSecondsToFormattedDateString(1666325400);
|
||||
expect(dateString).toEqual('2022-10-21');
|
||||
});
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
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 {
|
||||
DEFAULT_PER_PAGE,
|
||||
DEFAULT_PAGE,
|
||||
|
@ -42,27 +46,72 @@ export const convertDateToSeconds = (
|
|||
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) => {
|
||||
if (dateString) {
|
||||
// add midnight time to the date so it c uses the correct date
|
||||
// after converting to timezone
|
||||
return new Date(`${dateString}T00:10:00`);
|
||||
return convertDateAndTimeStringsToSeconds(dateString, '00:10:00');
|
||||
};
|
||||
|
||||
export const convertSecondsToDateObject = (seconds: number) => {
|
||||
if (seconds) {
|
||||
return new Date(seconds * 1000);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export const convertSecondsToFormattedDateTime = (seconds: number) => {
|
||||
if (seconds) {
|
||||
const dateObject = new Date(seconds * 1000);
|
||||
const dateObject = convertSecondsToDateObject(seconds);
|
||||
if (dateObject) {
|
||||
return format(dateObject, DATE_TIME_FORMAT);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export const convertSecondsToFormattedDate = (seconds: number) => {
|
||||
if (seconds) {
|
||||
const dateObject = new Date(seconds * 1000);
|
||||
return format(dateObject, DATE_FORMAT);
|
||||
export const convertDateObjectToFormattedHoursMinutes = (dateObject: Date) => {
|
||||
if (dateObject) {
|
||||
return format(dateObject, TIME_FORMAT_HOURS_MINUTES);
|
||||
}
|
||||
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;
|
||||
};
|
||||
|
@ -79,11 +128,20 @@ export const objectIsEmpty = (obj: object) => {
|
|||
export const getPageInfoFromSearchParams = (
|
||||
searchParams: any,
|
||||
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(
|
||||
searchParams.get('per_page') || defaultPerPage.toString(),
|
||||
searchParams.get(`${paginationQueryParamPrefixToUse}per_page`) ||
|
||||
defaultPerPage.toString(),
|
||||
10
|
||||
);
|
||||
|
||||
|
@ -139,3 +197,16 @@ export const getGroupFromModifiedModelId = (modifiedId: string) => {
|
|||
export const splitProcessModelId = (processModelId: string) => {
|
||||
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];
|
||||
};
|
||||
|
|
|
@ -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 };
|
||||
};
|
|
@ -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 };
|
||||
};
|
|
@ -5,21 +5,21 @@
|
|||
color: white;
|
||||
}
|
||||
|
||||
h1{
|
||||
height: 36px;
|
||||
font-family: 'IBM Plex Sans';
|
||||
font-style: normal;
|
||||
h1 {
|
||||
font-weight: 400;
|
||||
font-size: 28px;
|
||||
line-height: 36px;
|
||||
color: #161616;
|
||||
flex: none;
|
||||
order: 0;
|
||||
align-self: stretch;
|
||||
flex-grow: 0;
|
||||
margin-bottom: 1em
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-weight: 400;
|
||||
font-size: 20px;
|
||||
line-height: 28px;
|
||||
color: #161616;
|
||||
}
|
||||
|
||||
.span-tag {
|
||||
color: black;
|
||||
}
|
||||
|
@ -31,7 +31,7 @@ h1{
|
|||
border: 1px solid #393939;
|
||||
}
|
||||
.cds--btn.button-white-background:hover {
|
||||
background: #525252;
|
||||
background: lightgrey;
|
||||
}
|
||||
|
||||
.cds--breadcrumb-item a.cds--link:hover {
|
||||
|
@ -71,7 +71,7 @@ h1{
|
|||
}
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||
monospace;
|
||||
monospace;
|
||||
}
|
||||
|
||||
.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
|
||||
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 {
|
||||
display: inline-block;
|
||||
padding-right: 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;
|
||||
}
|
||||
|
|
|
@ -11,16 +11,18 @@ export interface RecentProcessModel {
|
|||
processModelDisplayName: string;
|
||||
}
|
||||
|
||||
export interface ProcessGroup {
|
||||
id: string;
|
||||
display_name: string;
|
||||
description?: string | null;
|
||||
}
|
||||
|
||||
export interface ProcessFileReference {
|
||||
export interface ProcessReference {
|
||||
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"
|
||||
file_name: string;
|
||||
has_lanes: boolean;
|
||||
is_executable: boolean;
|
||||
is_primary: boolean;
|
||||
}
|
||||
|
||||
export interface ProcessFile {
|
||||
|
@ -28,12 +30,17 @@ export interface ProcessFile {
|
|||
last_modified: string;
|
||||
name: string;
|
||||
process_model_id: string;
|
||||
references: ProcessFileReference[];
|
||||
references: ProcessReference[];
|
||||
size: number;
|
||||
type: string;
|
||||
file_contents?: string;
|
||||
}
|
||||
|
||||
export interface ProcessInstance {
|
||||
id: number;
|
||||
process_model_identifier: string;
|
||||
}
|
||||
|
||||
export interface ProcessModel {
|
||||
id: string;
|
||||
description: string;
|
||||
|
@ -42,6 +49,14 @@ export interface ProcessModel {
|
|||
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
|
||||
export type HotCrumbItem = [displayValue: string, url?: string];
|
||||
|
||||
|
@ -70,3 +85,28 @@ export interface PaginationObject {
|
|||
export interface CarbonComboBoxSelection {
|
||||
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[];
|
||||
}
|
||||
|
|
|
@ -20,10 +20,8 @@ import ReactFormEditor from './ReactFormEditor';
|
|||
import ErrorContext from '../contexts/ErrorContext';
|
||||
import ProcessInstanceLogList from './ProcessInstanceLogList';
|
||||
import MessageInstanceList from './MessageInstanceList';
|
||||
import SecretList from './SecretList';
|
||||
import SecretNew from './SecretNew';
|
||||
import SecretShow from './SecretShow';
|
||||
import AuthenticationList from './AuthenticationList';
|
||||
import Configuration from './Configuration';
|
||||
import JsonSchemaFormBuilder from './JsonSchemaFormBuilder';
|
||||
|
||||
export default function AdminRoutes() {
|
||||
const location = useLocation();
|
||||
|
@ -110,10 +108,11 @@ export default function AdminRoutes() {
|
|||
/>
|
||||
<Route path="process-instances" element={<ProcessInstanceList />} />
|
||||
<Route path="messages" element={<MessageInstanceList />} />
|
||||
<Route path="secrets" element={<SecretList />} />
|
||||
<Route path="secrets/new" element={<SecretNew />} />
|
||||
<Route path="secrets/:key" element={<SecretShow />} />
|
||||
<Route path="authentications" element={<AuthenticationList />} />
|
||||
<Route path="configuration/*" element={<Configuration />} />
|
||||
<Route
|
||||
path="process-models/:process_model_id/form-builder"
|
||||
element={<JsonSchemaFormBuilder />}
|
||||
/>
|
||||
</Routes>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
import MyCompletedInstances from '../components/MyCompletedInstances';
|
||||
|
||||
export default function CompletedInstances() {
|
||||
return <MyCompletedInstances />;
|
||||
}
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
import ProcessModelListTiles from '../components/ProcessModelListTiles';
|
||||
|
||||
export default function CreateNewInstance() {
|
||||
return (
|
||||
<ProcessModelListTiles
|
||||
headerElement={<h1>Process models available to you</h1>}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -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 />
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -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()}</>;
|
||||
}
|
|
@ -5,7 +5,7 @@ import { Link, useParams, useSearchParams } from 'react-router-dom';
|
|||
import PaginationForTable from '../components/PaginationForTable';
|
||||
import ProcessBreadcrumb from '../components/ProcessBreadcrumb';
|
||||
import {
|
||||
convertSecondsToFormattedDate,
|
||||
convertSecondsToFormattedDateString,
|
||||
getPageInfoFromSearchParams,
|
||||
modifyProcessModelPath,
|
||||
unModifyProcessModelPath,
|
||||
|
@ -65,10 +65,12 @@ export default function MessageInstanceList() {
|
|||
</td>
|
||||
<td>{rowToUse.message_identifier}</td>
|
||||
<td>{rowToUse.message_type}</td>
|
||||
<td>{rowToUse.failure_cause}</td>
|
||||
<td>{rowToUse.failure_cause || '-'}</td>
|
||||
<td>{rowToUse.status}</td>
|
||||
<td>
|
||||
{convertSecondsToFormattedDate(rowToUse.created_at_in_seconds)}
|
||||
{convertSecondsToFormattedDateString(
|
||||
rowToUse.created_at_in_seconds
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
|
@ -94,14 +96,8 @@ export default function MessageInstanceList() {
|
|||
|
||||
if (pagination) {
|
||||
const { page, perPage } = getPageInfoFromSearchParams(searchParams);
|
||||
let queryParamString = '';
|
||||
let breadcrumbElement = null;
|
||||
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 = (
|
||||
<ProcessBreadcrumb
|
||||
hotCrumbs={[
|
||||
|
@ -132,8 +128,6 @@ export default function MessageInstanceList() {
|
|||
perPage={perPage}
|
||||
pagination={pagination}
|
||||
tableToDisplay={buildTable()}
|
||||
queryParamString={queryParamString}
|
||||
path="/admin/messages"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -6,30 +6,38 @@ import PaginationForTable from '../components/PaginationForTable';
|
|||
import {
|
||||
getPageInfoFromSearchParams,
|
||||
modifyProcessModelPath,
|
||||
refreshAtInterval,
|
||||
} from '../helpers';
|
||||
import HttpService from '../services/HttpService';
|
||||
import { PaginationObject, RecentProcessModel } from '../interfaces';
|
||||
|
||||
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 [tasks, setTasks] = useState([]);
|
||||
const [pagination, setPagination] = useState<PaginationObject | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const { page, perPage } = getPageInfoFromSearchParams(
|
||||
searchParams,
|
||||
PER_PAGE_FOR_TASKS_ON_HOME_PAGE
|
||||
);
|
||||
const setTasksFromResult = (result: any) => {
|
||||
setTasks(result.results);
|
||||
setPagination(result.pagination);
|
||||
const getTasks = () => {
|
||||
const { page, perPage } = getPageInfoFromSearchParams(
|
||||
searchParams,
|
||||
PER_PAGE_FOR_TASKS_ON_HOME_PAGE
|
||||
);
|
||||
const setTasksFromResult = (result: any) => {
|
||||
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}`,
|
||||
successCallback: setTasksFromResult,
|
||||
});
|
||||
|
||||
getTasks();
|
||||
refreshAtInterval(REFRESH_INTERVAL, REFRESH_TIMEOUT, getTasks);
|
||||
}, [searchParams]);
|
||||
|
||||
let recentProcessModels: RecentProcessModel[] = [];
|
||||
|
@ -122,7 +130,7 @@ export default function HomePage() {
|
|||
});
|
||||
return (
|
||||
<>
|
||||
<h1>Processes I can start</h1>
|
||||
<h1>Recently viewed process models</h1>
|
||||
<Table striped bordered>
|
||||
<thead>
|
||||
<tr>
|
||||
|
@ -152,7 +160,6 @@ export default function HomePage() {
|
|||
perPageOptions={[2, PER_PAGE_FOR_TASKS_ON_HOME_PAGE, 25]}
|
||||
pagination={pagination}
|
||||
tableToDisplay={buildTable()}
|
||||
path="/tasks"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
@ -170,6 +177,7 @@ export default function HomePage() {
|
|||
return (
|
||||
<>
|
||||
{tasksWaitingForMe}
|
||||
<br />
|
||||
{relevantProcessModelSection}
|
||||
</>
|
||||
);
|
|
@ -1,42 +1,34 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
import { Link, useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import {
|
||||
Button,
|
||||
Table,
|
||||
// ExpandableTile,
|
||||
// TileAboveTheFoldContent,
|
||||
// TileBelowTheFoldContent,
|
||||
// TextInput,
|
||||
// ClickableTile,
|
||||
// @ts-ignore
|
||||
} from '@carbon/react';
|
||||
import { Can } from '@casl/react';
|
||||
import ProcessBreadcrumb from '../components/ProcessBreadcrumb';
|
||||
import PaginationForTable from '../components/PaginationForTable';
|
||||
import HttpService from '../services/HttpService';
|
||||
import {
|
||||
getPageInfoFromSearchParams,
|
||||
modifyProcessModelPath,
|
||||
} from '../helpers';
|
||||
import { CarbonComboBoxSelection, ProcessGroup } from '../interfaces';
|
||||
import { modifyProcessModelPath } from '../helpers';
|
||||
import { CarbonComboBoxSelection, PermissionsToCheck } from '../interfaces';
|
||||
import { useUriListForPermissions } from '../hooks/UriListForPermissions';
|
||||
import { usePermissionFetcher } from '../hooks/PermissionService';
|
||||
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() {
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [processGroups, setProcessGroups] = useState([]);
|
||||
const [pagination, setPagination] = useState(null);
|
||||
const [processModelAvailableItems, setProcessModelAvailableItems] = useState(
|
||||
[]
|
||||
);
|
||||
|
||||
const { targetUris } = useUriListForPermissions();
|
||||
const permissionRequestData: PermissionsToCheck = {
|
||||
[targetUris.processGroupListPath]: ['POST'],
|
||||
};
|
||||
const { ability } = usePermissionFetcher(permissionRequestData);
|
||||
|
||||
useEffect(() => {
|
||||
const setProcessGroupsFromResult = (result: any) => {
|
||||
setProcessGroups(result.results);
|
||||
setPagination(result.pagination);
|
||||
};
|
||||
const processResultForProcessModels = (result: any) => {
|
||||
const selectionArray = result.results.map((item: any) => {
|
||||
const label = `${item.id}`;
|
||||
|
@ -45,13 +37,6 @@ export default function ProcessGroupList() {
|
|||
});
|
||||
setProcessModelAvailableItems(selectionArray);
|
||||
};
|
||||
|
||||
const { page, perPage } = getPageInfoFromSearchParams(searchParams);
|
||||
// for browsing
|
||||
HttpService.makeCallToBackend({
|
||||
path: `/process-groups?per_page=${perPage}&page=${page}`,
|
||||
successCallback: setProcessGroupsFromResult,
|
||||
});
|
||||
// for search box
|
||||
HttpService.makeCallToBackend({
|
||||
path: `/process-models?per_page=1000`,
|
||||
|
@ -59,66 +44,6 @@ export default function ProcessGroupList() {
|
|||
});
|
||||
}, [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 processModelSearchOnChange = (selection: CarbonComboBoxSelection) => {
|
||||
const processModel = selection.selectedItem;
|
||||
|
@ -135,18 +60,21 @@ export default function ProcessGroupList() {
|
|||
);
|
||||
};
|
||||
|
||||
if (pagination) {
|
||||
if (processModelAvailableItems) {
|
||||
return (
|
||||
<>
|
||||
<ProcessBreadcrumb hotCrumbs={[['Process Groups']]} />
|
||||
<Button kind="secondary" href="/admin/process-groups/new">
|
||||
Add a process group
|
||||
</Button>
|
||||
<br />
|
||||
<Can I="POST" a={targetUris.processGroupListPath} ability={ability}>
|
||||
<Button kind="secondary" href="/admin/process-groups/new">
|
||||
Add a process group
|
||||
</Button>
|
||||
<br />
|
||||
<br />
|
||||
</Can>
|
||||
<br />
|
||||
{processModelSearchArea()}
|
||||
<br />
|
||||
{processGroupsDisplayArea()}
|
||||
<ProcessGroupListTiles />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ import { useEffect, useState } from 'react';
|
|||
import { Link, useSearchParams, useParams } from 'react-router-dom';
|
||||
// @ts-ignore
|
||||
import { Button, Table, Stack } from '@carbon/react';
|
||||
import { Can } from '@casl/react';
|
||||
import ProcessBreadcrumb from '../components/ProcessBreadcrumb';
|
||||
import PaginationForTable from '../components/PaginationForTable';
|
||||
import HttpService from '../services/HttpService';
|
||||
|
@ -10,7 +11,15 @@ import {
|
|||
modifyProcessModelPath,
|
||||
unModifyProcessModelPath,
|
||||
} 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() {
|
||||
const params = useParams();
|
||||
|
@ -18,9 +27,16 @@ export default function ProcessGroupShow() {
|
|||
|
||||
const [processGroup, setProcessGroup] = useState<ProcessGroup | null>(null);
|
||||
const [processModels, setProcessModels] = useState([]);
|
||||
const [processGroups, setProcessGroups] = useState([]);
|
||||
const [modelPagination, setModelPagination] = useState(null);
|
||||
const [groupPagination, setGroupPagination] = useState(null);
|
||||
const [modelPagination, setModelPagination] =
|
||||
useState<PaginationObject | null>(null);
|
||||
|
||||
const { targetUris } = useUriListForPermissions();
|
||||
const permissionRequestData: PermissionsToCheck = {
|
||||
[targetUris.processGroupListPath]: ['POST'],
|
||||
[targetUris.processGroupShowPath]: ['PUT'],
|
||||
[targetUris.processModelCreatePath]: ['POST'],
|
||||
};
|
||||
const { ability } = usePermissionFetcher(permissionRequestData);
|
||||
|
||||
useEffect(() => {
|
||||
const { page, perPage } = getPageInfoFromSearchParams(searchParams);
|
||||
|
@ -29,10 +45,6 @@ export default function ProcessGroupShow() {
|
|||
setProcessModels(result.results);
|
||||
setModelPagination(result.pagination);
|
||||
};
|
||||
const setProcessGroupFromResult = (result: any) => {
|
||||
setProcessGroups(result.results);
|
||||
setGroupPagination(result.pagination);
|
||||
};
|
||||
const processResult = (result: any) => {
|
||||
setProcessGroup(result);
|
||||
const unmodifiedProcessGroupId = unModifyProcessModelPath(
|
||||
|
@ -42,10 +54,6 @@ export default function ProcessGroupShow() {
|
|||
path: `/process-models?process_group_identifier=${unmodifiedProcessGroupId}&per_page=${perPage}&page=${page}`,
|
||||
successCallback: setProcessModelFromResult,
|
||||
});
|
||||
HttpService.makeCallToBackend({
|
||||
path: `/process-groups?process_group_identifier=${unmodifiedProcessGroupId}&per_page=${perPage}&page=${page}`,
|
||||
successCallback: setProcessGroupFromResult,
|
||||
});
|
||||
};
|
||||
HttpService.makeCallToBackend({
|
||||
path: `/process-groups/${params.process_group_id}`,
|
||||
|
@ -58,7 +66,9 @@ export default function ProcessGroupShow() {
|
|||
return null;
|
||||
}
|
||||
const rows = processModels.map((row: ProcessModel) => {
|
||||
const modifiedProcessModelId: String = modifyProcessModelPath((row as any).id);
|
||||
const modifiedProcessModelId: String = modifyProcessModelPath(
|
||||
(row as any).id
|
||||
);
|
||||
return (
|
||||
<tr key={row.id}>
|
||||
<td>
|
||||
|
@ -75,7 +85,7 @@ export default function ProcessGroupShow() {
|
|||
});
|
||||
return (
|
||||
<div>
|
||||
<h3>Process Models</h3>
|
||||
<h2>Process Models</h2>
|
||||
<Table striped bordered>
|
||||
<thead>
|
||||
<tr>
|
||||
|
@ -89,43 +99,7 @@ export default function ProcessGroupShow() {
|
|||
);
|
||||
};
|
||||
|
||||
const buildGroupTable = () => {
|
||||
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) {
|
||||
if (processGroup && modelPagination) {
|
||||
const { page, perPage } = getPageInfoFromSearchParams(searchParams);
|
||||
const modifiedProcessGroupId = modifyProcessModelPath(processGroup.id);
|
||||
return (
|
||||
|
@ -136,43 +110,51 @@ export default function ProcessGroupShow() {
|
|||
['', `process_group:${processGroup.id}`],
|
||||
]}
|
||||
/>
|
||||
<h1>Process Group: {processGroup.display_name}</h1>
|
||||
<ul>
|
||||
<Stack orientation="horizontal" gap={3}>
|
||||
<Button
|
||||
kind="secondary"
|
||||
href={`/admin/process-groups/new?parentGroupId=${processGroup.id}`}
|
||||
<Can I="POST" a={targetUris.processGroupListPath} ability={ability}>
|
||||
<Button
|
||||
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`}
|
||||
>
|
||||
Add a process model
|
||||
</Button>
|
||||
<Button
|
||||
href={`/admin/process-groups/${modifiedProcessGroupId}/edit`}
|
||||
variant="secondary"
|
||||
>
|
||||
Edit process group
|
||||
</Button>
|
||||
<Button
|
||||
href={`/admin/process-models/${modifiedProcessGroupId}/new`}
|
||||
>
|
||||
Add a process model
|
||||
</Button>
|
||||
</Can>
|
||||
<Can I="PUT" a={targetUris.processGroupShowPath} ability={ability}>
|
||||
<Button
|
||||
href={`/admin/process-groups/${modifiedProcessGroupId}/edit`}
|
||||
>
|
||||
Edit process group
|
||||
</Button>
|
||||
</Can>
|
||||
</Stack>
|
||||
<br />
|
||||
<br />
|
||||
<PaginationForTable
|
||||
page={page}
|
||||
perPage={perPage}
|
||||
pagination={modelPagination}
|
||||
tableToDisplay={buildModelTable()}
|
||||
path={`/admin/process-groups/${processGroup.id}`}
|
||||
/>
|
||||
{/* eslint-disable-next-line sonarjs/no-gratuitous-expressions */}
|
||||
{modelPagination && modelPagination.total > 0 && (
|
||||
<PaginationForTable
|
||||
page={page}
|
||||
perPage={perPage}
|
||||
pagination={modelPagination}
|
||||
tableToDisplay={buildModelTable()}
|
||||
/>
|
||||
)}
|
||||
<br />
|
||||
<br />
|
||||
<PaginationForTable
|
||||
page={page}
|
||||
perPage={perPage}
|
||||
pagination={groupPagination}
|
||||
tableToDisplay={buildGroupTable()}
|
||||
path={`/admin/process-groups/${processGroup.id}`}
|
||||
<ProcessGroupListTiles
|
||||
processGroup={processGroup}
|
||||
headerElement={<h2>Process Groups</h2>}
|
||||
/>
|
||||
</ul>
|
||||
</>
|
||||
|
|
|
@ -1,477 +1,15 @@
|
|||
import { useContext, useEffect, useMemo, useState } from 'react';
|
||||
import {
|
||||
Link,
|
||||
useNavigate,
|
||||
useParams,
|
||||
useSearchParams,
|
||||
} from 'react-router-dom';
|
||||
import { 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 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 '../components/ProcessModelSearch';
|
||||
import ProcessBreadcrumb from '../components/ProcessBreadcrumb';
|
||||
import ProcessInstanceListTable from '../components/ProcessInstanceListTable';
|
||||
import { getProcessModelFullIdentifierFromSearchParams } from '../helpers';
|
||||
|
||||
export default function ProcessInstanceList() {
|
||||
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 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 processModelFullIdentifier =
|
||||
getProcessModelFullIdentifierFromSearchParams(searchParams);
|
||||
|
@ -496,47 +34,11 @@ export default function ProcessInstanceList() {
|
|||
const processInstanceTitleElement = () => {
|
||||
return <h1>Process Instances</h1>;
|
||||
};
|
||||
|
||||
const toggleShowFilterOptions = () => {
|
||||
setShowFilterOptions(!showFilterOptions);
|
||||
};
|
||||
|
||||
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;
|
||||
return (
|
||||
<>
|
||||
{processInstanceBreadcrumbElement()}
|
||||
{processInstanceTitleElement()}
|
||||
<ProcessInstanceListTable />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -6,9 +6,9 @@ import PaginationForTable from '../components/PaginationForTable';
|
|||
import ProcessBreadcrumb from '../components/ProcessBreadcrumb';
|
||||
import {
|
||||
getPageInfoFromSearchParams,
|
||||
convertSecondsToFormattedDate,
|
||||
modifyProcessModelPath,
|
||||
unModifyProcessModelPath,
|
||||
convertSecondsToFormattedDateTime,
|
||||
} from '../helpers';
|
||||
import HttpService from '../services/HttpService';
|
||||
|
||||
|
@ -49,7 +49,7 @@ export default function ProcessInstanceLogList() {
|
|||
data-qa="process-instance-show-link"
|
||||
to={`/admin/process-models/${modifiedProcessModelId}/process-instances/${rowToUse.process_instance_id}/${rowToUse.spiff_step}`}
|
||||
>
|
||||
{convertSecondsToFormattedDate(rowToUse.timestamp)}
|
||||
{convertSecondsToFormattedDateTime(rowToUse.timestamp)}
|
||||
</Link>
|
||||
</td>
|
||||
</tr>
|
||||
|
@ -74,7 +74,6 @@ export default function ProcessInstanceLogList() {
|
|||
};
|
||||
|
||||
if (pagination) {
|
||||
console.log('params.process_model_id', params.process_model_id);
|
||||
const { page, perPage } = getPageInfoFromSearchParams(searchParams);
|
||||
return (
|
||||
<main>
|
||||
|
@ -99,7 +98,6 @@ export default function ProcessInstanceLogList() {
|
|||
perPage={perPage}
|
||||
pagination={pagination}
|
||||
tableToDisplay={buildTable()}
|
||||
path={`/admin/process-models/${modifiedProcessModelId}/process-instances/${params.process_instance_id}/logs`}
|
||||
/>
|
||||
</main>
|
||||
);
|
||||
|
|
|
@ -44,9 +44,9 @@ export default function ProcessInstanceReportList() {
|
|||
|
||||
const headerStuff = (
|
||||
<>
|
||||
<h1>Process Instance Reports</h1>
|
||||
<h1>Process Instance Perspectives</h1>
|
||||
<Button href="/admin/process-instances/reports/new">
|
||||
Add a process instance report
|
||||
Add a process instance perspective
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
|
@ -61,7 +61,7 @@ export default function ProcessInstanceReportList() {
|
|||
return (
|
||||
<main>
|
||||
{headerStuff}
|
||||
<p>No reports found</p>
|
||||
<p>No perspectives found</p>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -56,7 +56,7 @@ export default function ProcessInstanceReportNew() {
|
|||
return (
|
||||
<>
|
||||
<ProcessBreadcrumb />
|
||||
<h1>Add Process Model</h1>
|
||||
<h1>Add Process Instance Perspective</h1>
|
||||
<form onSubmit={addProcessInstanceReport}>
|
||||
<label htmlFor="identifier">
|
||||
identifier:
|
||||
|
|
|
@ -80,18 +80,17 @@ export default function ProcessInstanceReport() {
|
|||
processGroupId={params.process_group_id}
|
||||
linkProcessModel
|
||||
/>
|
||||
<h1>Process Instance Report: {params.report_identifier}</h1>
|
||||
<h1>Process Instance Perspective: {params.report_identifier}</h1>
|
||||
<Button
|
||||
href={`/admin/process-instances/reports/${params.report_identifier}/edit`}
|
||||
>
|
||||
Edit process instance report
|
||||
Edit process instance perspective
|
||||
</Button>
|
||||
<PaginationForTable
|
||||
page={page}
|
||||
perPage={perPage}
|
||||
pagination={pagination}
|
||||
tableToDisplay={buildTable()}
|
||||
path={`/admin/process-models/${params.process_group_id}/${params.process_model_id}/process-instances/report`}
|
||||
/>
|
||||
</main>
|
||||
);
|
||||
|
|
|
@ -23,6 +23,7 @@ import {
|
|||
Stack,
|
||||
// @ts-ignore
|
||||
} from '@carbon/react';
|
||||
import { Can } from '@casl/react';
|
||||
import ProcessBreadcrumb from '../components/ProcessBreadcrumb';
|
||||
import HttpService from '../services/HttpService';
|
||||
import ReactDiagramEditor from '../components/ReactDiagramEditor';
|
||||
|
@ -32,6 +33,9 @@ import {
|
|||
} from '../helpers';
|
||||
import ButtonWithConfirmation from '../components/ButtonWithConfirmation';
|
||||
import ErrorContext from '../contexts/ErrorContext';
|
||||
import { useUriListForPermissions } from '../hooks/UriListForPermissions';
|
||||
import { PermissionsToCheck } from '../interfaces';
|
||||
import { usePermissionFetcher } from '../hooks/PermissionService';
|
||||
|
||||
export default function ProcessInstanceShow() {
|
||||
const navigate = useNavigate();
|
||||
|
@ -50,6 +54,12 @@ export default function ProcessInstanceShow() {
|
|||
);
|
||||
const modifiedProcessModelId = params.process_model_id;
|
||||
|
||||
const { targetUris } = useUriListForPermissions();
|
||||
const permissionRequestData: PermissionsToCheck = {
|
||||
[targetUris.messageInstanceListPath]: ['GET'],
|
||||
};
|
||||
const { ability } = usePermissionFetcher(permissionRequestData);
|
||||
|
||||
const navigateToProcessInstances = (_result: any) => {
|
||||
navigate(
|
||||
`/admin/process-instances?process_model_identifier=${unModifiedProcessModelId}`
|
||||
|
@ -63,12 +73,12 @@ export default function ProcessInstanceShow() {
|
|||
});
|
||||
if (typeof params.spiff_step === 'undefined')
|
||||
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,
|
||||
});
|
||||
else
|
||||
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,
|
||||
});
|
||||
}, [params, modifiedProcessModelId]);
|
||||
|
@ -245,14 +255,20 @@ export default function ProcessInstanceShow() {
|
|||
>
|
||||
Logs
|
||||
</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}`}
|
||||
<Can
|
||||
I="GET"
|
||||
a={targetUris.messageInstanceListPath}
|
||||
ability={ability}
|
||||
>
|
||||
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>
|
||||
</Column>
|
||||
</Grid>
|
||||
|
@ -521,6 +537,7 @@ export default function ProcessInstanceShow() {
|
|||
elements.push(resumeButton(processInstanceToUse));
|
||||
elements.push(
|
||||
<ButtonWithConfirmation
|
||||
data-qa="process-instance-delete"
|
||||
kind="ghost"
|
||||
renderIcon={TrashCan}
|
||||
iconDescription="Delete"
|
||||
|
|
|
@ -18,7 +18,13 @@ import ProcessBreadcrumb from '../components/ProcessBreadcrumb';
|
|||
import HttpService from '../services/HttpService';
|
||||
import ErrorContext from '../contexts/ErrorContext';
|
||||
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() {
|
||||
const [showFileNameEditor, setShowFileNameEditor] = useState(false);
|
||||
|
@ -36,6 +42,10 @@ export default function ProcessModelEditDiagram() {
|
|||
const [markdownText, setMarkdownText] = useState<string | undefined>('');
|
||||
const [markdownEventBus, setMarkdownEventBus] = useState<any>(null);
|
||||
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);
|
||||
|
||||
|
@ -90,6 +100,23 @@ export default function ProcessModelEditDiagram() {
|
|||
|
||||
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(() => {
|
||||
const processResult = (result: ProcessModel) => {
|
||||
setProcessModel(result);
|
||||
|
@ -215,6 +242,7 @@ export default function ProcessModelEditDiagram() {
|
|||
secondaryButtonText="Cancel"
|
||||
onSecondarySubmit={handleFileNameCancel}
|
||||
onRequestSubmit={handleFileNameSave}
|
||||
onRequestClose={handleFileNameCancel}
|
||||
>
|
||||
<label>File Name:</label>
|
||||
<span>
|
||||
|
@ -278,7 +306,7 @@ export default function ProcessModelEditDiagram() {
|
|||
const options: any[] = [];
|
||||
dmnFiles.forEach((file) => {
|
||||
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 });
|
||||
|
@ -607,6 +635,7 @@ export default function ProcessModelEditDiagram() {
|
|||
primaryButtonText="Close"
|
||||
onRequestSubmit={handleScriptEditorClose}
|
||||
size="lg"
|
||||
onRequestClose={handleScriptEditorClose}
|
||||
>
|
||||
<Editor
|
||||
height={500}
|
||||
|
@ -644,6 +673,7 @@ export default function ProcessModelEditDiagram() {
|
|||
modalHeading="Edit Markdown"
|
||||
primaryButtonText="Close"
|
||||
onRequestSubmit={handleMarkdownEditorClose}
|
||||
onRequestClose={handleMarkdownEditorClose}
|
||||
size="lg"
|
||||
>
|
||||
<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 = (
|
||||
id: string,
|
||||
type: string
|
||||
|
@ -676,19 +745,18 @@ export default function ProcessModelEditDiagram() {
|
|||
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 file = findFileNameForReferenceId(processId, 'bpmn');
|
||||
if (file) {
|
||||
const processRef = processes.find((p) => {
|
||||
return p.identifier === processId;
|
||||
});
|
||||
if (processRef) {
|
||||
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,
|
||||
file_name: file.name,
|
||||
process_model_path: modifyProcessModelPath(
|
||||
processRef.process_model_id
|
||||
),
|
||||
file_name: processRef.file_name,
|
||||
}
|
||||
);
|
||||
window.open(path);
|
||||
|
@ -763,6 +831,7 @@ export default function ProcessModelEditDiagram() {
|
|||
onJsonFilesRequested={onJsonFilesRequested}
|
||||
onLaunchDmnEditor={onLaunchDmnEditor}
|
||||
onDmnFilesRequested={onDmnFilesRequested}
|
||||
onSearchProcessModels={onSearchProcessModels}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -790,7 +859,7 @@ export default function ProcessModelEditDiagram() {
|
|||
{newFileNameBox()}
|
||||
{scriptEditor()}
|
||||
{markdownEditor()}
|
||||
|
||||
{processModelSelector()}
|
||||
<div id="diagram-container" />
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -27,12 +27,26 @@ import {
|
|||
TableBody,
|
||||
// @ts-ignore
|
||||
} from '@carbon/react';
|
||||
import { Can } from '@casl/react';
|
||||
import ProcessBreadcrumb from '../components/ProcessBreadcrumb';
|
||||
import HttpService from '../services/HttpService';
|
||||
import ErrorContext from '../contexts/ErrorContext';
|
||||
import { modifyProcessModelPath, unModifyProcessModelPath } from '../helpers';
|
||||
import { ProcessFile, ProcessModel, RecentProcessModel } from '../interfaces';
|
||||
import {
|
||||
getGroupFromModifiedModelId,
|
||||
modifyProcessModelPath,
|
||||
} from '../helpers';
|
||||
import {
|
||||
PermissionsToCheck,
|
||||
ProcessFile,
|
||||
ProcessInstance,
|
||||
ProcessModel,
|
||||
RecentProcessModel,
|
||||
} from '../interfaces';
|
||||
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 = (
|
||||
processModelForStorage: ProcessModel
|
||||
|
@ -88,13 +102,23 @@ export default function ProcessModelShow() {
|
|||
const setErrorMessage = (useContext as any)(ErrorContext)[1];
|
||||
|
||||
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 [filesToUpload, setFilesToUpload] = useState<any>(null);
|
||||
const [showFileUploadModal, setShowFileUploadModal] =
|
||||
useState<boolean>(false);
|
||||
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(
|
||||
`${params.process_model_id}`
|
||||
);
|
||||
|
@ -111,52 +135,19 @@ export default function ProcessModelShow() {
|
|||
});
|
||||
}, [reloadModel, modifiedProcessModelId]);
|
||||
|
||||
const processModelRun = (processInstance: any) => {
|
||||
setErrorMessage(null);
|
||||
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>
|
||||
);
|
||||
}
|
||||
const processInstanceRunResultTag = () => {
|
||||
if (processInstance) {
|
||||
return (
|
||||
<div className="alert alert-success" role="alert">
|
||||
<p>
|
||||
Process Instance {processInstanceId} kicked off (
|
||||
Process Instance {processInstance.id} kicked off (
|
||||
<Link
|
||||
to={`/admin/process-models/${modifiedProcessModelId}/process-instances/${processInstanceId}`}
|
||||
to={`/admin/process-models/${modifiedProcessModelId}/process-instances/${processInstance.id}`}
|
||||
data-qa="process-instance-show-link"
|
||||
>
|
||||
view
|
||||
</Link>
|
||||
). {takeMeToMyTaskBlurb}
|
||||
).
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
@ -242,6 +233,22 @@ export default function ProcessModelShow() {
|
|||
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 url = profileModelFileEditUrl(processModelFile);
|
||||
if (url) {
|
||||
|
@ -255,50 +262,62 @@ export default function ProcessModelShow() {
|
|||
) => {
|
||||
const elements = [];
|
||||
elements.push(
|
||||
<Button
|
||||
kind="ghost"
|
||||
renderIcon={Edit}
|
||||
iconDescription="Edit File"
|
||||
hasIconOnly
|
||||
size="lg"
|
||||
data-qa={`edit-file-${processModelFile.name.replace('.', '-')}`}
|
||||
onClick={() => navigateToFileEdit(processModelFile)}
|
||||
/>
|
||||
<Can I="GET" a={targetUris.processModelFileCreatePath} ability={ability}>
|
||||
<Button
|
||||
kind="ghost"
|
||||
renderIcon={Edit}
|
||||
iconDescription="Edit File"
|
||||
hasIconOnly
|
||||
size="lg"
|
||||
data-qa={`edit-file-${processModelFile.name.replace('.', '-')}`}
|
||||
onClick={() => navigateToFileEdit(processModelFile)}
|
||||
/>
|
||||
</Can>
|
||||
);
|
||||
elements.push(
|
||||
<Button
|
||||
kind="ghost"
|
||||
renderIcon={Download}
|
||||
iconDescription="Download File"
|
||||
hasIconOnly
|
||||
size="lg"
|
||||
onClick={() => downloadFile(processModelFile.name)}
|
||||
/>
|
||||
<Can I="GET" a={targetUris.processModelFileCreatePath} ability={ability}>
|
||||
<Button
|
||||
kind="ghost"
|
||||
renderIcon={Download}
|
||||
iconDescription="Download File"
|
||||
hasIconOnly
|
||||
size="lg"
|
||||
onClick={() => downloadFile(processModelFile.name)}
|
||||
/>
|
||||
</Can>
|
||||
);
|
||||
|
||||
elements.push(
|
||||
<ButtonWithConfirmation
|
||||
kind="ghost"
|
||||
renderIcon={TrashCan}
|
||||
iconDescription="Delete File"
|
||||
hasIconOnly
|
||||
description={`Delete file: ${processModelFile.name}`}
|
||||
onConfirmation={() => {
|
||||
onDeleteFile(processModelFile.name);
|
||||
}}
|
||||
confirmButtonLabel="Delete"
|
||||
/>
|
||||
<Can
|
||||
I="DELETE"
|
||||
a={targetUris.processModelFileCreatePath}
|
||||
ability={ability}
|
||||
>
|
||||
<ButtonWithConfirmation
|
||||
kind="ghost"
|
||||
renderIcon={TrashCan}
|
||||
iconDescription="Delete File"
|
||||
hasIconOnly
|
||||
description={`Delete file: ${processModelFile.name}`}
|
||||
onConfirmation={() => {
|
||||
onDeleteFile(processModelFile.name);
|
||||
}}
|
||||
confirmButtonLabel="Delete"
|
||||
/>
|
||||
</Can>
|
||||
);
|
||||
if (processModelFile.name.match(/\.bpmn$/) && !isPrimaryBpmnFile) {
|
||||
elements.push(
|
||||
<Button
|
||||
kind="ghost"
|
||||
renderIcon={Favorite}
|
||||
iconDescription="Set As Primary File"
|
||||
hasIconOnly
|
||||
size="lg"
|
||||
onClick={() => onSetPrimaryFile(processModelFile.name)}
|
||||
/>
|
||||
<Can I="PUT" a={targetUris.processModelShowPath} ability={ability}>
|
||||
<Button
|
||||
kind="ghost"
|
||||
renderIcon={Favorite}
|
||||
iconDescription="Set As Primary File"
|
||||
hasIconOnly
|
||||
size="lg"
|
||||
onClick={() => onSetPrimaryFile(processModelFile.name)}
|
||||
/>
|
||||
</Can>
|
||||
);
|
||||
}
|
||||
return elements;
|
||||
|
@ -329,7 +348,11 @@ export default function ProcessModelShow() {
|
|||
let fileLink = null;
|
||||
const fileUrl = profileModelFileEditUrl(processModelFile);
|
||||
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 = (
|
||||
<TableRow key={processModelFile.name}>
|
||||
|
@ -343,7 +366,6 @@ export default function ProcessModelShow() {
|
|||
return constructedTag;
|
||||
});
|
||||
|
||||
// return <ul>{tags}</ul>;
|
||||
const headers = ['Name', 'Actions'];
|
||||
return (
|
||||
<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 = () => {
|
||||
setShowFileUploadModal(false);
|
||||
setFilesToUpload(null);
|
||||
};
|
||||
|
||||
const handleFileUpload = (event: any) => {
|
||||
|
@ -401,6 +403,7 @@ export default function ProcessModelShow() {
|
|||
});
|
||||
}
|
||||
setShowFileUploadModal(false);
|
||||
setFilesToUpload(null);
|
||||
};
|
||||
|
||||
const fileUploadModal = () => {
|
||||
|
@ -428,6 +431,7 @@ export default function ProcessModelShow() {
|
|||
iconDescription="Delete file"
|
||||
name=""
|
||||
multiple={false}
|
||||
onDelete={() => setFilesToUpload(null)}
|
||||
onChange={(event: any) => setFilesToUpload(event.target.files)}
|
||||
/>
|
||||
</Modal>
|
||||
|
@ -439,10 +443,11 @@ export default function ProcessModelShow() {
|
|||
return null;
|
||||
}
|
||||
return (
|
||||
<Grid fullWidth>
|
||||
<Grid condensed fullWidth>
|
||||
<Column md={5} lg={9} sm={3}>
|
||||
<Accordion align="end">
|
||||
<Accordion align="end" open>
|
||||
<AccordionItem
|
||||
open
|
||||
data-qa="files-accordion"
|
||||
title={
|
||||
<Stack orientation="horizontal">
|
||||
|
@ -454,47 +459,53 @@ export default function ProcessModelShow() {
|
|||
</Stack>
|
||||
}
|
||||
>
|
||||
<ButtonSet>
|
||||
<Button
|
||||
renderIcon={Upload}
|
||||
data-qa="upload-file-button"
|
||||
onClick={() => setShowFileUploadModal(true)}
|
||||
size="sm"
|
||||
kind=""
|
||||
className="button-white-background"
|
||||
>
|
||||
Upload File
|
||||
</Button>
|
||||
<Button
|
||||
renderIcon={Add}
|
||||
href={`/admin/process-models/${modifiedProcessModelId}/files?file_type=bpmn`}
|
||||
size="sm"
|
||||
>
|
||||
New BPMN File
|
||||
</Button>
|
||||
<Button
|
||||
renderIcon={Add}
|
||||
href={`/admin/process-models/${modifiedProcessModelId}/files?file_type=dmn`}
|
||||
size="sm"
|
||||
>
|
||||
New DMN File
|
||||
</Button>
|
||||
<Button
|
||||
renderIcon={Add}
|
||||
href={`/admin/process-models/${modifiedProcessModelId}/form?file_ext=json`}
|
||||
size="sm"
|
||||
>
|
||||
New JSON File
|
||||
</Button>
|
||||
<Button
|
||||
renderIcon={Add}
|
||||
href={`/admin/process-models/${modifiedProcessModelId}/form?file_ext=md`}
|
||||
size="sm"
|
||||
>
|
||||
New Markdown File
|
||||
</Button>
|
||||
</ButtonSet>
|
||||
<br />
|
||||
<Can
|
||||
I="POST"
|
||||
a={targetUris.processModelFileCreatePath}
|
||||
ability={ability}
|
||||
>
|
||||
<ButtonSet>
|
||||
<Button
|
||||
renderIcon={Upload}
|
||||
data-qa="upload-file-button"
|
||||
onClick={() => setShowFileUploadModal(true)}
|
||||
size="sm"
|
||||
kind=""
|
||||
className="button-white-background"
|
||||
>
|
||||
Upload File
|
||||
</Button>
|
||||
<Button
|
||||
renderIcon={Add}
|
||||
href={`/admin/process-models/${modifiedProcessModelId}/files?file_type=bpmn`}
|
||||
size="sm"
|
||||
>
|
||||
New BPMN File
|
||||
</Button>
|
||||
<Button
|
||||
renderIcon={Add}
|
||||
href={`/admin/process-models/${modifiedProcessModelId}/files?file_type=dmn`}
|
||||
size="sm"
|
||||
>
|
||||
New DMN File
|
||||
</Button>
|
||||
<Button
|
||||
renderIcon={Add}
|
||||
href={`/admin/process-models/${modifiedProcessModelId}/form?file_ext=json`}
|
||||
size="sm"
|
||||
>
|
||||
New JSON File
|
||||
</Button>
|
||||
<Button
|
||||
renderIcon={Add}
|
||||
href={`/admin/process-models/${modifiedProcessModelId}/form?file_ext=md`}
|
||||
size="sm"
|
||||
>
|
||||
New Markdown File
|
||||
</Button>
|
||||
</ButtonSet>
|
||||
<br />
|
||||
</Can>
|
||||
{processModelFileList()}
|
||||
</AccordionItem>
|
||||
</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>
|
||||
<Stack orientation="horizontal" gap={3}>
|
||||
<Button onClick={processInstanceCreateAndRun} variant="primary">
|
||||
Run
|
||||
</Button>
|
||||
<Button
|
||||
href={`/admin/process-models/${modifiedProcessModelId}/edit`}
|
||||
variant="secondary"
|
||||
<Can
|
||||
I="POST"
|
||||
a={targetUris.processInstanceActionPath}
|
||||
ability={ability}
|
||||
>
|
||||
Edit process model
|
||||
</Button>
|
||||
<ProcessInstanceRun
|
||||
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>
|
||||
<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()}
|
||||
<br />
|
||||
<br />
|
||||
<h3>Process Instances</h3>
|
||||
{processInstancesUl()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -132,6 +132,7 @@ export default function ReactFormEditor() {
|
|||
secondaryButtonText="Cancel"
|
||||
onSecondarySubmit={handleFileNameCancel}
|
||||
onRequestSubmit={handleFileNameSave}
|
||||
onRequestClose={handleFileNameCancel}
|
||||
>
|
||||
<label>File Name:</label>
|
||||
<span>
|
||||
|
@ -174,6 +175,19 @@ export default function ReactFormEditor() {
|
|||
<Button onClick={saveFile} variant="danger" data-qa="file-save-button">
|
||||
Save
|
||||
</Button>
|
||||
{params.file_name ? null : (
|
||||
<Button
|
||||
onClick={() =>
|
||||
navigate(
|
||||
`/admin/process-models/${params.process_model_id}/form-builder`
|
||||
)
|
||||
}
|
||||
variant="danger"
|
||||
data-qa="form-builder-button"
|
||||
>
|
||||
Form Builder
|
||||
</Button>
|
||||
)}
|
||||
{params.file_name ? (
|
||||
<ButtonWithConfirmation
|
||||
description={`Delete file ${params.file_name}?`}
|
||||
|
|
|
@ -42,12 +42,12 @@ export default function SecretList() {
|
|||
return (
|
||||
<tr key={(row as any).key}>
|
||||
<td>
|
||||
<Link to={`/admin/secrets/${(row as any).key}`}>
|
||||
<Link to={`/admin/configuration/secrets/${(row as any).key}`}>
|
||||
{(row as any).id}
|
||||
</Link>
|
||||
</td>
|
||||
<td>
|
||||
<Link to={`/admin/secrets/${(row as any).key}`}>
|
||||
<Link to={`/admin/configuration/secrets/${(row as any).key}`}>
|
||||
{(row as any).key}
|
||||
</Link>
|
||||
</td>
|
||||
|
@ -83,7 +83,6 @@ export default function SecretList() {
|
|||
perPage={perPage}
|
||||
pagination={pagination as any}
|
||||
tableToDisplay={buildTable()}
|
||||
path="/admin/secrets"
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
|
@ -97,7 +96,7 @@ export default function SecretList() {
|
|||
<div>
|
||||
<h1>Secrets</h1>
|
||||
{SecretsDisplayArea()}
|
||||
<Button href="/admin/secrets/new">Add a secret</Button>
|
||||
<Button href="/admin/configuration/secrets/new">Add a secret</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -12,11 +12,11 @@ export default function SecretNew() {
|
|||
const navigate = useNavigate();
|
||||
|
||||
const navigateToSecret = (_result: any) => {
|
||||
navigate(`/admin/secrets/${key}`);
|
||||
navigate(`/admin/configuration/secrets/${key}`);
|
||||
};
|
||||
|
||||
const navigateToSecrets = () => {
|
||||
navigate(`/admin/secrets`);
|
||||
navigate(`/admin/configuration/secrets`);
|
||||
};
|
||||
|
||||
const changeSpacesToDash = (someString: string) => {
|
||||
|
|
|
@ -26,10 +26,6 @@ export default function SecretShow() {
|
|||
}
|
||||
};
|
||||
|
||||
// const reloadSecret = (_result: any) => {
|
||||
// window.location.reload();
|
||||
// };
|
||||
|
||||
const updateSecretValue = () => {
|
||||
if (secret && secretValue) {
|
||||
secret.value = secretValue;
|
||||
|
@ -48,7 +44,7 @@ export default function SecretShow() {
|
|||
};
|
||||
|
||||
const navigateToSecrets = (_result: any) => {
|
||||
navigate(`/admin/secrets`);
|
||||
navigate(`/admin/configuration/secrets`);
|
||||
};
|
||||
|
||||
const deleteSecret = () => {
|
||||
|
|
|
@ -1,13 +1,30 @@
|
|||
import { useContext, useEffect, useState } from 'react';
|
||||
import { Link, useNavigate, useParams } from 'react-router-dom';
|
||||
import Form from '@rjsf/core';
|
||||
// @ts-ignore
|
||||
import { Button, Stack } from '@carbon/react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
|
||||
// FIXME: npm install @rjsf/validator-ajv8 and use it as soon as
|
||||
// rawErrors is fixed.
|
||||
// https://react-jsonschema-form.readthedocs.io/en/latest/usage/validation/
|
||||
// https://github.com/rjsf-team/react-jsonschema-form/issues/2309 links to a codesandbox that might be useful to fork
|
||||
// if we wanted to file a defect against rjsf to show the difference between validator-ajv6 and validator-ajv8.
|
||||
// https://github.com/rjsf-team/react-jsonschema-form/blob/main/docs/api-reference/uiSchema.md talks about rawErrors
|
||||
import validator from '@rjsf/validator-ajv6';
|
||||
|
||||
import {
|
||||
TabList,
|
||||
Tab,
|
||||
Tabs,
|
||||
Grid,
|
||||
Column,
|
||||
// @ts-ignore
|
||||
} from '@carbon/react';
|
||||
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
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 ErrorContext from '../contexts/ErrorContext';
|
||||
import { modifyProcessModelPath } from '../helpers';
|
||||
|
||||
export default function TaskShow() {
|
||||
const [task, setTask] = useState(null);
|
||||
|
@ -18,16 +35,22 @@ export default function TaskShow() {
|
|||
const setErrorMessage = (useContext as any)(ErrorContext)[1];
|
||||
|
||||
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({
|
||||
path: `/tasks/${params.process_instance_id}/${params.task_id}`,
|
||||
successCallback: setTask,
|
||||
successCallback: processResult,
|
||||
// This causes the page to continuously reload
|
||||
// failureCallback: setErrorMessage,
|
||||
});
|
||||
HttpService.makeCallToBackend({
|
||||
path: `/process-instance/${params.process_instance_id}/tasks`,
|
||||
successCallback: setUserTasks,
|
||||
});
|
||||
}, [params]);
|
||||
|
||||
const processSubmitResult = (result: any) => {
|
||||
|
@ -56,39 +79,52 @@ export default function TaskShow() {
|
|||
|
||||
const buildTaskNavigation = () => {
|
||||
let userTasksElement;
|
||||
let selectedTabIndex = 0;
|
||||
if (userTasks) {
|
||||
userTasksElement = (userTasks as any).map(function getUserTasksElement(
|
||||
userTask: any
|
||||
userTask: any,
|
||||
index: number
|
||||
) {
|
||||
const taskUrl = `/tasks/${params.process_instance_id}/${userTask.id}`;
|
||||
if (userTask.id === params.task_id) {
|
||||
return <span>{userTask.name}</span>;
|
||||
selectedTabIndex = index;
|
||||
return <Tab selected>{userTask.title}</Tab>;
|
||||
}
|
||||
if (userTask.state === 'COMPLETED') {
|
||||
return (
|
||||
<Link to={taskUrl} data-qa={`form-nav-${userTask.name}`}>
|
||||
{userTask.name}
|
||||
</Link>
|
||||
<Tab
|
||||
onClick={() => navigate(taskUrl)}
|
||||
data-qa={`form-nav-${userTask.name}`}
|
||||
>
|
||||
{userTask.title}
|
||||
</Tab>
|
||||
);
|
||||
}
|
||||
if (userTask.state === 'FUTURE') {
|
||||
return <span style={{ color: 'red' }}>{userTask.name}</span>;
|
||||
return <Tab disabled>{userTask.title}</Tab>;
|
||||
}
|
||||
if (userTask.state === 'READY') {
|
||||
return (
|
||||
<Link to={taskUrl} data-qa={`form-nav-${userTask.name}`}>
|
||||
{userTask.name} - Current
|
||||
</Link>
|
||||
<Tab
|
||||
onClick={() => navigate(taskUrl)}
|
||||
data-qa={`form-nav-${userTask.name}`}
|
||||
>
|
||||
{userTask.title}
|
||||
</Tab>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
return (
|
||||
<Stack orientation="horizontal" gap={3}>
|
||||
<Button href="/tasks">Go Back To List</Button>
|
||||
{userTasksElement}
|
||||
</Stack>
|
||||
<Tabs
|
||||
title="Steps in this process instance involving people"
|
||||
selectedIndex={selectedTabIndex}
|
||||
>
|
||||
<TabList aria-label="List of tabs" contained>
|
||||
{userTasksElement}
|
||||
</TabList>
|
||||
</Tabs>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -132,14 +168,19 @@ export default function TaskShow() {
|
|||
}
|
||||
|
||||
return (
|
||||
<Form
|
||||
formData={taskData}
|
||||
onSubmit={handleFormSubmit}
|
||||
schema={jsonSchema}
|
||||
uiSchema={formUiSchema}
|
||||
>
|
||||
{reactFragmentToHideSubmitButton}
|
||||
</Form>
|
||||
<Grid fullWidth condensed>
|
||||
<Column md={5} lg={8} sm={4}>
|
||||
<Form
|
||||
formData={taskData}
|
||||
onSubmit={handleFormSubmit}
|
||||
schema={jsonSchema}
|
||||
uiSchema={formUiSchema}
|
||||
validator={validator}
|
||||
>
|
||||
{reactFragmentToHideSubmitButton}
|
||||
</Form>
|
||||
</Column>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -157,7 +198,7 @@ export default function TaskShow() {
|
|||
);
|
||||
};
|
||||
|
||||
if (task) {
|
||||
if (task && userTasks) {
|
||||
const taskToUse = task as any;
|
||||
let statusString = '';
|
||||
if (taskToUse.state !== 'READY') {
|
||||
|
|
|
@ -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;
|
|
@ -0,0 +1,2 @@
|
|||
export { default } from './AddButton';
|
||||
export * from './AddButton';
|
|
@ -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;
|
|
@ -0,0 +1,2 @@
|
|||
export { default } from './ArrayFieldItemTemplate';
|
||||
export * from './ArrayFieldItemTemplate';
|
|
@ -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;
|
|
@ -0,0 +1,2 @@
|
|||
export { default } from './ArrayFieldTemplate';
|
||||
export * from './ArrayFieldTemplate';
|
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
export { default } from './BaseInputTemplate';
|
||||
export * from './BaseInputTemplate';
|
|
@ -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;
|
|
@ -0,0 +1,2 @@
|
|||
export { default } from './CarbonForm';
|
||||
export * from './CarbonForm';
|
|
@ -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;
|
|
@ -0,0 +1,2 @@
|
|||
export { default } from './CheckboxWidget';
|
||||
export * from './CheckboxWidget';
|
|
@ -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;
|
|
@ -0,0 +1,2 @@
|
|||
export { default } from './CheckboxesWidget';
|
||||
export * from './CheckboxesWidget';
|
|
@ -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;
|
|
@ -0,0 +1,2 @@
|
|||
export { default } from './DateTimeWidget';
|
||||
export * from './DateTimeWidget';
|
|
@ -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;
|
|
@ -0,0 +1,2 @@
|
|||
export { default } from './DateWidget';
|
||||
export * from './DateWidget';
|
|
@ -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;
|
|
@ -0,0 +1,2 @@
|
|||
export { default } from './DescriptionField';
|
||||
export * from './DescriptionField';
|
|
@ -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;
|
|
@ -0,0 +1,2 @@
|
|||
export { default } from './ErrorList';
|
||||
export * from './ErrorList';
|
|
@ -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;
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
export { default } from './FieldErrorTemplate';
|
||||
export * from './FieldErrorTemplate';
|
|
@ -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>;
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
export { default } from './FieldHelpTemplate';
|
||||
export * from './FieldHelpTemplate';
|
|
@ -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;
|
|
@ -0,0 +1,2 @@
|
|||
export { default } from './FieldTemplate';
|
||||
export * from './FieldTemplate';
|
|
@ -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'} />
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
export { default } from './IconButton';
|
||||
export * from './IconButton';
|
|
@ -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;
|
|
@ -0,0 +1,2 @@
|
|||
export { default } from './ObjectFieldTemplate';
|
||||
export * from './ObjectFieldTemplate';
|
|
@ -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;
|
|
@ -0,0 +1,2 @@
|
|||
export { default } from './RadioWidget';
|
||||
export * from './RadioWidget';
|
|
@ -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;
|
|
@ -0,0 +1,2 @@
|
|||
export { default } from './RangeWidget';
|
||||
export * from './RangeWidget';
|
|
@ -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;
|
|
@ -0,0 +1,2 @@
|
|||
export { default } from './SelectWidget';
|
||||
export * from './SelectWidget';
|
|
@ -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;
|
|
@ -0,0 +1,2 @@
|
|||
export { default } from './SubmitButton';
|
||||
export * from './SubmitButton';
|
|
@ -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
Loading…
Reference in New Issue