Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[feature]: Create resources #51

Merged
merged 29 commits into from
Jan 24, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
ebe249f
feat: create resource form
deeonwuli Jan 6, 2025
53bc91c
feat: save resources to datastore and get all resources
deeonwuli Jan 6, 2025
b9a1536
feat: refactor resource types to use RESPONSE_DOCUMENT and implement …
deeonwuli Jan 7, 2025
5fad1ff
Merge branch 'feature/user-upload-case-data' of https://github.com/Ey…
deeonwuli Jan 8, 2025
ee2bc64
fix: merge conflict errors
deeonwuli Jan 8, 2025
4cfa743
feat: file upload and download
deeonwuli Jan 8, 2025
2048fd9
feat: add new option in Select component
deeonwuli Jan 9, 2025
a232ed5
feat: delete resources (wip)
deeonwuli Jan 9, 2025
6f2be3c
feat: implement resource deletion functionality and improve resource …
deeonwuli Jan 9, 2025
977fc58
feat: display popup when existing resource is saved
deeonwuli Jan 13, 2025
9da21b8
feat: implement resource user permissions
deeonwuli Jan 13, 2025
d22196b
feat: update translation files
deeonwuli Jan 13, 2025
c210e06
fix: update existing resource in datastore
deeonwuli Jan 13, 2025
4cf04c8
fix: remove unnecessary error handling
deeonwuli Jan 13, 2025
5bc7df7
fix: refactor navigation logic for resource form type
deeonwuli Jan 13, 2025
d42e16d
fix: simplify navigation logic in onCancelForm
deeonwuli Jan 13, 2025
eb659d8
feat: add admin contact message for resource creation
deeonwuli Jan 13, 2025
2df05d7
fix: update resource folder form field
deeonwuli Jan 13, 2025
d37a619
fix: simplify navigation logic in onCancelForm
deeonwuli Jan 13, 2025
c50899a
fix: correct value assignment in validateField function
deeonwuli Jan 13, 2025
8c12e0c
fix: update translation and addNew button label
deeonwuli Jan 15, 2025
41b5335
refactor: update saveResource logic
deeonwuli Jan 15, 2025
1c4575a
refactor: update deleteResourceFile logic and repository structure
deeonwuli Jan 15, 2025
bbb5374
feat: add confirmation modal for resource deletion
deeonwuli Jan 15, 2025
829f5ba
fix: update translation files
deeonwuli Jan 15, 2025
b6aacdf
Merge branch 'development' of https://github.com/EyeSeeTea/zebra-dev …
deeonwuli Jan 20, 2025
c8a53e3
refactor: use userGroupRepository to get resource permission userGroups
deeonwuli Jan 20, 2025
bb82dc2
refactor: use resource entity in resource saving logic
deeonwuli Jan 21, 2025
22e9e65
fix: update file type in ResourceFileD2Repository
deeonwuli Jan 23, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 28 additions & 2 deletions i18n/en.pot
Original file line number Diff line number Diff line change
Expand Up @@ -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 ""
Expand Down Expand Up @@ -150,6 +150,9 @@ msgstr ""
msgid "Last updated: "
msgstr ""

msgid "Add"
msgstr ""

msgid "Median"
msgstr ""

Expand Down Expand Up @@ -338,6 +341,14 @@ msgid ""
"you sure you want to continue?"
msgstr ""

msgid "Resource saved successfully"
msgstr ""

msgid ""
"You have uploaded a new resource with an existing file name. This action "
"will replace the current resource. Are you sure you want to continue?"
msgstr ""

msgid "Add another"
msgstr ""

Expand Down Expand Up @@ -404,9 +415,24 @@ 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 ""

msgid "Resources"
msgstr ""

msgid "No resources created"
msgstr ""

msgid "You can upload a file to create a new resource"
msgstr ""

msgid "Contact admin to create resources"
msgstr ""

msgctxt "DATA"
msgid "HISTORICAL_CASE"
msgstr ""
32 changes: 28 additions & 4 deletions i18n/es.po
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
msgid ""
msgstr ""
"Project-Id-Version: i18next-conv\n"
"POT-Creation-Date: 2025-01-14T15:51:01.421Z\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"
Expand Down Expand Up @@ -149,6 +149,9 @@ msgstr ""
msgid "Last updated: "
msgstr ""

msgid "Add"
msgstr "Añadir"

msgid "Median"
msgstr ""

Expand Down Expand Up @@ -337,6 +340,14 @@ msgid ""
"you sure you want to continue?"
msgstr ""

msgid "Resource saved successfully"
msgstr ""

msgid ""
"You have uploaded a new resource with an existing file name. This action "
"will replace the current resource. Are you sure you want to continue?"
msgstr ""

msgid "Add another"
msgstr ""

Expand Down Expand Up @@ -403,16 +414,29 @@ 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 ""

