From 30b1fba6ff32e2ad0e8aa36e1a0c611105f29507 Mon Sep 17 00:00:00 2001 From: josephalexantony-aot Date: Wed, 4 Dec 2024 04:02:07 -0800 Subject: [PATCH 1/9] FWF-4012:[Feature] - Migration-UI and API integration --- .../src/apiManager/endpoints/index.js | 1 + .../src/apiManager/services/FormServices.js | 6 + .../src/components/Form/EditForm/FlowEdit.js | 150 +++++++++++++++--- .../src/components/Form/EditForm/FormEdit.js | 73 +++++---- 4 files changed, 177 insertions(+), 53 deletions(-) diff --git a/forms-flow-web/src/apiManager/endpoints/index.js b/forms-flow-web/src/apiManager/endpoints/index.js index bd2a78f75d..e814da4d33 100644 --- a/forms-flow-web/src/apiManager/endpoints/index.js +++ b/forms-flow-web/src/apiManager/endpoints/index.js @@ -33,6 +33,7 @@ const API = { FORM_IMPORT: `${WEB_BASE_URL}/import`, PUBLISH: `${WEB_BASE_URL}/form//publish`, UN_PUBLISH: `${WEB_BASE_URL}/form//unpublish`, + PROCESS_MIGRATE: `${WEB_BASE_URL}/process/migrate`, FORM_HISTORY: `${WEB_BASE_URL}/form/form-history`, LANG_UPDATE: `${WEB_BASE_URL}/user/locale`, FORM_PROCESSES: `${WEB_BASE_URL}/form/formid`, diff --git a/forms-flow-web/src/apiManager/services/FormServices.js b/forms-flow-web/src/apiManager/services/FormServices.js index 3525891acc..f81ed427b5 100644 --- a/forms-flow-web/src/apiManager/services/FormServices.js +++ b/forms-flow-web/src/apiManager/services/FormServices.js @@ -25,6 +25,12 @@ export const formUpdate = (form_id,formData) => { return RequestService.httpPUTRequest(`${API.FORM_DESIGN}/${form_id}`, formData); }; +export const processMigrate = (migrationData) => { + const migrationUrl = replaceUrl(API.PROCESS_MIGRATE); + return RequestService.httpPOSTRequest(migrationUrl, migrationData ); +}; + + export const getFormHistory = (form_id, page = null, limit = null) => { let url = `${API.FORM_HISTORY}/${form_id}`; if (page !== null && limit !== null) { diff --git a/forms-flow-web/src/components/Form/EditForm/FlowEdit.js b/forms-flow-web/src/components/Form/EditForm/FlowEdit.js index 1dc8ff0e8b..253ff6c19b 100644 --- a/forms-flow-web/src/components/Form/EditForm/FlowEdit.js +++ b/forms-flow-web/src/components/Form/EditForm/FlowEdit.js @@ -3,13 +3,15 @@ import React, { useImperativeHandle, useRef, useState, + useEffect } from "react"; import { CustomButton, HistoryIcon, ConfirmModal, HistoryModal, - CurlyBracketsIcon + CurlyBracketsIcon, + StrokeLine } from "@formsflow/components"; import { Card } from "react-bootstrap"; import { useTranslation } from "react-i18next"; @@ -18,6 +20,8 @@ import { useMutation } from "react-query"; import { setProcessData } from "../../../actions/processActions.js"; import BpmnEditor from "../../Modeler/Editors/BpmnEditor/BpmEditor"; import LoadingOverlay from "react-loading-overlay-ts"; +import { push } from "connected-react-router"; +import { processMigrate } from "../../../apiManager/services/FormServices"; import { updateProcess, getProcessHistory, @@ -34,7 +38,9 @@ import userRoles from "../../../constants/permissions.js"; import BPMNViewer from "../../BPMN/BpmnViewer.js"; import TaskVariableModal from "../../Modals/TaskVariableModal.js"; -const FlowEdit = forwardRef(({ isPublished = false, CategoryType, setWorkflowIsChanged }, ref) => { +const FlowEdit = forwardRef(({ isPublished = false, CategoryType, + setWorkflowIsChanged, migration, setMigration, redirectUrl, + isMigrated = true, mapperId }, ref) => { const { t } = useTranslation(); const dispatch = useDispatch(); const bpmnRef = useRef(); @@ -47,7 +53,11 @@ const FlowEdit = forwardRef(({ isPublished = false, CategoryType, setWorkflowIsC const { createDesigns } = userRoles(); const [showTaskVarModal, setShowTaskVarModal] = useState(false); const [isWorkflowChanged, setIsWorkflowChanged] = useState(false); - const formData = useSelector((state) => state.form?.form || {}); + const formData = useSelector((state) => state.form?.form || {}); + const [isMigrationChecked, setIsMigrationChecked] = useState(false); + const [showMigrationModal, setShowMigrationModal] = useState(false); + const computedStyle = getComputedStyle(document.documentElement); + const lightGray = computedStyle.getPropertyValue("--ff-gray-200"); /* --------- fetching all process history when click history button --------- */ const { data: { data: historiesData } = {}, // response data destructured @@ -75,16 +85,48 @@ const FlowEdit = forwardRef(({ isPublished = false, CategoryType, setWorkflowIsC const handleDiscardModal = () => setShowDiscardModal(!showDiscardModal); const handleToggleHistoryModal = () => setShowHistoryModal(!showHistoryModal); - const enableWorkflowChange = ()=>{ + const enableWorkflowChange = () => { setIsWorkflowChanged(true); setWorkflowIsChanged(true); // this function passed from parent }; - const disableWorkflowChange = ()=>{ + const disableWorkflowChange = () => { setIsWorkflowChanged(false); setWorkflowIsChanged(false); // this function passed from parent }; + useEffect(() => { + if (migration) { + setShowMigrationModal(true); + setMigration(false); + } + }, [migration]); + + const handleSaveClick = () => { + if (!isMigrated) { + setShowMigrationModal(true); + } else { + saveFlow(); + } + }; + + const handleMigraion = () => { + const migrationData = { + mapperId: mapperId, + processKey: processData.processKey + }; + processMigrate(migrationData) + .then(() => { + dispatch(push(`${redirectUrl}formflow`)); + }) + .catch((err) => { + console.log(err); + }) + .finally(() => { + setShowMigrationModal(false); + }); + }; + //handle discard changes const handleDiscardConfirm = () => { if (bpmnRef.current) { @@ -144,7 +186,7 @@ const FlowEdit = forwardRef(({ isPublished = false, CategoryType, setWorkflowIsC useImperativeHandle(ref, () => ({ saveFlow, - handleImport: (xml)=>{bpmnRef.current?.handleImport(xml);} + handleImport: (xml) => { bpmnRef.current?.handleImport(xml); } })); const handlePreviewAndVariables = () => { setShowTaskVarModal(true); @@ -153,6 +195,10 @@ const FlowEdit = forwardRef(({ isPublished = false, CategoryType, setWorkflowIsC setShowTaskVarModal(false); }; + const handleCloseMigration = () => { + setShowMigrationModal(false); + }; + return ( <> @@ -205,7 +251,7 @@ const FlowEdit = forwardRef(({ isPublished = false, CategoryType, setWorkflowIsC size="md" className="mx-2" label={t("Save Flow")} - onClick={saveFlow} + onClick={handleSaveClick} disabled={isPublished || !isWorkflowChanged} dataTestid="save-flow-layout" ariaLabel={t("Save Flow Layout")} @@ -231,24 +277,82 @@ const FlowEdit = forwardRef(({ isPublished = false, CategoryType, setWorkflowIsC text={t("Loading...")} >
- {!createDesigns ? ( + {!createDesigns ? ( - ) : ( - - )} -
+ ) : ( + + )} +
+ {showMigrationModal && +
+ {t(`We have switched to a new 1-to-1 relationship structure, + where 1 form contains both the layout (visual of the form) + and the flow (the actions that get executed after the + form's submission). Due to this 1-to-1 relationship, + each layout (previously known as "form") will have a + flow associated with it, so you cannot reuse flows - + one flow cannot be executed by different forms.`)} +
+
+ {t(`This form shares a flow with a few other forms. As this is + not allowed under the new structure, we will permanently + link this flow with this form. For the other forms reusing + this flow, we will automatically duplicate the flow. When + flows are duplicated, their history is not carried over. + You need to pick which form keeps the history and which + forms get duplicates without history.`)} +
+
+ {t(`If this is the form you wish to keep the flow's history with, + confirm below. If this is not the form, then hit cancel, find + the form you want, make a minor change, press "Save Layout" + or "Save Flow," and confirm it there.`)} +
+
+ + +
+ setIsMigrationChecked(prev => !prev)} + checked={isMigrationChecked} + /> + +
+
+ + } + primaryBtnDisable={!isMigrationChecked} + messageSecondary={null} // You can set this to `null` or remove it entirely if unused + primaryBtnAction={handleMigraion} + onClose={handleCloseMigration} + primaryBtnText={t("Link this form that will keep the current flow and its history")} + secondaryBtnText={t("Cancel")} + secondayBtnAction={handleCloseMigration} + size="sm" + />} + - )} + )} ); }); diff --git a/forms-flow-web/src/components/Form/EditForm/FormEdit.js b/forms-flow-web/src/components/Form/EditForm/FormEdit.js index d3c6f8fa3b..ac93012fcc 100644 --- a/forms-flow-web/src/components/Form/EditForm/FormEdit.js +++ b/forms-flow-web/src/components/Form/EditForm/FormEdit.js @@ -119,8 +119,9 @@ const EditComponent = () => { const defaultPrimaryBtnText = "Confirm And Replace"; const [primaryButtonText, setPrimaryButtonText] = useState(defaultPrimaryBtnText); const { createDesigns } = userRoles(); - const [formChangeState, setFormChangeState] = useState({initial:false,changed:false}); + const [formChangeState, setFormChangeState] = useState({ initial: false, changed: false }); const [workflowIsChanged, setWorkflowIsChanged] = useState(false); + const [migration, setMigration] = useState(false); /* --------- validate form title exist or not --------- */ const { @@ -131,13 +132,13 @@ const EditComponent = () => { ({ title }) => validateFormName(title), { - onSuccess:({data}, {createButtonClicked,...variables})=>{ + onSuccess: ({ data }, { createButtonClicked, ...variables }) => { if (data && data.code === "FORM_EXISTS") { setNameError(data.message); // Set exact error message } else { setNameError(""); - if(createButtonClicked){ + if (createButtonClicked) { handlePublishAsNewVersion(variables); } } @@ -172,11 +173,11 @@ const EditComponent = () => { const handleImport = async (fileContent, UploadActionType, - selectedLayoutVersion, selectedFlowVersion) => { + selectedLayoutVersion, selectedFlowVersion) => { if (!isValidUploadActionType(UploadActionType)) return; - + const data = prepareImportData(UploadActionType, selectedLayoutVersion, selectedFlowVersion); - + try { const res = await formImport(fileContent, JSON.stringify(data)); await handleImportResponse(res, fileContent, data.action); @@ -184,7 +185,7 @@ const EditComponent = () => { handleImportError(err); } }; - + // Helper function to validate the action type const isValidUploadActionType = (actionType) => { if (!["validate", "import"].includes(actionType)) { @@ -194,7 +195,7 @@ const EditComponent = () => { } return true; }; - + // Helper function to prepare data for the API request const prepareImportData = (actionType, selectedLayoutVersion, selectedFlowVersion) => { const data = { @@ -241,7 +242,7 @@ const EditComponent = () => { // Handle actions based on extracted form and action type if (formExtracted) { if (action === "validate") { - setFormTitle(formExtracted.forms[0]?.formTitle || ""); + setFormTitle(formExtracted.forms[0]?.formTitle || ""); } else if (responseData.formId) { updateLayout(formExtracted); } @@ -598,7 +599,7 @@ const EditComponent = () => { type: "components", value: _cloneDeep(formData.components), }); - setFormChangeState(prev=>({...prev,changed:false})); + setFormChangeState(prev => ({ ...prev, changed: false })); handleToggleConfirmModal(); }; @@ -649,23 +650,23 @@ const EditComponent = () => { }); }; - const captureFormChanges = ()=>{ + const captureFormChanges = () => { setFormChangeState((prev) => { - let key = null; + let key = null; if (!prev.initial) { key = "initial"; } else if (!prev.changed) { key = "changed"; } - return key ? {...prev, [key]:true} : prev; + return key ? { ...prev, [key]: true } : prev; }); }; - const formChange = (newForm) => { + const formChange = (newForm) => { captureFormChanges(); dispatchFormAction({ type: "formChange", value: newForm }); }; - + const confirmPublishOrUnPublish = async () => { try { @@ -674,7 +675,7 @@ const EditComponent = () => { setIsPublishLoading(true); if (!isPublished) { await flowRef.current.saveFlow(false); - await saveFormData({showToast:false}); + await saveFormData({ showToast: false }); } await actionFunction(processListData.id); if (isPublished) { @@ -918,10 +919,22 @@ const EditComponent = () => { } }; + const handlePublishClick = () => { + if (!processListData.isMigrated) { + if (!isPublished) { + setMigration(true); + } else { + openConfirmModal("unpublish"); + } + } else { + openConfirmModal(isPublished ? "unpublish" : "publish"); + } + }; + return (
- + { size="md" label={t(publishText)} buttonLoading={isPublishLoading} - onClick={() => { - isPublished - ? openConfirmModal("unpublish") - : openConfirmModal("publish"); - }} + onClick={handlePublishClick} dataTestid="handle-publish-testid" ariaLabel={`${t(publishText)} ${t("Button")}`} /> @@ -1073,7 +1082,7 @@ const EditComponent = () => { options={{ language: lang, - alwaysConfirmComponentRemoval:true, + alwaysConfirmComponentRemoval: true, i18n: RESOURCE_BUNDLES_DATA, }} onDeleteComponent={captureFormChanges} @@ -1087,16 +1096,20 @@ const EditComponent = () => { className={`wraper flow-wraper ${isFlowLayout ? "visible" : ""}`} > {/* TBD: Add a loader instead. */} - {isProcessDetailsLoading ? <>loading... : loading... : }
+ } + primaryBtnDisable={!isMigrationChecked} + messageSecondary={null} // You can set this to `null` or remove it entirely if unused + primaryBtnAction={handleMigraion} + onClose={handleCloseMigration} + primaryBtnText={t("Link this form that will keep the current flow and its history")} + secondaryBtnText={t("Cancel")} + secondayBtnAction={handleCloseMigration} + size="sm" + />} + - )} + )} ); }); diff --git a/forms-flow-web/src/components/Form/EditForm/FormEdit.js b/forms-flow-web/src/components/Form/EditForm/FormEdit.js index d3c6f8fa3b..ac93012fcc 100644 --- a/forms-flow-web/src/components/Form/EditForm/FormEdit.js +++ b/forms-flow-web/src/components/Form/EditForm/FormEdit.js @@ -119,8 +119,9 @@ const EditComponent = () => { const defaultPrimaryBtnText = "Confirm And Replace"; const [primaryButtonText, setPrimaryButtonText] = useState(defaultPrimaryBtnText); const { createDesigns } = userRoles(); - const [formChangeState, setFormChangeState] = useState({initial:false,changed:false}); + const [formChangeState, setFormChangeState] = useState({ initial: false, changed: false }); const [workflowIsChanged, setWorkflowIsChanged] = useState(false); + const [migration, setMigration] = useState(false); /* --------- validate form title exist or not --------- */ const { @@ -131,13 +132,13 @@ const EditComponent = () => { ({ title }) => validateFormName(title), { - onSuccess:({data}, {createButtonClicked,...variables})=>{ + onSuccess: ({ data }, { createButtonClicked, ...variables }) => { if (data && data.code === "FORM_EXISTS") { setNameError(data.message); // Set exact error message } else { setNameError(""); - if(createButtonClicked){ + if (createButtonClicked) { handlePublishAsNewVersion(variables); } } @@ -172,11 +173,11 @@ const EditComponent = () => { const handleImport = async (fileContent, UploadActionType, - selectedLayoutVersion, selectedFlowVersion) => { + selectedLayoutVersion, selectedFlowVersion) => { if (!isValidUploadActionType(UploadActionType)) return; - + const data = prepareImportData(UploadActionType, selectedLayoutVersion, selectedFlowVersion); - + try { const res = await formImport(fileContent, JSON.stringify(data)); await handleImportResponse(res, fileContent, data.action); @@ -184,7 +185,7 @@ const EditComponent = () => { handleImportError(err); } }; - + // Helper function to validate the action type const isValidUploadActionType = (actionType) => { if (!["validate", "import"].includes(actionType)) { @@ -194,7 +195,7 @@ const EditComponent = () => { } return true; }; - + // Helper function to prepare data for the API request const prepareImportData = (actionType, selectedLayoutVersion, selectedFlowVersion) => { const data = { @@ -241,7 +242,7 @@ const EditComponent = () => { // Handle actions based on extracted form and action type if (formExtracted) { if (action === "validate") { - setFormTitle(formExtracted.forms[0]?.formTitle || ""); + setFormTitle(formExtracted.forms[0]?.formTitle || ""); } else if (responseData.formId) { updateLayout(formExtracted); } @@ -598,7 +599,7 @@ const EditComponent = () => { type: "components", value: _cloneDeep(formData.components), }); - setFormChangeState(prev=>({...prev,changed:false})); + setFormChangeState(prev => ({ ...prev, changed: false })); handleToggleConfirmModal(); }; @@ -649,23 +650,23 @@ const EditComponent = () => { }); }; - const captureFormChanges = ()=>{ + const captureFormChanges = () => { setFormChangeState((prev) => { - let key = null; + let key = null; if (!prev.initial) { key = "initial"; } else if (!prev.changed) { key = "changed"; } - return key ? {...prev, [key]:true} : prev; + return key ? { ...prev, [key]: true } : prev; }); }; - const formChange = (newForm) => { + const formChange = (newForm) => { captureFormChanges(); dispatchFormAction({ type: "formChange", value: newForm }); }; - + const confirmPublishOrUnPublish = async () => { try { @@ -674,7 +675,7 @@ const EditComponent = () => { setIsPublishLoading(true); if (!isPublished) { await flowRef.current.saveFlow(false); - await saveFormData({showToast:false}); + await saveFormData({ showToast: false }); } await actionFunction(processListData.id); if (isPublished) { @@ -918,10 +919,22 @@ const EditComponent = () => { } }; + const handlePublishClick = () => { + if (!processListData.isMigrated) { + if (!isPublished) { + setMigration(true); + } else { + openConfirmModal("unpublish"); + } + } else { + openConfirmModal(isPublished ? "unpublish" : "publish"); + } + }; + return (
- + { size="md" label={t(publishText)} buttonLoading={isPublishLoading} - onClick={() => { - isPublished - ? openConfirmModal("unpublish") - : openConfirmModal("publish"); - }} + onClick={handlePublishClick} dataTestid="handle-publish-testid" ariaLabel={`${t(publishText)} ${t("Button")}`} /> @@ -1073,7 +1082,7 @@ const EditComponent = () => { options={{ language: lang, - alwaysConfirmComponentRemoval:true, + alwaysConfirmComponentRemoval: true, i18n: RESOURCE_BUNDLES_DATA, }} onDeleteComponent={captureFormChanges} @@ -1087,16 +1096,20 @@ const EditComponent = () => { className={`wraper flow-wraper ${isFlowLayout ? "visible" : ""}`} > {/* TBD: Add a loader instead. */} - {isProcessDetailsLoading ? <>loading... : loading... : }