From ebe249f5d3518749f7b4d9ae4b8b25034b9466af Mon Sep 17 00:00:00 2001
From: Chukwudumebi Onwuli <37223065+deeonwuli@users.noreply.github.com>
Date: Mon, 6 Jan 2025 15:59:34 +0100
Subject: [PATCH 01/27] feat: create resource form
---
src/domain/entities/ConfigurableForm.ts | 11 ++-
.../DiseaseOutbreakEvent.ts | 2 +
src/domain/entities/resources/Resource.ts | 8 ++
.../usecases/GetConfigurableFormUseCase.ts | 10 ++
.../usecases/GetDiseaseOutbreakByIdUseCase.ts | 1 +
src/domain/usecases/SaveEntityUseCase.ts | 3 +
.../resources/GetResourceConfigurableForm.ts | 44 +++++++++
src/webapp/components/form/FieldWidget.tsx | 4 +
src/webapp/components/form/FormFieldsState.ts | 23 ++++-
src/webapp/hooks/useRoutes.ts | 1 +
src/webapp/pages/form-page/FormPage.tsx | 3 +-
.../pages/form-page/mapEntityToFormState.ts | 3 +
.../form-page/mapFormStateToEntityData.ts | 22 +++++
.../mapResourcesToInitialFormState.ts | 98 +++++++++++++++++++
src/webapp/pages/form-page/useForm.ts | 10 ++
.../updateDiseaseOutbreakEventFormState.ts | 3 +-
src/webapp/pages/resources/ResourcesPage.tsx | 47 ++++++++-
17 files changed, 285 insertions(+), 8 deletions(-)
create mode 100644 src/domain/entities/resources/Resource.ts
create mode 100644 src/domain/usecases/utils/resources/GetResourceConfigurableForm.ts
create mode 100644 src/webapp/pages/form-page/resources/mapResourcesToInitialFormState.ts
diff --git a/src/domain/entities/ConfigurableForm.ts b/src/domain/entities/ConfigurableForm.ts
index fb472a46..497099b8 100644
--- a/src/domain/entities/ConfigurableForm.ts
+++ b/src/domain/entities/ConfigurableForm.ts
@@ -14,6 +14,7 @@ import { ActionPlanAttrs } from "./incident-action-plan/ActionPlan";
import { ResponseAction } from "./incident-action-plan/ResponseAction";
import { IncidentManagementTeam } from "./incident-management-team/IncidentManagementTeam";
import { Role } from "./incident-management-team/Role";
+import { Resource } from "./resources/Resource";
export type DiseaseOutbreakEventOptions = {
dataSources: Option[];
@@ -120,6 +121,13 @@ export type SingleResponseActionFormData = BaseFormData & {
options: IncidentResponseActionOptions;
};
+export type ResourceFormData = BaseFormData & {
+ type: "resource";
+ entity: Maybe;
+ uploadedResourceFile: Maybe;
+ uploadedResourceFileId: Maybe;
+};
+
export type IncidentManagementTeamRoleOptions = {
roles: Role[];
teamMembers: TeamMember[];
@@ -143,4 +151,5 @@ export type ConfigurableForm =
| ActionPlanFormData
| ResponseActionFormData
| SingleResponseActionFormData
- | IncidentManagementTeamMemberFormData;
+ | IncidentManagementTeamMemberFormData
+ | ResourceFormData;
diff --git a/src/domain/entities/disease-outbreak-event/DiseaseOutbreakEvent.ts b/src/domain/entities/disease-outbreak-event/DiseaseOutbreakEvent.ts
index d6cbf1c4..ee9422ee 100644
--- a/src/domain/entities/disease-outbreak-event/DiseaseOutbreakEvent.ts
+++ b/src/domain/entities/disease-outbreak-event/DiseaseOutbreakEvent.ts
@@ -6,6 +6,7 @@ import { Code, NamedRef } from "../Ref";
import { RiskAssessment } from "../risk-assessment/RiskAssessment";
import { Maybe } from "../../../utils/ts-utils";
import { ValidationError } from "../ValidationError";
+import { Resource } from "../resources/Resource";
export const hazardTypes = [
"Biological:Human",
@@ -80,6 +81,7 @@ export type DiseaseOutbreakEventAttrs = DiseaseOutbreakEventBaseAttrs & {
riskAssessment: Maybe;
incidentActionPlan: Maybe;
incidentManagementTeam: Maybe;
+ resource: Maybe;
};
/**
diff --git a/src/domain/entities/resources/Resource.ts b/src/domain/entities/resources/Resource.ts
new file mode 100644
index 00000000..4d8fa123
--- /dev/null
+++ b/src/domain/entities/resources/Resource.ts
@@ -0,0 +1,8 @@
+export enum ResourceType {
+ TEMPLATE = "template",
+ RESOURCE_DOCUMENT = "resource-document",
+}
+
+export type Resource = {
+ resourceType: ResourceType;
+};
diff --git a/src/domain/usecases/GetConfigurableFormUseCase.ts b/src/domain/usecases/GetConfigurableFormUseCase.ts
index baf9adf3..3451c942 100644
--- a/src/domain/usecases/GetConfigurableFormUseCase.ts
+++ b/src/domain/usecases/GetConfigurableFormUseCase.ts
@@ -18,6 +18,7 @@ import {
getSingleResponseActionConfigurableForm,
} from "./utils/incident-action/GetResponseActionConfigurableForm";
import { getIncidentManagementTeamWithOptions } from "./utils/incident-management-team/GetIncidentManagementTeamWithOptions";
+import { getResourceConfigurableForm } from "./utils/resources/GetResourceConfigurableForm";
import { getRiskAssessmentGradingConfigurableForm } from "./utils/risk-assessment/GetGradingConfigurableForm";
import { getRiskAssessmentQuestionnaireConfigurableForm } from "./utils/risk-assessment/GetQuestionnaireConfigurableForm";
import { getRiskAssessmentSummaryConfigurableForm } from "./utils/risk-assessment/GetSummaryConfigurableForm";
@@ -124,6 +125,15 @@ export class GetConfigurableFormUseCase {
},
configurations
);
+ case "resource":
+ if (!eventTrackerDetails)
+ return Future.error(
+ new Error(
+ "Disease outbreak id is required for incident management team member builder"
+ )
+ );
+
+ return getResourceConfigurableForm(eventTrackerDetails);
default:
return Future.error(new Error("Form type not supported"));
}
diff --git a/src/domain/usecases/GetDiseaseOutbreakByIdUseCase.ts b/src/domain/usecases/GetDiseaseOutbreakByIdUseCase.ts
index 93fa90e1..5a8aff30 100644
--- a/src/domain/usecases/GetDiseaseOutbreakByIdUseCase.ts
+++ b/src/domain/usecases/GetDiseaseOutbreakByIdUseCase.ts
@@ -81,6 +81,7 @@ export class GetDiseaseOutbreakByIdUseCase {
riskAssessment: riskAssessment,
incidentActionPlan: undefined, //IAP is fetched on menu click. It is not needed here.
incidentManagementTeam: undefined, //IMT is fetched on menu click. It is not needed here.
+ resource: undefined, // Resource is fetched on menu click. It is not needed here.
});
return Future.success(diseaseOutbreakEvent);
});
diff --git a/src/domain/usecases/SaveEntityUseCase.ts b/src/domain/usecases/SaveEntityUseCase.ts
index b1f581a0..fb835928 100644
--- a/src/domain/usecases/SaveEntityUseCase.ts
+++ b/src/domain/usecases/SaveEntityUseCase.ts
@@ -117,6 +117,9 @@ export class SaveEntityUseCase {
);
}
}
+ case "resource":
+ console.log("saved resource");
+ return Future.success(undefined);
default:
return Future.error(new Error("Form type not supported"));
}
diff --git a/src/domain/usecases/utils/resources/GetResourceConfigurableForm.ts b/src/domain/usecases/utils/resources/GetResourceConfigurableForm.ts
new file mode 100644
index 00000000..3a2c757b
--- /dev/null
+++ b/src/domain/usecases/utils/resources/GetResourceConfigurableForm.ts
@@ -0,0 +1,44 @@
+import { FutureData } from "../../../../data/api-futures";
+import { FormLables, ResourceFormData } from "../../../entities/ConfigurableForm";
+import { DiseaseOutbreakEvent } from "../../../entities/disease-outbreak-event/DiseaseOutbreakEvent";
+import { Future } from "../../../entities/generic/Future";
+import { ResourceType } from "../../../entities/resources/Resource";
+import { Rule } from "../../../entities/Rule";
+
+export function getResourceConfigurableForm(
+ eventTrackerDetails: DiseaseOutbreakEvent
+): FutureData {
+ const { rules, labels } = getResourceLabelsRules();
+
+ const resourceFormData: ResourceFormData = {
+ type: "resource",
+ entity: eventTrackerDetails.resource,
+ uploadedResourceFile: undefined,
+ uploadedResourceFileId: undefined,
+ labels: labels,
+ rules: rules,
+ };
+
+ return Future.success(resourceFormData);
+}
+
+function getResourceLabelsRules(): { rules: Rule[]; labels: FormLables } {
+ return {
+ // TODO: Get labels from Datastore used in mapEntityToInitialFormState to create initial form state
+ labels: {
+ errors: {
+ field_is_required: "This field is required",
+ field_is_required_na: "This field is required when not applicable",
+ },
+ },
+ // TODO: Get rules from Datastore used in applyRulesInFormState
+ rules: [
+ {
+ type: "toggleSectionsVisibilityByFieldValue",
+ fieldId: "resourceType",
+ fieldValue: ResourceType.RESOURCE_DOCUMENT,
+ sectionIds: ["resourceFolder_section"],
+ },
+ ],
+ };
+}
diff --git a/src/webapp/components/form/FieldWidget.tsx b/src/webapp/components/form/FieldWidget.tsx
index bfdcdfcf..f6061c0c 100644
--- a/src/webapp/components/form/FieldWidget.tsx
+++ b/src/webapp/components/form/FieldWidget.tsx
@@ -100,5 +100,9 @@ export const FieldWidget: React.FC = React.memo((props): JSX.E
/>
);
}
+
+ case "file": {
+ return ;
+ }
}
});
diff --git a/src/webapp/components/form/FormFieldsState.ts b/src/webapp/components/form/FormFieldsState.ts
index 2b11d789..c8e68d0d 100644
--- a/src/webapp/components/form/FormFieldsState.ts
+++ b/src/webapp/components/form/FormFieldsState.ts
@@ -6,7 +6,15 @@ import { ValidationError, ValidationErrorKey } from "../../../domain/entities/Va
import { FormSectionState } from "./FormSectionsState";
import { Rule } from "../../../domain/entities/Rule";
-export type FieldType = "text" | "boolean" | "select" | "radio" | "date" | "user" | "addNew";
+export type FieldType =
+ | "text"
+ | "boolean"
+ | "select"
+ | "radio"
+ | "date"
+ | "user"
+ | "addNew"
+ | "file";
type FormFieldStateBase = {
id: string;
@@ -57,6 +65,12 @@ export type FormAvatarFieldState = FormFieldStateBase> & {
options: User[];
};
+export type FormFileFieldState = FormFieldStateBase> & {
+ type: "file";
+ fileId: Maybe;
+ fileNameLabel?: string;
+};
+
export type AddNewFieldState = FormFieldStateBase & {
type: "addNew";
};
@@ -67,7 +81,8 @@ export type FormFieldState =
| FormMultipleOptionsFieldState
| FormBooleanFieldState
| FormDateFieldState
- | FormAvatarFieldState;
+ | FormAvatarFieldState
+ | FormFileFieldState;
// HELPERS:
@@ -137,6 +152,8 @@ export function getFieldWithEmptyValue(field: FormFieldState): FormFieldState {
return { ...field, value: null };
case "user":
return { ...field, value: undefined };
+ case "file":
+ return { ...field, value: undefined, fileId: undefined };
}
}
@@ -202,7 +219,7 @@ export function validateField(
? {
property: field.id,
errors: errors,
- value: field.value,
+ value: field.type === "file" ? field.fileId : field.value,
}
: undefined;
}
diff --git a/src/webapp/hooks/useRoutes.ts b/src/webapp/hooks/useRoutes.ts
index 9b50504f..4424a72b 100644
--- a/src/webapp/hooks/useRoutes.ts
+++ b/src/webapp/hooks/useRoutes.ts
@@ -23,6 +23,7 @@ const formTypes = [
"incident-response-actions",
"incident-response-action",
"incident-management-team-member-assignment",
+ "resource",
] as const satisfies FormType[];
const formType = `:formType(${join(formTypes, "|")})` as const;
diff --git a/src/webapp/pages/form-page/FormPage.tsx b/src/webapp/pages/form-page/FormPage.tsx
index 5408a886..31765155 100644
--- a/src/webapp/pages/form-page/FormPage.tsx
+++ b/src/webapp/pages/form-page/FormPage.tsx
@@ -15,7 +15,8 @@ export type FormType =
| "incident-action-plan"
| "incident-response-actions"
| "incident-response-action"
- | "incident-management-team-member-assignment";
+ | "incident-management-team-member-assignment"
+ | "resource";
export const FormPage: React.FC = React.memo(() => {
const { formType, id } = useParams<{
diff --git a/src/webapp/pages/form-page/mapEntityToFormState.ts b/src/webapp/pages/form-page/mapEntityToFormState.ts
index 386ecb7f..46529590 100644
--- a/src/webapp/pages/form-page/mapEntityToFormState.ts
+++ b/src/webapp/pages/form-page/mapEntityToFormState.ts
@@ -15,6 +15,7 @@ import {
mapSingleIncidentResponseActionToInitialFormState,
} from "./incident-action/mapIncidentActionToInitialFormState";
import { mapIncidentManagementTeamMemberToInitialFormState } from "./incident-management-team-member-assignment/mapIncidentManagementTeamMemberToInitialFormState";
+import { mapResourcesToInitialFormState } from "./resources/mapResourcesToInitialFormState";
import {
mapRiskAssessmentQuestionnaireToInitialFormState,
mapRiskAssessmentSummaryToInitialFormState,
@@ -56,6 +57,8 @@ export function mapEntityToFormState(options: {
);
case "incident-management-team-member-assignment":
return mapIncidentManagementTeamMemberToInitialFormState(configurableForm);
+ case "resource":
+ return mapResourcesToInitialFormState(configurableForm);
}
}
diff --git a/src/webapp/pages/form-page/mapFormStateToEntityData.ts b/src/webapp/pages/form-page/mapFormStateToEntityData.ts
index 8f360a09..26492d14 100644
--- a/src/webapp/pages/form-page/mapFormStateToEntityData.ts
+++ b/src/webapp/pages/form-page/mapFormStateToEntityData.ts
@@ -25,6 +25,7 @@ import {
RiskAssessmentSummaryFormData,
ResponseActionFormData,
SingleResponseActionFormData,
+ ResourceFormData,
} from "../../../domain/entities/ConfigurableForm";
import { Maybe } from "../../../utils/ts-utils";
import { RiskAssessmentGrading } from "../../../domain/entities/risk-assessment/RiskAssessmentGrading";
@@ -53,6 +54,7 @@ import {
import { TeamMember } from "../../../domain/entities/incident-management-team/TeamMember";
import { TEAM_ROLE_FIELD_ID } from "./incident-management-team-member-assignment/mapIncidentManagementTeamMemberToInitialFormState";
import { incidentManagementTeamBuilderCodesWithoutRoles } from "../../../data/repositories/consts/IncidentManagementTeamBuilderConstants";
+import { Resource, ResourceType } from "../../../domain/entities/resources/Resource";
export function mapFormStateToEntityData(
formState: FormState,
@@ -136,6 +138,15 @@ export function mapFormStateToEntityData(
};
return incidentManagementTeamMemberForm;
}
+ case "resource": {
+ const resource = mapFormStateToResource(formState, formData);
+ const resourceForm: ResourceFormData = {
+ ...formData,
+ entity: resource,
+ };
+
+ return resourceForm;
+ }
default:
return formData;
@@ -756,6 +767,17 @@ function mapFormStateToIncidentManagementTeamMember(
});
}
+function mapFormStateToResource(formState: FormState, _formData: ResourceFormData): Resource {
+ const allFields: FormFieldState[] = getAllFieldsFromSections(formState.sections);
+
+ const resource: Resource = {
+ resourceType: allFields.find(field => field.id.includes("resourceType"))
+ ?.value as ResourceType,
+ };
+
+ return resource;
+}
+
function extractIndex(input: string): number | undefined {
const parts = input.split("_");
const lastPart = parts[parts.length - 1];
diff --git a/src/webapp/pages/form-page/resources/mapResourcesToInitialFormState.ts b/src/webapp/pages/form-page/resources/mapResourcesToInitialFormState.ts
new file mode 100644
index 00000000..df59d820
--- /dev/null
+++ b/src/webapp/pages/form-page/resources/mapResourcesToInitialFormState.ts
@@ -0,0 +1,98 @@
+import { ResourceFormData } from "../../../../domain/entities/ConfigurableForm";
+import { ResourceType } from "../../../../domain/entities/resources/Resource";
+import { FormState } from "../../../components/form/FormState";
+
+export function mapResourcesToInitialFormState(formData: ResourceFormData): FormState {
+ const { entity: resource, uploadedResourceFile, uploadedResourceFileId } = formData;
+
+ const isResourceDocument = resource?.resourceType === ResourceType.RESOURCE_DOCUMENT;
+
+ return {
+ id: "",
+ title: "Resources",
+ subtitle: "",
+ saveButtonLabel: "Save",
+ isValid: false,
+ sections: [
+ {
+ title: "Resource type",
+ id: "resourceType_section",
+ isVisible: true,
+ required: true,
+ fields: [
+ {
+ id: "resourceType",
+ placeholder: "Select a resource type",
+ isVisible: true,
+ errors: [],
+ type: "select",
+ multiple: false,
+ options: [
+ {
+ value: ResourceType.RESOURCE_DOCUMENT,
+ label: "Resource document",
+ },
+ {
+ value: ResourceType.TEMPLATE,
+ label: "Template",
+ },
+ ],
+ value: resource?.resourceType || "",
+ required: true,
+ },
+ ],
+ },
+ {
+ title: "Resource name",
+ id: "resourceName_section",
+ isVisible: true,
+ required: true,
+ fields: [
+ {
+ id: "resourceName",
+ isVisible: true,
+ errors: [],
+ type: "text",
+ value: "",
+ required: true,
+ },
+ ],
+ },
+ {
+ title: "Resource folder",
+ id: "resourceFolder_section",
+ isVisible: isResourceDocument,
+ required: true,
+ fields: [
+ {
+ id: "resourceFolder",
+ isVisible: isResourceDocument,
+ errors: [],
+ type: "select",
+ options: [],
+ multiple: false,
+ value: isResourceDocument ? "" : "",
+ required: true,
+ },
+ ],
+ },
+ {
+ title: "Resource file",
+ id: "resourceFile_section",
+ isVisible: true,
+ required: false,
+ fields: [
+ {
+ id: "resourceFile",
+ isVisible: true,
+ errors: [],
+ type: "file",
+ value: uploadedResourceFile || undefined,
+ fileId: uploadedResourceFileId || undefined,
+ required: false,
+ },
+ ],
+ },
+ ],
+ };
+}
diff --git a/src/webapp/pages/form-page/useForm.ts b/src/webapp/pages/form-page/useForm.ts
index a340bd27..7fa0f435 100644
--- a/src/webapp/pages/form-page/useForm.ts
+++ b/src/webapp/pages/form-page/useForm.ts
@@ -443,6 +443,13 @@ export function useForm(formType: FormType, id?: Id): State {
type: "success",
});
break;
+ case "resource":
+ goTo(RouteName.RESOURCES);
+ setGlobalMessage({
+ text: i18n.t(`Resource saved successfully`),
+ type: "success",
+ });
+ break;
}
},
err => {
@@ -479,6 +486,9 @@ export function useForm(formType: FormType, id?: Id): State {
id: currentEventTracker.id,
});
break;
+ case "resource":
+ goTo(RouteName.RESOURCES);
+ break;
default:
goTo(RouteName.EVENT_TRACKER, {
id: currentEventTracker.id,
diff --git a/src/webapp/pages/form-page/utils/updateDiseaseOutbreakEventFormState.ts b/src/webapp/pages/form-page/utils/updateDiseaseOutbreakEventFormState.ts
index c098c297..32bdf483 100644
--- a/src/webapp/pages/form-page/utils/updateDiseaseOutbreakEventFormState.ts
+++ b/src/webapp/pages/form-page/utils/updateDiseaseOutbreakEventFormState.ts
@@ -86,8 +86,9 @@ function validateFormState(
configurableForm?.currentIncidentManagementTeam?.teamHierarchy || [],
updatedField.id
);
- break;
}
+ case "resource":
+ break;
}
return [...formValidationErrors, ...entityValidationErrors];
diff --git a/src/webapp/pages/resources/ResourcesPage.tsx b/src/webapp/pages/resources/ResourcesPage.tsx
index f3ce3e51..33f2a95a 100644
--- a/src/webapp/pages/resources/ResourcesPage.tsx
+++ b/src/webapp/pages/resources/ResourcesPage.tsx
@@ -1,8 +1,51 @@
-import React from "react";
+import React, { useCallback } from "react";
import { Layout } from "../../components/layout/Layout";
import i18n from "../../../utils/i18n";
+import { Section } from "../../components/section/Section";
+import { Button } from "@material-ui/core";
+import { FileFileUpload } from "material-ui/svg-icons";
+import { RouteName, useRoutes } from "../../hooks/useRoutes";
export const ResourcesPage: React.FC = React.memo(() => {
- return ;
+ const { goTo } = useRoutes();
+
+ const onUploadFileClick = useCallback(() => {
+ goTo(RouteName.CREATE_FORM, { formType: "resource" });
+ }, [goTo]);
+
+ const uploadButton = (
+ }
+ >
+ {i18n.t("Upload File")}
+
+ );
+
+ return (
+
+
+
+
+
Response Documents
+
hi
+
+
+
+
+
+ );
});
From 53bc91cd87dad01335332207c024f626a924b0a0 Mon Sep 17 00:00:00 2001
From: Chukwudumebi Onwuli <37223065+deeonwuli@users.noreply.github.com>
Date: Mon, 6 Jan 2025 22:02:12 +0100
Subject: [PATCH 02/27] feat: save resources to datastore and get all resources
---
src/CompositionRoot.ts | 10 ++
.../ResourceDataStoreRepository.ts | 46 ++++++++
.../test/ResourceTestRepository.ts | 31 +++++
src/domain/entities/ConfigurableForm.ts | 4 +
.../DiseaseOutbreakEvent.ts | 2 +-
src/domain/entities/resources/Resource.ts | 14 ++-
src/domain/repositories/ResourceRepository.ts | 9 ++
.../usecases/GetDiseaseOutbreakByIdUseCase.ts | 2 +-
src/domain/usecases/GetResourcesUseCase.ts | 30 +++++
src/domain/usecases/SaveEntityUseCase.ts | 5 +-
.../resources/GetResourceConfigurableForm.ts | 2 +-
.../pages/form-page/mapEntityToFormState.ts | 4 +-
.../form-page/mapFormStateToEntityData.ts | 16 ++-
...te.ts => mapResourceToInitialFormState.ts} | 15 +--
src/webapp/pages/resources/ResourcesPage.tsx | 110 +++++++++++++++---
15 files changed, 262 insertions(+), 38 deletions(-)
create mode 100644 src/data/repositories/ResourceDataStoreRepository.ts
create mode 100644 src/data/repositories/test/ResourceTestRepository.ts
create mode 100644 src/domain/repositories/ResourceRepository.ts
create mode 100644 src/domain/usecases/GetResourcesUseCase.ts
rename src/webapp/pages/form-page/resources/{mapResourcesToInitialFormState.ts => mapResourceToInitialFormState.ts} (88%)
diff --git a/src/CompositionRoot.ts b/src/CompositionRoot.ts
index f54db51d..673007d8 100644
--- a/src/CompositionRoot.ts
+++ b/src/CompositionRoot.ts
@@ -71,6 +71,10 @@ import { CompleteEventTrackerUseCase } from "./domain/usecases/CompleteEventTrac
import { UserGroupD2Repository } from "./data/repositories/UserGroupD2Repository";
import { UserGroupRepository } from "./domain/repositories/UserGroupRepository";
import { UserGroupTestRepository } from "./data/repositories/test/UserGroupTestRepository";
+import { ResourceRepository } from "./domain/repositories/ResourceRepository";
+import { ResourceDataStoreRepository } from "./data/repositories/ResourceDataStoreRepository";
+import { ResourceTestRepository } from "./data/repositories/test/ResourceTestRepository";
+import { GetResourcesUseCase } from "./domain/usecases/GetResourcesUseCase";
export type CompositionRoot = ReturnType;
@@ -91,6 +95,7 @@ type Repositories = {
systemRepository: SystemRepository;
configurationsRepository: ConfigurationsRepository;
userGroupRepository: UserGroupRepository;
+ resourceRepository: ResourceRepository;
};
function getCompositionRoot(repositories: Repositories) {
@@ -144,6 +149,9 @@ function getCompositionRoot(repositories: Repositories) {
charts: {
getCases: new GetChartConfigByTypeUseCase(repositories.chartConfigRepository),
},
+ resources: {
+ get: new GetResourcesUseCase(repositories),
+ },
};
}
@@ -166,6 +174,7 @@ export function getWebappCompositionRoot(api: D2Api) {
systemRepository: new SystemD2Repository(api),
configurationsRepository: new ConfigurationsD2Repository(api),
userGroupRepository: new UserGroupD2Repository(api),
+ resourceRepository: new ResourceDataStoreRepository(api),
};
return getCompositionRoot(repositories);
@@ -189,6 +198,7 @@ export function getTestCompositionRoot() {
systemRepository: new SystemTestRepository(),
configurationsRepository: new ConfigurationsTestRepository(),
userGroupRepository: new UserGroupTestRepository(),
+ resourceRepository: new ResourceTestRepository(),
};
return getCompositionRoot(repositories);
diff --git a/src/data/repositories/ResourceDataStoreRepository.ts b/src/data/repositories/ResourceDataStoreRepository.ts
new file mode 100644
index 00000000..4912b5e5
--- /dev/null
+++ b/src/data/repositories/ResourceDataStoreRepository.ts
@@ -0,0 +1,46 @@
+import { D2Api } from "@eyeseetea/d2-api/2.36";
+import { ResourceRepository } from "../../domain/repositories/ResourceRepository";
+import { DataStoreClient } from "../DataStoreClient";
+import { Resource } from "../../domain/entities/resources/Resource";
+import { FutureData } from "../api-futures";
+import { Future } from "../../domain/entities/generic/Future";
+import { ResourceFormData } from "../../domain/entities/ConfigurableForm";
+
+const RESOURCES_KEY = "resources";
+
+export class ResourceDataStoreRepository implements ResourceRepository {
+ private dataStoreClient: DataStoreClient;
+
+ constructor(private api: D2Api) {
+ this.dataStoreClient = new DataStoreClient(api);
+ }
+
+ getAllResources(): FutureData {
+ return this.dataStoreClient
+ .getObject(RESOURCES_KEY)
+ .flatMap(resources => Future.success(resources ?? []));
+ }
+
+ saveResource(formData: ResourceFormData): FutureData {
+ const { entity: resource } = formData;
+ if (!resource) throw new Error("No resource form data found");
+
+ return this.getAllResources().flatMap(resourcesInDataStore => {
+ const updatedResources = resourcesInDataStore.some(
+ resourceInDataStore => resourceInDataStore.resourceLabel === resource.resourceLabel
+ )
+ ? resourcesInDataStore.map(resourceInDataStore =>
+ resourceInDataStore.resourceLabel === resource.resourceLabel
+ ? resource
+ : resourceInDataStore
+ )
+ : [...resourcesInDataStore, resource];
+
+ return this.dataStoreClient.saveObject(RESOURCES_KEY, updatedResources);
+ });
+ }
+
+ deleteResource(): FutureData {
+ throw new Error("Method not implemented.");
+ }
+}
diff --git a/src/data/repositories/test/ResourceTestRepository.ts b/src/data/repositories/test/ResourceTestRepository.ts
new file mode 100644
index 00000000..25488d7e
--- /dev/null
+++ b/src/data/repositories/test/ResourceTestRepository.ts
@@ -0,0 +1,31 @@
+import { ResourceFormData } from "../../../domain/entities/ConfigurableForm";
+import { Future } from "../../../domain/entities/generic/Future";
+import { Resource, ResourceType } from "../../../domain/entities/resources/Resource";
+import { ResourceRepository } from "../../../domain/repositories/ResourceRepository";
+import { FutureData } from "../../api-futures";
+
+export class ResourceTestRepository implements ResourceRepository {
+ getAllResources(): FutureData {
+ const resources: Resource[] = [
+ {
+ resourceLabel: "Incident Action Plan",
+ resourceType: ResourceType.TEMPLATE,
+ },
+ {
+ resourceLabel: "Excel line list",
+ resourceType: ResourceType.RESOURCE_DOCUMENT,
+ resourceFolder: "Case line lists",
+ },
+ ];
+
+ return Future.success(resources);
+ }
+
+ saveResource(_formData: ResourceFormData): FutureData {
+ return Future.success(undefined);
+ }
+
+ deleteResource(): FutureData {
+ throw new Error("Method not implemented.");
+ }
+}
diff --git a/src/domain/entities/ConfigurableForm.ts b/src/domain/entities/ConfigurableForm.ts
index 497099b8..ba82cbd4 100644
--- a/src/domain/entities/ConfigurableForm.ts
+++ b/src/domain/entities/ConfigurableForm.ts
@@ -64,6 +64,10 @@ export type IncidentResponseActionOptions = {
verification: Option[];
};
+export type ResourceOptions = {
+ resourceType: Option[];
+};
+
export type FormLables = {
errors: Record;
};
diff --git a/src/domain/entities/disease-outbreak-event/DiseaseOutbreakEvent.ts b/src/domain/entities/disease-outbreak-event/DiseaseOutbreakEvent.ts
index ee9422ee..1d5f966d 100644
--- a/src/domain/entities/disease-outbreak-event/DiseaseOutbreakEvent.ts
+++ b/src/domain/entities/disease-outbreak-event/DiseaseOutbreakEvent.ts
@@ -81,7 +81,7 @@ export type DiseaseOutbreakEventAttrs = DiseaseOutbreakEventBaseAttrs & {
riskAssessment: Maybe;
incidentActionPlan: Maybe;
incidentManagementTeam: Maybe;
- resource: Maybe;
+ resource: Resource[];
};
/**
diff --git a/src/domain/entities/resources/Resource.ts b/src/domain/entities/resources/Resource.ts
index 4d8fa123..637cf054 100644
--- a/src/domain/entities/resources/Resource.ts
+++ b/src/domain/entities/resources/Resource.ts
@@ -3,6 +3,18 @@ export enum ResourceType {
RESOURCE_DOCUMENT = "resource-document",
}
-export type Resource = {
+type ResourceBase = {
resourceType: ResourceType;
+ resourceLabel: string;
};
+
+export type ResourceDocument = ResourceBase & {
+ resourceType: ResourceType.RESOURCE_DOCUMENT;
+ resourceFolder: string;
+};
+
+export type Template = ResourceBase & {
+ resourceType: ResourceType.TEMPLATE;
+};
+
+export type Resource = ResourceDocument | Template;
diff --git a/src/domain/repositories/ResourceRepository.ts b/src/domain/repositories/ResourceRepository.ts
new file mode 100644
index 00000000..9bd98a72
--- /dev/null
+++ b/src/domain/repositories/ResourceRepository.ts
@@ -0,0 +1,9 @@
+import { FutureData } from "../../data/api-futures";
+import { ResourceFormData } from "../entities/ConfigurableForm";
+import { Resource } from "../entities/resources/Resource";
+
+export interface ResourceRepository {
+ getAllResources(): FutureData;
+ saveResource(formData: ResourceFormData): FutureData;
+ deleteResource(): FutureData;
+}
diff --git a/src/domain/usecases/GetDiseaseOutbreakByIdUseCase.ts b/src/domain/usecases/GetDiseaseOutbreakByIdUseCase.ts
index 5a8aff30..2e8e7d5f 100644
--- a/src/domain/usecases/GetDiseaseOutbreakByIdUseCase.ts
+++ b/src/domain/usecases/GetDiseaseOutbreakByIdUseCase.ts
@@ -81,7 +81,7 @@ export class GetDiseaseOutbreakByIdUseCase {
riskAssessment: riskAssessment,
incidentActionPlan: undefined, //IAP is fetched on menu click. It is not needed here.
incidentManagementTeam: undefined, //IMT is fetched on menu click. It is not needed here.
- resource: undefined, // Resource is fetched on menu click. It is not needed here.
+ resource: [], // Resource is fetched on menu click. It is not needed here.
});
return Future.success(diseaseOutbreakEvent);
});
diff --git a/src/domain/usecases/GetResourcesUseCase.ts b/src/domain/usecases/GetResourcesUseCase.ts
new file mode 100644
index 00000000..3cd74ea7
--- /dev/null
+++ b/src/domain/usecases/GetResourcesUseCase.ts
@@ -0,0 +1,30 @@
+import { FutureData } from "../../data/api-futures";
+import { Future } from "../entities/generic/Future";
+import { ResourceDocument, ResourceType, Template } from "../entities/resources/Resource";
+import { ResourceRepository } from "../repositories/ResourceRepository";
+
+type ResourceData = {
+ templates: Template[];
+ resourceDocuments: ResourceDocument[];
+};
+
+export class GetResourcesUseCase {
+ constructor(
+ private options: {
+ resourceRepository: ResourceRepository;
+ }
+ ) {}
+
+ public execute(): FutureData {
+ return this.options.resourceRepository.getAllResources().flatMap(resources => {
+ const templates = resources.filter(
+ resource => resource.resourceType === ResourceType.TEMPLATE
+ ) as Template[];
+ const resourceDocuments = resources.filter(
+ resource => resource.resourceType === ResourceType.RESOURCE_DOCUMENT
+ ) as ResourceDocument[];
+
+ return Future.success({ templates: templates, resourceDocuments: resourceDocuments });
+ });
+ }
+}
diff --git a/src/domain/usecases/SaveEntityUseCase.ts b/src/domain/usecases/SaveEntityUseCase.ts
index fb835928..1d8bb0d6 100644
--- a/src/domain/usecases/SaveEntityUseCase.ts
+++ b/src/domain/usecases/SaveEntityUseCase.ts
@@ -13,6 +13,7 @@ import { saveDiseaseOutbreak } from "./utils/disease-outbreak/SaveDiseaseOutbrea
import { RoleRepository } from "../repositories/RoleRepository";
import { Configurations } from "../entities/AppConfigurations";
import moment from "moment";
+import { ResourceRepository } from "../repositories/ResourceRepository";
export class SaveEntityUseCase {
constructor(
@@ -23,6 +24,7 @@ export class SaveEntityUseCase {
incidentManagementTeamRepository: IncidentManagementTeamRepository;
teamMemberRepository: TeamMemberRepository;
roleRepository: RoleRepository;
+ resourceRepository: ResourceRepository;
}
) {}
@@ -118,8 +120,7 @@ export class SaveEntityUseCase {
}
}
case "resource":
- console.log("saved resource");
- return Future.success(undefined);
+ return this.options.resourceRepository.saveResource(formData);
default:
return Future.error(new Error("Form type not supported"));
}
diff --git a/src/domain/usecases/utils/resources/GetResourceConfigurableForm.ts b/src/domain/usecases/utils/resources/GetResourceConfigurableForm.ts
index 3a2c757b..b7d6e52f 100644
--- a/src/domain/usecases/utils/resources/GetResourceConfigurableForm.ts
+++ b/src/domain/usecases/utils/resources/GetResourceConfigurableForm.ts
@@ -12,7 +12,7 @@ export function getResourceConfigurableForm(
const resourceFormData: ResourceFormData = {
type: "resource",
- entity: eventTrackerDetails.resource,
+ entity: eventTrackerDetails.resource.find(resource => resource.resourceLabel === "abc"),
uploadedResourceFile: undefined,
uploadedResourceFileId: undefined,
labels: labels,
diff --git a/src/webapp/pages/form-page/mapEntityToFormState.ts b/src/webapp/pages/form-page/mapEntityToFormState.ts
index 46529590..0b7cc404 100644
--- a/src/webapp/pages/form-page/mapEntityToFormState.ts
+++ b/src/webapp/pages/form-page/mapEntityToFormState.ts
@@ -15,7 +15,7 @@ import {
mapSingleIncidentResponseActionToInitialFormState,
} from "./incident-action/mapIncidentActionToInitialFormState";
import { mapIncidentManagementTeamMemberToInitialFormState } from "./incident-management-team-member-assignment/mapIncidentManagementTeamMemberToInitialFormState";
-import { mapResourcesToInitialFormState } from "./resources/mapResourcesToInitialFormState";
+import { mapResourceToInitialFormState } from "./resources/mapResourceToInitialFormState";
import {
mapRiskAssessmentQuestionnaireToInitialFormState,
mapRiskAssessmentSummaryToInitialFormState,
@@ -58,7 +58,7 @@ export function mapEntityToFormState(options: {
case "incident-management-team-member-assignment":
return mapIncidentManagementTeamMemberToInitialFormState(configurableForm);
case "resource":
- return mapResourcesToInitialFormState(configurableForm);
+ return mapResourceToInitialFormState(configurableForm);
}
}
diff --git a/src/webapp/pages/form-page/mapFormStateToEntityData.ts b/src/webapp/pages/form-page/mapFormStateToEntityData.ts
index 26492d14..256c71b9 100644
--- a/src/webapp/pages/form-page/mapFormStateToEntityData.ts
+++ b/src/webapp/pages/form-page/mapFormStateToEntityData.ts
@@ -139,7 +139,7 @@ export function mapFormStateToEntityData(
return incidentManagementTeamMemberForm;
}
case "resource": {
- const resource = mapFormStateToResource(formState, formData);
+ const resource = mapFormStateToResource(formState);
const resourceForm: ResourceFormData = {
...formData,
entity: resource,
@@ -767,12 +767,20 @@ function mapFormStateToIncidentManagementTeamMember(
});
}
-function mapFormStateToResource(formState: FormState, _formData: ResourceFormData): Resource {
+function mapFormStateToResource(formState: FormState): Resource {
const allFields: FormFieldState[] = getAllFieldsFromSections(formState.sections);
+ const resourceType = allFields.find(field => field.id.includes("resourceType"))
+ ?.value as ResourceType;
+ const resourceLabel = allFields.find(field => field.id.includes("resourceLabel"))
+ ?.value as string;
+ const resourceFolder = allFields.find(field => field.id.includes("resourceFolder"))
+ ?.value as string;
+
const resource: Resource = {
- resourceType: allFields.find(field => field.id.includes("resourceType"))
- ?.value as ResourceType,
+ resourceType: resourceType,
+ resourceLabel: resourceLabel,
+ resourceFolder: resourceFolder,
};
return resource;
diff --git a/src/webapp/pages/form-page/resources/mapResourcesToInitialFormState.ts b/src/webapp/pages/form-page/resources/mapResourceToInitialFormState.ts
similarity index 88%
rename from src/webapp/pages/form-page/resources/mapResourcesToInitialFormState.ts
rename to src/webapp/pages/form-page/resources/mapResourceToInitialFormState.ts
index df59d820..8a850392 100644
--- a/src/webapp/pages/form-page/resources/mapResourcesToInitialFormState.ts
+++ b/src/webapp/pages/form-page/resources/mapResourceToInitialFormState.ts
@@ -2,9 +2,8 @@ import { ResourceFormData } from "../../../../domain/entities/ConfigurableForm";
import { ResourceType } from "../../../../domain/entities/resources/Resource";
import { FormState } from "../../../components/form/FormState";
-export function mapResourcesToInitialFormState(formData: ResourceFormData): FormState {
+export function mapResourceToInitialFormState(formData: ResourceFormData): FormState {
const { entity: resource, uploadedResourceFile, uploadedResourceFileId } = formData;
-
const isResourceDocument = resource?.resourceType === ResourceType.RESOURCE_DOCUMENT;
return {
@@ -44,16 +43,16 @@ export function mapResourcesToInitialFormState(formData: ResourceFormData): Form
},
{
title: "Resource name",
- id: "resourceName_section",
+ id: "resourceLabel_section",
isVisible: true,
required: true,
fields: [
{
- id: "resourceName",
+ id: "resourceLabel",
isVisible: true,
errors: [],
type: "text",
- value: "",
+ value: resource?.resourceLabel || "",
required: true,
},
],
@@ -68,10 +67,8 @@ export function mapResourcesToInitialFormState(formData: ResourceFormData): Form
id: "resourceFolder",
isVisible: isResourceDocument,
errors: [],
- type: "select",
- options: [],
- multiple: false,
- value: isResourceDocument ? "" : "",
+ type: "text",
+ value: isResourceDocument ? resource.resourceFolder : "",
required: true,
},
],
diff --git a/src/webapp/pages/resources/ResourcesPage.tsx b/src/webapp/pages/resources/ResourcesPage.tsx
index 33f2a95a..d8f0ca12 100644
--- a/src/webapp/pages/resources/ResourcesPage.tsx
+++ b/src/webapp/pages/resources/ResourcesPage.tsx
@@ -1,4 +1,4 @@
-import React, { useCallback } from "react";
+import React, { useCallback, useEffect, useState } from "react";
import { Layout } from "../../components/layout/Layout";
import i18n from "../../../utils/i18n";
@@ -6,14 +6,55 @@ import { Section } from "../../components/section/Section";
import { Button } from "@material-ui/core";
import { FileFileUpload } from "material-ui/svg-icons";
import { RouteName, useRoutes } from "../../hooks/useRoutes";
+import { useAppContext } from "../../contexts/app-context";
+import { ResourceDocument, Template } from "../../../domain/entities/resources/Resource";
+import { Maybe } from "../../../utils/ts-utils";
+import { DescriptionOutlined, FolderOutlined } from "@material-ui/icons";
export const ResourcesPage: React.FC = React.memo(() => {
+ const { compositionRoot } = useAppContext();
+ // const { changeCurrentEventTracker, getCurrentEventTracker } = useCurrentEventTracker();
+ // const currentEventTracker = getCurrentEventTracker();
+
const { goTo } = useRoutes();
const onUploadFileClick = useCallback(() => {
goTo(RouteName.CREATE_FORM, { formType: "resource" });
}, [goTo]);
+ const [resources, setResources] = useState<
+ Maybe<{
+ templates: Template[];
+ resourceDocuments: ResourceDocument[];
+ }>
+ >(undefined);
+
+ useEffect(() => {
+ compositionRoot.resources.get.execute().run(
+ resources => {
+ setResources(resources);
+ },
+ error => console.debug({ error })
+ );
+ }, [compositionRoot.resources.get]);
+
+ // useEffect(() => {
+ // if (
+ // currentEventTracker &&
+ // (currentEventTracker.incidentActionPlan?.actionPlan?.lastUpdated !==
+ // incidentActionPlan?.actionPlan?.lastUpdated ||
+ // currentEventTracker.incidentActionPlan?.responseActions.length !==
+ // incidentActionPlan?.responseActions.length)
+ // ) {
+ // const updatedEventTracker = new DiseaseOutbreakEvent({
+ // ...currentEventTracker,
+ // resource: resources,
+ // });
+
+ // changeCurrentEventTracker(updatedEventTracker);
+ // }
+ // }, [changeCurrentEventTracker, currentEventTracker]);
+
const uploadButton = (
{resourceFileId && userCanDelete && (
-
- {isDeleting && }
- deleteResource(resourceFileId)}>
+ <>
+ setOpenModal(true)}>
-
+
+ setOpenModal(false)}
+ title={"Delete resource"}
+ closeLabel={i18n.t("Cancel")}
+ alignFooterButtons="end"
+ footerButtons={
+ <>
+ {isDeleting && }
+ deleteResource(resourceFileId)}
+ >
+ {i18n.t("Delete")}
+
+ >
+ }
+ >
+
+ {i18n.t(
+ "Are you sure you want to delete this resource? This action cannot be undone."
+ )}
+
+
+ >
)}
);
@@ -78,3 +105,8 @@ const StyledTemplateLabel = styled.div`
margin: 0;
}
`;
+
+const StyledButton = styled(Button)`
+ background-color: ${props => props.theme.palette.error.main};
+ color: ${props => props.theme.palette.common.white};
+`;
From 829f5ba69373a0fc6e71d050ee1f8a6d6d11e837 Mon Sep 17 00:00:00 2001
From: Chukwudumebi Onwuli <37223065+deeonwuli@users.noreply.github.com>
Date: Wed, 15 Jan 2025 17:16:26 +0100
Subject: [PATCH 24/27] fix: update translation files
---
i18n/en.pot | 7 +++++--
i18n/es.po | 6 +++++-
2 files changed, 10 insertions(+), 3 deletions(-)
diff --git a/i18n/en.pot b/i18n/en.pot
index e7202a77..7e3cdd57 100644
--- a/i18n/en.pot
+++ b/i18n/en.pot
@@ -5,8 +5,8 @@ msgstr ""
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1)\n"
-"POT-Creation-Date: 2025-01-15T15:08:07.179Z\n"
-"PO-Revision-Date: 2025-01-15T15:08:07.180Z\n"
+"POT-Creation-Date: 2025-01-15T16:05:28.418Z\n"
+"PO-Revision-Date: 2025-01-15T16:05:28.418Z\n"
msgid "Low"
msgstr ""
@@ -316,6 +316,9 @@ msgstr ""
msgid "Are you sure you want to delete this team role?"
msgstr ""
+msgid "Are you sure you want to delete this resource? This action cannot be undone."
+msgstr ""
+
msgid "Upload File"
msgstr ""
diff --git a/i18n/es.po b/i18n/es.po
index 6b56a9c2..e390ef36 100644
--- a/i18n/es.po
+++ b/i18n/es.po
@@ -1,7 +1,7 @@
msgid ""
msgstr ""
"Project-Id-Version: i18next-conv\n"
-"POT-Creation-Date: 2025-01-15T15:08:07.179Z\n"
+"POT-Creation-Date: 2025-01-15T16:05:28.418Z\n"
"PO-Revision-Date: 2018-10-25T09:02:35.143Z\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
@@ -315,6 +315,10 @@ msgstr ""
msgid "Are you sure you want to delete this team role?"
msgstr ""
+msgid ""
+"Are you sure you want to delete this resource? This action cannot be undone."
+msgstr ""
+
msgid "Upload File"
msgstr ""
From c8a53e35d656113beeca867b7ca318f168cacc9a Mon Sep 17 00:00:00 2001
From: Chukwudumebi Onwuli <37223065+deeonwuli@users.noreply.github.com>
Date: Mon, 20 Jan 2025 09:43:06 +0100
Subject: [PATCH 25/27] refactor: use userGroupRepository to get resource
permission userGroups
---
i18n/en.pot | 4 +--
src/CompositionRoot.ts | 10 +-----
.../ResourcePermissionsD2Repository.ts | 30 ----------------
.../test/ResourcePermissionsTestRepository.ts | 15 --------
.../ResourcePermissionsRepository.ts | 7 ----
.../GetResourceUserPermissionsUseCase.ts | 34 ++++++++++++++++---
6 files changed, 33 insertions(+), 67 deletions(-)
delete mode 100644 src/data/repositories/ResourcePermissionsD2Repository.ts
delete mode 100644 src/data/repositories/test/ResourcePermissionsTestRepository.ts
delete mode 100644 src/domain/repositories/ResourcePermissionsRepository.ts
diff --git a/i18n/en.pot b/i18n/en.pot
index d89ad317..42ec926d 100644
--- a/i18n/en.pot
+++ b/i18n/en.pot
@@ -5,8 +5,8 @@ msgstr ""
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1)\n"
-"POT-Creation-Date: 2025-01-15T17:33:26.984Z\n"
-"PO-Revision-Date: 2025-01-15T17:33:26.984Z\n"
+"POT-Creation-Date: 2025-01-20T08:19:05.880Z\n"
+"PO-Revision-Date: 2025-01-20T08:19:05.880Z\n"
msgid "Low"
msgstr ""
diff --git a/src/CompositionRoot.ts b/src/CompositionRoot.ts
index 599b4bce..40a3cc41 100644
--- a/src/CompositionRoot.ts
+++ b/src/CompositionRoot.ts
@@ -83,9 +83,6 @@ import { DeleteResourceFileUseCase } from "./domain/usecases/DeleteResourceFileU
import { ResourceFileTestRepository } from "./data/repositories/test/ResourceFileTestRepository";
import { ResourceFileRepository } from "./domain/repositories/ResourceFileRepository";
import { ResourceFileD2Repository } from "./data/repositories/ResourceFileD2Repository";
-import { ResourcePermissionsD2Repository } from "./data/repositories/ResourcePermissionsD2Repository";
-import { ResourcePermissionsTestRepository } from "./data/repositories/test/ResourcePermissionsTestRepository";
-import { ResourcePermissionsRepository } from "./domain/repositories/ResourcePermissionsRepository";
import { GetResourceUserPermissionsUseCase } from "./domain/usecases/GetResourceUserPermissionsUseCase";
export type CompositionRoot = ReturnType;
@@ -110,7 +107,6 @@ type Repositories = {
userGroupRepository: UserGroupRepository;
resourceRepository: ResourceRepository;
resourceFileRepository: ResourceFileRepository;
- resourcePermissionsRepository: ResourcePermissionsRepository;
};
function getCompositionRoot(repositories: Repositories) {
@@ -172,9 +168,7 @@ function getCompositionRoot(repositories: Repositories) {
repositories.resourceFileRepository
),
deleteResourceFile: new DeleteResourceFileUseCase(repositories),
- getPermissions: new GetResourceUserPermissionsUseCase(
- repositories.resourcePermissionsRepository
- ),
+ getPermissions: new GetResourceUserPermissionsUseCase(repositories),
},
};
}
@@ -201,7 +195,6 @@ export function getWebappCompositionRoot(api: D2Api) {
userGroupRepository: new UserGroupD2Repository(api),
resourceRepository: new ResourceD2Repository(api),
resourceFileRepository: new ResourceFileD2Repository(api),
- resourcePermissionsRepository: new ResourcePermissionsD2Repository(api),
};
return getCompositionRoot(repositories);
@@ -228,7 +221,6 @@ export function getTestCompositionRoot() {
userGroupRepository: new UserGroupTestRepository(),
resourceRepository: new ResourceTestRepository(),
resourceFileRepository: new ResourceFileTestRepository(),
- resourcePermissionsRepository: new ResourcePermissionsTestRepository(),
};
return getCompositionRoot(repositories);
diff --git a/src/data/repositories/ResourcePermissionsD2Repository.ts b/src/data/repositories/ResourcePermissionsD2Repository.ts
deleted file mode 100644
index ec67e71f..00000000
--- a/src/data/repositories/ResourcePermissionsD2Repository.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-import { Future } from "../../domain/entities/generic/Future";
-import {
- ResourcePermissions,
- RTSL_ZEBRA_ACCESS_RESOURCES,
- RTSL_ZEBRA_ADMIN_RESOURCES,
- RTSL_ZEBRA_DATA_CAPTURE_RESOURCES,
-} from "../../domain/entities/resources/ResourcePermissions";
-import { User } from "../../domain/entities/User";
-import { ResourcePermissionsRepository } from "../../domain/repositories/ResourcePermissionsRepository";
-import { D2Api } from "../../types/d2-api";
-import { FutureData } from "../api-futures";
-import { getUserGroupByCode } from "./utils/MetadataHelper";
-
-export class ResourcePermissionsD2Repository implements ResourcePermissionsRepository {
- constructor(private api: D2Api) {}
-
- getPermissions(currentUser: User): FutureData {
- return Future.joinObj({
- adminUserGroup: getUserGroupByCode(this.api, RTSL_ZEBRA_ADMIN_RESOURCES),
- dataCaptureUserGroup: getUserGroupByCode(this.api, RTSL_ZEBRA_DATA_CAPTURE_RESOURCES),
- accessUserGroup: getUserGroupByCode(this.api, RTSL_ZEBRA_ACCESS_RESOURCES),
- }).map(({ adminUserGroup, dataCaptureUserGroup, accessUserGroup }) => {
- const isAdmin = currentUser.belongToUserGroup(adminUserGroup.id);
- const isDataCapture = currentUser.belongToUserGroup(dataCaptureUserGroup.id);
- const isAccess = currentUser.belongToUserGroup(accessUserGroup.id);
-
- return { isAdmin, isDataCapture, isAccess };
- });
- }
-}
diff --git a/src/data/repositories/test/ResourcePermissionsTestRepository.ts b/src/data/repositories/test/ResourcePermissionsTestRepository.ts
deleted file mode 100644
index 0ad69d75..00000000
--- a/src/data/repositories/test/ResourcePermissionsTestRepository.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import { Future } from "../../../domain/entities/generic/Future";
-import { ResourcePermissions } from "../../../domain/entities/resources/ResourcePermissions";
-import { User } from "../../../domain/entities/User";
-import { ResourcePermissionsRepository } from "../../../domain/repositories/ResourcePermissionsRepository";
-import { FutureData } from "../../api-futures";
-
-export class ResourcePermissionsTestRepository implements ResourcePermissionsRepository {
- getPermissions(_currentUser: User): FutureData {
- return Future.success({
- isAdmin: true,
- isDataCapture: false,
- isAccess: true,
- });
- }
-}
diff --git a/src/domain/repositories/ResourcePermissionsRepository.ts b/src/domain/repositories/ResourcePermissionsRepository.ts
deleted file mode 100644
index 69365cca..00000000
--- a/src/domain/repositories/ResourcePermissionsRepository.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-import { FutureData } from "../../data/api-futures";
-import { ResourcePermissions } from "../entities/resources/ResourcePermissions";
-import { User } from "../entities/User";
-
-export interface ResourcePermissionsRepository {
- getPermissions(currentUser: User): FutureData;
-}
diff --git a/src/domain/usecases/GetResourceUserPermissionsUseCase.ts b/src/domain/usecases/GetResourceUserPermissionsUseCase.ts
index 0ff81042..f634af2b 100644
--- a/src/domain/usecases/GetResourceUserPermissionsUseCase.ts
+++ b/src/domain/usecases/GetResourceUserPermissionsUseCase.ts
@@ -1,12 +1,38 @@
import { FutureData } from "../../data/api-futures";
-import { ResourcePermissions } from "../entities/resources/ResourcePermissions";
+import { Future } from "../entities/generic/Future";
+import {
+ ResourcePermissions,
+ RTSL_ZEBRA_ACCESS_RESOURCES,
+ RTSL_ZEBRA_ADMIN_RESOURCES,
+ RTSL_ZEBRA_DATA_CAPTURE_RESOURCES,
+} from "../entities/resources/ResourcePermissions";
import { User } from "../entities/User";
-import { ResourcePermissionsRepository } from "../repositories/ResourcePermissionsRepository";
+import { UserGroupRepository } from "../repositories/UserGroupRepository";
export class GetResourceUserPermissionsUseCase {
- constructor(private resourcePermissionsRepository: ResourcePermissionsRepository) {}
+ constructor(
+ private options: {
+ userGroupRepository: UserGroupRepository;
+ }
+ ) {}
public execute(currentUser: User): FutureData {
- return this.resourcePermissionsRepository.getPermissions(currentUser);
+ return Future.joinObj({
+ adminUserGroup: this.options.userGroupRepository.getUserGroupByCode(
+ RTSL_ZEBRA_ADMIN_RESOURCES
+ ),
+ dataCaptureUserGroup: this.options.userGroupRepository.getUserGroupByCode(
+ RTSL_ZEBRA_DATA_CAPTURE_RESOURCES
+ ),
+ accessUserGroup: this.options.userGroupRepository.getUserGroupByCode(
+ RTSL_ZEBRA_ACCESS_RESOURCES
+ ),
+ }).map(({ adminUserGroup, dataCaptureUserGroup, accessUserGroup }) => {
+ const isAdmin = currentUser.belongToUserGroup(adminUserGroup.id);
+ const isDataCapture = currentUser.belongToUserGroup(dataCaptureUserGroup.id);
+ const isAccess = currentUser.belongToUserGroup(accessUserGroup.id);
+
+ return { isAdmin, isDataCapture, isAccess };
+ });
}
}
From bb82dc2a7a1045761b23f910a11614f029548e50 Mon Sep 17 00:00:00 2001
From: Chukwudumebi Onwuli <37223065+deeonwuli@users.noreply.github.com>
Date: Tue, 21 Jan 2025 12:36:30 +0100
Subject: [PATCH 26/27] refactor: use resource entity in resource saving logic
---
src/data/repositories/ResourceD2Repository.ts | 23 ++++---------------
.../test/ResourceTestRepository.ts | 3 +--
src/domain/entities/ConfigurableForm.ts | 1 -
src/domain/repositories/ResourceRepository.ts | 3 +--
src/domain/usecases/SaveEntityUseCase.ts | 14 +++++++----
.../resources/GetResourceConfigurableForm.ts | 1 -
.../form-page/mapFormStateToEntityData.ts | 2 --
.../mapResourceToInitialFormState.ts | 4 ++--
8 files changed, 19 insertions(+), 32 deletions(-)
diff --git a/src/data/repositories/ResourceD2Repository.ts b/src/data/repositories/ResourceD2Repository.ts
index 9f383a7c..22735bd9 100644
--- a/src/data/repositories/ResourceD2Repository.ts
+++ b/src/data/repositories/ResourceD2Repository.ts
@@ -4,7 +4,6 @@ import { DataStoreClient } from "../DataStoreClient";
import { isExistingResource, Resource } from "../../domain/entities/resources/Resource";
import { FutureData } from "../api-futures";
import { Future } from "../../domain/entities/generic/Future";
-import { ResourceFormData } from "../../domain/entities/ConfigurableForm";
import { Id } from "../../domain/entities/Ref";
const RESOURCES_KEY = "resources";
@@ -22,36 +21,24 @@ export class ResourceD2Repository implements ResourceRepository {
.flatMap(resources => Future.success(resources ?? []));
}
- saveResource(formData: ResourceFormData): FutureData {
- const { entity: resource, uploadedResourceFileId } = formData;
-
+ saveResource(resource: Resource): FutureData {
if (!resource) return Future.error(new Error("No resource form data found"));
- if (!uploadedResourceFileId) return Future.error(new Error("No resource file id found"));
return this.getAllResources().flatMap(resourcesInDataStore => {
- const updatedResources = this.getResourcesToSave(
- resourcesInDataStore,
- resource,
- uploadedResourceFileId
- );
+ const updatedResources = this.getResourcesToSave(resourcesInDataStore, resource);
return this.dataStoreClient.saveObject(RESOURCES_KEY, updatedResources);
});
}
- private getResourcesToSave(
- resourcesInDataStore: Resource[],
- resource: Resource,
- resourceFileId: string
- ) {
+ private getResourcesToSave(resourcesInDataStore: Resource[], resource: Resource) {
const isResourceExisting = isExistingResource(resourcesInDataStore, resource);
- const resourceWithFileId = { ...resource, resourceFileId: resourceFileId };
const updatedResources = isResourceExisting
? resourcesInDataStore.map(resourceInDataStore =>
- isResourceExisting ? resourceWithFileId : resourceInDataStore
+ isResourceExisting ? resource : resourceInDataStore
)
- : [...resourcesInDataStore, resourceWithFileId];
+ : [...resourcesInDataStore, resource];
return updatedResources;
}
diff --git a/src/data/repositories/test/ResourceTestRepository.ts b/src/data/repositories/test/ResourceTestRepository.ts
index 5f34630f..bded4adb 100644
--- a/src/data/repositories/test/ResourceTestRepository.ts
+++ b/src/data/repositories/test/ResourceTestRepository.ts
@@ -1,5 +1,4 @@
import { Id } from "@eyeseetea/d2-api";
-import { ResourceFormData } from "../../../domain/entities/ConfigurableForm";
import { Future } from "../../../domain/entities/generic/Future";
import { Resource, ResourceType } from "../../../domain/entities/resources/Resource";
import { ResourceRepository } from "../../../domain/repositories/ResourceRepository";
@@ -24,7 +23,7 @@ export class ResourceTestRepository implements ResourceRepository {
return Future.success(resources);
}
- saveResource(_formData: ResourceFormData): FutureData {
+ saveResource(_resource: Resource): FutureData {
return Future.success(undefined);
}
diff --git a/src/domain/entities/ConfigurableForm.ts b/src/domain/entities/ConfigurableForm.ts
index 79e6081d..636224b4 100644
--- a/src/domain/entities/ConfigurableForm.ts
+++ b/src/domain/entities/ConfigurableForm.ts
@@ -134,7 +134,6 @@ export type ResourceFormData = BaseFormData & {
type: "resource";
entity: Maybe;
uploadedResourceFile: Maybe;
- uploadedResourceFileId: Maybe;
options: ResourceOptions;
};
diff --git a/src/domain/repositories/ResourceRepository.ts b/src/domain/repositories/ResourceRepository.ts
index e232d271..a64681ad 100644
--- a/src/domain/repositories/ResourceRepository.ts
+++ b/src/domain/repositories/ResourceRepository.ts
@@ -1,10 +1,9 @@
import { FutureData } from "../../data/api-futures";
-import { ResourceFormData } from "../entities/ConfigurableForm";
import { Id } from "../entities/Ref";
import { Resource } from "../entities/resources/Resource";
export interface ResourceRepository {
getAllResources(): FutureData;
- saveResource(formData: ResourceFormData): FutureData;
+ saveResource(resource: Resource): FutureData;
deleteResource(fileId: Id): FutureData;
}
diff --git a/src/domain/usecases/SaveEntityUseCase.ts b/src/domain/usecases/SaveEntityUseCase.ts
index 8d53d0e1..5ed6cc77 100644
--- a/src/domain/usecases/SaveEntityUseCase.ts
+++ b/src/domain/usecases/SaveEntityUseCase.ts
@@ -159,10 +159,16 @@ export class SaveEntityUseCase {
return this.options.resourceFileRepository
.uploadFile(uploadedResourceFile)
.flatMap(resourceFileId => {
- return this.options.resourceRepository.saveResource({
- ...formData,
- uploadedResourceFileId: resourceFileId,
- });
+ const { entity } = formData;
+
+ if (!entity) return Future.error(new Error("No resource found"));
+
+ const resource = {
+ ...entity,
+ resourceFileId: resourceFileId,
+ };
+
+ return this.options.resourceRepository.saveResource(resource);
});
}
default:
diff --git a/src/domain/usecases/utils/resources/GetResourceConfigurableForm.ts b/src/domain/usecases/utils/resources/GetResourceConfigurableForm.ts
index 53288791..6a8b89c5 100644
--- a/src/domain/usecases/utils/resources/GetResourceConfigurableForm.ts
+++ b/src/domain/usecases/utils/resources/GetResourceConfigurableForm.ts
@@ -35,7 +35,6 @@ export function getResourceConfigurableForm(props: {
resourceFolder: resourceFolderOptions,
},
uploadedResourceFile: undefined,
- uploadedResourceFileId: undefined,
labels: labels,
rules: rules,
};
diff --git a/src/webapp/pages/form-page/mapFormStateToEntityData.ts b/src/webapp/pages/form-page/mapFormStateToEntityData.ts
index 00118ad3..d8dff913 100644
--- a/src/webapp/pages/form-page/mapFormStateToEntityData.ts
+++ b/src/webapp/pages/form-page/mapFormStateToEntityData.ts
@@ -150,13 +150,11 @@ export function mapFormStateToEntityData(
const allFields: FormFieldState[] = getAllFieldsFromSections(formState.sections);
const uploadedResourceFileValue = getFileFieldValue("resourceFile", allFields);
- const uploadedResourceFileId = getFieldFileIdById("resourceFile", allFields);
const resourceForm: ResourceFormData = {
...formData,
entity: resource,
uploadedResourceFile: uploadedResourceFileValue,
- uploadedResourceFileId: uploadedResourceFileId,
};
return resourceForm;
diff --git a/src/webapp/pages/form-page/resources/mapResourceToInitialFormState.ts b/src/webapp/pages/form-page/resources/mapResourceToInitialFormState.ts
index 1acda6c8..7a418c7c 100644
--- a/src/webapp/pages/form-page/resources/mapResourceToInitialFormState.ts
+++ b/src/webapp/pages/form-page/resources/mapResourceToInitialFormState.ts
@@ -17,7 +17,7 @@ export function mapResourceToInitialFormState(
formData: ResourceFormData,
resourcePermissions: ResourcePermissions
): FormState {
- const { entity: resource, uploadedResourceFile, uploadedResourceFileId, options } = formData;
+ const { entity: resource, uploadedResourceFile, options } = formData;
const isResourceDocument = resource?.resourceType === ResourceType.RESPONSE_DOCUMENT;
const { resourceType, resourceFolder } = options;
@@ -114,7 +114,7 @@ export function mapResourceToInitialFormState(
errors: [],
type: "file",
value: uploadedResourceFile || undefined,
- fileId: uploadedResourceFileId || undefined,
+ fileId: resource?.resourceFileId || undefined,
required: true,
data: undefined,
fileTemplate: undefined,
From 22e9e65177e2377ed1dc4600bae7868dc08e821b Mon Sep 17 00:00:00 2001
From: Chukwudumebi Onwuli <37223065+deeonwuli@users.noreply.github.com>
Date: Thu, 23 Jan 2025 13:39:14 +0100
Subject: [PATCH 27/27] fix: update file type in ResourceFileD2Repository
---
src/data/repositories/ResourceFileD2Repository.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/data/repositories/ResourceFileD2Repository.ts b/src/data/repositories/ResourceFileD2Repository.ts
index 3d789033..bd4eef18 100644
--- a/src/data/repositories/ResourceFileD2Repository.ts
+++ b/src/data/repositories/ResourceFileD2Repository.ts
@@ -22,7 +22,7 @@ export class ResourceFileD2Repository implements ResourceFileRepository {
return apiToFuture(this.api.files.get(fileId))
.map(blob => {
- return new File([blob], "file", { type: "application/pdf" });
+ return new File([blob], "file", { type: blob.type });
})
.flatMap(file =>
Future.success({