Skip to content

Conversation

@Josephalexantony-aot
Copy link
Member

@Josephalexantony-aot Josephalexantony-aot commented Nov 26, 2025

User description

📝 Pull Request Summary

Description:

On Saving as new version we were using the old api call
updated the api call for new POST
=> for Save as new version
=> for Publish as new version

Related Jira Ticket:
https://aottech.atlassian.net/browse/FWF-5726

DEPENDENCY PR:

Type of Change:

  • ✨ Feature
  • 🐞 Bug Fix
  • ♻️ Refactor
  • 🧹 Code Cleanup
  • 🧪 Test Addition / Update
  • 🔄 SYNC PR (for syncing different repos)
  • 📦 Dependency / Package Updates
  • 📘 Documentation Update
  • 🚀 Deployment / Config Change / Yaml changes

💻 Frontend Changes

Modules/Components Affected:

Summary of Frontend Changes:

UI/UX Review:

  • Required
  • Not Applicable

Screenshots / Screen Recordings (if applicable):


⚙️ Backend Changes (Java / Python)

Modules/Endpoints Affected:

Summary of Backend Changes:

API Testing:

  • Done
  • Not Applicable

Screenshots / Screen Recordings (if applicable):

✅ Checklist

  • Code builds successfully without lint or type errors
  • Unit tests added or updated [Backend]
  • UI verified [Frontend]
  • Documentation updated (README / Confluence / API Docs)
  • No sensitive information (keys, passwords, secrets) committed
  • I have updated the CHANGELOG with relevant details
  • I have given a clear and meaningful PR title and description as per standard format
  • I have verified cross-repo dependencies (if any)
  • I have confirmed backward compatibility with previous releases

Details:


👥 Reviewer Notes


PR Type

Enhancement


Description

  • Switch to createFormWithWorkflow API

  • Preserve and map workflow/process data

  • Include authorization and anonymity settings

  • Improve navigation and error handling


Diagram Walkthrough

flowchart LR
  A["Form data preparation"] --> B["Build authorizations"]
  B --> C["Assemble mapper/task variables"]
  C --> D["Attach workflow (BPMN/XML) if present"]
  D --> E["POST createFormWithWorkflow"]
  E --> F["Dispatch mapper/process/auth updates"]
  F --> G["Redirect to new form edit"]
Loading

File Walkthrough

Relevant files
Enhancement
FormEdit.js
Integrate new create-with-workflow API and payload             

forms-flow-web/src/routes/Design/Forms/FormEdit.js

  • Replace formCreate with createFormWithWorkflow.
  • Build payload: formData, authorizations, mapper, workflow.
  • Handle anonymous/access roles and task variables.
  • Enhance redirects and error handling with safe checks.
+223/-30

@github-actions
Copy link

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 3 🔵🔵🔵⚪⚪
🧪 No relevant tests
🔒 No security concerns identified
⚡ Recommended focus areas for review

Possible Issue

The new async implementation of handlePublishAsNewVersion sets setFormSubmitted(true) but only resets it in finally. Ensure all early returns (e.g., workflow XML extraction failure path) also reset formSubmitted and any saving flags to avoid leaving the UI in a stuck "submitting" state.