msgid "Resources"
msgstr ""

msgid "No resources created"
msgstr ""

msgid "You can upload a file to create a new resource"
msgstr ""

msgid "Contact admin to create resources"
msgstr ""

msgctxt "DATA"
msgid "HISTORICAL_CASE"
msgstr ""

#~ msgid "Add"
#~ msgstr "Añadir"

#~ msgid "List"
#~ msgstr "Listar"

Expand Down
24 changes: 24 additions & 0 deletions src/CompositionRoot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,16 @@ import { UserGroupD2Repository } from "./data/repositories/UserGroupD2Repository
import { UserGroupRepository } from "./domain/repositories/UserGroupRepository";
import { UserGroupTestRepository } from "./data/repositories/test/UserGroupTestRepository";
import { GetAllAlertsPerformanceOverviewMetricsUseCase } from "./domain/usecases/GetAllAlertsPerformanceOverviewMetricsUseCase";
import { ResourceRepository } from "./domain/repositories/ResourceRepository";
import { ResourceD2Repository } from "./data/repositories/ResourceD2Repository";
import { ResourceTestRepository } from "./data/repositories/test/ResourceTestRepository";
import { GetResourcesUseCase } from "./domain/usecases/GetResourcesUseCase";
import { DownloadResourceFileUseCase } from "./domain/usecases/DownloadResourceFileUseCase";
import { DeleteResourceFileUseCase } from "./domain/usecases/DeleteResourceFileUseCase";
import { ResourceFileTestRepository } from "./data/repositories/test/ResourceFileTestRepository";
import { ResourceFileRepository } from "./domain/repositories/ResourceFileRepository";
import { ResourceFileD2Repository } from "./data/repositories/ResourceFileD2Repository";
import { GetResourceUserPermissionsUseCase } from "./domain/usecases/GetResourceUserPermissionsUseCase";

export type CompositionRoot = ReturnType<typeof getCompositionRoot>;

Expand All @@ -95,6 +105,8 @@ type Repositories = {
configurationsRepository: ConfigurationsRepository;
casesFileRepository: CasesFileRepository;
userGroupRepository: UserGroupRepository;
resourceRepository: ResourceRepository;
resourceFileRepository: ResourceFileRepository;
};

function getCompositionRoot(repositories: Repositories) {
Expand Down Expand Up @@ -150,6 +162,14 @@ function getCompositionRoot(repositories: Repositories) {
charts: {
getCases: new GetChartConfigByTypeUseCase(repositories.chartConfigRepository),
},
resources: {
get: new GetResourcesUseCase(repositories.resourceRepository),
downloadResourceFile: new DownloadResourceFileUseCase(
repositories.resourceFileRepository
),
deleteResourceFile: new DeleteResourceFileUseCase(repositories),
getPermissions: new GetResourceUserPermissionsUseCase(repositories),
},
};
}

Expand All @@ -173,6 +193,8 @@ export function getWebappCompositionRoot(api: D2Api) {
configurationsRepository: new ConfigurationsD2Repository(api),
casesFileRepository: new CasesFileD2Repository(api, dataStoreClient),
userGroupRepository: new UserGroupD2Repository(api),
resourceRepository: new ResourceD2Repository(api),
resourceFileRepository: new ResourceFileD2Repository(api),
};

return getCompositionRoot(repositories);
Expand All @@ -197,6 +219,8 @@ export function getTestCompositionRoot() {
configurationsRepository: new ConfigurationsTestRepository(),
casesFileRepository: new CasesFileTestRepository(),
userGroupRepository: new UserGroupTestRepository(),
resourceRepository: new ResourceTestRepository(),
resourceFileRepository: new ResourceFileTestRepository(),
};

return getCompositionRoot(repositories);
Expand Down
54 changes: 54 additions & 0 deletions src/data/repositories/ResourceD2Repository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { D2Api } from "../../types/d2-api";
import { ResourceRepository } from "../../domain/repositories/ResourceRepository";
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 { Id } from "../../domain/entities/Ref";

const RESOURCES_KEY = "resources";

