diff --git a/forms-flow-api/src/formsflow_api/models/form_process_mapper.py b/forms-flow-api/src/formsflow_api/models/form_process_mapper.py index 19f43e57a1..c21968dd86 100644 --- a/forms-flow-api/src/formsflow_api/models/form_process_mapper.py +++ b/forms-flow-api/src/formsflow_api/models/form_process_mapper.py @@ -389,9 +389,23 @@ def find_all_active_forms( @classmethod def find_forms_by_title(cls, form_title, exclude_id) -> FormProcessMapper: """Find all form process mapper that matches the provided form title.""" - query = cls.query.filter(FormProcessMapper.form_name == form_title) + latest_mapper = ( + db.session.query( + func.max(cls.id).label("latest_id"), + cls.parent_form_id, + ) + .group_by(cls.parent_form_id) + .subquery() + ) + query = ( + db.session.query(cls) + .join(latest_mapper, cls.id == latest_mapper.c.latest_id) + .filter(cls.form_name == form_title, cls.deleted.is_(False)) + ) + if exclude_id is not None: - query = query.filter(FormProcessMapper.parent_form_id != exclude_id) + query = query.filter(cls.parent_form_id != exclude_id) + query = cls.tenant_authorization(query=query) return query.all() diff --git a/forms-flow-api/src/formsflow_api/services/form_process_mapper.py b/forms-flow-api/src/formsflow_api/services/form_process_mapper.py index d9d8caf459..4cd2dff79d 100644 --- a/forms-flow-api/src/formsflow_api/services/form_process_mapper.py +++ b/forms-flow-api/src/formsflow_api/services/form_process_mapper.py @@ -495,6 +495,7 @@ def _sanitize_form_json(self, form_json, tenant_key): "access", "submissionAccess", "parentFormId", + "owner", ] for key in keys_to_remove: form_json.pop(key, None) @@ -803,39 +804,34 @@ def validate_form_name_path_title(request, **kwargs): FormProcessMapperService.validate_title_name_path(title, path, name) if current_app.config.get("MULTI_TENANCY_ENABLED"): - # In multitenant environment, validate title exists validation on mapper & path, name in formio. - if title: - FormProcessMapperService.validate_form_title(title) user: UserContext = kwargs["user"] tenant_key = user.tenant_key name = f"{tenant_key}-{name}" path = f"{tenant_key}-{path}" + # Validate title exists validation on mapper & path, name in formio. + if title: + FormProcessMapperService.validate_form_title(title) + # Validate path, name exits in formio. + if path or name: query_params = f"name={name}&path={path}&select=title,path,name" - else: - # In non-multitenant environment, validate title, path, name in formio. - query_params = ( - f"title={title}&name={name}&path={path}&select=title,path,name" + # Initialize the FormioService and get the access token + formio_service = FormioService() + form_io_token = formio_service.get_formio_access_token() + validation_response = formio_service.get_form_search( + query_params, form_io_token ) - # Initialize the FormioService and get the access token - formio_service = FormioService() - form_io_token = formio_service.get_formio_access_token() - # Call the external validation API - validation_response = formio_service.get_form_search( - query_params, form_io_token - ) - - # Check if the validation response has any results - if validation_response: - # Check if the form ID matches - if ( - form_id - and len(validation_response) == 1 - and validation_response[0].get("_id") == form_id - ): - return {} - # If there are results but no matching ID, the form name is still considered invalid - raise BusinessException(BusinessErrorCode.FORM_EXISTS) + # Check if the validation response has any results + if validation_response: + # Check if the form ID matches + if ( + form_id + and len(validation_response) == 1 + and validation_response[0].get("_id") == form_id + ): + return {} + # If there are results but no matching ID, the form name is still considered invalid + raise BusinessException(BusinessErrorCode.FORM_EXISTS) # If no results, the form name is valid return {} diff --git a/forms-flow-api/src/formsflow_api/services/import_support.py b/forms-flow-api/src/formsflow_api/services/import_support.py index 2102017129..39d2dbe50b 100644 --- a/forms-flow-api/src/formsflow_api/services/import_support.py +++ b/forms-flow-api/src/formsflow_api/services/import_support.py @@ -16,8 +16,10 @@ from formsflow_api.constants import BusinessErrorCode from formsflow_api.models import AuthType, FormHistory, Process, ProcessType from formsflow_api.schemas import ( + FormProcessMapperSchema, ImportEditRequestSchema, ImportRequestSchema, + ProcessDataSchema, form_schema, form_workflow_schema, ) @@ -256,19 +258,18 @@ def validate_form( name = f"{tenant_key}-{name}" path = f"{tenant_key}-{path}" + if len(title) > 200 or len(name) > 200: + raise BusinessException(BusinessErrorCode.INVALID_FORM_TITLE_LENGTH) + # Build query params based on validation type if validate_path_only and mapper: # In case of edit import validate title in mapper table & path in formio. FormProcessMapperService.validate_form_title(title, mapper.parent_form_id) query_params = f"path={path}&select=title,path,name,_id" - elif not validate_path_only and current_app.config.get("MULTI_TENANCY_ENABLED"): - # In case of new import in multitenant env, validate title in mapper table & path,name in formio. + else: + # In case of new import validate title in mapper table & path,name in formio. FormProcessMapperService.validate_form_title(title, exclude_id=None) query_params = f"path={path}&name={name}&select=title,path,name,_id" - else: - query_params = ( - f"title={title}&name={name}&path={path}&select=title,path,name" - ) current_app.logger.info(f"Validating form exists...{query_params}") response = self.get_form_by_query(query_params) return response @@ -361,6 +362,7 @@ def save_process_data( # pylint: disable=too-many-arguments, too-many-positiona ) process.save() current_app.logger.info("Process data saved successfully...") + return process def version_response(self, form_major, form_minor, workflow_major, workflow_minor): """Version response.""" @@ -432,47 +434,54 @@ def import_new_form_workflow( ) process_name = updated_process_name if updated_process_name else process_name current_app.logger.info(f"Process Name: {process_name}") - self.save_process_data( + process = self.save_process_data( workflow_data, process_name, is_new=True, process_type=process_type ) - return form_id + return mapper, process def import_form( self, selected_form_version, form_json, mapper, form_only=False, **kwargs - ): # pylint: disable=too-many-locals + ): # pylint: disable=too-many-locals, too-many-statements """Import form as major or minor version.""" current_app.logger.info("Form import inprogress...") # Get current form by mapper form_id current_form = self.get_form_by_formid(mapper.form_id) - new_path = form_json.get("path") - new_title = form_json.get("title") - anonymous = kwargs.get("anonymous", False) - description = kwargs.get("description", None) - title_changed = bool(not form_only and mapper.form_name != new_title) + name = current_form.get("name") + title_changed = bool( + not form_only and mapper.form_name != form_json.get("title") + ) + if form_only: + # In case of form only import take title, path from current form + # and anonymous, description from mapper + path = current_form.get("path") + title = current_form.get("title") + anonymous = mapper.is_anonymous + description = mapper.description + else: + # form+workflow import take title, path, anonymous, description from incoming form json + path = form_json.get("path") + title = form_json.get("title") + anonymous = kwargs.get("anonymous", False) + description = kwargs.get("description", None) anonymous_changed = bool( anonymous is not None and mapper.is_anonymous != anonymous ) + if selected_form_version == "major": # Update current form with random value to path, name & title # Create new form with current form name, title & path from incoming form # Create mapper entry for new form version, mark previous version inactive & delete # Capture form history current_app.logger.info("Form import major version inprogress...") - path = current_form.get("path") - name = current_form.get("name") - title = current_form.get("title") # Update name & path of current form - current_form["path"] = f"{path}-v-{uuid1().hex}" + current_form["path"] = f"{current_form['path']}-v-{uuid1().hex}" current_form["name"] = f"{name}-v-{uuid1().hex}" - current_form["title"] = f"{title}-v-{uuid1().hex}" FormProcessMapperService.form_design_update(current_form, mapper.form_id) # Create new form with current form name - form_json["parentFormId"] = mapper.parent_form_id - form_json["name"] = name - # Update path of current form with pathname & title from imported form in case of edit import # But incase of form only no validation done, so use current form path & title itself. - form_json["title"] = title if form_only else new_title - form_json["path"] = path if form_only else new_path + form_json["title"] = title + form_json["path"] = path + form_json["parentFormId"] = mapper.parent_form_id form_json = self.set_form_and_submission_access(form_json, anonymous) form_response = self.form_create(form_json) form_id = form_response.get("_id") @@ -490,7 +499,7 @@ def import_form( "formName": form_response.get("title"), "formType": mapper.form_type, "parentFormId": mapper.parent_form_id, - "anonymous": mapper.is_anonymous if form_only else anonymous, + "anonymous": anonymous, "taskVariables": json.loads(mapper.task_variable), "processKey": mapper.process_key, "processName": mapper.process_name, @@ -499,10 +508,10 @@ def import_form( "formTypeChanged": False, "titleChanged": title_changed, "anonymousChanged": anonymous_changed, - "description": mapper.description if form_only else description, + "description": description, "isMigrated": mapper.is_migrated, } - FormProcessMapperService.mapper_create(mapper_data) + mapper = FormProcessMapperService.mapper_create(mapper_data) FormProcessMapperService.mark_unpublished(mapper.id) else: current_app.logger.info("Form import minor version inprogress...") @@ -513,6 +522,10 @@ def import_form( # Minor version update form components in formio & create form history. form_components = {} form_components["components"] = form_json.get("components") + # Incase of form+workflow title/path is updated even in minor version + form_components["title"] = title + form_components["path"] = path + form_components["parentFormId"] = mapper.parent_form_id form_response = self.form_update(form_components, form_id) form_response["componentChanged"] = True form_response["parentFormId"] = mapper.parent_form_id @@ -522,24 +535,25 @@ def import_form( current_app.logger.info("Updating mapper & form logs...") mapper.description = description mapper.is_anonymous = anonymous + mapper.form_name = title mapper.save() form_logs_data = { "titleChanged": title_changed, - "formName": new_title, + "formName": title, "anonymousChanged": anonymous_changed, "anonymous": anonymous, "formId": form_id, "parentFormId": mapper.parent_form_id, } FormHistoryService.create_form_logs_without_clone(data=form_logs_data) - return form_id + return mapper def import_edit_form(self, file_data, selected_form_version, form_json, mapper): """Import edit form.""" current_app.logger.info("Form import with form+workflow json inprogress...") anonymous = file_data.get("forms")[0].get("anonymous") or False description = file_data.get("forms")[0].get("formDescription", "") - form_id = self.import_form( + mapper = self.import_form( selected_form_version, form_json, mapper, @@ -553,7 +567,7 @@ def import_edit_form(self, file_data, selected_form_version, form_json, mapper): file_data["authorizations"][0][auth]["resourceId"] = mapper.parent_form_id # Update authorizations for the form self.create_authorization(file_data["authorizations"][0]) - return form_id + return mapper @user_context def import_form_workflow( @@ -568,7 +582,9 @@ def import_form_workflow( action = input_data.get("action") user: UserContext = kwargs["user"] tenant_key = user.tenant_key - form_id = None + mapper = None + process = None + response = {} # Check if the action is valid if action not in ["validate", "import"]: @@ -600,7 +616,7 @@ def import_form_workflow( form_json = self.append_tenant_key_form_name_path( form_json, tenant_key ) - form_id = self.import_new_form_workflow( + mapper, process = self.import_new_form_workflow( file_data, form_json, workflow_data, process_type ) else: @@ -614,7 +630,6 @@ def import_form_workflow( if mapper.status == FormProcessMapperStatus.ACTIVE.value: # Raise an exception if the user try to update published form raise BusinessException(BusinessErrorCode.FORM_INVALID_OPERATION) - form_id = mapper.form_id if valid_file == ".json": file_data = self.read_json_data(file) # Validate input json file whether only form or form+workflow @@ -637,7 +652,7 @@ def import_form_workflow( selected_form_version = edit_request.get("form", {}).get( "selectedVersion" ) - form_id = self.import_form( + mapper = self.import_form( selected_form_version, form_json, mapper, form_only=True ) elif self.validate_input_json(file_data, form_workflow_schema): @@ -680,7 +695,7 @@ def import_form_workflow( form_json = self.append_tenant_key_form_name_path( form_json, tenant_key ) - form_id = self.import_edit_form( + mapper = self.import_edit_form( file_data, selected_form_version, form_json, mapper ) if not skip_workflow: @@ -689,7 +704,7 @@ def import_form_workflow( workflow_data, process_type = self.get_process_details( file_data ) - self.save_process_data( + process = self.save_process_data( workflow_data, mapper.process_key, selected_workflow_version, @@ -715,9 +730,13 @@ def import_form_workflow( "selectedVersion" ) file_content = file.read().decode("utf-8") - self.save_process_data( + process = self.save_process_data( file_content, mapper.process_key, selected_workflow_version, ) - return {"formId": form_id} + if mapper: + response["mapper"] = FormProcessMapperSchema().dump(mapper) + if process: + response["process"] = ProcessDataSchema().dump(process) + return response diff --git a/forms-flow-api/tests/unit/api/test_import_support.py b/forms-flow-api/tests/unit/api/test_import_support.py index ad4e658f4e..1944e17e59 100644 --- a/forms-flow-api/tests/unit/api/test_import_support.py +++ b/forms-flow-api/tests/unit/api/test_import_support.py @@ -463,7 +463,6 @@ def test_import_new(app, client, session, jwt, mock_redis_client): # Test case 2: Import new form+workflow - import with patch.object(ImportService, "import_form_workflow") as mock_import_service: - mock_response = {"formId": "66f3adcc80926673336c3c49"} mock_import_service.return_value = mock_response # Prepare the file content @@ -482,7 +481,6 @@ def test_import_new(app, client, session, jwt, mock_redis_client): response = client.post("/import", data=form_data, headers=headers) assert response.status_code == 200 assert response.json is not None - assert response.json == {"formId": "66f3adcc80926673336c3c49"} # Test case 3: Import with invalid json. form_content = json.dumps(form_json_data()) @@ -549,7 +547,6 @@ def test_import_edit(app, client, session, jwt, mock_redis_client): # Test case 2: Import edit form+workflow with patch.object(ImportService, "import_form_workflow") as mock_import_service: - mock_response = {"formId": "66f3adcc80926673336c3c49"} mock_import_service.return_value = mock_response # Prepare the file content @@ -571,11 +568,9 @@ def test_import_edit(app, client, session, jwt, mock_redis_client): response = client.post("/import", data=form_data, headers=headers) assert response.status_code == 200 assert response.json is not None - assert response.json == {"formId": "66f3adcc80926673336c3c49"} # Test case 3: Import edit - only form with patch.object(ImportService, "import_form_workflow") as mock_import_service: - mock_response = {"formId": "66f3adcc80926673336c3c49"} mock_import_service.return_value = mock_response # Prepare the file content @@ -597,11 +592,9 @@ def test_import_edit(app, client, session, jwt, mock_redis_client): response = client.post("/import", data=form_data, headers=headers) assert response.status_code == 200 assert response.json is not None - assert response.json == {"formId": "66f3adcc80926673336c3c49"} # Test case 4: Import edit - only workflow with patch.object(ImportService, "import_form_workflow") as mock_import_service: - mock_response = {"formId": "66f3adcc80926673336c3c49"} mock_import_service.return_value = mock_response # Prepare the file content @@ -627,4 +620,3 @@ def test_import_edit(app, client, session, jwt, mock_redis_client): response = client.post("/import", data=form_data, headers=headers) assert response.status_code == 200 assert response.json is not None - assert response.json == {"formId": "66f3adcc80926673336c3c49"} diff --git a/forms-flow-web/src/components/Form/EditForm/FormEdit.js b/forms-flow-web/src/components/Form/EditForm/FormEdit.js index c5883e3263..92ffd4c8b3 100644 --- a/forms-flow-web/src/components/Form/EditForm/FormEdit.js +++ b/forms-flow-web/src/components/Form/EditForm/FormEdit.js @@ -62,7 +62,7 @@ import userRoles from "../../../constants/permissions.js"; import { generateUniqueId, isFormComponentsChanged } from "../../../helper/helper.js"; import { useMutation } from "react-query"; import NavigateBlocker from "../../CustomComponents/NavigateBlocker"; -import { setProcessData } from "../../../actions/processActions.js"; +import { setProcessData, setFormPreviosData, setFormProcessesData } from "../../../actions/processActions.js"; // constant values const DUPLICATE = "DUPLICATE"; @@ -232,20 +232,23 @@ const EditComponent = () => { setFormSubmitted(false); const formExtracted = await extractForm(fileContent); const { data: responseData } = res; - if (!responseData) return; - // Set file items based on response data - setFileItems({ - workflow: extractVersionInfo(responseData.workflow), - form: extractVersionInfo(responseData.form), - }); + if (!responseData || !formExtracted) return; - // Handle actions based on extracted form and action type - if (formExtracted) { - if (action === "validate") { - setFormTitle(formExtracted.forms[0]?.formTitle || ""); - } else if (responseData.formId) { - updateLayout(formExtracted); + /* -------------------------- if action is validate ------------------------- */ + if (action === "validate") { + setFileItems({ + workflow: extractVersionInfo(responseData.workflow), + form: extractVersionInfo(responseData.form), + }); + setFormTitle(formExtracted.forms[0]?.formTitle || ""); + }else{ + /* ------------------------- if the form id changed ------------------------- */ + const formId = responseData.mapper?.formId; + if(formId && formData._id != formId){ + dispatch(push(`${redirectUrl}formflow/${formId}/edit/`)); + return; } + updateLayout({formExtracted, responseData}); } }; @@ -268,16 +271,37 @@ const EditComponent = () => { - const updateLayout = (formExtracted) => { - const { forms, xml } = formExtracted || {}; - const extractedFormComponents = forms[0]?.components || forms[0]?.content?.components; - dispatchFormAction({ - type: "components", - value: _cloneDeep(extractedFormComponents), - }); - if (xml) { - flowRef.current?.handleImport(xml); + const updateLayout = ({formExtracted, responseData}) => { + /* --------- the response data will contain' mapper and process' key -------- */ + const { forms } = formExtracted || {}; + const { process, mapper } = responseData; + const isNotFormPlusWorkflow = !forms[0]?.content; + const extractedForm = forms[0]?.content || forms[0]; + + /* if form changed then the response contain mapper key and will update + 1. formio's form data + 2. mapper data + 3. current form data */ + if(mapper && extractedForm){ + if(isNotFormPlusWorkflow){ + dispatchFormAction({ + type: "components", + value: _cloneDeep(extractedForm.components), + }); + }else{ + const currentFormDataWithImportedData = {...formData, ...extractedForm}; + dispatch(setFormSuccessData("form", currentFormDataWithImportedData)); + dispatch(setFormPreviosData(mapper)); + dispatch(setFormProcessesData(mapper)); + dispatchFormAction({type:"replaceForm",value: currentFormDataWithImportedData}); + } } + + /* ---------- if workflow changed then need to updated process dat ---------- */ + if (process) { + dispatch(setProcessData(process)); + } + handleCloseSelectedAction(); }; diff --git a/forms-flow-web/src/components/Form/List.js b/forms-flow-web/src/components/Form/List.js index b8b1e5e4b5..5698dbd1e7 100644 --- a/forms-flow-web/src/components/Form/List.js +++ b/forms-flow-web/src/components/Form/List.js @@ -169,61 +169,50 @@ const List = React.memo((props) => { onClose(); }; - const handleImport = async (fileContent, UploadActionType) => { - if(UploadActionType === "import") { - setImportLoader(true); - } - let data = {}; - switch (UploadActionType) { - case "validate": - data = { - importType: "new", - action: "validate", - }; - break; - case "import": - setFormSubmitted(true); - data = { - importType: "new", - action: "import", - }; - break; - default: - console.error("Invalid UploadActionType provided"); - return; + const handleImport = async (fileContent, actionType) => { + + + let data; + if(UploadActionType[actionType]){ + data = { importType: "new", action: UploadActionType[actionType?.toUpperCase()]}; + }else{ + console.error("Invalid UploadActionType provided"); + return; } - const dataString = JSON.stringify(data); - formImport(fileContent, dataString) - .then((res) => { - setImportLoader(false); - setFormSubmitted(false); + if (actionType === UploadActionType.IMPORT) { + setImportLoader(true); + setFormSubmitted(true); + } - if (data.action == "validate") { - 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 { - res?.data?.formId && dispatch(push(`${redirectUrl}formflow/${res.data.formId}/edit/`)); - } - }) - .catch((err) => { - setImportLoader(false); - setFormSubmitted(false); - setImportError(err?.response?.data?.message); - }); + try { + const dataString = JSON.stringify(data); + const res = await formImport(fileContent, dataString); + const { data: responseData } = res; + const formId = responseData.mapper?.formId; + + setImportLoader(false); + setFormSubmitted(false); + + if (actionType === UploadActionType.VALIDATE ) { + + const formExtracted = await FileService.extractFileDetails(fileContent); + + if (Array.isArray(formExtracted?.forms)) { + setFormTitle(formExtracted?.forms[0]?.formTitle || ""); + setUploadFormDescription(formExtracted?.forms[0]?.formDescription || ""); + } + + } else if (formId) { + dispatch(push(`${redirectUrl}formflow/${formId}/edit/`)); + } + } catch (err) { + setImportLoader(false); + setFormSubmitted(false); + setImportError(err?.response?.data?.message); + } }; - + useEffect(() => { fetchForms(); diff --git a/forms-flow-web/src/components/Modals/ImportProcess.js b/forms-flow-web/src/components/Modals/ImportProcess.js index 091e644eda..c686a4e31c 100644 --- a/forms-flow-web/src/components/Modals/ImportProcess.js +++ b/forms-flow-web/src/components/Modals/ImportProcess.js @@ -71,15 +71,15 @@ const ImportProcess = React.memo(({ // Handle importing process and dispatching upon success const processImport = async (fileContent) => { try { - const extractedXml = await extractFileDetails(fileContent); - + const {xml} = await extractFileDetails(fileContent); + if(!xml) return; if (processId) { // Update an existing process - setImportXml(extractedXml); + setImportXml(xml); closeImport(); } else { // Create a new process and redirect - const response = await createProcess({ data: extractedXml, type: fileType === ".bpmn" ? "bpmn" : "dmn" }); + const response = await createProcess({ data: xml, type: fileType === ".bpmn" ? "bpmn" : "dmn" }); if (response) { dispatch(push(`${redirectUrl}${baseUrl}${response.data.processKey}`)); }