const handlePublishAsNewVersion = async ({ description, title }) => {
  setFormSubmitted(true);
  const newFormData = manipulatingFormData(
    _.cloneDeep(form),
    MULTITENANCY_ENABLED,
    tenantKey,
    formAccessRoles,
    submissionAccessRoles
  );

  const newPathAndName = generateUniqueId("duplicate-version-");
  newFormData.path = newPathAndName;
  newFormData.title = title;
  newFormData.name = newPathAndName;
  newFormData.componentChanged = true;
  delete newFormData.machineName;
  delete newFormData.parentFormId;
  newFormData.newVersion = true;
  newFormData.description = description;
  newFormData.display = formDetails.display;
  newFormData.anonymous = isAnonymous;
  if (isAnonymous) {
    newFormData.access = addAndRemoveAnonymouseId(_cloneDeep(formAccessRoles), "read_all", true);
    newFormData.submissionAccess = addAndRemoveAnonymouseId(_cloneDeep(submissionAccessRoles), "create_own", true);
  } else {
    newFormData.access = formAccessRoles;
    newFormData.submissionAccess = submissionAccessRoles;
  }
  delete newFormData._id;

  const duplicateWorkflow =
    selectedAction === ACTION_OPERATIONS.DUPLICATE
      ? {
          processData: processData?.processData,
          processType: processData?.processType,
        }
      : null;

  try {
    const parentFormId = processListData?.parentFormId || null;
    const taskVariables = Object.values(savedFormVariables).map((variable) => ({
      key: variable.key,
      label: variable.altVariable || variable.labelOfComponent || "",
      type: variable.type || "hidden",
    }));

    const filterAuthorizationData = (authorizationData) => {
      if (authorizationData.selectedOption === "submitter") {
        return { roles: [], userName: null, resourceDetails: { submitter: true } };
      }
      if (authorizationData.selectedOption === "specifiedRoles") {
        return {
          roles: convertMultiSelectOptionToValue(authorizationData.selectedRoles, "role"),
          userName: "",
        };
      }
      return { roles: [], userName: preferred_username };
    };

    const authorizations = {
      application: {
        resourceId: parentFormId,
        resourceDetails: { submitter: false },
        ...filterAuthorizationData(rolesState.APPLICATION),
      },
      designer: {
        resourceId: parentFormId,
        resourceDetails: {},
        ...filterAuthorizationData(rolesState.DESIGN),
      },
      form: {
        resourceId: parentFormId,
        resourceDetails: {},
        roles:
          rolesState.FORM.selectedOption === "specifiedRoles"
            ? convertMultiSelectOptionToValue(rolesState.FORM.selectedRoles, "role")
            : [],
      },
    };

    const payload = {
      formData: newFormData,
      authorizations,
      mapper: {
        formName: title,
        description,
        anonymous: isAnonymous,
        parentFormId,
        formType: processListData?.formType || form?.type,
        taskVariables,
      },
    };

    if (duplicateWorkflow?.processData) {
      payload.processData = duplicateWorkflow.processData;
      payload.processType = duplicateWorkflow.processType || "BPMN";
    }

    const { data } = await createFormWithWorkflow(payload);
    const createdForm = data?.formData || data;

    if (data?.mapper) {
      dispatch(setFormPreviosData(data.mapper));
      dispatch(setFormProcessesData(data.mapper));
    }
    if (data?.process) {
      dispatch(setProcessData(data.process));
    }
    if (data?.authorizations) {
      dispatch(setFormAuthorizationDetails(data.authorizations));
    }

    if (createdForm) {
      dispatch(setFormSuccessData("form", createdForm));
      const newFormId = createdForm?._id;
      if (newFormId) {
        dispatch(push(`${redirectUrl}formflow/${newFormId}/edit`));
      }
    }
  } catch (err) {
    const error = err.response?.data || err.message;
    setNameError(error?.errors?.name?.message);
  } finally {
    setFormSubmitted(false);
  }
};
Data Consistency

Authorization building uses preferred_username fallback; verify this is always defined in context. If not, payload may contain undefined userName. Consider defaulting to empty string or explicit null consistently across branches.

const filterAuthorizationData = (authorizationData) => {
  if (authorizationData.selectedOption === "submitter") {
    return { roles: [], userName: null, resourceDetails: { submitter: true } };
  }
  if (authorizationData.selectedOption === "specifiedRoles") {
    return {
      roles: convertMultiSelectOptionToValue(authorizationData.selectedRoles, "role"),
      userName: "",
    };
  }
  return { roles: [], userName: preferred_username };
};

const authorizations = {
  application: {
    resourceId: parentFormId,
    resourceDetails: { submitter: false },
    ...filterAuthorizationData(rolesState.APPLICATION),
  },
  designer: {
    resourceId: parentFormId,
    resourceDetails: {},
    ...filterAuthorizationData(rolesState.DESIGN),
  },
  form: {
    resourceId: parentFormId,
    resourceDetails: {},
    roles:
      rolesState.FORM.selectedOption === "specifiedRoles"
        ? convertMultiSelectOptionToValue(rolesState.FORM.selectedRoles, "role")
        : [],
  },
};
Error Handling

Name error is set from error?.errors?.name?.message in one catch path but not dispatched to failure state; also other save-as-new-version path dispatches failure but not surface field-level validation. Align error handling and toasts across both flows for consistent UX.

  const createdForm = data?.formData || data;

  if (data?.mapper) {
    dispatch(setFormPreviosData(data.mapper));
    dispatch(setFormProcessesData(data.mapper));
  }
  if (data?.process) {
    dispatch(setProcessData(data.process));
  }
  if (data?.authorizations) {
    dispatch(setFormAuthorizationDetails(data.authorizations));
  }

  if (createdForm) {
    dispatch(setFormSuccessData("form", createdForm));
    const newFormId = createdForm?._id;
    if (newFormId) {
      dispatch(push(`${redirectUrl}formflow/${newFormId}/edit`));
    }
  }
} catch (err) {
  const error = err.response?.data || err.message;
  setNameError(error?.errors?.name?.message);
} finally {
  setFormSubmitted(false);
}

@sonarqubecloud
Copy link

Quality Gate Failed Quality Gate failed

Failed conditions
38.4% Duplication on New Code (required ≤ 3%)

See analysis details on SonarQube Cloud

@github-actions
Copy link

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
Possible issue
Add defensive null checks

Guard against missing or non-object savedFormVariables before Object.values(...) to
prevent runtime crashes. Also, safely access nested error fields when setting
nameError to avoid reading properties of undefined.

