Skip to content

Commit

Permalink
Merge branch 'develop' of https://github.com/fahad-aot/forms-flow-ai
Browse files Browse the repository at this point in the history
…into feature/FWF-3923-Update-taskvariable-with-type
fahad-aot committed Nov 20, 2024
2 parents 37c6c8d + 0061580 commit 6f8f07a
Showing 9 changed files with 572 additions and 651 deletions.
78 changes: 44 additions & 34 deletions forms-flow-web/src/components/Form/EditForm/FormEdit.js
Original file line number Diff line number Diff line change
@@ -16,6 +16,7 @@ import {
PreviewIcon,
FormBuilderModal,
HistoryModal,
ImportModal
} from "@formsflow/components";
import { RESOURCE_BUNDLES_DATA } from "../../../resourceBundles/i18n";
import LoadingOverlay from "react-loading-overlay-ts";
@@ -36,8 +37,7 @@ import {
unPublish,
getFormHistory,
} from "../../../apiManager/services/FormServices";
import ImportModal from "../../Modals/ImportModal.js";
import FileService from "../../../services/FileService";
import FileService from "../../../services/FileService";
import {
setFormFailureErrorData,
setFormSuccessData,
@@ -53,7 +53,7 @@ import {
} from "../../../apiManager/services/processServices";
import {
setProcessData,
} from "../../../actions/processActions.js";
} from "../../../actions/processActions.js";
import _ from "lodash";
import SettingsModal from "../../Modals/SettingsModal";
import FlowEdit from "./FlowEdit.js";
@@ -62,7 +62,7 @@ import NewVersionModal from "../../Modals/NewVersionModal";
import { currentFormReducer } from "../../../modules/formReducer.js";
import { toast } from "react-toastify";
import userRoles from "../../../constants/permissions.js";
import { generateUniqueId, isFormComponentsChanged} from "../../../helper/helper.js";
import { generateUniqueId, isFormComponentsChanged } from "../../../helper/helper.js";
import { useMutation } from "react-query";

