Skip to content

Commit

Permalink
Merge pull request #2338 from fahad-aot/feature/FWF-3661-Create-task-…
Browse files Browse the repository at this point in the history
…variable-UI

Feature/fwf 3661 create task variable UI
arun-s-aot authored Nov 12, 2024
2 parents f28c0cb + be619f7 commit f70485a
Showing 6 changed files with 381 additions and 11 deletions.
4 changes: 2 additions & 2 deletions forms-flow-web/src/components/CustomComponents/MultiSelect.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { useState, useRef, useEffect } from "react";
import { ListGroup } from "react-bootstrap";
import { CustomPill } from "@formsflow/components";
import { CustomPill,DeleteIcon } from "@formsflow/components";
import PropTypes from 'prop-types';

const RoleSelector = ({ allRoles = [], selectedRoles = [], setSelectedRoles }) => {
@@ -62,7 +62,7 @@ const RoleSelector = ({ allRoles = [], selectedRoles = [], setSelectedRoles }) =
<CustomPill
key={role + index}
label={role}
icon={true}
icon={<DeleteIcon color="#253DF4" />}
bg="primary"
onClick={() => removeRole(role)}
/>
25 changes: 21 additions & 4 deletions forms-flow-web/src/components/Form/EditForm/FlowEdit.js
Original file line number Diff line number Diff line change
@@ -9,6 +9,7 @@ import {
HistoryIcon,
ConfirmModal,
HistoryModal,
CurlyBracketsIcon
} from "@formsflow/components";
import { Card } from "react-bootstrap";
import { useTranslation } from "react-i18next";
@@ -29,8 +30,9 @@ import {
validateProcess,
} from "../../../helper/processHelper.js";
import PropTypes from "prop-types";
import TaskVariableModal from "../../Modals/TaskVariableModal.js";

const FlowEdit = forwardRef(({ isPublished = false, CategoryType }, ref) => {
const FlowEdit = forwardRef(({ isPublished = false, CategoryType,form }, ref) => {
const { t } = useTranslation();
const dispatch = useDispatch();
const bpmnRef = useRef();
@@ -40,6 +42,7 @@ const FlowEdit = forwardRef(({ isPublished = false, CategoryType }, ref) => {
const [showDiscardModal, setShowDiscardModal] = useState(false);
const [showHistoryModal, setShowHistoryModal] = useState(false);
const [isReverted, setIsReverted] = useState(false);
const [showTaskVarModal, setShowTaskVarModal] = useState(false);
/* --------- fetching all process history when click history button --------- */
const {
data: { data: historiesData } = {}, // response data destructured
@@ -124,7 +127,12 @@ const FlowEdit = forwardRef(({ isPublished = false, CategoryType }, ref) => {
useImperativeHandle(ref, () => ({
saveFlow,
}));

const handlePreviewAndVariables = () => {
setShowTaskVarModal(true);
};
const CloseTaskVarModal = () => {
setShowTaskVarModal(false);
};

return (
<>
@@ -163,8 +171,9 @@ const FlowEdit = forwardRef(({ isPublished = false, CategoryType }, ref) => {
variant="secondary"
size="md"
className="mx-2"
label={t("Preview & Variables")}
onClick={() => console.log("handlePreviewAndVariables")}
icon={<CurlyBracketsIcon />}
label={t("Variables")}
onClick={() => handlePreviewAndVariables()}
dataTestid="preview-and-variables-testid"
ariaLabel={t("{Preview and Variables Button}")}
/>
@@ -222,6 +231,13 @@ const FlowEdit = forwardRef(({ isPublished = false, CategoryType }, ref) => {
historyCount={historiesData?.totalCount || 0}
currentVersionId={processData.id}
/>
{showTaskVarModal && (
<TaskVariableModal
form={form}
showTaskVarModal={showTaskVarModal}
onClose={CloseTaskVarModal}
/>
)}
</>
);
});
@@ -231,6 +247,7 @@ FlowEdit.propTypes = {
WORKFLOW: PropTypes.string.isRequired,
}).isRequired,
isPublished: PropTypes.bool.isRequired,
form: PropTypes.object.isRequired,
};

export default FlowEdit;
1 change: 1 addition & 0 deletions forms-flow-web/src/components/Form/EditForm/FormEdit.js
Original file line number Diff line number Diff line change
@@ -1017,6 +1017,7 @@ const EditComponent = () => {
ref={flowRef}
CategoryType={CategoryType}
isPublished={isPublished}
form={form}
/>}
</div>
<button
352 changes: 352 additions & 0 deletions forms-flow-web/src/components/Modals/TaskVariableModal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,352 @@
import React, { useEffect, useRef, useState, useCallback } from "react";
import { Modal } from "react-bootstrap";
import { useTranslation } from "react-i18next";
import {
CloseIcon,
CustomInfo,
CustomButton,
CustomPill,
FormInput,
} from "@formsflow/components";
import { Form } from "@aot-technologies/formio-react";
import PropTypes from "prop-types";
import { useDispatch ,useSelector } from "react-redux";
import {
saveFormProcessMapperPut,
} from "../../apiManager/services/processServices";


//TBD in case of Bundle form display
const PillList = React.memo(({ alternativeLabels, onRemove }) => {
const { t } = useTranslation();
return (
<div className="pill-container">
{Object.entries(alternativeLabels).length > 0 ? (
Object.entries(alternativeLabels).map(
([key, { altVariable, labelOfComponent }]) => (
<CustomPill
key={key}
label={altVariable || labelOfComponent}
icon={<CloseIcon color="#253DF4" data-testid="pill-remove-icon" />}
bg="#E7E9FE"
onClick={() => onRemove(key)}
secondaryLabel={key}
/>
)
)
) : (
<p className="select-text">{t("Pick variables below")}</p>
)}
</div>
);
});
// PropTypes for PillList
PillList.propTypes = {
alternativeLabels: PropTypes.object.isRequired,
onRemove: PropTypes.func.isRequired,
};
const FormComponent = React.memo(
({ form,
alternativeLabels,
setAlternativeLabels,
selectedComponent,
setSelectedComponent
}) => {
const [showElement, setShowElement] = useState(false);
const formRef = useRef(null);
const { t } = useTranslation();


const handleClick = useCallback(
(e) => {

const formioComponent = e.target.closest(".formio-component");
const highlightedElement = document.querySelector(".formio-hilighted");

if (highlightedElement) {
highlightedElement.classList.remove("formio-hilighted");
}

if (formioComponent) {
setShowElement(true);
formioComponent.classList.add("formio-hilighted");

let classes = Array.from(formioComponent.classList);
classes = classes.filter((cls) =>
cls.startsWith("formio-component-")
);
const typeClass = classes[classes.length - 2];
const keyClass = classes[classes.length - 1];

const labelElement = formioComponent.querySelector("label");
let label = "";
if (labelElement) {
label = Array.from(labelElement.childNodes)
.filter(
(node) =>
!(
node.nodeType === Node.ELEMENT_NODE &&
node.classList.contains("sr-only")
)
)
.map((node) => node.textContent.trim())
.join(" ");
}

const componentKey = keyClass?.split("-").pop();
const componentType = typeClass?.split("-").pop();
// const currentComponent = utils.getComponent(form.components,componentKey);
setSelectedComponent({
key: componentKey,
type: componentType,
label,
altVariable: alternativeLabels[componentKey]?.altVariable || "",
});
} else {
setSelectedComponent({
key: null,
type: "",
label: "",
altVariable: "",
});
}
},
[alternativeLabels]
);

useEffect(() => {
const formHilighter = document.querySelector(".form-hilighter");

formHilighter?.addEventListener("click", handleClick);

return () => {
formHilighter?.removeEventListener("click", handleClick);
};
}, [handleClick]);

const handleAddAlternative = () => {
if (selectedComponent.key) {
setAlternativeLabels((prev) => ({
...prev,
[selectedComponent.key]: {
altVariable: selectedComponent.altVariable,
labelOfComponent: selectedComponent.label,
key:selectedComponent.key
},
}));
const highlightedElement = document.querySelector(".formio-hilighted");
if (highlightedElement) {
highlightedElement.classList.remove("formio-hilighted");
}
}
setShowElement(false);
};

return (
<div className="d-flex">
<div className="flex-grow-1 form-hilighter form-field-container">
<Form
form={form}
options={{
viewAsHtml: true,
readOnly: true,
}}
formReady={(e) => {
formRef.current = e;
}}
/>
</div>

<div className="field-details-container">
{showElement ? (
<div className="details-section">
<div className="d-flex flex-column">
<span>{t("Type")}:</span>
<span className="text-bold"> {selectedComponent.type}</span>
</div>
<div className="d-flex flex-column">
<span>{t("Variable")}:</span>
<span className="text-bold">{selectedComponent.key}</span>
{/* TBD in case of Bundle */}
</div>
<FormInput
type="text"
ariaLabel="Add alternative label input"
dataTestid="Add-alternative-input"
label="Add Alternative Label"
value={selectedComponent.altVariable}
onChange={(e) =>
setSelectedComponent((prev) => ({
...prev,
altVariable: e.target.value,
}))
}
/>
<CustomButton
dataTestid="Add-alternative-btn"
ariaLabel="Add alternative label button"
size="sm"
label={
alternativeLabels[selectedComponent.key]
? t("Update Variable")
: t("Add Variable")
}
onClick={handleAddAlternative}
className="w-75"
disabled={selectedComponent.
altVariable === alternativeLabels[selectedComponent.key]?.altVariable} //TBD need to create a variable to compare values
/>
</div>
) : (
<p className="select-text">{t("Select a form field on the left")}</p>
)}
</div>
</div>
);
}
);

// PropTypes for FormComponent
FormComponent.propTypes = {
form: PropTypes.object.isRequired,
alternativeLabels: PropTypes.object.isRequired,
setAlternativeLabels: PropTypes.func.isRequired,
selectedComponent: PropTypes.object.isRequired,
setSelectedComponent: PropTypes.func.isRequired,
};
const TaskVariableModal = React.memo(
({ showTaskVarModal, onClose, form }) => {
const { t } = useTranslation();
const dispatch = useDispatch();
const formProcessList = useSelector(
(state) => state.process.formProcessList
);

const [alternativeLabels, setAlternativeLabels] = useState({});

useEffect(() => {
if (formProcessList?.taskVariables?.length > 0) {
const updatedLabels = {};
formProcessList.taskVariables.forEach(({ key, label }) => {
updatedLabels[key] = {
key,
altVariable: label, // Use label from taskVariables as altVariable
labelOfComponent: label, // Set the same label for labelOfComponent
};
});
setAlternativeLabels(updatedLabels);
}
}, [formProcessList]);
const [selectedComponent, setSelectedComponent] = useState({
key: null,
type: "",
label: "",
altVariable: "",
});
const removeSelectedVariable = useCallback((key) => {
setSelectedComponent((prev) => ({
...prev,
altVariable: "",
}));
setAlternativeLabels((prev) => {
const newLabels = { ...prev };
delete newLabels[key];
return newLabels;
});

}, []);

const handleClose = () => onClose();

const handleSaveTaskVariable = async() => {
const currentTaskVariables = Object.values(alternativeLabels).map(
(i) => ({
key: i.key,
label: i.altVariable || i.labelOfComponent, // If altVariable exists, use it, otherwise it will be labelOfComponent
})
);
const mapper = {
formId: formProcessList.formId,
id: formProcessList.id,
parentFormId: formProcessList.parentFormId,
taskVariables:currentTaskVariables,
formName: formProcessList.formName
};
await dispatch(saveFormProcessMapperPut({ mapper}));

onClose();
};
return (
<Modal
show={showTaskVarModal}
onHide={handleClose}
className="task-variable-modal"
size="lg"
centered={true}
>
<Modal.Header>
<Modal.Title>
{t("Variables for Flow, Submissions, and Tasks")}
</Modal.Title>
<div className="d-flex align-items-center">
<CloseIcon width="16.5" height="16.5" onClick={handleClose} />
</div>
</Modal.Header>
<Modal.Body>
<div className="info-pill-container">
<CustomInfo
heading="Note"
content="To use variables in the flow, as well as sorting by them in
the submissions and tasks you need to specify which variables you want to import from the layout. Variables get imported into the system at the time of the submission, if the variables that are needed
are not selected prior to the form submission THEY WILL NOT BE AVAILABLE in the flow, submissions, and tasks."
/>
<div>
<label className="selected-var-text">{t("Selected Variables")}</label>
<PillList
alternativeLabels={alternativeLabels}
onRemove={removeSelectedVariable}
/>
</div>
</div>
<div className="variable-container">
<FormComponent
form={form}
alternativeLabels={alternativeLabels}
setAlternativeLabels={setAlternativeLabels}
setSelectedComponent={setSelectedComponent}
selectedComponent={selectedComponent}
/>
</div>
</Modal.Body>
<Modal.Footer>
<CustomButton
variant="primary"
size="md"
className=""
label={t("Save")}
ariaLabel="save task variable btn"
dataTestid="save-task-variable-btn"
onClick={handleSaveTaskVariable}
/>
<CustomButton
variant="secondary"
size="md"
className=""
label={t("Cancel")}
ariaLabel="Cancel btn"
dataTestid="Cancel-btn"
onClick={handleClose}
/>
</Modal.Footer>
</Modal>
);
}
);

// PropTypes for TaskVariableModal
TaskVariableModal.propTypes = {
showTaskVarModal: PropTypes.bool.isRequired,
onClose: PropTypes.func.isRequired,
form: PropTypes.object.isRequired
};
export default TaskVariableModal;
4 changes: 2 additions & 2 deletions forms-flow-web/src/components/Modeler/DecisionTable.js
Original file line number Diff line number Diff line change
@@ -29,7 +29,7 @@ const DecisionTable = React.memo(() => {
const [currentDmnSort, setCurrentDmnSort] = useState({
activeKey: "name",
name: { sortOrder: "asc" },
id: { sortOrder: "asc" },
processKey: { sortOrder: "asc" },
modified: { sortOrder: "asc" },
status: { sortOrder: "asc" },
});
@@ -175,7 +175,7 @@ const contents = [
</th>
<th className="w-20" scope="col">
<SortableHeader
columnKey="id"
columnKey="processKey"
title="ID"
currentSort={currentDmnSort}
handleSort={handleSort}
6 changes: 3 additions & 3 deletions forms-flow-web/src/components/Modeler/SubFlowTable.js
Original file line number Diff line number Diff line change
@@ -34,7 +34,7 @@ const SubFlow = React.memo(() => {
const [currentBpmnSort, setCurrentBpmnSort] = useState({
activeKey: "name",
name: { sortOrder: "asc" },
id: { sortOrder: "asc" },
processKey: { sortOrder: "asc" },
modified: { sortOrder: "asc" },
status: { sortOrder: "asc" },
});
@@ -193,8 +193,8 @@ const SubFlow = React.memo(() => {
</th>
<th className="w-20" scope="col">
<SortableHeader
columnKey="id"
title="id"
columnKey="processKey"
title="ID"
currentSort={currentBpmnSort}
handleSort={handleSort}
/>

0 comments on commit f70485a

Please sign in to comment.