forms-flow-web/src/routes/Design/Forms/FormEdit.js [1789-1914]

 const handlePublishAsNewVersion = async ({ description, title }) => {
   setFormSubmitted(true);
   const newFormData = manipulatingFormData(
     _.cloneDeep(form),
     MULTITENANCY_ENABLED,
     tenantKey,
     form._id
   );
   ...
   try {
     const parentFormId = processListData?.parentFormId || null;
-    const taskVariables = Object.values(savedFormVariables).map((variable) => ({
-      key: variable.key,
-      label: variable.altVariable || variable.labelOfComponent || "",
-      type: variable.type || "hidden",
+    const variablesSource = savedFormVariables && typeof savedFormVariables === "object" ? savedFormVariables : {};
+    const taskVariables = Object.values(variablesSource).map((variable) => ({
+      key: variable?.key,
+      label: variable?.altVariable || variable?.labelOfComponent || "",
+      type: variable?.type || "hidden",
     }));
     ...
     const { data } = await createFormWithWorkflow(payload);
     const createdForm = data?.formData || data;
     ...
   } catch (err) {
-    const error = err.response?.data || err.message;
-    setNameError(error?.errors?.name?.message);
+    const error = err?.response?.data || err?.message || {};
+    setNameError(error?.errors?.name?.message || "");
   } finally {
     setFormSubmitted(false);
   }
 };
Suggestion importance[1-10]: 6

__

Why: Guarding savedFormVariables and safer error handling are reasonable stability improvements; the snippet exists within the new async handlePublishAsNewVersion block. Impact is moderate since runtime crashes could occur if unexpected shapes are passed.

Low
Validate access role structures

Ensure formAccessRoles and submissionAccessRoles are valid arrays/objects before
cloning and assignment to avoid runtime errors when undefined. Fallback to empty
arrays to prevent sending undefined fields to the API.

forms-flow-web/src/routes/Design/Forms/FormEdit.js [1808-1816]

+const safeFormAccess = Array.isArray(formAccessRoles) ? formAccessRoles : [];
+const safeSubmissionAccess = Array.isArray(submissionAccessRoles) ? submissionAccessRoles : [];
 if (isAnonymous) {
-  newFormData.access = addAndRemoveAnonymouseId(_cloneDeep(formAccessRoles), "read_all", true);
-  newFormData.submissionAccess = addAndRemoveAnonymouseId(_cloneDeep(submissionAccessRoles), "create_own", true);
+  newFormData.access = addAndRemoveAnonymouseId(_cloneDeep(safeFormAccess), "read_all", true);
+  newFormData.submissionAccess = addAndRemoveAnonymouseId(_cloneDeep(safeSubmissionAccess), "create_own", true);
 } else {
-  newFormData.access = formAccessRoles;
-  newFormData.submissionAccess = submissionAccessRoles;
+  newFormData.access = safeFormAccess;
+  newFormData.submissionAccess = safeSubmissionAccess;
 }
Suggestion importance[1-10]: 5

__

Why: Adding array checks before cloning/assigning access roles can prevent errors when roles are undefined. The code block exists in the new hunk and the change is a safe guard, but not critical.

Low
General
Harden authorization data handling

Add safe checks for authorizationData to prevent crashes when rolesState.* is
undefined. Default selectedRoles to an empty array to avoid passing undefined into
the converter.

forms-flow-web/src/routes/Design/Forms/FormEdit.js [1835-1847]

 const filterAuthorizationData = (authorizationData) => {
-  if (authorizationData.selectedOption === "submitter") {
+  const opt = authorizationData?.selectedOption;
+  if (opt === "submitter") {
     return { roles: [], userName: null, resourceDetails: { submitter: true } };
   }
-  if (authorizationData.selectedOption === "specifiedRoles") {
+  if (opt === "specifiedRoles") {
+    const selectedRoles = Array.isArray(authorizationData?.selectedRoles) ? authorizationData.selectedRoles : [];
     return {
-      roles: convertMultiSelectOptionToValue(authorizationData.selectedRoles, "role"),
+      roles: convertMultiSelectOptionToValue(selectedRoles, "role"),
       userName: "",
     };
   }
   return { roles: [], userName: preferred_username };
 };
Suggestion importance[1-10]: 6

__

Why: Adding null checks to filterAuthorizationData avoids crashes if authorizationData is missing or malformed; it matches the newly added function in the PR. This improves robustness with moderate impact.

Low

type: variable.type || "hidden",
}));

const filterAuthorizationData = (authorizationData) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Josephalexantony-aot seems like this is a duplicate method as identified by sonarcloud

Copy link
Contributor

@arun-s-aot arun-s-aot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Josephalexantony-aot Please check whether the duplicated code can be minimised or added to single function.
Also since this is save and publish functionality, lets test the changes by merging to EE repo feature branch so that it wont affect Nocode flow . Temporarly holding for now.
cc @vinaayakh-aot

@arun-s-aot arun-s-aot marked this pull request as draft December 2, 2025 10:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants