Skip to content

Commit 07f80df

Browse files
Merged in r2-2904-multimodule-workflow (pull request #7095)
R2-2904: Refactoring workflow to work with multiple modules Approved-by: Dennis Hernandez
2 parents 7ca27dd + c72b091 commit 07f80df

File tree

29 files changed

+410
-150
lines changed

29 files changed

+410
-150
lines changed

app/javascript/components/application/selectors.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ export const getWorkflowLabels = (state, id, recordType) => {
7171
export const getAllWorkflowLabels = (state, recordType) => {
7272
return selectUserModules(state).reduce((prev, current) => {
7373
if (![MODULES.GBV, MODULES.MRM].includes(current.get("unique_id"))) {
74-
prev.push([current.name, current.getIn(["workflows", recordType], [])]);
74+
prev.push([current.name, current.getIn(["workflows", recordType], []), current.unique_id]);
7575
}
7676

7777
return prev;

app/javascript/components/index-filters/components/filter-types/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Copyright (c) 2014 - 2023 UNICEF. All rights reserved.
22

33
export { default as CheckboxFilter } from "./checkbox-filter";
4+
export { default as WorkflowFilter } from "./workflow-filter";
45
export { default as ChipsFilter } from "./chips-filter";
56
export { default as ToggleFilter } from "./toggle-filter";
67
export { default as SwitchFilter } from "./switch-filter";

app/javascript/components/index-filters/components/filter-types/styles.css

+4
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@
77
width: 100%;
88
}
99

10+
.panelContent {
11+
margin-top: var(--sp-2)
12+
}
13+
1014
.toggleButton {
1115
color: var(--c-blue);
1216
border: 1px solid var(--c-blue);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
// Copyright (c) 2014 - 2023 UNICEF. All rights reserved.
2+
3+
import { useEffect, useState, useRef } from "react";
4+
import PropTypes from "prop-types";
5+
import { useFormContext } from "react-hook-form";
6+
import { FormGroup, FormControlLabel, FormLabel, FormControl, Checkbox } from "@mui/material";
7+
import { useLocation } from "react-router-dom";
8+
import qs from "qs";
9+
10+
import css from "../styles.css";
11+
import FieldSelect from "../date-filter/field-select";
12+
import Panel from "../../panel";
13+
import { getOption } from "../../../../record-form";
14+
import { useI18n } from "../../../../i18n";
15+
import {
16+
registerInput,
17+
whichOptions,
18+
optionText,
19+
handleMoreFiltersChange,
20+
resetSecondaryFilter,
21+
setMoreFilterOnPrimarySection
22+
} from "../utils";
23+
import handleFilterChange, { getFilterProps } from "../value-handlers";
24+
import { useMemoizedSelector } from "../../../../../libs";
25+
import { selectUserModules } from "../../../../application";
26+
import { MODULES } from "../../../../../config";
27+
28+
import { NAME } from "./constants";
29+
30+
function Component({ filter, moreSectionFilters = {}, setMoreSectionFilters, mode, reset, setReset }) {
31+
const i18n = useI18n();
32+
const { register, unregister, setValue, user, getValues } = useFormContext();
33+
const valueRef = useRef();
34+
const modules = useMemoizedSelector(state => selectUserModules(state));
35+
const location = useLocation();
36+
const moduleIDFromSearchQuery = qs.parse(location.search)?.module_id?.[0];
37+
38+
const moduleOptions = modules
39+
.map(primeroModule => ({ id: primeroModule.unique_id, display_name: primeroModule.name }))
40+
.filter(primeroModule => ![MODULES.GBV, MODULES.MRM].includes(primeroModule.id))
41+
.toJS();
42+
43+
const { options, fieldName, optionStringsSource, isObject } = getFilterProps({
44+
filter,
45+
user,
46+
i18n
47+
});
48+
49+
const defaultValue = isObject ? {} : [];
50+
const [inputValue, setInputValue] = useState(defaultValue);
51+
const [selectValue, setSelectValue] = useState(moduleIDFromSearchQuery || getValues("module_id.0"));
52+
53+
const setSecondaryValues = (name, values) => {
54+
setValue(name, values);
55+
setInputValue(values);
56+
};
57+
58+
const handleReset = () => {
59+
setValue(fieldName, defaultValue);
60+
resetSecondaryFilter(mode?.secondary, fieldName, getValues()[fieldName], moreSectionFilters, setMoreSectionFilters);
61+
};
62+
63+
useEffect(() => {
64+
registerInput({
65+
register,
66+
name: fieldName,
67+
ref: valueRef,
68+
defaultValue,
69+
setInputValue
70+
});
71+
72+
setMoreFilterOnPrimarySection(moreSectionFilters, fieldName, setSecondaryValues);
73+
74+
if (reset && !mode?.defaultFilter) {
75+
setValue(fieldName, defaultValue);
76+
handleReset();
77+
}
78+
79+
return () => {
80+
unregister(fieldName);
81+
if (setReset) {
82+
setReset(false);
83+
}
84+
};
85+
}, [register, unregister, fieldName]);
86+
87+
const lookups = useMemoizedSelector(state => getOption(state, optionStringsSource, i18n.locale));
88+
89+
const filterOptions = whichOptions({
90+
optionStringsSource,
91+
lookups,
92+
options: options?.[moduleOptions.length === 1 ? moduleOptions[0].id : selectValue],
93+
i18n
94+
});
95+
96+
const handleSelectChange = event => {
97+
const { value } = event.target;
98+
99+
setSelectValue(value);
100+
setValue("module_id", [value]);
101+
};
102+
103+
const handleChange = event => {
104+
handleFilterChange({
105+
type: isObject ? "objectCheckboxes" : "checkboxes",
106+
event,
107+
setInputValue,
108+
inputValue,
109+
setValue,
110+
fieldName
111+
});
112+
113+
if (mode?.secondary) {
114+
handleMoreFiltersChange(moreSectionFilters, setMoreSectionFilters, fieldName, getValues()[fieldName]);
115+
}
116+
};
117+
const renderOptions = () => {
118+
return filterOptions?.map(option => {
119+
return (
120+
<FormControlLabel
121+
key={`${fieldName}-${option.id}-form-control`}
122+
control={
123+
<Checkbox
124+
onChange={handleChange}
125+
value={option.id}
126+
checked={isObject ? option.key in inputValue : inputValue.includes(String(option.id))}
127+
/>
128+
}
129+
label={optionText(option, i18n)}
130+
/>
131+
);
132+
});
133+
};
134+
135+
return (
136+
<Panel
137+
filter={filter}
138+
getValues={getValues}
139+
handleReset={handleReset}
140+
selectedDefaultValueField={isObject ? "or" : null}
141+
>
142+
<FormControl component="fieldset" sx={{ mt: 0, width: "100%" }}>
143+
<FormLabel component="legend">{i18n.t("cases.status")}</FormLabel>
144+
{moduleOptions.length > 1 && (
145+
<FieldSelect options={moduleOptions} handleSelectedField={handleSelectChange} selectedField={selectValue} />
146+
)}
147+
<div className={css.panelContent}>{filterOptions && <FormGroup>{renderOptions()}</FormGroup>}</div>
148+
</FormControl>
149+
</Panel>
150+
);
151+
}
152+
153+
Component.displayName = NAME;
154+
155+
Component.propTypes = {
156+
filter: PropTypes.object.isRequired,
157+
mode: PropTypes.shape({
158+
defaultFilter: PropTypes.bool,
159+
secondary: PropTypes.bool
160+
}),
161+
moreSectionFilters: PropTypes.object,
162+
reset: PropTypes.bool,
163+
setMoreSectionFilters: PropTypes.func,
164+
setReset: PropTypes.func
165+
};
166+
167+
export default Component;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
// Copyright (c) 2014 - 2023 UNICEF. All rights reserved.
2+
3+
/* eslint-disable import/prefer-default-export */
4+
5+
export const NAME = "WorkflowFilter";
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// Copyright (c) 2014 - 2023 UNICEF. All rights reserved.
2+
3+
import * as constants from "./constants";
4+
5+
describe("<IndexFilters /> - filter-types/workflow-filter/constants", () => {
6+
it("should have known constant", () => {
7+
const clone = { ...constants };
8+
9+
["NAME"].forEach(property => {
10+
expect(clone).to.have.property(property);
11+
delete clone[property];
12+
});
13+
14+
expect(clone).to.be.empty;
15+
});
16+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
// Copyright (c) 2014 - 2023 UNICEF. All rights reserved.
2+
3+
export { default } from "./component";
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// Copyright (c) 2014 - 2023 UNICEF. All rights reserved.
2+
3+
import index from "./index";
4+
5+
describe("<IndexFilters /> - filter-types/workflow-filter/index", () => {
6+
const clone = { ...index };
7+
8+
it("should have known properties", () => {
9+
expect(clone).to.be.an("object");
10+
});
11+
});

app/javascript/components/index-filters/components/more-section.jsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ function MoreSection({
5050
return (
5151
<Filter
5252
filter={filter}
53-
key={`${filter.name}-secondary-filter`}
53+
key={[`${filter.name}-secondary-filter`, filter.module_id].join("-")}
5454
mode={mode}
5555
moreSectionFilters={moreSectionFilters}
5656
setMoreSectionFilters={setMoreSectionFilters}

app/javascript/components/index-filters/constants.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ export const FILTER_TYPES = Object.freeze({
77
MULTI_TOGGLE: "multi_toggle",
88
CHIPS: "chips",
99
TOGGLE: "toggle",
10-
DATES: "dates"
10+
DATES: "dates",
11+
WORKFLOW: "workflow"
1112
});
1213

1314
export const ID_SEARCH = "id_search";

app/javascript/components/index-filters/utils/build-name-filter.js

+5-5
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,15 @@ import { APPROVALS, APPROVALS_TYPES } from "../../../config";
55
export default (item, i18n, approvalsLabels) => {
66
switch (item) {
77
case `${APPROVALS}.${APPROVALS_TYPES.assessment}`:
8-
return approvalsLabels.get("assessment");
8+
return approvalsLabels.get(APPROVALS_TYPES.assessment);
99
case `${APPROVALS}.${APPROVALS_TYPES.case_plan}`:
10-
return approvalsLabels.get("case_plan");
10+
return approvalsLabels.get(APPROVALS_TYPES.case_plan);
1111
case `${APPROVALS}.${APPROVALS_TYPES.closure}`:
12-
return approvalsLabels.get("closure");
12+
return approvalsLabels.get(APPROVALS_TYPES.closure);
1313
case `${APPROVALS}.${APPROVALS_TYPES.action_plan}`:
14-
return approvalsLabels.get("action_plan");
14+
return approvalsLabels.get(APPROVALS_TYPES.action_plan);
1515
case `${APPROVALS}.${APPROVALS_TYPES.gbv_closure}`:
16-
return approvalsLabels.get("gbv_closure");
16+
return approvalsLabels.get(APPROVALS_TYPES.gbv_closure);
1717
default:
1818
return i18n.t(item);
1919
}

app/javascript/components/index-filters/utils/filter-type.js

+4-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ import {
77
SwitchFilter,
88
DateFilter,
99
ToggleFilter,
10-
SelectFilter
10+
SelectFilter,
11+
WorkflowFilter
1112
} from "../components/filter-types";
1213

1314
export default type => {
@@ -24,6 +25,8 @@ export default type => {
2425
return ChipsFilter;
2526
case FILTER_TYPES.MULTI_SELECT:
2627
return SelectFilter;
28+
case FILTER_TYPES.WORKFLOW:
29+
return WorkflowFilter;
2730
default:
2831
return null;
2932
}

app/javascript/components/pages/dashboard/components/workflow-individual-cases/component.jsx

+15-9
Original file line numberDiff line numberDiff line change
@@ -20,22 +20,28 @@ function Component({ loadingIndicator }) {
2020
const workflowLabels = useMemoizedSelector(state => getAllWorkflowLabels(state, RECORD_TYPES.cases));
2121
const casesWorkflow = useMemoizedSelector(state => getWorkflowIndividualCases(state));
2222

23-
const renderSteps = workflow =>
23+
const renderSteps = (workflow, moduleID) =>
2424
workflow
2525
.filter(step => step.id !== CLOSED)
2626
.map(step => {
27-
return <WorkFlowStep step={step} casesWorkflow={casesWorkflow} i18n={i18n} key={step.id} />;
27+
return <WorkFlowStep step={step} casesWorkflow={casesWorkflow} i18n={i18n} key={step.id} moduleID={moduleID} />;
2828
});
2929

30+
function panelTitle(name) {
31+
const title = [i18n.t("dashboard.workflow")];
32+
33+
if (workflowLabels.length > 1) {
34+
title.push(`- ${name}`);
35+
}
36+
37+
return title.join(" ");
38+
}
39+
3040
return (
3141
<Permission resources={RESOURCES.dashboards} actions={ACTIONS.DASH_WORKFLOW}>
32-
{workflowLabels.map(([name, workflow]) => (
33-
<OptionsBox
34-
title={`${i18n.t("dashboard.workflow")} - ${name}`}
35-
hasData={Boolean(casesWorkflow.size)}
36-
{...loadingIndicator}
37-
>
38-
<div className={css.container}>{renderSteps(workflow)}</div>
42+
{workflowLabels.map(([name, workflow, moduleID]) => (
43+
<OptionsBox title={panelTitle(name)} hasData={Boolean(casesWorkflow.size)} {...loadingIndicator}>
44+
<div className={css.container}>{renderSteps(workflow, moduleID)}</div>
3945
</OptionsBox>
4046
))}
4147
</Permission>

app/javascript/components/pages/dashboard/components/workflow-individual-cases/components/workflow-step.jsx

+3-2
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,10 @@ import css from "../styles.css";
1414

1515
import { NAME } from "./constants";
1616

17-
function WorkFlowStep({ step = {}, casesWorkflow = fromJS({}), i18n }) {
17+
function WorkFlowStep({ step = {}, casesWorkflow = fromJS({}), i18n, moduleID }) {
1818
const dispatch = useDispatch();
1919

20-
const workflowData = casesWorkflow.getIn(["indicators", "workflow", step.id], fromJS({}));
20+
const workflowData = casesWorkflow.getIn(["indicators", `workflow_${moduleID}`, step.id], fromJS({}));
2121
const count = workflowData.get("count", 0);
2222
const query = workflowData.get("query", fromJS({}));
2323

@@ -49,6 +49,7 @@ WorkFlowStep.displayName = NAME;
4949
WorkFlowStep.propTypes = {
5050
casesWorkflow: PropTypes.object,
5151
i18n: PropTypes.object,
52+
moduleID: PropTypes.string,
5253
step: PropTypes.object
5354
};
5455

app/javascript/components/pages/dashboard/components/workflow-individual-cases/components/workflow-step.spec.js

+3-2
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ describe("<WorkFlowStep> - pages/dashboard/components/workflow-individual-cases/
1010
name: "dashboard.workflow",
1111
type: "indicator",
1212
indicators: {
13-
workflow: {
13+
"workflow_test-module": {
1414
new: {
1515
count: 10,
1616
query: ["workflow=new"]
@@ -25,7 +25,8 @@ describe("<WorkFlowStep> - pages/dashboard/components/workflow-individual-cases/
2525
},
2626
casesWorkflow,
2727
css: {},
28-
i18n: { t: value => value }
28+
i18n: { t: value => value },
29+
moduleID: "test-module"
2930
};
3031

3132
beforeEach(() => {

app/javascript/components/pages/dashboard/container.spec.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ describe("<Dashboard />", () => {
7474
});
7575

7676
it("should render a <WorkflowIndividualCases /> component", () => {
77-
expect(screen.queryAllByText("dashboard.workflow - CP")).toHaveLength(1);
77+
expect(screen.queryAllByText("dashboard.workflow")).toHaveLength(1);
7878
});
7979

8080
it("should render a <Approvals /> component", () => {

0 commit comments

Comments
 (0)