export class ResourceD2Repository implements ResourceRepository {
private dataStoreClient: DataStoreClient;

constructor(private api: D2Api) {
this.dataStoreClient = new DataStoreClient(api);
}

getAllResources(): FutureData<Resource[]> {
return this.dataStoreClient
.getObject<Resource[]>(RESOURCES_KEY)
.flatMap(resources => Future.success(resources ?? []));
}

saveResource(resource: Resource): FutureData<void> {
if (!resource) return Future.error(new Error("No resource form data found"));

return this.getAllResources().flatMap(resourcesInDataStore => {
const updatedResources = this.getResourcesToSave(resourcesInDataStore, resource);

return this.dataStoreClient.saveObject<Resource[]>(RESOURCES_KEY, updatedResources);
});
}

private getResourcesToSave(resourcesInDataStore: Resource[], resource: Resource) {
const isResourceExisting = isExistingResource(resourcesInDataStore, resource);

const updatedResources = isResourceExisting
? resourcesInDataStore.map(resourceInDataStore =>
isResourceExisting ? resource : resourceInDataStore
)
: [...resourcesInDataStore, resource];
return updatedResources;
}

deleteResource(fileId: Id): FutureData<void> {
return this.getAllResources().flatMap(resources => {
const updatedResources = resources.filter(
resource => resource.resourceFileId !== fileId
);

return this.dataStoreClient.saveObject<Resource[]>(RESOURCES_KEY, updatedResources);
});
}
}
41 changes: 41 additions & 0 deletions src/data/repositories/ResourceFileD2Repository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { D2Api } from "../../types/d2-api";
import { ResourceFile } from "../../domain/entities/resources/ResourceFile";
import { apiToFuture, FutureData } from "../api-futures";
import { Future } from "../../domain/entities/generic/Future";
import { Id } from "../../domain/entities/Ref";
import { ResourceFileRepository } from "../../domain/repositories/ResourceFileRepository";

export class ResourceFileD2Repository implements ResourceFileRepository {
constructor(private api: D2Api) {}

uploadFile(file: File): FutureData<Id> {
return apiToFuture(
this.api.files.upload({
name: file.name,
data: file,
})
).flatMap(fileResource => Future.success(fileResource.id));
}

downloadFile(fileId: Id): FutureData<ResourceFile> {
if (!fileId) return Future.error(new Error("No file id found"));

return apiToFuture(this.api.files.get(fileId))
.map(blob => {
return new File([blob], "file", { type: blob.type });
})
.flatMap(file =>
Future.success({
fileId: fileId,
file: file,
})
);
}

deleteResourceFile(fileId: Id): FutureData<void> {
return apiToFuture(this.api.files.delete(fileId)).flatMap(response => {
if (response.httpStatus === "OK") return Future.success(undefined);
else return Future.error(new Error("Error while deleting resource file"));
});
}
}
22 changes: 22 additions & 0 deletions src/data/repositories/test/ResourceFileTestRepository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Id } from "@eyeseetea/d2-api";
import { Future } from "../../../domain/entities/generic/Future";
import { FutureData } from "../../api-futures";
import { ResourceFileRepository } from "../../../domain/repositories/ResourceFileRepository";
import { ResourceFile } from "../../../domain/entities/resources/ResourceFile";

export class ResourceFileTestRepository implements ResourceFileRepository {
uploadFile(_file: File): FutureData<Id> {
return Future.success("test-file-id");
}

downloadFile(fileId: Id): FutureData<ResourceFile> {
return Future.success({
fileId: fileId,
file: new File(["test"], "test.txt", { type: "text/plain" }),
});
}

deleteResourceFile(_fileId: Id): FutureData<void> {
return Future.success(undefined);
}
}
33 changes: 33 additions & 0 deletions src/data/repositories/test/ResourceTestRepository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Id } from "@eyeseetea/d2-api";
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<Resource[]> {
const resources: Resource[] = [
{
resourceLabel: "Incident Action Plan",
resourceType: ResourceType.TEMPLATE,
resourceFileId: "123",
},
{
resourceLabel: "Excel line list",
resourceType: ResourceType.RESPONSE_DOCUMENT,
resourceFolder: "Case line lists",
resourceFileId: "456",
},
];

return Future.success(resources);
}

saveResource(_resource: Resource): FutureData<void> {
return Future.success(undefined);
}

deleteResource(_fileId: Id): FutureData<void> {
return Future.success(undefined);
}
}
16 changes: 15 additions & 1 deletion src/domain/entities/ConfigurableForm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,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";
import { OrgUnit } from "./OrgUnit";

export type DiseaseOutbreakEventOptions = {
Expand Down Expand Up @@ -62,6 +63,11 @@ export type IncidentResponseActionOptions = {
verification: Option[];
};

export type ResourceOptions = {
resourceType: Option[];
resourceFolder: Option[];
};

export type FormLables = {
errors: Record<string, string>;
};
Expand Down Expand Up @@ -124,6 +130,13 @@ export type SingleResponseActionFormData = BaseFormData & {
options: IncidentResponseActionOptions;
};

export type ResourceFormData = BaseFormData & {
type: "resource";
entity: Maybe<Resource>;
uploadedResourceFile: Maybe<File>;
options: ResourceOptions;
};

export type IncidentManagementTeamRoleOptions = {
roles: Role[];
teamMembers: TeamMember[];
Expand All @@ -147,4 +160,5 @@ export type ConfigurableForm =
| ActionPlanFormData
| ResponseActionFormData
| SingleResponseActionFormData
| IncidentManagementTeamMemberFormData;
| IncidentManagementTeamMemberFormData
| ResourceFormData;
Loading
Loading