Skip to content

Commit

Permalink
Merge pull request #52 from EyeSeeTea/feat/create-text-editor-field
Browse files Browse the repository at this point in the history
[feature]: Create text editor field type
  • Loading branch information
bhavananarayanan authored Jan 24, 2025
2 parents 37f671d + 945ebbf commit dac07fb
Show file tree
Hide file tree
Showing 11 changed files with 241 additions and 32 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-dropzone": "^14.3.5",
"react-quill": "^2.0.0",
"react-router-dom": "5.2.0",
"real-cancellable-promise": "^1.1.2",
"string-ts": "2.2.0",
Expand Down
4 changes: 4 additions & 0 deletions src/webapp/components/form/FieldWidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { DatePicker } from "../date-picker/DatePicker";
import { Checkbox } from "../checkbox/Checkbox";
import { FormFieldState, updateFieldState, SheetData } from "./FormFieldsState";
import { ImportFile } from "../import-file/ImportFile";
import { TextEditor } from "../text-editor/TextEditor";

export type FieldWidgetProps = {
onChange: (updatedField: FormFieldState) => void;
Expand Down Expand Up @@ -80,6 +81,9 @@ export const FieldWidget: React.FC<FieldWidgetProps> = React.memo((props): JSX.E
<TextInput {...commonProps} value={field.value} />
);

case "text-editor":
return <TextEditor {...commonProps} value={field.value} />;

case "date":
return <DatePicker {...commonProps} value={field.value} />;

Expand Down
11 changes: 9 additions & 2 deletions src/webapp/components/form/FormFieldsState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ export type FieldType =
| "date"
| "user"
| "addNew"
| "file";
| "file"
| "text-editor";

type FormFieldStateBase<T> = {
id: string;
Expand Down Expand Up @@ -82,6 +83,10 @@ export type FormFileFieldState = FormFieldStateBase<Maybe<File>> & {
fileNameLabel?: string;
};

export type FormTextEditorFieldState = FormFieldStateBase<string> & {
type: "text-editor";
};

export type AddNewFieldState = FormFieldStateBase<null> & {
type: "addNew";
};
Expand All @@ -93,7 +98,8 @@ export type FormFieldState =
| FormBooleanFieldState
| FormDateFieldState
| FormAvatarFieldState
| FormFileFieldState;
| FormFileFieldState
| FormTextEditorFieldState;

// HELPERS:

Expand Down Expand Up @@ -162,6 +168,7 @@ export function getFieldIdFromIdsDictionary<T extends Record<string, string>>(
export function getFieldWithEmptyValue(field: FormFieldState): FormFieldState {
switch (field.type) {
case "text":
case "text-editor":
return { ...field, value: "" };
case "boolean":
return { ...field, value: false };
Expand Down
40 changes: 30 additions & 10 deletions src/webapp/components/form/form-summary/ActionPlanFormSummary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,12 @@ import { Loader } from "../../loader/Loader";
import { useSnackbar } from "@eyeseetea/d2-ui-components";
import { Id } from "../../../../domain/entities/Ref";
import { FormType } from "../../../pages/form-page/FormPage";
import { IncidentActionFormSummaryData } from "../../../pages/incident-action-plan/useIncidentActionPlan";
import {
IncidentActionFormSummaryData,
IncidentActionSummary,
} from "../../../pages/incident-action-plan/useIncidentActionPlan";
import { Maybe } from "../../../../utils/ts-utils";
import { TextPreview } from "../../text-editor/TextEditor";

type ActionPlanFormSummaryProps = {
id: Id;
Expand Down Expand Up @@ -46,12 +50,30 @@ export const ActionPlanFormSummary: React.FC<ActionPlanFormSummaryProps> = React
</Button>
);

const getSummaryColumn = useCallback((index: number, label: string, value: string) => {
return (
<Typography key={index}>
<Box fontWeight="bold">{i18n.t(label)}:</Box> {i18n.t(value)}
</Typography>
);
const getSummaryColumn = useCallback((incidentActionSummary: IncidentActionSummary) => {
const { field, label, value } = incidentActionSummary;

switch (field) {
case "criticalInfoRequirements":
case "expectedResults":
case "planningAssumptions":
case "responseObjectives":
case "responseStrategies":
case "responseActivitiesNarrative":
return (
<Typography key={field}>
<Box fontWeight="bold">{i18n.t(label)}:</Box>
<TextPreview value={value} />
</Typography>
);
default:
return (
<Typography key={field}>
<Box fontWeight="bold">{i18n.t(label)}:</Box>
{i18n.t(value)}
</Typography>
);
}
}, []);

return formSummary ? (
Expand All @@ -63,9 +85,7 @@ export const ActionPlanFormSummary: React.FC<ActionPlanFormSummaryProps> = React
titleVariant="secondary"
>
<SummaryContainer>
{formSummary.summary.map((labelWithValue, index) =>
getSummaryColumn(index, labelWithValue.label, labelWithValue.value)
)}
{formSummary.summary.map(summaryItem => getSummaryColumn(summaryItem))}
</SummaryContainer>
</Section>
</>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { FormSummaryData } from "../../../pages/event-tracker/useDiseaseOutbreak
import { Maybe } from "../../../../utils/ts-utils";
import { Id } from "../../../../domain/entities/Ref";
import { GlobalMessage } from "../../../pages/form-page/useForm";
import { TextPreview } from "../../text-editor/TextEditor";

export type EventTrackerFormSummaryProps = {
id: Id;
Expand Down Expand Up @@ -141,7 +142,7 @@ export const EventTrackerFormSummary: React.FC<EventTrackerFormSummaryProps> = R
<Box fontWeight="bold" display="inline">
{i18n.t("Notes")}:
</Box>{" "}
{formSummary.notes}
<TextPreview value={formSummary.notes} />
</StyledType>
</Section>
</>
Expand Down
111 changes: 111 additions & 0 deletions src/webapp/components/text-editor/TextEditor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { FormHelperText, InputLabel } from "@material-ui/core";
import React from "react";
import ReactQuill from "react-quill";
import styled from "styled-components";
import "react-quill/dist/quill.snow.css";

type TextEditorProps = {
id: string;
label?: string;
value: string;
onChange: (newValue: string) => void;
helperText?: string;
errorText?: string;
required?: boolean;
disabled?: boolean;
error?: boolean;
};

export const TextEditor: React.FC<TextEditorProps> = React.memo(
({
id,
label,
value,
onChange,
helperText = "",
errorText = "",
required = false,
disabled = false,
error = false,
}) => {
return (
<TextEditorContainer>
{label && (
<Label className={required ? "required" : ""} htmlFor={id}>
{label}
</Label>
)}

<ReactQuill
value={value}
onChange={onChange}
modules={{ toolbar: toolbarOptions }}
formats={formats}
readOnly={disabled}
/>

<StyledFormHelperText id={`${id}-helper-text`} error={error && !!errorText}>
{error && !!errorText ? errorText : helperText}{" "}
</StyledFormHelperText>
</TextEditorContainer>
);
}
);

export const TextPreview: React.FC<{
value: string;
}> = React.memo(({ value }) => {
return <StyledTextPreview dangerouslySetInnerHTML={{ __html: value }} />;
});

const StyledTextPreview = styled.div`
.ql-size-large {
font-size: 1.5em;
}
.ql-size-huge {
font-size: 2.5em;
}
.ql-size-small {
font-size: 0.75em;
}
`;

const TextEditorContainer = styled.div`
width: 100%;
.ql-editor {
line-height: 2;
p {
margin: 8px;
}
}
`;

const Label = styled(InputLabel)`
display: inline-block;
font-weight: 700;
font-size: 0.875rem;
color: ${props => props.theme.palette.text.primary};
margin-block-end: 8px;
&.required::after {
content: "*";
color: ${props => props.theme.palette.common.red};
margin-inline-start: 4px;
}
`;

const StyledFormHelperText = styled(FormHelperText)<{ error?: boolean }>`
color: ${props =>
props.error ? props.theme.palette.common.red700 : props.theme.palette.common.grey700};
`;

const toolbarOptions = [
[{ bold: true }, { italic: true }, { underline: true }],
[{ size: ["small", false, "large", "huge"] }],
[{ color: [] }],
[{ align: [] }],
[{ list: "ordered" }, { list: "bullet" }],
];

const formats = ["bold", "italic", "underline", "size", "color", "align", "list", "bullet"];
Original file line number Diff line number Diff line change
Expand Up @@ -720,9 +720,8 @@ function getInitialFormStateForDiseaseOutbreakEvent(
id: fromIdsDictionary("notes"),
isVisible: true,
errors: [],
type: "text",
type: "text-editor",
value: diseaseOutbreakEvent?.notes || "",
multiline: true,
},
],
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,9 +101,8 @@ export function mapIncidentActionPlanToInitialFormState(
id: `${actionPlanConstants.criticalInfoRequirements}-section`,
isVisible: true,
errors: [],
type: "text",
type: "text-editor",
value: incidentActionPlan?.criticalInfoRequirements || "",
multiline: true,
required: true,
},
],
Expand All @@ -119,9 +118,8 @@ export function mapIncidentActionPlanToInitialFormState(
id: `${actionPlanConstants.planningAssumptions}-section`,
isVisible: true,
errors: [],
type: "text",
type: "text-editor",
value: incidentActionPlan?.planningAssumptions || "",
multiline: true,
required: true,
helperText:
"Evidence based facts and assumptions in the context of developing the plan.",
Expand All @@ -139,9 +137,8 @@ export function mapIncidentActionPlanToInitialFormState(
id: `${actionPlanConstants.responseObjectives}-section`,
isVisible: true,
errors: [],
type: "text",
type: "text-editor",
value: incidentActionPlan?.responseObjectives || "",
multiline: true,
required: true,
helperText: "SMART: Specific, measurable, achievable, relevant, time-bound",
},
Expand All @@ -158,9 +155,8 @@ export function mapIncidentActionPlanToInitialFormState(
id: `${actionPlanConstants.responseStrategies}-section`,
isVisible: true,
errors: [],
type: "text",
type: "text-editor",
value: incidentActionPlan?.responseStrategies || "",
multiline: true,
required: true,
},
],
Expand All @@ -175,9 +171,8 @@ export function mapIncidentActionPlanToInitialFormState(
id: `${actionPlanConstants.expectedResults}-section`,
isVisible: true,
errors: [],
type: "text",
type: "text-editor",
value: incidentActionPlan?.expectedResults || "",
multiline: true,
helperText: "Clear objectives for functional area",
},
],
Expand All @@ -192,9 +187,8 @@ export function mapIncidentActionPlanToInitialFormState(
id: `${actionPlanConstants.responseActivitiesNarrative}-section`,
isVisible: true,
errors: [],
type: "text",
type: "text-editor",
value: incidentActionPlan?.responseActivitiesNarrative || "",
multiline: true,
},
],
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -723,7 +723,7 @@ function getRiskAssessmentStdQuestionSection(
label: "Rational",
isVisible: true,
errors: [],
type: "text",
type: "text-editor",
value: riskAssessmentQuestionnaire ? riskAssessmentQuestionnaire[id].rational : "",
required: false,
showIsRequired: true,
Expand Down Expand Up @@ -824,7 +824,7 @@ function getRiskAssessmentCustomQuestionSection(
label: "Rational",
isVisible: true,
errors: [],
type: "text",
type: "text-editor",
value: riskAssessmentQuestionnaire
? riskAssessmentQuestionnaire[id][index]?.rational || ""
: "",
Expand Down
10 changes: 9 additions & 1 deletion src/webapp/pages/incident-action-plan/useIncidentActionPlan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
} from "../../../data/repositories/utils/DateTimeHelper";
import { TableColumn, TableRowType } from "../../components/table/BasicTable";
import {
actionPlanConstants,
getIAPTypeByCode,
getPhoecLevelByCode,
getStatusTypeByCode,
Expand All @@ -22,9 +23,11 @@ import { useCurrentEventTracker } from "../../contexts/current-event-tracker-con
import { DiseaseOutbreakEvent } from "../../../domain/entities/disease-outbreak-event/DiseaseOutbreakEvent";
import _c from "../../../domain/entities/generic/Collection";

export type IncidentActionSummary = Option & { field: keyof typeof actionPlanConstants };

export type IncidentActionFormSummaryData = {
subTitle: string;
summary: Option[];
summary: IncidentActionSummary[];
};

export type UIIncidentActionOptions = {
Expand Down Expand Up @@ -222,22 +225,27 @@ const mapIncidentActionPlanToFormSummary = (
{
label: "Response mode critical information requirements (CIRs)",
value: actionPlan.criticalInfoRequirements ?? "",
field: "criticalInfoRequirements",
},
{
label: "Planning assumptions",
value: actionPlan.planningAssumptions ?? "",
field: "planningAssumptions",
},
{
label: "Response objectives (SMART)",
value: actionPlan.responseObjectives ?? "",
field: "responseObjectives",
},
{
label: "Sections, functional area operational objectives, expected results",
value: actionPlan.expectedResults ?? "",
field: "expectedResults",
},
{
label: "Response activities narrative",
value: actionPlan.responseActivitiesNarrative ?? "",
field: "responseActivitiesNarrative",
},
],
};
Expand Down
Loading

0 comments on commit dac07fb

Please sign in to comment.