// constant values
@@ -117,18 +117,21 @@ const EditComponent = () => {
const [formTitle, setFormTitle] = useState("");
const [importError, setImportError] = useState("");
const [importLoader, setImportLoader] = useState(false);
const defaultPrimaryBtnText = "Confirm And Replace";
const [primaryButtonText, setPrimaryButtonText] = useState(defaultPrimaryBtnText);
const { createDesigns } = userRoles();

/* --------- validate form title exist or not --------- */
const {
/* --------- validate form title exist or not --------- */
const {
mutate: validateFormTitle, // this function will trigger the api call
isLoading: validationLoading,
// isError: error,
} = useMutation(
({ title }) =>
validateFormName(title) ,
validateFormName(title),
{
onSuccess:({data}, {createButtonClicked,...variables})=>{

if (data && data.code === "FORM_EXISTS") {
setNameError(data.message); // Set exact error message
} else {
@@ -138,17 +141,23 @@ const EditComponent = () => {
}
}
},
onError:(error)=>{
onError: (error) => {
const errorMessage = error.response?.data?.message || "An error occurred while validating the form name.";
setNameError(errorMessage); // Set the error message from the server
}
}
);

const UploadActionType = {
IMPORT: "import",
VALIDATE: "validate",
};

useEffect(() => {
if (importError !== "") {
setPrimaryButtonText("Try Again");
}
}, [importError]);

const [fileItems, setFileItems] = useState({
workflow: {
majorVersion: null,
@@ -160,14 +169,9 @@ const EditComponent = () => {
},
});

const handleImport = async (
fileContent,
UploadActionType,
selectedLayoutVersion,
selectedFlowVersion
) => {
setImportLoader(true);

const handleImport = async (fileContent, UploadActionType,
selectedLayoutVersion, selectedFlowVersion) => {
// Validate UploadActionType before proceeding
if (!["validate", "import"].includes(UploadActionType)) {
console.error("Invalid UploadActionType provided");
@@ -181,6 +185,7 @@ const EditComponent = () => {
// Set form submission state for "import" action

if (UploadActionType === "import") {
setImportLoader(true);
setFormSubmitted(true);
// Handle selectedLayoutVersion logic
if (selectedLayoutVersion || selectedFlowVersion) {
@@ -218,16 +223,21 @@ const EditComponent = () => {
});
}
if (data.action === "validate") {
FileService.extractFormDetails(fileContent, (formExtracted) => {
if (formExtracted) {
setFormTitle(formExtracted.formTitle);
} else {
console.log("No valid form found.");
}
});
FileService.extractFileDetails(fileContent)
.then((formExtracted) => {
if (formExtracted) {
setFormTitle(formExtracted.formTitle); // Set the form title if form is found
} else {
console.log("No valid form found.");
}
})
.catch((error) => {
console.error("Error extracting file details:", error); // Catch any errors in the process
});
} else if (responseData?.formId) {
handleCloseSelectedAction();
dispatch(push(`${redirectUrl}formflow/${responseData.formId}/edit/`));
handleCloseSelectedAction();
dispatch(push(`${redirectUrl}formflow/${responseData.formId}/edit/`));

}
} catch (err) {
setImportLoader(false);
@@ -302,6 +312,7 @@ const EditComponent = () => {
},
});
setImportError("");
setPrimaryButtonText(defaultPrimaryBtnText);
}
if (selectedAction === DUPLICATE) {
setNameError("");
@@ -447,7 +458,7 @@ const EditComponent = () => {
submissionAccess: accessDetails.submissionAccess,
access: accessDetails.formAccess,
};

try{
await dispatch(saveFormProcessMapperPut({ mapper, authorizations }));
const updateFormResponse = await formUpdate(form._id, formData);
@@ -545,8 +556,7 @@ const EditComponent = () => {
setNewActionModal(true);
};


const handlePublishAsNewVersion = ({description, title}) => {
const handlePublishAsNewVersion = ({ description, title }) => {
setFormSubmitted(true);
const newFormData = manipulatingFormData(
_.cloneDeep(form),
@@ -1036,27 +1046,27 @@ const EditComponent = () => {
descriptionLabel={t("New Form Description")}
showBuildForm={selectedAction === DUPLICATE}
isLoading={formSubmitted || validationLoading}
onClose={handleCloseSelectedAction}
onClose={handleCloseSelectedAction}
primaryBtnLabel={t("Save and Edit form")}
primaryBtnAction={handlePublishAsNewVersion}
setNameError={setNameError}
nameValidationOnBlur={validateFormNameOnBlur}
nameError={nameError}
/>

<ImportModal
{selectedAction === IMPORT && <ImportModal
importLoader={importLoader}
importError={importError}
importFormModal={selectedAction === IMPORT}
showModal={selectedAction === IMPORT}
uploadActionType={UploadActionType}
formName={formTitle}
formSubmitted={formSubmitted}
onClose={handleCloseSelectedAction}
handleImport={handleImport}
fileItems={fileItems}
headerText="Import File"
primaryButtonText="Confirm And Replace"
/>
primaryButtonText={primaryButtonText}
fileType=".json, .bpmn"
/>}

<ExportModal
showExportModal={selectedAction === EXPORT}
96 changes: 46 additions & 50 deletions forms-flow-web/src/components/Form/List.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import React, { useEffect, useState } from "react";
import { connect, useSelector, useDispatch } from "react-redux";
import CreateFormModal from "../Modals/CreateFormModal.js";
import ImportModal from "../Modals/ImportModal.js";
import { push } from "connected-react-router";
import { toast } from "react-toastify";
import { addTenantkey } from "../../helper/helper";
@@ -31,17 +30,13 @@ import {
import FormTable from "./constants/FormTable";
import ClientTable from "./constants/ClientTable";
import _ from "lodash";
import { CustomButton } from "@formsflow/components";
import _camelCase from "lodash/camelCase";
import { formCreate, formImport,validateFormName } from "../../apiManager/services/FormServices";
import { formCreate, formImport, validateFormName } from "../../apiManager/services/FormServices";
import { setFormSuccessData } from "../../actions/formActions";
import { CustomSearch } from "@formsflow/components";
import userRoles from "../../constants/permissions.js";
import FileService from "../../services/FileService";
import {FormBuilderModal} from "@formsflow/components";
import { FormBuilderModal, ImportModal, CustomSearch, CustomButton } from "@formsflow/components";
import { useMutation } from "react-query";


const List = React.memo((props) => {
const { createDesigns, createSubmissions, viewDesigns } = userRoles();
const { t } = useTranslation();
@@ -72,7 +67,7 @@ const List = React.memo((props) => {

/* --------- validate form title exist or not --------- */
const {
mutate: validateFormTitle, // this function will trigger the api call
mutate: validateFormTitle, // this function will trigger the API call
isLoading: validationLoading,
// isError: error,
} = useMutation(
@@ -81,23 +76,22 @@ const List = React.memo((props) => {
{
onSuccess:({data},
{createButtonClicked,...variables})=>{
if (data && data.code === "FORM_EXISTS") {
setNameError(data.message); // Set exact error message
} else {
setNameError("");
// if the modal clicked createButton need call handleBuild
if(createButtonClicked){
handleBuild(variables);
}
if (data && data.code === "FORM_EXISTS") {
setNameError(data.message); // Set exact error message
} else {
setNameError("");
// if the modal clicked createButton, need to call handleBuild
if (createButtonClicked) {
handleBuild(variables);
}
},
onError:(error)=>{
const errorMessage = error.response?.data?.message || "An error occurred while validating the form name.";
setNameError(errorMessage); // Set the error message from the server
}

}
);
},
onError: (error) => {
const errorMessage = error?.response?.data?.message || "An error occurred while validating the form name.";
setNameError(errorMessage); // Set the error message from the server
},
}
);

useEffect(() => {
setSearch(searchText);
@@ -136,7 +130,6 @@ const List = React.memo((props) => {
const [newFormModal, setNewFormModal] = useState(false);
const [description, setUploadFormDescription] = useState("");
const [formTitle, setFormTitle] = useState("");

useEffect(() => {
dispatch(setFormCheckList([]));
}, [dispatch]);
@@ -175,7 +168,9 @@ const List = React.memo((props) => {
};

const handleImport = async (fileContent, UploadActionType) => {
setImportLoader(true);
if(UploadActionType === "import") {
setImportLoader(true);
}
let data = {};
switch (UploadActionType) {
case "validate":
@@ -203,13 +198,17 @@ const List = React.memo((props) => {
setFormSubmitted(false);

if (data.action == "validate") {
FileService.extractFormDetails(fileContent, (formExtracted) => {
FileService.extractFileDetails(fileContent)
.then((formExtracted) => {
if (formExtracted) {
setFormTitle(formExtracted.formTitle);
setUploadFormDescription(formExtracted.formDescription);
} else {
console.log("No valid form found.");
}
})
.catch((error) => {
console.error("Error extracting form:", error);
});
}
else {
@@ -236,52 +235,50 @@ const List = React.memo((props) => {
searchText,
]);

const validateForm = ({title}) => {
const validateForm = ({ title }) => {
if (!title || title.trim() === "") {
return "This field is required";
return "This field is required";
}
return null;
};

const validateFormNameOnBlur = ({title,...rest}) => {
//the reset variable contain title, description, display also sign for clicked in create button
const error = validateForm({title});

if (error) {
setNameError(error);
return;
}
validateFormTitle({title, ...rest});

};


const handleBuild = ({description, display, title}) => {
const handleBuild = ({ description, display, title }) => {
setFormSubmitted(true);
const error = validateForm({title});
const error = validateForm({ title });
if (error) {
setNameError(error);
return;
}
const name = _camelCase(title);
const newForm = {
const newForm = {
display,
tags: ["common"],
submissionAccess:submissionAccess,
componentChanged:true,
newVersion:true,
access:formAccess,
submissionAccess: submissionAccess,
componentChanged: true,
newVersion: true,
access: formAccess,
title,
name,
description,
path:name.toLowerCase(),
path: name.toLowerCase(),
};

if (MULTITENANCY_ENABLED && tenantKey) {
newForm.tenantKey = tenantKey;
newForm.path = addTenantkey(newForm.path, tenantKey);
newForm.name = addTenantkey(newForm.name, tenantKey);
}

newForm.tenantKey = tenantKey;
newForm.path = addTenantkey(newForm.path, tenantKey);
newForm.name = addTenantkey(newForm.name, tenantKey);
}
formCreate(newForm).then((res) => {
const form = res.data;
dispatch(setFormSuccessData("form", form));
@@ -301,8 +298,6 @@ const List = React.memo((props) => {
setFormSubmitted(false);
});
};


return (
<>
{(forms.isActive || designerFormLoading || isBPMFormListLoading) &&
@@ -353,17 +348,17 @@ const List = React.memo((props) => {
showBuildForm={showBuildForm}
isLoading={formSubmitted || validationLoading}
onClose={onCloseBuildModal}
onAction={handleAction}
onAction={handleAction}
primaryBtnAction={handleBuild}
setNameError={setNameError}
nameValidationOnBlur={validateFormNameOnBlur}
nameError={nameError}
buildForm={true}
buildForm={true}
/>
<ImportModal
{ importFormModal && <ImportModal
importLoader={importLoader}
importError={importError}
importFormModal={importFormModal}
showModal={importFormModal}
uploadActionType={UploadActionType}
formName={formTitle}
formSubmitted={formSubmitted}
@@ -372,7 +367,8 @@ const List = React.memo((props) => {
handleImport={handleImport}
headerText="Import New Form"
primaryButtonText="Confirm and Edit form"
/>
fileType=".json"
/> }
</div>
</div>

1 change: 1 addition & 0 deletions forms-flow-web/src/components/Modals/ActionModal.js
Original file line number Diff line number Diff line change
@@ -120,6 +120,7 @@ const ActionModal = React.memo(

<CustomButton
variant="secondary"
disabled={published}
size="sm"
label="Import"
icon={<ImportIcon />}
262 changes: 0 additions & 262 deletions forms-flow-web/src/components/Modals/ImportModal.js

This file was deleted.

128 changes: 128 additions & 0 deletions forms-flow-web/src/components/Modals/ImportProcess.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import React, { useState, useEffect } from "react";
import PropTypes from "prop-types";
import { ImportModal } from "@formsflow/components";
import FileService from "../../services/FileService";
import { createProcess } from "../../apiManager/services/processServices";
import { useSelector, useDispatch } from "react-redux";
import { push } from "connected-react-router";
import { MULTITENANCY_ENABLED } from "../../constants/constants";

const ImportProcess = React.memo(({
showModal,
closeImport,
processId,
processVersion,
setImportXml,
fileType
}) => {
const tenantKey = useSelector((state) => state.tenants?.tenantId);
const dispatch = useDispatch();

// Determine redirect URL and text based on file type
const redirectUrl = MULTITENANCY_ENABLED ? `/tenant/${tenantKey}/` : "/";
const baseUrl = fileType === ".bpmn" ? "subflow/edit/" : "decision-table/edit/";
const defaultPrimaryBtnText = fileType === ".bpmn" ? "Create And Edit BPMN" : "Create And Edit DMN";
const [importError, setImportError] = useState("");
const [importLoader, setImportLoader] = useState(false);
const [primaryButtonText, setPrimaryButtonText] = useState(defaultPrimaryBtnText);

const getHeaderText = () => {
if (processId) {
return "Import File";
}
return `Import New ${fileType === ".bpmn" ? "BPMN" : "DMN"}`;
};

const headerText = getHeaderText();
const UploadActionType = {
IMPORT: "import",
VALIDATE: "validate"
};

// Update button text based on the import error state
useEffect(() => {
setPrimaryButtonText(importError ? "Try Again" : defaultPrimaryBtnText);
}, [importError]);

// Main import handler function
const handleImport = async (fileContent, actionType) => {
setImportLoader(true);

if (!isValidActionType(actionType)) return setImportLoader(false);

if (actionType === UploadActionType.VALIDATE && !isValidFileType(fileContent)) {
setImportError(`The file format is invalid. Please import a ${fileType} file.`);
return setImportLoader(false);
}

if (actionType === UploadActionType.IMPORT) {
await processImport(fileContent);
}

setImportLoader(false);
};

// Check if the action type is valid
const isValidActionType = (actionType) => ["validate", "import"].includes(actionType);

// Validate file type
const isValidFileType = (file) => file?.name?.endsWith(fileType);

// Handle importing process and dispatching upon success
const processImport = async (fileContent) => {
try {
const extractedXml = await extractFileDetails(fileContent);

if (processId) {
// Update an existing process
setImportXml(extractedXml);
closeImport();
} else {
// Create a new process and redirect
const response = await createProcess({ data: extractedXml, type: fileType === ".bpmn" ? "bpmn" : "dmn" });
if (response) {
dispatch(push(`${redirectUrl}${baseUrl}${response.data.processKey}`));
}
closeImport();
}
} catch (error) {
console.error("Error during import:", error);
setImportError(error?.response?.data?.message || "An error occurred during import.");
}
};

// Extract file details asynchronously
const extractFileDetails = (fileContent) => {
return new Promise((resolve, reject) => {
FileService.extractFileDetails(fileContent, (result) => {
result ? resolve(result) : reject("No valid XML found in the file.");
});
});
};

return (
<ImportModal
showModal={showModal}
importLoader={importLoader}
importError={importError}
uploadActionType={UploadActionType}
onClose={closeImport}
handleImport={handleImport}
fileType={fileType}
headerText={headerText}
primaryButtonText={primaryButtonText}
processVersion={processVersion}
/>
);
});

ImportProcess.propTypes = {
showModal: PropTypes.bool.isRequired,
closeImport: PropTypes.func.isRequired,
processId: PropTypes.string,
processVersion: PropTypes.string,
setImportXml: PropTypes.func.isRequired,
fileType: PropTypes.oneOf([".bpmn", ".dmn"]).isRequired
};

export default ImportProcess;
286 changes: 155 additions & 131 deletions forms-flow-web/src/components/Modeler/DecisionTable.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
import React, { useState, useEffect } from "react";
import { useSelector, useDispatch } from "react-redux";
import { CustomButton,
CustomSearch ,
ReusableProcessTableRow ,
TableFooter,
NoDataFound,
BuildModal} from "@formsflow/components";
import {
CustomButton,
CustomSearch,
ReusableProcessTableRow,
TableFooter,
NoDataFound,
BuildModal
} from "@formsflow/components";
import LoadingOverlay from "react-loading-overlay-ts";
import { useTranslation } from "react-i18next";
import SortableHeader from "../CustomComponents/SortableHeader";
import { fetchAllProcesses } from "../../apiManager/services/processServices";
import { MULTITENANCY_ENABLED } from "../../constants/constants";
import { push } from "connected-react-router";
import {
setDmnSearchText,
setIsPublicDiagram,
} from "../../actions/processActions";
import ImportProcess from "../Modals/ImportProcess";
import { setDmnSearchText, setIsPublicDiagram } from "../../actions/processActions";

const DecisionTable = React.memo(() => {
const dispatch = useDispatch();
@@ -34,38 +34,44 @@ const DecisionTable = React.memo(() => {
modified: { sortOrder: "asc" },
status: { sortOrder: "asc" },
});
const [importDecisionTable, setImportDecisionTable] = useState(false);
const closeDmnImport = () => {
setImportDecisionTable(false);
};
const [searchDmnLoading, setSearchDmnLoading] = useState(false);
const redirectUrl = MULTITENANCY_ENABLED ? `/tenant/${tenantKey}/` : "/";
const [search, setSearch] = useState(searchText || "");
const [showBuildModal, setShowBuildModal] = useState(false);
const handleBuildClick = () => {
dispatch(
push(`${redirectUrl}decision-table/create`));
};
dispatch(push(`${redirectUrl}decision-table/create`));
};
const handleImportClick = () => {
setShowBuildModal(false);
setImportDecisionTable(true);
};
const contents = [
{
id: 1,
heading: "Build",
body: "Create the DMN from scratch",
onClick: handleBuildClick
},
{
id: 2,
heading: "Import",
body: "Upload DMN from a file",
onClick: handleImportClick
}
];


const handleImportClick = () => {
console.log("Import clicked");
};
const contents = [
{
id: 1,
heading: "Build",
body: "Create the DMN from scratch",
onClick: handleBuildClick
},
{
id: 2,
heading: "Import",
body: "Upload DMN from a file",
onClick: handleImportClick
}
];

useEffect(() => {
if (!search?.trim()) {
dispatch(setDmnSearchText(""));
}
}, [search]);


useEffect(() => {
setIsLoading(true);
dispatch(
@@ -86,6 +92,8 @@ const contents = [
)
);
}, [dispatch, activePage, limit, searchText, currentDmnSort]);


const handleSort = (key) => {
setCurrentDmnSort((prevSort) => {
const newSortOrder = prevSort[key].sortOrder === "asc" ? "desc" : "asc";
@@ -134,110 +142,126 @@ const contents = [
const handleBuildModal = () => {
setShowBuildModal(false);
};


return (
<>
<div className="d-md-flex justify-content-between align-items-center pb-3 flex-wrap">
<div className="d-md-flex align-items-center p-0 search-box input-group input-group width-25">
<CustomSearch
search={search}
setSearch={setSearch}
handleSearch={handleSearch}
handleClearSearch={handleClearSearch}
placeholder={t("Search Decision Table")}
searchLoading={searchDmnLoading}
title={t("Search DMN Name")}
dataTestId="DMN-search-input"
/>
<div className="d-md-flex justify-content-between align-items-center pb-3 flex-wrap">
<div className="d-md-flex align-items-center p-0 search-box input-group input-group width-25">
<CustomSearch
search={search}
setSearch={setSearch}
handleSearch={handleSearch}
handleClearSearch={handleClearSearch}
placeholder={t("Search Decision Table")}
searchLoading={searchDmnLoading}
title={t("Search DMN Name")}
dataTestId="DMN-search-input"
/>
</div>
<div className="d-md-flex justify-content-end align-items-center ">
<CustomButton
variant="primary"
size="sm"
label={t("New DMN")}
dataTestid="create-DMN-button"
ariaLabel="Create DMN"
onClick={() => handleCreateDMN()}
/>
</div>
<LoadingOverlay active={isLoading} spinner text={t("Loading...")}>
<div className="min-height-400 pt-3">
<div className="custom-tables-wrapper">
<table className="table custom-tables table-responsive-sm">
<thead className="table-header">
<tr>
<th className="w-25" scope="col">
<SortableHeader
columnKey="name"
title="Name"
currentSort={currentDmnSort}
handleSort={handleSort}
className="ms-4"
/>
</th>
<th className="w-20" scope="col">
<SortableHeader
columnKey="id"
title="ID"
currentSort={currentDmnSort}
handleSort={handleSort}
/>
</th>
<th className="w-15" scope="col">
<SortableHeader
columnKey="modified"
title="Last Edited"
currentSort={currentDmnSort}
handleSort={handleSort}
/>
</th>
<th className="w-15" scope="col">
<SortableHeader
columnKey="status"
title="Status"
currentSort={currentDmnSort}
handleSort={handleSort}
/>
</th>
<th
className="w-25"
colSpan="4"
aria-label="edit bpmn button "
></th>
<th
className="w-25"
colSpan="4"
aria-label="edit bpmn button "
></th>
</tr>
</thead>
{dmn.length ?
<tbody>
{dmn.map((dmnItem) => (
<ReusableProcessTableRow
key={dmnItem.id}
item={dmnItem}
gotoEdit={gotoEdit}
buttonLabel="Dmn"
/>
))}
<TableFooter
limit={limit}
activePage={activePage}
totalCount={totalCount}
handlePageChange={handlePageChange}
onLimitChange={onLimitChange}
pageOptions={pageOptions}
/>
</tbody> : !isLoading ? (
<NoDataFound />
) : null}
</table>
</div>
</div>
</LoadingOverlay>
</div>
<div className="d-md-flex justify-content-end align-items-center ">
<CustomButton
variant="primary"
size="sm"
label={t("New DMN")}
dataTestid="create-DMN-button"
ariaLabel="Create DMN"
onClick={() => handleCreateDMN()}
<BuildModal
show={showBuildModal}
onClose={handleBuildModal}
title={t("New DMN")}
contents={contents}
/>
{importDecisionTable && (
<ImportProcess
showModal={importDecisionTable}
closeImport={closeDmnImport}
fileType=".dmn"
/>
</div>
<LoadingOverlay active={isLoading} spinner text={t("Loading...")}>
<div className="min-height-400 pt-3">
<div className="custom-tables-wrapper">
<table className="table custom-tables table-responsive-sm">
<thead className="table-header">
<tr>
<th className="w-25" scope="col">
<SortableHeader
columnKey="name"
title="Name"
currentSort={currentDmnSort}
handleSort={handleSort}
className="ms-4"
/>
</th>
<th className="w-20" scope="col">
<SortableHeader
columnKey="processKey"
title="ID"
currentSort={currentDmnSort}
handleSort={handleSort}
/>
</th>
<th className="w-15" scope="col">
<SortableHeader
columnKey="modified"
title="Last Edited"
currentSort={currentDmnSort}
handleSort={handleSort}
/>
</th>
<th className="w-15" scope="col">
<SortableHeader
columnKey="status"
title="Status"
currentSort={currentDmnSort}
handleSort={handleSort}
/>
</th>
<th
className="w-25"
colSpan="4"
aria-label="edit bpmn button "
></th>
</tr>
</thead>
{dmn.length ?
<tbody>
{dmn.map((dmnItem) => (
<ReusableProcessTableRow
key={dmnItem.id}
item={dmnItem}
gotoEdit={gotoEdit}
buttonLabel="Dmn"
/>
))}
<TableFooter
limit={limit}
activePage={activePage}
totalCount={totalCount}
handlePageChange={handlePageChange}
onLimitChange={onLimitChange}
pageOptions={pageOptions}
/>
</tbody> : !isLoading ? (
<NoDataFound />
) : null}
</table>
</div>
</div>
</LoadingOverlay>
</div>
<BuildModal
show={showBuildModal}
onClose={handleBuildModal}
title={t(`New DMN`)}
contents={contents}/>
</>
);
)}
</>);
});

DecisionTable.propTypes = {};

export default DecisionTable;
64 changes: 42 additions & 22 deletions forms-flow-web/src/components/Modeler/ProcessCreateEdit.js
Original file line number Diff line number Diff line change
@@ -17,7 +17,7 @@ import { MULTITENANCY_ENABLED } from "../../constants/constants";
import { useTranslation } from "react-i18next";
import {
createNewProcess,
createNewDecision,
createNewDecision,
} from "../../components/Modeler/helpers/helper";
import {
CustomButton,
@@ -47,8 +47,10 @@ import {
} from "../../actions/processActions";
import { useMutation, useQuery } from "react-query";
import LoadingOverlay from "react-loading-overlay-ts";
import ImportProcess from "../Modals/ImportProcess";

const EXPORT = "EXPORT";
const IMPORT = "IMPORT";
const CategoryType = { FORM: "FORM", WORKFLOW: "WORKFLOW" };

const ProcessCreateEdit = ({ type }) => {
@@ -57,19 +59,19 @@ const ProcessCreateEdit = ({ type }) => {
const isBPMN = type === "BPMN";
const Process = isBPMN
? {
name: "Subflow",
type: "BPMN",
route: "subflow",
extension: ".bpmn",
fileType: "text/bpmn",
}
name: "Subflow",
type: "BPMN",
route: "subflow",
extension: ".bpmn",
fileType: "text/bpmn",
}
: {
name: "Decision Table",
type: "DMN",
route: "decision-table",
extension: ".dmn",
fileType: "text/dmn",
};
name: "Decision Table",
type: "DMN",
route: "decision-table",
extension: ".dmn",
fileType: "text/dmn",
};

const diagramType = Process.type;
const dispatch = useDispatch();
@@ -97,7 +99,7 @@ const ProcessCreateEdit = ({ type }) => {
const [isPublished, setIsPublished] = useState(
processData?.status === "Published"
);

const [showConfirmModal, setShowConfirmModal] = useState(false);
const [modalType, setModalType] = useState("");
const [isPublishLoading, setIsPublishLoading] = useState(false);
@@ -259,9 +261,9 @@ const ProcessCreateEdit = ({ type }) => {
return isCreate
? await createProcess(payload)
: await updateProcess({
...payload,
id: processData.id, // ID needed only for update
});
...payload,
id: processData.id, // ID needed only for update
});
};

const handleSaveSuccess = (response, isCreate, isPublished) => {
@@ -374,22 +376,22 @@ const ProcessCreateEdit = ({ type }) => {
const handleExport = async () => {
try {
let data = "";
if(isCreate){
if (isCreate) {
const modeler = getModeler(isBPMN);
data = await createXMLFromModeler(modeler);
}else{
} else {
data = processData?.processData;
}

const isValid = isBPMN
? await validateProcess(data,lintErrors)
? await validateProcess(data, lintErrors)
: await validateDecisionNames(data);

if (isValid) {
const element = document.createElement("a");
const file = new Blob([data], { type: Process.fileType });
element.href = URL.createObjectURL(file);

element.download = fileName;
document.body.appendChild(element);
element.click();
@@ -511,6 +513,12 @@ const ProcessCreateEdit = ({ type }) => {
};

const modalContent = getModalContent();
const handleImportData = (xml) => {
const ref = isBPMN ? bpmnRef : dmnRef;
if (ref.current) {
ref.current?.handleImport(xml);
}
};

return (
<div>
@@ -689,11 +697,23 @@ const ProcessCreateEdit = ({ type }) => {
currentVersionId={processData.id}
disableAllRevertButton={isPublished}
/>
{selectedAction === IMPORT && <ImportProcess
showModal={selectedAction === IMPORT}
closeImport={() => setSelectedAction(null)}
processId={processData.id}
processVersion={{
type: process.type,
majorVersion: processData?.majorVersion,
minorVersion: processData?.minorVersion
}}
setImportXml={handleImportData}
fileType={process.extension}
/>}
</div>
);
};
ProcessCreateEdit.propTypes = {
type: PropTypes.string.isRequired,
};

export default ProcessCreateEdit;
export default ProcessCreateEdit;
229 changes: 107 additions & 122 deletions forms-flow-web/src/components/Modeler/SubFlowTable.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import React, { useEffect, useState } from "react";
import { CustomButton,
CustomSearch ,
TableFooter ,
ReusableProcessTableRow,
NoDataFound,
BuildModal} from "@formsflow/components";
import {
CustomButton,
CustomSearch,
TableFooter,
ReusableProcessTableRow,
NoDataFound,
BuildModal,
} from "@formsflow/components";
import { useSelector, useDispatch } from "react-redux";
import { useTranslation } from "react-i18next";

import { fetchAllProcesses } from "../../apiManager/services/processServices";
import LoadingOverlay from "react-loading-overlay-ts";
import {
@@ -17,19 +18,21 @@ import {
import { push } from "connected-react-router";
import { MULTITENANCY_ENABLED } from "../../constants/constants";
import SortableHeader from "../CustomComponents/SortableHeader";

import ImportProcess from "../Modals/ImportProcess";

const SubFlow = React.memo(() => {
const searchText = useSelector((state) => state.process.bpmnSearchText);
const tenantKey = useSelector((state) => state.tenants?.tenantId);
const redirectUrl = MULTITENANCY_ENABLED ? `/tenant/${tenantKey}/` : "/";
const [limit, setLimit] = useState(5);
const dispatch = useDispatch();
const { t } = useTranslation();
const searchText = useSelector((state) => state.process.bpmnSearchText);
const tenantKey = useSelector((state) => state.tenants?.tenantId);
const processList = useSelector((state) => state.process.processList);
const totalCount = useSelector((state) => state.process.totalBpmnCount);
const [importSubflow, setImportSubflow] = useState(false);

// Local states
const [activePage, setActivePage] = useState(1);
const [isLoading, setIsLoading] = useState(true);
const process = useSelector((state) => state.process.processList);
const totalCount = useSelector((state) => state.process.totalBpmnCount);
const [limit, setLimit] = useState(5);
const [search, setSearch] = useState(searchText || "");
const [searchBpmnLoading, setSearchBpmnLoading] = useState(false);
const [currentBpmnSort, setCurrentBpmnSort] = useState({
@@ -40,33 +43,33 @@ const SubFlow = React.memo(() => {
status: { sortOrder: "asc" },
});
const [showBuildModal, setShowBuildModal] = useState(false);
const handleBuildClick = () => {
dispatch(
push(`${redirectUrl}subflow/create`));
};

const handleImportClick = () => {
console.log("Import clicked");
const redirectUrl = MULTITENANCY_ENABLED ? `/tenant/${tenantKey}/` : "/";

const ShowImportModal = () => {
setShowBuildModal(false);
setImportSubflow(true);
};
const contents = [
{

// Modal contents
const modalContents = [
{
id: 1,
heading: "Build",
heading: "Build",
body: "Create the BPMN from scratch",
onClick: handleBuildClick
onClick: () => dispatch(push(`${redirectUrl}subflow/create`)),
},
{
{
id: 2,
heading: "Import",
heading: "Import",
body: "Upload BPMN from a file",
onClick: handleImportClick
}
onClick: ShowImportModal,
},
];

useEffect(() => {
if (!search?.trim()) {
dispatch(setBpmnSearchText(""));
}
}, [search]);
if (!search.trim()) dispatch(setBpmnSearchText(""));
}, [search, dispatch]);

useEffect(() => {
setIsLoading(true);
dispatch(
@@ -75,7 +78,7 @@ const SubFlow = React.memo(() => {
pageNo: activePage,
tenant_key: tenantKey,
processType: "BPMN",
limit: limit,
limit,
searchKey: search,
sortBy: currentBpmnSort.activeKey,
sortOrder: currentBpmnSort[currentBpmnSort.activeKey].sortOrder,
@@ -86,41 +89,24 @@ const SubFlow = React.memo(() => {
}
)
);
}, [dispatch, activePage, limit, searchText, currentBpmnSort]);
}, [dispatch, activePage, limit, search, tenantKey, currentBpmnSort]);

const handleSort = (key) => {
setCurrentBpmnSort((prevSort) => {
const newSortOrder = prevSort[key].sortOrder === "asc" ? "desc" : "asc";
return {
...prevSort,
activeKey: key,
[key]: { sortOrder: newSortOrder },
};
});
setCurrentBpmnSort((prevConfig) => ({
...prevConfig,
activeKey: key,
[key]: { sortOrder: prevConfig[key].sortOrder === "asc" ? "desc" : "asc" },
}));
};

const pageOptions = [
{
text: "5",
value: 5,
},
{
text: "25",
value: 25,
},
{
text: "50",
value: 50,
},
{
text: "100",
value: 100,
},
{
text: "All",
value: totalCount,
},
{ text: "5", value: 5 },
{ text: "25", value: 25 },
{ text: "50", value: 50 },
{ text: "100", value: 100 },
{ text: "All", value: totalCount },
];

const handlePageChange = (page) => setActivePage(page);
const onLimitChange = (newLimit) => {
setLimit(newLimit);
@@ -131,17 +117,16 @@ const SubFlow = React.memo(() => {
setActivePage(1);
dispatch(setBpmnSearchText(""));
};

const handleSearch = () => {
setSearchBpmnLoading(true);
setActivePage(1);
dispatch(setBpmnSearchText(search));
};

const gotoEdit = (data) => {
if (MULTITENANCY_ENABLED) {
dispatch(setIsPublicDiagram(!!data.tenantId));
}
dispatch(push(`${redirectUrl}subflow/edit/${data.processKey}`)
);
if (MULTITENANCY_ENABLED) dispatch(setIsPublicDiagram(!!data.tenantId));
dispatch(push(`${redirectUrl}subflow/edit/${data.processKey}`));
};

const handleCreateBPMN = () => {
@@ -153,29 +138,30 @@ const SubFlow = React.memo(() => {

return (
<>
<div className="d-md-flex justify-content-between align-items-center pb-3 flex-wrap">
<div className="d-md-flex align-items-center p-0 search-box input-group input-group width-25">
<CustomSearch
search={search}
setSearch={setSearch}
handleSearch={handleSearch}
handleClearSearch={handleClearSearch}
placeholder={t("Search BPMN Name")}
searchLoading={searchBpmnLoading}
title={t("Search BPMN Name")}
dataTestId="BPMN-search-input"
/>
</div>
<div className="d-md-flex justify-content-end align-items-center ">
<CustomButton
variant="primary"
size="sm"
label="New BPMN"
className=""
dataTestid="create-BPMN-button"
ariaLabel="Create BPMN"
onClick={() => handleCreateBPMN()}
/>
<div className="d-md-flex justify-content-between align-items-center pb-3 flex-wrap">
<div className="d-md-flex align-items-center p-0 search-box input-group input-group width-25">
<CustomSearch
search={search}
setSearch={setSearch}
handleSearch={handleSearch}
handleClearSearch={handleClearSearch}
placeholder={t("Search BPMN Name")}
searchLoading={searchBpmnLoading}
title={t("Search BPMN Name")}
dataTestId="BPMN-search-input"
/>
</div>
<div className="d-md-flex justify-content-end align-items-center ">
<CustomButton
variant="primary"
size="sm"
label="New BPMN"
className=""
dataTestid="create-BPMN-button"
ariaLabel="Create BPMN"
onClick={handleCreateBPMN}
/>
</div>
</div>
<LoadingOverlay active={isLoading} spinner text={t("Loading...")}>
<div className="min-height-400 pt-3">
@@ -216,44 +202,43 @@ const SubFlow = React.memo(() => {
handleSort={handleSort}
/>
</th>
<th
className="w-25"
colSpan="4"
aria-label="edit bpmn button "
></th>
<th className="w-25" colSpan="4" aria-label="edit bpmn button "></th>
</tr>
</thead>
{process.length ?
<tbody>
{process.map((processItem) => (
<ReusableProcessTableRow
key={processItem.id}
item={processItem}
gotoEdit={gotoEdit}
buttonLabel="Bpmn"
{processList.length ? (
<tbody>
{processList.map((processItem) => (
<ReusableProcessTableRow
key={processItem.id}
item={processItem}
gotoEdit={gotoEdit}
buttonLabel="Bpmn"
/>
))}
<TableFooter
limit={limit}
activePage={activePage}
totalCount={totalCount}
handlePageChange={handlePageChange}
onLimitChange={onLimitChange}
pageOptions={pageOptions}
/>
))}
<TableFooter
limit={limit}
activePage={activePage}
totalCount={totalCount}
handlePageChange={handlePageChange}
onLimitChange={onLimitChange}
pageOptions={pageOptions}
/>
</tbody> : !isLoading ? (
<NoDataFound />
) : null}
</tbody>
) : !isLoading && <NoDataFound />
}
</table>
</div>
</div>
</LoadingOverlay>
</div>
<BuildModal
show={showBuildModal}
onClose={handleBuildModal}
title={t(`New BPMN`)}
contents={contents}/>
<BuildModal
show={showBuildModal}
onClose={handleBuildModal}
title={t(`New BPMN`)}
contents={modalContents}
/>
{importSubflow && (
<ImportProcess showModal={importSubflow} closeImport={() => setImportSubflow(false)} fileType=".bpmn" />
)}
</>
);
});
79 changes: 49 additions & 30 deletions forms-flow-web/src/services/FileService.js
Original file line number Diff line number Diff line change
@@ -33,46 +33,65 @@ const uploadFile = (evt, callback) => {
}
};

const extractFormDetails = (fileContent, callback) => {
const fileObj = fileContent; // The file object passed directly
if (fileObj) {
const reader = new FileReader(); // Initialize FileReader to read the file
reader.readAsText(fileObj); // Read the file as text
const extractFileDetails = (fileContent) => {
return new Promise((resolve, reject) => {
const fileObj = fileContent; // The file object passed directly
if (fileObj) {
const reader = new FileReader(); // Initialize FileReader to read the file
reader.readAsText(fileObj); // Read the file as text

reader.onload = (e) => {
try {
const fileContents = JSON.parse(e.target.result); // Parse file content as JSON
reader.onload = (e) => {
try {
const fileExtension = fileObj.name.split('.').pop().toLowerCase();

if (fileExtension === 'json') {
// Handle JSON file parsing
const fileContents = JSON.parse(e.target.result);

// Check if 'forms' exist and is an array in fileContents
if (fileContents && fileContents.forms && Array.isArray(fileContents.forms)) {
const formToUpload = fileContents.forms[0]; // Extract the first form (or adjust as needed)
callback(formToUpload); // Pass the extracted form details to the callback function
} else {
console.error("No 'forms' array found in the file.");
callback(null); // Pass null if the 'forms' array is missing
// Check if 'forms' exist and is an array in fileContents
if (fileContents && fileContents.forms && Array.isArray(fileContents.forms)) {
const formToUpload = fileContents.forms[0]; // Extract the first form
resolve(formToUpload); // Resolve with the extracted form details
} else {
console.error("No 'forms' array found in the file.");
reject("No valid form found."); // Reject with an error message if 'forms' is missing
}
} else if (['bpmn', 'dmn'].includes(fileExtension)) {
// Handle XML file parsing
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(e.target.result, "application/xml");

if (xmlDoc?.getElementsByTagName("parsererror").length > 0) {
reject("Invalid XML file."); // Reject if XML parsing fails
} else {
// Return the entire XML document as a string
const xmlString = new XMLSerializer().serializeToString(xmlDoc);
resolve(xmlString); // Resolve with the XML string
}
} else {
reject("Unsupported file type."); // Reject if the file type is unsupported
}
} catch (error) {
reject("Error processing file."); // Reject if there's a general error during processing
}
} catch (error) {
console.error("Error parsing JSON file:", error);
callback(null); // Pass null in case of JSON parsing error
}
};
};

reader.onerror = () => {
console.error("Error reading the file.");
callback(null); // Pass null in case of a file reading error
};
} else {
console.error("No file selected.");
callback(null); // Pass null if no file is provided
}
reader.onerror = () => {
console.error("Error reading the file.");
reject("Error reading the file."); // Reject in case of a file reading error
};
} else {
console.error("No file selected.");
reject("No file selected."); // Reject if no file is provided
}
});
};



const FileService = {
uploadFile,
downloadFile,
extractFormDetails,
extractFileDetails,
};

export default FileService;

0 comments on commit 6f8f07a

Please sign in to comment.