From 366b36591a4c896099fd007b102da4673b571b91 Mon Sep 17 00:00:00 2001 From: RomanBachaloSigmaSoftware Date: Mon, 28 Jul 2025 14:53:26 +0300 Subject: [PATCH 1/6] use iam-sdk and fix loading issue --- .../components/TriggerForm/TriggerForm.jsx | 10 +-- .../pages/TriggerWorkflow/TriggerWorkflow.jsx | 2 +- server/api/apiFactory.js | 63 ------------------- server/api/index.js | 6 -- server/package-lock.json | 18 ++++++ server/package.json | 1 + server/services/workflowsService.js | 29 +++++---- 7 files changed, 42 insertions(+), 87 deletions(-) delete mode 100644 server/api/apiFactory.js delete mode 100644 server/api/index.js diff --git a/client/src/components/TriggerForm/TriggerForm.jsx b/client/src/components/TriggerForm/TriggerForm.jsx index 2b99559..55989b0 100644 --- a/client/src/components/TriggerForm/TriggerForm.jsx +++ b/client/src/components/TriggerForm/TriggerForm.jsx @@ -49,10 +49,10 @@ const TriggerForm = ({ workflowId, templateType }) => { case "-": try { api.workflows.getWorkflowTriggerRequirements(workflowId).then(data => { - const result = Object.values(data.data.trigger_input_schema) - .filter(entry => entry.field_name !== "startDate") + const result = Object.values(data.data.triggerInputSchema) + .filter(entry => entry.fieldName !== "startDate") .map(entry => ({ - field_name: entry.field_name, + fieldName: entry.fieldName, })); setRelevantFormFields(generateDynamicForm(result, 'Custom')); @@ -66,8 +66,8 @@ const TriggerForm = ({ workflowId, templateType }) => { const generateDynamicForm = (fieldNames) => { return fieldNames.map((field) => ({ - fieldHeader: field.field_name, - fieldName: field.field_name, + fieldHeader: field.fieldName, + fieldName: field.fieldName, value: '', })); }; diff --git a/client/src/pages/TriggerWorkflow/TriggerWorkflow.jsx b/client/src/pages/TriggerWorkflow/TriggerWorkflow.jsx index 34c68c8..1a4ca9d 100644 --- a/client/src/pages/TriggerWorkflow/TriggerWorkflow.jsx +++ b/client/src/pages/TriggerWorkflow/TriggerWorkflow.jsx @@ -24,7 +24,7 @@ const TriggerWorkflow = () => { const getWorkflowDefinitions = async () => { setWorkflowListLoading(true); const definitionsResponse = await api.workflows.getWorkflowDefinitions(); - const workflowDefinitions = definitionsResponse.data.data.workflows.filter(definition => definition.status !== 'inactive') + const workflowDefinitions = definitionsResponse.data.data.filter(definition => definition.status !== 'inactive') .map(definition => { if (workflows.length) { const foundWorkflow = workflows.find(workflow => workflow.id === definition.id); diff --git a/server/api/apiFactory.js b/server/api/apiFactory.js deleted file mode 100644 index 759facd..0000000 --- a/server/api/apiFactory.js +++ /dev/null @@ -1,63 +0,0 @@ -const configureInterceptors = (api, accessToken) => { - // Request interceptor for API calls - api.interceptors.request.use( - async config => { - config.headers = { - Accept: 'application/json', - 'Content-Type': 'application/json', - }; - config.headers.Authorization = `Bearer ${accessToken}`; - return config; - }, - error => { - Promise.reject(error); - } - ); - - api.interceptors.response.use( - response => response, - error => { - // eslint-disable-next-line no-console - console.error(`API call failed. Error: ${error}`); - return Promise.reject(error); - } - ); - return api; -}; - -const createAPI = (axios, accessToken) => { - const api = configureInterceptors( - axios.create({ - withCredentials: false, - }), - accessToken - ); - return api; -}; - -const createMaestroApi = (axios, basePath, accountId, accessToken) => { - const api = createAPI(axios, accessToken); - - const getWorkflowDefinitions = async params => { - const response = await api.get(`${basePath}/accounts/${accountId}/workflows`, { params }); - return response.data; - }; - - const getTriggerRequirements = async workflowId => { - const response = await api.get(`${basePath}/accounts/${accountId}/workflows/${workflowId}/trigger-requirements`); - return response.data; - }; - - const triggerWorkflow = async (args, triggerUrl) => { - const response = await api.post(triggerUrl, args); - return response.data; - }; - - return { - getWorkflowDefinitions, - getTriggerRequirements, - triggerWorkflow, - }; -}; - -module.exports = { createMaestroApi }; diff --git a/server/api/index.js b/server/api/index.js deleted file mode 100644 index 1b6ac16..0000000 --- a/server/api/index.js +++ /dev/null @@ -1,6 +0,0 @@ -const axios = require('axios'); -const { createMaestroApi } = require('./apiFactory'); - -const initMaestroApi = (accountId, basePath, accessToken) => createMaestroApi(axios, basePath, accountId, accessToken); - -module.exports = { initMaestroApi }; diff --git a/server/package-lock.json b/server/package-lock.json index 44ce19b..887722a 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -8,6 +8,7 @@ "name": "server", "version": "1.0.0", "dependencies": { + "@docusign/iam-sdk": "1.0.0-beta.3", "axios": "^1.7.7", "body-parser": "^1.20.3", "chalk": "^4.1.2", @@ -62,6 +63,14 @@ "resolved": "https://registry.npmjs.org/@devhigley/parse-proxy/-/parse-proxy-1.0.3.tgz", "integrity": "sha512-ozRQ9CgWF4JXNNae1zUEpb2fbqH61oxtZz2sdR7a0ci5mi9pSP3EvoU7g4idZYi+CXP32gsvH7kTYZJCGW3DKQ==" }, + "node_modules/@docusign/iam-sdk": { + "version": "1.0.0-beta.3", + "resolved": "https://registry.npmjs.org/@docusign/iam-sdk/-/iam-sdk-1.0.0-beta.3.tgz", + "integrity": "sha512-kK3lftOtfc+smFcmjURy9XHwhFQ1aXFj+hpRnWjLWl5juXi5/J79xILJVasJyX25T4GEvJP1946BgfpllNn9xg==", + "peerDependencies": { + "zod": ">= 3" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz", @@ -4758,6 +4767,15 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.0.10.tgz", + "integrity": "sha512-3vB+UU3/VmLL2lvwcY/4RV2i9z/YU0DTV/tDuYjrwmx5WeJ7hwy+rGEEx8glHp6Yxw7ibRbKSaIFBgReRPe5KA==", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/server/package.json b/server/package.json index c9370dd..f0700fa 100644 --- a/server/package.json +++ b/server/package.json @@ -11,6 +11,7 @@ }, "author": "", "dependencies": { + "@docusign/iam-sdk": "1.0.0-beta.3", "axios": "^1.7.7", "body-parser": "^1.20.3", "chalk": "^4.1.2", diff --git a/server/services/workflowsService.js b/server/services/workflowsService.js index 544823f..d684ef8 100644 --- a/server/services/workflowsService.js +++ b/server/services/workflowsService.js @@ -2,37 +2,42 @@ * @file * This file handles work with docusign maestro and esign services. * Scenarios implemented: - * - Workflow definition creation. - * - Workflow definition publishing. * - Workflow definition triggering, which create workflow instance. - * - Workflow instance cancellation. - * - Workflow instance fetching. + * - Workflow definitions fetching. + * - Workflow trigger requirements fetching. */ -const { initMaestroApi } = require('../api'); +const iam = require('@docusign/iam-sdk'); class WorkflowsService { static getWorkflowDefinitions = async args => { - const api = initMaestroApi(args.accountId, args.basePath, args.accessToken); - const definitions = await api.getWorkflowDefinitions({}); + const client = new iam.IamClient({ accessToken: args.accessToken }); + const definitions = await client.maestro.workflows.getWorkflowsList({ accountId: args.accountId }); return definitions; }; static getWorkflowTriggerRequirements = async args => { - const api = initMaestroApi(args.accountId, args.basePath, args.accessToken); - const triggerRequirements = await api.getTriggerRequirements(args.workflowId); + const client = new iam.IamClient({ accessToken: args.accessToken }); + const triggerRequirements = await client.maestro.workflows.getWorkflowTriggerRequirements({ + accountId: args.accountId, + workflowId: args.workflowId, + }); return triggerRequirements; }; - static triggerWorkflowInstance = async (args, payload, triggerRequirements) => { - const api = initMaestroApi(args.accountId, args.basePath, args.accessToken); + static triggerWorkflowInstance = async (args, payload) => { + const client = new iam.IamClient({ accessToken: args.accessToken }); const triggerPayload = { instance_name: 'test', trigger_inputs: payload, }; - const triggerResponse = await api.triggerWorkflow(triggerPayload, triggerRequirements.trigger_http_config.url); + const triggerResponse = await client.maestro.workflows.triggerWorkflow({ + accountId: args.accountId, + workflowId: args.workflowId, + triggerWorkflow: triggerPayload, + }); return triggerResponse; }; From 7d7d1b2bb4507ae9c1f4d6f22bf85542b49f00cb Mon Sep 17 00:00:00 2001 From: anna Date: Fri, 1 Aug 2025 12:59:16 +0200 Subject: [PATCH 2/6] added back home page --- client/src/App.jsx | 4 +- client/src/assets/img/workflow-manage.svg | 5 ++ client/src/assets/text.json | 21 ++++- client/src/components/Card/Card.jsx | 22 ++++++ client/src/components/Card/Card.module.css | 76 +++++++++++++++++++ client/src/components/LoginForm/LoginForm.jsx | 2 +- .../WorkflowTriggerResult.jsx | 2 +- .../components/TriggerForm/TriggerForm.jsx | 2 +- .../WorkflowDescription.jsx | 6 +- client/src/constants.js | 10 +++ client/src/pages/Hero/Hero.jsx | 4 +- client/src/pages/Home/Home.jsx | 42 ++++++++++ client/src/pages/Home/Home.module.css | 34 +++++++++ .../pages/TriggerWorkflow/TriggerWorkflow.jsx | 5 +- .../TriggerWorkflowForm.jsx | 30 ++++---- 15 files changed, 240 insertions(+), 25 deletions(-) create mode 100644 client/src/assets/img/workflow-manage.svg create mode 100644 client/src/components/Card/Card.jsx create mode 100644 client/src/components/Card/Card.module.css create mode 100644 client/src/pages/Home/Home.jsx create mode 100644 client/src/pages/Home/Home.module.css diff --git a/client/src/App.jsx b/client/src/App.jsx index 5e7dabe..2ad1827 100644 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -6,6 +6,7 @@ import TriggerWorkflowAuthenticated from './pages/TriggerWorkflow/TriggerWorkflo import TriggerWorkflowFormAuthenticated from './pages/TriggerWorkflowForm/TriggerWorkflowForm.jsx'; import { LoginStatus, ROUTE } from './constants.js'; import { api } from './api'; +import HomeAuthenticated from './pages/Home/Home.jsx'; import { authorizeUser, closeLoadingCircleInPopup, @@ -30,7 +31,7 @@ function App() { const { data: userInfo } = await api.acg.callbackExecute(code); dispatch(authorizeUser(LoginStatus.ACG, userInfo.name, userInfo.email)); - navigate(ROUTE.TRIGGER); + navigate(ROUTE.HOME); dispatch(closePopupWindow()); dispatch(closeLoadingCircleInPopup()); }; @@ -41,6 +42,7 @@ function App() { return ( } /> + } /> } /> } /> diff --git a/client/src/assets/img/workflow-manage.svg b/client/src/assets/img/workflow-manage.svg new file mode 100644 index 0000000..d7a2795 --- /dev/null +++ b/client/src/assets/img/workflow-manage.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/client/src/assets/text.json b/client/src/assets/text.json index 8a6bf9f..3dc021b 100644 --- a/client/src/assets/text.json +++ b/client/src/assets/text.json @@ -41,6 +41,16 @@ "field2": "HR manager email" } }, + "home": { + "card1": { + "title": "Trigger a workflow", + "description": "Get a list of workflow definitions and trigger a workflow instance" + }, + "card2": { + "title": "Manage workflows", + "description": "Get the status of existing workflow instances and cancel instances" + } + }, "loader": { "title": "Waiting for log in" }, @@ -62,6 +72,12 @@ "step3Description": "Finally, call the WorkflowTrigger: triggerWorkflow endpoint to trigger the workflow instance. The response of this call includes the workflowInstanceUrl where the workflow participant can complete the workflow steps." } }, + "manageWorkflow": { + "mainDescription": "This scenario displays a list of workflow instances, the status of those instances, and the option to cancel them.", + "step1Description": "The WorkflowInstanceManagement: getWorkflowInstances endpoint is called to get a list of the workflow instances that have been triggered by the sample app.", + "step2Description": "To get the status of a given workflow instance, the WorkflowInstanceManagement: getWorkflowInstance method is called.", + "step3Description": "If a user chooses to cancel a workflow instance, the WorkflowInstanceManagement: cancelWorkflowInstance method is called." + }, "workflowList": { "doNotHaveWorkflow": "You do not have any workflows in your account", "pleaseCreateWorkflow": "Please manually create a workflow in your account before using the sample app.", @@ -93,7 +109,10 @@ "triggerNewWorkflow": "Trigger new workflow ->", "getStarted": "Get started", "moreInfo": "More info", - "backHome": "← Back to workflows" + "backToWorkflows": "← Back to workflows", + "updateWorkflow": "Update workflow status", + "cancelWorkflow": "Cancel workflow", + "backHome": "← Back to home" }, "links": { "github": "https://github.com/docusign/sample-app-workflows-node", diff --git a/client/src/components/Card/Card.jsx b/client/src/components/Card/Card.jsx new file mode 100644 index 0000000..5ce92cd --- /dev/null +++ b/client/src/components/Card/Card.jsx @@ -0,0 +1,22 @@ +import { Link } from 'react-router-dom'; +import styles from './Card.module.css'; +import textContent from '../../assets/text.json'; + +const Card = ({ img, title, description, linkTo }) => { + return ( +
+
+ +

{title}

+
{description}
+ + + +
+
+ ); +}; + +export default Card; \ No newline at end of file diff --git a/client/src/components/Card/Card.module.css b/client/src/components/Card/Card.module.css new file mode 100644 index 0000000..44960a1 --- /dev/null +++ b/client/src/components/Card/Card.module.css @@ -0,0 +1,76 @@ +.cardBody { + display: flex; + height: 254px; + width: 384px; + background-color: var(--grey-light); + border-radius: 8px; + overflow: visible; +} + +.cardBody img { + width: 44px; + height: 44px; +} + +.cardBody h4 { + margin: 8px auto; + color: var(--black-main); + text-align: center; + font-size: 20px; + line-height: 24px; + font-weight: 500; +} + +.cardBody h5 { + margin: 16px auto 0 auto; + min-height: 42px; + color: var(--black-main); + text-align: center; + font-size: 14px; + line-height: 20px; + font-weight: 300; +} + +.cardBody button { + margin-top: 10px; + height: 40px; + max-width: 156px; + background-color: var(--primary-main); + border-radius: 2px; + + color: var(--white-main); + font-size: 16px; + font-weight: 400; + line-height: 24px; +} + +.cardBody button:hover { + background-color: var(--secondary-main); +} + +.cardContainer { + height: 100%; + width: 100%; + padding: 32px; + display: flex; + flex-direction: column; + align-items: center; + overflow: visible; +} + +.buttonGroup { + display: flex; + flex-direction: row; +} + +.moreInfo { + line-height: 10px !important; + background-color: var(--grey-light) !important; + color: var(--black-light) !important; +} + +.moreInfo:hover, +.moreInfo:focus, +.moreInfo:active { + border: none; +} \ No newline at end of file diff --git a/client/src/components/LoginForm/LoginForm.jsx b/client/src/components/LoginForm/LoginForm.jsx index f10cee9..1fb1e91 100644 --- a/client/src/components/LoginForm/LoginForm.jsx +++ b/client/src/components/LoginForm/LoginForm.jsx @@ -26,7 +26,7 @@ const LoginForm = ({ togglePopup }) => { if (authType === LoginStatus.JWT) { const { data: userInfo } = await api.jwt.login(); dispatch(authorizeUser(authType, userInfo.name, userInfo.email)); - navigate(ROUTE.TRIGGER); + navigate(ROUTE.HOME); dispatch(closePopupWindow()); dispatch(closeLoadingCircleInPopup()); } diff --git a/client/src/components/Popups/WorkflowTriggerResult/WorkflowTriggerResult.jsx b/client/src/components/Popups/WorkflowTriggerResult/WorkflowTriggerResult.jsx index 0fb69ba..b174301 100644 --- a/client/src/components/Popups/WorkflowTriggerResult/WorkflowTriggerResult.jsx +++ b/client/src/components/Popups/WorkflowTriggerResult/WorkflowTriggerResult.jsx @@ -13,7 +13,7 @@ const WorkflowTriggerResult = ({ workflowInstanceUrl }) => { const handleFinishTrigger = async () => { dispatch(closePopupWindow()); - navigate(ROUTE.TRIGGER); + navigate(ROUTE.HOME); }; return ( diff --git a/client/src/components/TriggerForm/TriggerForm.jsx b/client/src/components/TriggerForm/TriggerForm.jsx index 55989b0..448e925 100644 --- a/client/src/components/TriggerForm/TriggerForm.jsx +++ b/client/src/components/TriggerForm/TriggerForm.jsx @@ -80,7 +80,7 @@ const TriggerForm = ({ workflowId, templateType }) => { const handleCloseTriggerPopup = () => { dispatch(closePopupWindow()); - navigate(ROUTE.TRIGGER); + navigate(ROUTE.HOME); }; const handleSubmit = async event => { diff --git a/client/src/components/WorkflowDescription/WorkflowDescription.jsx b/client/src/components/WorkflowDescription/WorkflowDescription.jsx index 3d63d91..4e1b4df 100644 --- a/client/src/components/WorkflowDescription/WorkflowDescription.jsx +++ b/client/src/components/WorkflowDescription/WorkflowDescription.jsx @@ -2,15 +2,15 @@ import { Link } from 'react-router-dom'; import styles from './WorkflowDescription.module.css'; import textContent from '../../assets/text.json'; -const WorkflowDescription = ({ title, behindTheScenesComponent, backRoute }) => { +const WorkflowDescription = ({ title, behindTheScenesComponent, backRoute, backText }) => { return (
{backRoute && ( - + )} - +

{title}

)} + + {interactionType === WorkflowItemsInteractionType.MANAGE && ( +
handleFocusDropdown(idx)} + onBlur={() => handleBlurDropdown(idx)} + > + + +
+ )}
))}
- + ); }; diff --git a/client/src/components/WorkflowList/WorkflowList.module.css b/client/src/components/WorkflowList/WorkflowList.module.css index 628bea1..404207a 100644 --- a/client/src/components/WorkflowList/WorkflowList.module.css +++ b/client/src/components/WorkflowList/WorkflowList.module.css @@ -1,5 +1,6 @@ .listGroup { width: 100%; + @media screen and (min-width: 992px) { margin-left: 34px; max-height: 62vh; @@ -56,7 +57,7 @@ border: 1px solid var(--black-extralight); } -.list { +.list2 { max-height: 63.5vh; padding: 1rem 0; -webkit-overflow-scrolling: touch; @@ -69,23 +70,36 @@ background-color: var(--white-main); } -.list > .listRow { +.list3 { + max-height: 63.5vh; + padding: 1rem 0; + -webkit-overflow-scrolling: touch; + display: grid; + grid-template-columns: 1fr 1fr 1fr; + width: 100%; + justify-items: normal; + align-items: center; + gap: 14px; + background-color: var(--white-main); +} + +.list>.listRow { display: contents; } -.list > .listRow > * { +.list>.listRow>* { text-align: center; } -.list > .listRow > *:nth-child(1) { +.list>.listRow>*:nth-child(1) { justify-self: start; } -.list > .listRow > *:nth-child(2) { +.list>.listRow>*:nth-child(2) { justify-self: center; } -.list > .listRow > *:nth-child(3) { +.list>.listRow>*:nth-child(3) { justify-self: end; } @@ -96,17 +110,33 @@ margin-left: 10px; } +.listRow .cell2 { + display: flex; + flex-direction: row; + gap: 16px; + margin-left: 10px; +} + .listRow .cell3 { margin-right: 10px; } .headerRow { - display: flex; flex-direction: row; width: 100%; + display: grid; + grid-template-columns: 1fr 1fr 1fr; + width: 100%; + justify-items: normal; + align-items: center; + gap: 14px; margin-bottom: 0.5rem; } +.headerRow .statusHeader { + justify-self: center; +} + .headerRow div { display: flex; flex-direction: row; @@ -186,6 +216,20 @@ background-color: var(--primary-main); } +.dropdownItemDisabled { + pointer-events: none; + background-color: var(--gray-light) !important; + color: var(--gray-dark); + cursor: not-allowed; + opacity: 0.6; +} + +.dropdownItemDisabled:hover, +.dropdownItemDisabled:active { + background-color: var(--gray-light) !important; + cursor: not-allowed; +} + .emptyListContainer { display: flex; flex-direction: column; @@ -235,4 +279,4 @@ height: 40px; text-align: center; text-decoration: none; -} +} \ No newline at end of file diff --git a/client/src/components/WorkflowStatusPill/WorkflowStatusPill.jsx b/client/src/components/WorkflowStatusPill/WorkflowStatusPill.jsx new file mode 100644 index 0000000..0570a6c --- /dev/null +++ b/client/src/components/WorkflowStatusPill/WorkflowStatusPill.jsx @@ -0,0 +1,11 @@ +import styles from './WorkflowStatusPill.module.css'; + +const WorkflowStatusPill = ({ status }) => { + return ( +
+ {status} +
+ ); +} + +export default WorkflowStatusPill; \ No newline at end of file diff --git a/client/src/components/WorkflowStatusPill/WorkflowStatusPill.module.css b/client/src/components/WorkflowStatusPill/WorkflowStatusPill.module.css new file mode 100644 index 0000000..b95166f --- /dev/null +++ b/client/src/components/WorkflowStatusPill/WorkflowStatusPill.module.css @@ -0,0 +1,18 @@ +.workflowStatusPill { + height: 20px; + width: 80px; + border-radius: 100px; + text-align: center; + font-size: 12px; + font-weight: 400; +} + +.active { + background-color: var(--green-light); + color: var(--green-main); +} + +.paused { + background-color: var(--grey-light); + color: var(--black-main); +} \ No newline at end of file diff --git a/client/src/constants.js b/client/src/constants.js index e5cb29c..7d5fab5 100644 --- a/client/src/constants.js +++ b/client/src/constants.js @@ -17,10 +17,8 @@ export const WorkflowItemsInteractionType = { }; export const WorkflowStatus = { - Failed: 'Failed', - InProgress: 'In Progress', - Completed: 'Completed', - NotRun: 'Not Run', + active: 'active', + paused: 'paused', }; export const WorkflowTriggerResponse = { diff --git a/client/src/pages/ManageWorkflow/ManageWorkflow.jsx b/client/src/pages/ManageWorkflow/ManageWorkflow.jsx new file mode 100644 index 0000000..8e22583 --- /dev/null +++ b/client/src/pages/ManageWorkflow/ManageWorkflow.jsx @@ -0,0 +1,85 @@ +import { useEffect, useState } from 'react'; +import { useLocation } from 'react-router-dom'; +import { useDispatch, useSelector } from 'react-redux'; +import styles from './ManageWorkflow.module.css'; +import Header from '../../components/Header/Header.jsx'; +import Footer from '../../components/Footer/Footer.jsx'; +import textContent from '../../assets/text.json'; +import withAuth from '../../hocs/withAuth/withAuth.jsx'; +import WorkflowList from '../../components/WorkflowList/WorkflowList.jsx'; +import WorkflowDescription from '../../components/WorkflowDescription/WorkflowDescription.jsx'; +import ManageBehindTheScenes from '../../components/WorkflowDescription/BehindTheScenes/ManageBehindTheScenes.jsx'; +import { ROUTE, WorkflowItemsInteractionType, TemplateType, LoginStatus } from '../../constants.js'; +import { api } from '../../api'; +import { updateWorkflowDefinitions } from '../../store/actions'; + +const ManageWorkflow = () => { + const [isWorkflowListLoading, setWorkflowListLoading] = useState(false); + const dispatch = useDispatch(); + const location = useLocation(); + const workflows = useSelector(state => state.workflows.workflows); + const authType = useSelector(state => state.auth.authType); + + useEffect(() => { + const getWorkflowDefinitions = async () => { + setWorkflowListLoading(true); + const definitionsResponse = await api.workflows.getWorkflowDefinitions(); + const workflowDefinitions = definitionsResponse.data.data.filter(definition => definition.status !== 'inactive') + .map(definition => { + if (workflows.length) { + const foundWorkflow = workflows.find(workflow => workflow.id === definition.id); + if (foundWorkflow) return foundWorkflow; + } + + const templateKeys = Object.keys(TemplateType); + const foundKey = templateKeys.find(key => definition.name.startsWith(TemplateType[key].name)); + if (!foundKey) { + if (authType === LoginStatus.JWT) + return null; + + return { + id: definition.id, + name: definition.name, + instanceState: definition.status, + }; + } + + return { + id: definition.id, + name: `${TemplateType[foundKey]?.name}`, + instanceState: definition.status, + }; + }) + .filter(definition => !!definition); + + dispatch(updateWorkflowDefinitions(workflowDefinitions)); + setWorkflowListLoading(false); + }; + + getWorkflowDefinitions(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [dispatch, location.pathname]); + + return ( +
+
+
+ } + backRoute={ROUTE.HOME} + backText={textContent.buttons.backHome} + /> + +
+
+
+ ); +}; + +const ManageWorkflowAuthenticated = withAuth(ManageWorkflow); +export default ManageWorkflowAuthenticated; \ No newline at end of file diff --git a/client/src/pages/ManageWorkflow/ManageWorkflow.module.css b/client/src/pages/ManageWorkflow/ManageWorkflow.module.css new file mode 100644 index 0000000..bda2f13 --- /dev/null +++ b/client/src/pages/ManageWorkflow/ManageWorkflow.module.css @@ -0,0 +1,8 @@ +.contentContainer { + display: flex; + flex-direction: row; + margin-left: 5%; + margin-right: 5%; + width: 90%; + justify-content: space-between; +} \ No newline at end of file From 897ffcbcf836c15e85b1cc1b6e9e70066b88c16a Mon Sep 17 00:00:00 2001 From: anna Date: Tue, 5 Aug 2025 16:21:54 +0200 Subject: [PATCH 5/6] setting up the backend calls --- client/src/components/WorkflowList/WorkflowList.jsx | 11 ++++------- .../WorkflowStatusPill/WorkflowStatusPill.module.css | 4 ++-- server/constants.js | 2 +- 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/client/src/components/WorkflowList/WorkflowList.jsx b/client/src/components/WorkflowList/WorkflowList.jsx index 4395d45..fa48799 100644 --- a/client/src/components/WorkflowList/WorkflowList.jsx +++ b/client/src/components/WorkflowList/WorkflowList.jsx @@ -33,11 +33,10 @@ const WorkflowList = ({ items, interactionType, isLoading }) => { const handlePauseWorkflow = async workflow => { setLoadingWorkflow({ id: workflow.id, isLoading: true }); const { data: workflowInstance } = await api.workflows.pauseWorkflow(workflow); - - if (workflowInstance.instanceState !== workflow.instanceState) { + if (workflowInstance.status !== workflow.instanceState) { const updatedWorkflows = workflows.map(w => { if (w.id !== workflow.id) return { ...w }; - return { ...w, instanceState: workflowInstance.instanceState }; + return { ...w, instanceState: workflowInstance.status }; }); dispatch(updateWorkflowDefinitions(updatedWorkflows)); } @@ -48,14 +47,12 @@ const WorkflowList = ({ items, interactionType, isLoading }) => { const handleResumeWorkflow = async workflow => { setLoadingWorkflow({ id: workflow.id, isLoading: true }); - console.log("Resuming"); const { data: workflowInstance } = await api.workflows.resumeWorkflow(workflow); - console.log(workflowInstance); - if (workflowInstance.instanceState !== workflow.instanceState) { + if (workflowInstance.status !== workflow.instanceState) { const updatedWorkflows = workflows.map(w => { if (w.id !== workflow.id) return { ...w }; - return { ...w, instanceState: workflowInstance.instanceState }; + return { ...w, instanceState: workflowInstance.status }; }); dispatch(updateWorkflowDefinitions(updatedWorkflows)); } diff --git a/client/src/components/WorkflowStatusPill/WorkflowStatusPill.module.css b/client/src/components/WorkflowStatusPill/WorkflowStatusPill.module.css index b95166f..07bae72 100644 --- a/client/src/components/WorkflowStatusPill/WorkflowStatusPill.module.css +++ b/client/src/components/WorkflowStatusPill/WorkflowStatusPill.module.css @@ -13,6 +13,6 @@ } .paused { - background-color: var(--grey-light); - color: var(--black-main); + background-color: var(--red-light); + color: var(--error-main); } \ No newline at end of file diff --git a/server/constants.js b/server/constants.js index f82a176..05986eb 100644 --- a/server/constants.js +++ b/server/constants.js @@ -18,7 +18,7 @@ const ISSUES = { TRIGGER_ISSUE: 'Incompatible workflow', }; -const MAESTRO_SCOPES = ['signature', 'aow_manage', 'impersonation']; +const MAESTRO_SCOPES = ['signature', 'aow_manage']; module.exports = { scopes: MAESTRO_SCOPES, From 47abd9d44dbe571936d6d59f89d568d7272c08ee Mon Sep 17 00:00:00 2001 From: RomanBachaloSigmaSoftware Date: Thu, 7 Aug 2025 14:58:32 +0300 Subject: [PATCH 6/6] add instance list and info parts --- client/src/api/index.js | 4 + client/src/assets/img/dropdown-arrow.svg | 7 ++ client/src/assets/img/instance-canceled.svg | 7 ++ client/src/assets/img/instance-completed.svg | 9 ++ client/src/assets/text.json | 8 ++ .../components/InstanceInfo/InstanceInfo.jsx | 74 ++++++++++++ .../InstanceInfo/InstanceInfo.module.css | 113 ++++++++++++++++++ .../components/InstanceList/InstanceList.jsx | 35 ++++++ .../InstanceList/InstanceList.module.css | 23 ++++ .../components/WorkflowList/WorkflowList.jsx | 28 ++++- .../WorkflowList/WorkflowList.module.css | 14 ++- .../pages/ManageWorkflow/ManageWorkflow.jsx | 20 +++- server/controllers/workflowsController.js | 6 +- 13 files changed, 339 insertions(+), 9 deletions(-) create mode 100644 client/src/assets/img/dropdown-arrow.svg create mode 100644 client/src/assets/img/instance-canceled.svg create mode 100644 client/src/assets/img/instance-completed.svg create mode 100644 client/src/components/InstanceInfo/InstanceInfo.jsx create mode 100644 client/src/components/InstanceInfo/InstanceInfo.module.css create mode 100644 client/src/components/InstanceList/InstanceList.jsx create mode 100644 client/src/components/InstanceList/InstanceList.module.css diff --git a/client/src/api/index.js b/client/src/api/index.js index dff7de9..e84082f 100644 --- a/client/src/api/index.js +++ b/client/src/api/index.js @@ -108,5 +108,9 @@ export const api = Object.freeze({ const response = await instance.get(`/workflows/${workflowId}/instances`); return response; }, + cancelWorkflowInstance: async (workflowId, instanceId) => { + const response = await instance.post(`/workflows/${workflowId}/instances/${instanceId}/cancel`); + return response; + } }, }); diff --git a/client/src/assets/img/dropdown-arrow.svg b/client/src/assets/img/dropdown-arrow.svg new file mode 100644 index 0000000..e824a42 --- /dev/null +++ b/client/src/assets/img/dropdown-arrow.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/client/src/assets/img/instance-canceled.svg b/client/src/assets/img/instance-canceled.svg new file mode 100644 index 0000000..c00ad92 --- /dev/null +++ b/client/src/assets/img/instance-canceled.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/client/src/assets/img/instance-completed.svg b/client/src/assets/img/instance-completed.svg new file mode 100644 index 0000000..cdae130 --- /dev/null +++ b/client/src/assets/img/instance-completed.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/client/src/assets/text.json b/client/src/assets/text.json index d9b89fe..24436cd 100644 --- a/client/src/assets/text.json +++ b/client/src/assets/text.json @@ -88,6 +88,14 @@ "workflowStatus": "Workflow status" } }, + "instanceList": { + "cancelButton": "Cancel", + "columns": { + "instance": "Instance", + "progress": "Progress", + "startedBy": "Started by" + } + }, "pageTitles": { "manageWorkflow": "Manage workflows", "triggerWorkflow": "Trigger a workflow instance", diff --git a/client/src/components/InstanceInfo/InstanceInfo.jsx b/client/src/components/InstanceInfo/InstanceInfo.jsx new file mode 100644 index 0000000..59958bc --- /dev/null +++ b/client/src/components/InstanceInfo/InstanceInfo.jsx @@ -0,0 +1,74 @@ +import styles from './InstanceInfo.module.css'; +import instanceCompletedSvg from '../../assets/img/instance-completed.svg'; +import instanceCanceledSvg from '../../assets/img/instance-canceled.svg'; +import textContent from '../../assets/text.json'; +import { api } from '../../api'; +import { useState } from 'react'; +import StatusLoader from '../StatusLoader/StatusLoader.jsx'; +import { useDispatch, useSelector } from 'react-redux'; +import { updateWorkflowDefinitions } from '../../store/actions/workflows.action.js'; + +const InstanceInfo = ({ workflowId, instance }) => { + const dispatch = useDispatch(); + const workflows = useSelector(state => state.workflows.workflows); + const [isStatusRefreshing, setIsStatusRefreshing] = useState(false); + + const { lastCompletedStep, totalSteps, workflowStatus } = instance; + const progress = Math.min((lastCompletedStep / totalSteps) * 100, 100); + const isFailed = workflowStatus.toLowerCase() === 'failed'; + const isInProgress = workflowStatus.toLowerCase() === 'in progress'; + const isSuccessful = workflowStatus.toLowerCase() === 'completed'; + + const handleCancelInstance = async () => { + setIsStatusRefreshing(true); + await api.workflows.cancelWorkflowInstance(workflowId, instance.id); + + const updatedWorkflows = workflows.map(workflow => { + if (workflow.id !== workflowId) return workflow; + + const newInstanceList = workflow.instances.map(inst => { + if (inst.id !== instance.id) return inst; + + const updatedInstance = { ...inst, workflowStatus: 'Canceled' }; + return updatedInstance; + }) + + const updatedWorkflow = { ...workflow, instances: newInstanceList }; + return updatedWorkflow; + }) + + dispatch(updateWorkflowDefinitions(updatedWorkflows)); + setIsStatusRefreshing(false); + } + + return ( +
+
+

{instance.name}

+
+ +
+ {isStatusRefreshing ? ( + + ) : isInProgress || isFailed ? ( + <> + +
{instance.workflowStatus}
+ + ) : ( +

{workflowStatus}{workflowStatus}

+ )} +
+ +
+

{instance.startedByName}

+
+ +
+ +
+
+ ); +} + +export default InstanceInfo; diff --git a/client/src/components/InstanceInfo/InstanceInfo.module.css b/client/src/components/InstanceInfo/InstanceInfo.module.css new file mode 100644 index 0000000..b12814a --- /dev/null +++ b/client/src/components/InstanceInfo/InstanceInfo.module.css @@ -0,0 +1,113 @@ +.instanceListRow { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 12px; +} + +.cell { + padding: 24px; +} + +.cell p { + text-align: start !important; + width: 110px !important; + padding: 0; + margin: 0; + font-size: 12px; + font-weight: 400; + color: var(--black-extralight); +} + +.cell h4 { + min-width: 145px; + padding: 0; + margin: 0; + font-size: 14px; + font-weight: 400; + color: var(--black-main); + text-align: left; +} + +.cell h5 { + min-width: 145px; + padding: 0; + margin: 0; + font-size: 12px; + font-weight: 400; + color: var(--black-main); + text-align: left; +} + +.cell img { + padding-right: 10px; +} + +.cell button { + padding: 0; + margin: 0; + height: 36px; + width: 150px; + border-radius: 2px; + font-size: 14px; + font-weight: 500; + color: var(--black-main); + background-color: var(--grey-main); + border: 1px solid var(--black-extralight); +} + +progress { + color: green; + height: 9px; + width: 100%; + border-radius: 10px; + margin: -10px 0 10px; + + /* Firefox: Unfilled portion of the progress bar */ + background: rgb(222, 222, 222); +} + +/* Firefox: Filled portion of the progress bar */ +progress::-moz-progress-bar { + background: currentColor; + border-radius: 10px; +} + +/* Chrome & Safari: Unfilled portion of the progress bar */ +progress::-webkit-progress-bar { + background: rgb(223, 223, 223); + border-radius: 10px; +} + +/* Chrome & Safari: Filled portion of the progress bar */ +progress::-webkit-progress-value { + background: currentColor; + border-radius: 10px; +} + +.progressFail { + color: darkred; +} +.progressSuccess { + color: green; +} + +.instanceListRow button:disabled, +.instanceListRow button[disabled] { + pointer-events: none; + background-color: var(--gray-light) !important; + color: var(--gray-dark); + cursor: not-allowed; + opacity: 0.6; +} + +.instanceListRow button { + max-height: 32px; + font-size: 16px; + font-weight: 400; + background-color: var(--white-main); +} + +.instanceListRow button:hover { + cursor: pointer; + background-color: var(--blue-light); +} diff --git a/client/src/components/InstanceList/InstanceList.jsx b/client/src/components/InstanceList/InstanceList.jsx new file mode 100644 index 0000000..a27eece --- /dev/null +++ b/client/src/components/InstanceList/InstanceList.jsx @@ -0,0 +1,35 @@ +import styles from './InstanceList.module.css'; +import textContent from '../../assets/text.json'; +import InstanceInfo from '../InstanceInfo/InstanceInfo' + +const InstanceList = ({ workflowId, items }) => { + if (!items?.length) + return ( +
+
+

{textContent.instanceList.doNotHaveInstance}

+

{textContent.instanceList.pleaseTriggerWorkflow}

+
+
+ ); + + return ( +
+
+
+

{textContent.instanceList.columns.instance}

+

{textContent.instanceList.columns.progress}

+

{textContent.instanceList.columns.startedBy}

+
+ +
+ {items.map((item) => ( + + ))} +
+
+
+ ); +}; + +export default InstanceList; diff --git a/client/src/components/InstanceList/InstanceList.module.css b/client/src/components/InstanceList/InstanceList.module.css new file mode 100644 index 0000000..2c820cb --- /dev/null +++ b/client/src/components/InstanceList/InstanceList.module.css @@ -0,0 +1,23 @@ +.instanceList { + display: flex; + flex-direction: column; + gap: 12px; +} + +.headerRow { + position: sticky; + top: -1rem; + background-color: var(--white-main); + flex-direction: row; + width: 100%; + display: grid; + grid-template-columns: 1fr 1fr 1fr 1fr; + height: 30px; + width: 100%; + justify-items: center; + justify-self: center; + align-items: center; + gap: 8px; + margin-top: 0; + margin-bottom: 0; +} diff --git a/client/src/components/WorkflowList/WorkflowList.jsx b/client/src/components/WorkflowList/WorkflowList.jsx index fa48799..95e56cf 100644 --- a/client/src/components/WorkflowList/WorkflowList.jsx +++ b/client/src/components/WorkflowList/WorkflowList.jsx @@ -7,9 +7,11 @@ import { useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import WorkflowStatusPill from '../WorkflowStatusPill/WorkflowStatusPill.jsx'; import dropdownSvg from '../../assets/img/dropdown.svg'; +import dropdownArrowSvg from '../../assets/img/dropdown-arrow.svg'; import { api } from '../../api'; import { updateWorkflowDefinitions } from '../../store/actions'; import StatusLoader from '../StatusLoader/StatusLoader.jsx'; +import InstanceList from '../InstanceList/InstanceList.jsx'; const WorkflowList = ({ items, interactionType, isLoading }) => { const dispatch = useDispatch(); @@ -17,6 +19,7 @@ const WorkflowList = ({ items, interactionType, isLoading }) => { const workflows = useSelector(state => state.workflows.workflows); const [loadingWorkflow, setLoadingWorkflow] = useState({ id: '', isLoading: false }); const [dropdownOptions, setDropdownOptions] = useState({ id: '', isOpen: false }); + const [openLists, setOpenLists] = useState(new Set()); const handleFocusDropdown = idx => { setTimeout(() => { @@ -30,6 +33,18 @@ const WorkflowList = ({ items, interactionType, isLoading }) => { }, 100); }; + const toggleDropdown = (index) => { + setOpenLists((prev) => { + const updated = new Set(prev); + if (updated.has(index)) { + updated.delete(index); + } else { + updated.add(index); + } + return updated; + }); + }; + const handlePauseWorkflow = async workflow => { setLoadingWorkflow({ id: workflow.id, isLoading: true }); const { data: workflowInstance } = await api.workflows.pauseWorkflow(workflow); @@ -96,7 +111,7 @@ const WorkflowList = ({ items, interactionType, isLoading }) => { {interactionType === WorkflowItemsInteractionType.MANAGE && (
-

{textContent.workflowList.columns.workflowName}

+

{textContent.workflowList.columns.workflowName}

{textContent.workflowList.columns.workflowStatus}

)} @@ -109,7 +124,10 @@ const WorkflowList = ({ items, interactionType, isLoading }) => { }`} style={items.length >= 2 ? listStyles : {}}> {items.map((item, idx) => (
-
+
toggleDropdown(idx)}> + {interactionType === WorkflowItemsInteractionType.MANAGE && ( + + )}

{WorkflowItemsInteractionType.TRIGGER ? item.name : item.instanceName}

@@ -168,6 +186,12 @@ const WorkflowList = ({ items, interactionType, isLoading }) => {
)} + + {interactionType === WorkflowItemsInteractionType.MANAGE && openLists.has(idx) && ( +
+ +
+ )} ))} diff --git a/client/src/components/WorkflowList/WorkflowList.module.css b/client/src/components/WorkflowList/WorkflowList.module.css index 404207a..beafe5d 100644 --- a/client/src/components/WorkflowList/WorkflowList.module.css +++ b/client/src/components/WorkflowList/WorkflowList.module.css @@ -103,13 +103,17 @@ justify-self: end; } -.listRow .cell1 { +.listRow .cell1, .dropdownCell { display: flex; flex-direction: row; gap: 16px; margin-left: 10px; } +.listRow .dropdownCell:hover { + cursor: pointer; +} + .listRow .cell2 { display: flex; flex-direction: row; @@ -121,6 +125,10 @@ margin-right: 10px; } +.listRow .instanceListRow { + grid-column: 1 / -1; +} + .headerRow { flex-direction: row; width: 100%; @@ -133,6 +141,10 @@ margin-bottom: 0.5rem; } +.headerRow .nameHeader { + justify-self: center; +} + .headerRow .statusHeader { justify-self: center; } diff --git a/client/src/pages/ManageWorkflow/ManageWorkflow.jsx b/client/src/pages/ManageWorkflow/ManageWorkflow.jsx index 8e22583..db7aeed 100644 --- a/client/src/pages/ManageWorkflow/ManageWorkflow.jsx +++ b/client/src/pages/ManageWorkflow/ManageWorkflow.jsx @@ -24,13 +24,25 @@ const ManageWorkflow = () => { const getWorkflowDefinitions = async () => { setWorkflowListLoading(true); const definitionsResponse = await api.workflows.getWorkflowDefinitions(); - const workflowDefinitions = definitionsResponse.data.data.filter(definition => definition.status !== 'inactive') - .map(definition => { + const workflowDefinitions = await Promise.all(definitionsResponse.data.data.filter(definition => definition.status !== 'inactive') + .map(async definition => { if (workflows.length) { const foundWorkflow = workflows.find(workflow => workflow.id === definition.id); if (foundWorkflow) return foundWorkflow; } + const instancesResponse = await api.workflows.getWorkflowInstances(definition.id); + const instances = instancesResponse.data.data.map(instance => { + return { + id: instance.id, + name: instance.name, + lastCompletedStep: instance.lastCompletedStep, + totalSteps: instance.totalSteps, + workflowStatus: instance.workflowStatus, + startedByName: instance.startedByName, + }; + }); + const templateKeys = Object.keys(TemplateType); const foundKey = templateKeys.find(key => definition.name.startsWith(TemplateType[key].name)); if (!foundKey) { @@ -41,6 +53,7 @@ const ManageWorkflow = () => { id: definition.id, name: definition.name, instanceState: definition.status, + instances: instances, }; } @@ -48,9 +61,10 @@ const ManageWorkflow = () => { id: definition.id, name: `${TemplateType[foundKey]?.name}`, instanceState: definition.status, + instances: instances, }; }) - .filter(definition => !!definition); + .filter(definition => !!definition)); dispatch(updateWorkflowDefinitions(workflowDefinitions)); setWorkflowListLoading(false); diff --git a/server/controllers/workflowsController.js b/server/controllers/workflowsController.js index 2664434..8f2bf79 100644 --- a/server/controllers/workflowsController.js +++ b/server/controllers/workflowsController.js @@ -137,7 +137,7 @@ class WorkflowsController { accountId: req.session.accountId, }; - const result = await WorkflowsService.pauseWorkflow(args); + const result = await WorkflowsService.cancelWorkflow(args); res.status(200).send(result); } catch (error) { this.handleErrorResponse(error, res); @@ -147,8 +147,8 @@ class WorkflowsController { static handleErrorResponse(error, res) { this.logger.error(`handleErrorResponse: ${error}`); - const errorCode = error?.response?.statusCode; - const errorMessage = error?.response?.body?.message; + const errorCode = error?.response?.statusCode || error?.statusCode; + const errorMessage = error?.response?.body?.message || error?.message || error?.rawMessage; // use custom error message if Maestro is not enabled for the account if (errorCode === 403) {