diff --git a/src/data/repositories/ConfigD2Repository.ts b/src/data/repositories/ConfigD2Repository.ts index 9d5123b0..ce5f19df 100644 --- a/src/data/repositories/ConfigD2Repository.ts +++ b/src/data/repositories/ConfigD2Repository.ts @@ -5,6 +5,7 @@ import { Region } from "$/domain/entities/Region"; import { Future, FutureData } from "$/domain/entities/generic/Future"; import { ConfigRepository } from "$/domain/repositories/ConfigRepository"; import { D2Api } from "$/types/d2-api"; +import { extractFirstTwoLetters, extractPrefix } from "$/utils/string"; export class ConfigD2Repository implements ConfigRepository { constructor(private api: D2Api) {} @@ -43,7 +44,8 @@ export class ConfigD2Repository implements ConfigRepository { return d2Response.objects.map(region => ({ id: region.id, name: region.name, - code: this.extractRegionCode(region.code), + // org. unit code includes the region code in the first two letters before the underscore + code: extractFirstTwoLetters(region.code), })); }); } @@ -55,19 +57,12 @@ export class ConfigD2Repository implements ConfigRepository { paging: false, }) ).map(d2Response => { - return d2Response.objects.map(region => ({ - id: region.id, - name: region.name, - code: this.extractCode(region.name), + return d2Response.objects.map(d2UserGroup => ({ + id: d2UserGroup.id, + name: d2UserGroup.name, + // user group name includes the region code in the first two letters + code: extractPrefix(d2UserGroup.name), })); }); } - - private extractRegionCode(code: string): string { - return (code.slice(0, 2) || "").toUpperCase(); - } - - private extractCode(code: string): string { - return (code.split("_")[0] || "").toUpperCase(); - } } diff --git a/src/data/repositories/DataSetD2Api.ts b/src/data/repositories/DataSetD2Api.ts index 68486ad7..75bd0c6f 100644 --- a/src/data/repositories/DataSetD2Api.ts +++ b/src/data/repositories/DataSetD2Api.ts @@ -7,14 +7,13 @@ import { DataSet, DataSetList, OrgUnit, - Permissions, } from "$/domain/entities/DataSet"; import { Paginated } from "$/domain/entities/Paginated"; import { GetDataSetOptions } from "$/domain/repositories/DataSetRepository"; import { Future, FutureData } from "$/domain/entities/generic/Future"; import { Maybe } from "$/utils/ts-utils"; import { Id } from "$/domain/entities/Ref"; -import { Permission } from "$/domain/entities/Permission"; +import { Permission, Permissions } from "$/domain/entities/Permission"; import _ from "$/domain/entities/generic/Collection"; import { Project } from "$/domain/entities/Project"; import { D2ApiCategoryOption } from "$/data/repositories/D2ApiCategoryOption"; diff --git a/src/data/repositories/DataSetD2Repository.ts b/src/data/repositories/DataSetD2Repository.ts index 108a0bd8..8f0d7985 100644 --- a/src/data/repositories/DataSetD2Repository.ts +++ b/src/data/repositories/DataSetD2Repository.ts @@ -2,7 +2,7 @@ import { D2AttributeValue, MetadataPick } from "@eyeseetea/d2-api/2.36"; import { D2Api, MetadataResponse } from "$/types/d2-api"; import { apiToFuture } from "$/data/api-futures"; -import { AccessData, DataSet, DataSetList } from "$/domain/entities/DataSet"; +import { DataSet, DataSetList } from "$/domain/entities/DataSet"; import { Paginated } from "$/domain/entities/Paginated"; import { DataSetName, @@ -12,11 +12,7 @@ import { import { Future, FutureData } from "$/domain/entities/generic/Future"; import { getUid } from "$/utils/uid"; import _ from "$/domain/entities/generic/Collection"; -import { - DataSetD2Api, - OctalNotationPermission, - dataSetFieldsWithOrgUnits, -} from "$/data/repositories/DataSetD2Api"; +import { DataSetD2Api, dataSetFieldsWithOrgUnits } from "$/data/repositories/DataSetD2Api"; import { Maybe } from "$/utils/ts-utils"; import { chunkRequest, runMetadata } from "$/data/utils"; import { D2Config } from "$/data/repositories/D2ApiMetadata"; @@ -314,11 +310,11 @@ export class DataSetD2Repository implements DataSetRepository { }), userGroupAccesses: _(dataSet.access) .filter(access => access.type === "groups") - .map(access => { + .map(groupAccess => { return { - access: this.d2DataSetApi.generateFullPermission(access.permissions), - id: access.id, - displayName: access.name, + access: this.d2DataSetApi.generateFullPermission(groupAccess.permissions), + id: groupAccess.id, + displayName: groupAccess.name, }; }) .value(), @@ -330,20 +326,6 @@ export class DataSetD2Repository implements DataSetRepository { }; } - private convertSharingGroupsToAccessData(d2UserGroups: Maybe): AccessData[] { - if (!d2UserGroups || Object.keys(d2UserGroups).length === 0) return []; - - return Object.values(d2UserGroups).map(({ id, access }) => ({ - id, - permissions: { - data: this.d2DataSetApi.buildPermission(access, "data"), - metadata: this.d2DataSetApi.buildPermission(access, "metadata"), - }, - name: "", - type: "groups", - })); - } - private buildDataSetElements(dataSet: DataSetToSave) { const relatedDataElements = dataSet.indicators .filter(indicator => indicator.type === "outcomes") @@ -416,7 +398,6 @@ type D2DataSetSection = { indicators: Ref[]; }; -type SharingUserGroup = Record; const indicatorTypeLabel: Record = { outcomes: "Outcomes", outputs: "Outputs", diff --git a/src/data/repositories/RegionD2Repository.ts b/src/data/repositories/RegionD2Repository.ts deleted file mode 100644 index 0cbe7da8..00000000 --- a/src/data/repositories/RegionD2Repository.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Future, FutureData } from "$/domain/entities/generic/Future"; -import { Region } from "$/domain/entities/Region"; -import { RegionRepository } from "$/domain/repositories/RegionRepository"; -import { D2Api } from "$/types/d2-api"; - -export class RegionD2Repository implements RegionRepository { - constructor(private _api: D2Api) {} - - get(): FutureData { - return Future.success([]); - } -} diff --git a/src/domain/entities/DataSet.ts b/src/domain/entities/DataSet.ts index 2cdb9aac..2a769cc3 100644 --- a/src/domain/entities/DataSet.ts +++ b/src/domain/entities/DataSet.ts @@ -1,4 +1,4 @@ -import { Permission } from "$/domain/entities/Permission"; +import { Permission, Permissions } from "$/domain/entities/Permission"; import { Project } from "$/domain/entities/Project"; import { Id, ISODateString, Ref } from "$/domain/entities/Ref"; import { Struct } from "$/domain/entities/generic/Struct"; @@ -10,6 +10,7 @@ import { validateOrgUnits, validateRequired } from "$/domain/entities/generic/Va import { Indicator } from "$/domain/entities/Indicator"; import { Config, UserGroup } from "$/domain/entities/Config"; import { DataSetToSave } from "$/domain/entities/DataSetToSave"; +import { extractFirstTwoLetters, extractPrefix } from "$/utils/string"; export type DataSetAttrs = { created: ISODateString; @@ -29,7 +30,6 @@ export type DataSetAttrs = { }; export type OrgUnit = { id: Id; code: string; name: string; path: Id[] }; -export type Permissions = { data: Permission; metadata: Permission }; export type AccessData = { id: Id; permissions: Permissions; name: string; type: AccessType }; export type AccessType = "users" | "groups"; @@ -57,11 +57,16 @@ export class DataSet extends Struct() { updateProject(project: Maybe, config: Config): DataSet { const name = project ? `${project.name} DataSet` : ""; - const orgUnits = project ? project.orgsUnits : this.orgUnits; + const orgsUnits = project ? project.orgsUnits : this.orgUnits; const accessGroupsFromProject = this.getAccessFromProject(project, config); - return this._update({ access: accessGroupsFromProject, project, name, orgUnits }); + return this._update({ + access: accessGroupsFromProject, + project, + name, + orgUnits: orgsUnits, + }); } updateAccess(config: Config): DataSet { @@ -109,7 +114,7 @@ export class DataSet extends Struct() { : []; } - validateSharingStep(): ValidationError[] { + validateRegionCodes(): ValidationError[] { if (this.project) return []; const regionCodesFromOrgUnits = this.getRegionCodesFromAccess(); @@ -128,7 +133,7 @@ export class DataSet extends Struct() { getRegionCodesFromAccess(): string[] { return _(this.access) .filter(access => access.type === "groups") - .compactMap(access => access.name.split("_")[0]) + .compactMap(access => extractPrefix(access.name)) .uniq() .value(); } @@ -146,7 +151,7 @@ export class DataSet extends Struct() { private getValidationErrors(): ValidationError[] { const setupErrors = this.buildSetupErrors(); const indicatorsErrors = this.validateIndicatorsStep(); - const sharingErrors = this.validateSharingStep(); + const sharingErrors = this.validateRegionCodes(); return [...setupErrors, ...indicatorsErrors, ...sharingErrors]; } @@ -179,7 +184,7 @@ export class DataSet extends Struct() { } private getAccessFromOrgUnits(orgUnits: OrgUnit[], config: Config): AccessData[] { - const orgsUnitsCodes = orgUnits.map(orgUnit => orgUnit.code.slice(0, 2)); + const orgsUnitsCodes = orgUnits.map(orgUnit => extractFirstTwoLetters(orgUnit.code)); const regions = config.regions.filter(region => orgsUnitsCodes.includes(region.code)); const regionsCodes = regions.map(region => region.code); @@ -193,7 +198,7 @@ export class DataSet extends Struct() { private getAccessFromProject(project: Maybe, config: Config): AccessData[] { if (!project || !project.code) return []; - const regionCode = (project.code?.slice(0, 2) || "").toUpperCase(); + const regionCode = extractFirstTwoLetters(project.code); const region = config.regions.find(region => region.code === regionCode); const userGroups = config.userGroups.filter(userGroup => userGroup.code === region?.code); diff --git a/src/domain/entities/Permission.ts b/src/domain/entities/Permission.ts index 81d0c564..d9431e42 100644 --- a/src/domain/entities/Permission.ts +++ b/src/domain/entities/Permission.ts @@ -1,4 +1,3 @@ -import { Permissions } from "$/domain/entities/DataSet"; import { Struct } from "$/domain/entities/generic/Struct"; export type PermissionAttrs = { read: boolean; write: boolean }; @@ -13,12 +12,14 @@ export class Permission extends Struct() { } static setDefaultPermissionsForGroups(isAdmin: boolean): Permissions { - const adminDataPermission = Permission.create({ read: true, write: true }); + const readWritePermission = Permission.create({ read: true, write: true }); return { - data: adminDataPermission, + data: readWritePermission, metadata: isAdmin - ? adminDataPermission + ? readWritePermission : Permission.create({ read: true, write: false }), }; } } + +export type Permissions = { data: Permission; metadata: Permission }; diff --git a/src/domain/entities/__tests__/DataSet.spec.ts b/src/domain/entities/__tests__/DataSet.spec.ts index db37d295..645f9adf 100644 --- a/src/domain/entities/__tests__/DataSet.spec.ts +++ b/src/domain/entities/__tests__/DataSet.spec.ts @@ -22,7 +22,7 @@ describe("DataSet", () => { configTest ); - const errors = dataSetToSave.validateSharingStep(); + const errors = dataSetToSave.validateRegionCodes(); const errorMessage = getErrorMessageFromErrors(errors); expect(errors.length).toBeGreaterThan(0); expect(errorMessage).toMatch("Select at least one country"); @@ -49,7 +49,7 @@ describe("DataSet", () => { ], }).updateAccess(configTest); - const result = dataSetToSave.validateSharingStep(); + const result = dataSetToSave.validateRegionCodes(); expect(result).toHaveLength(0); expectUserGroups(dataSetToSave); @@ -62,7 +62,7 @@ describe("DataSet", () => { project: undefined, }).updateProject(projectTest, configTest); - const result = dataSetToSave.validateSharingStep(); + const result = dataSetToSave.validateRegionCodes(); expect(result).toHaveLength(0); expectUserGroups(dataSetToSave); diff --git a/src/domain/repositories/RegionRepository.ts b/src/domain/repositories/RegionRepository.ts deleted file mode 100644 index c587fec2..00000000 --- a/src/domain/repositories/RegionRepository.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { Region } from "$/domain/entities/Region"; -import { FutureData } from "$/domain/entities/generic/Future"; - -export interface RegionRepository { - get(): FutureData; -} diff --git a/src/utils/permission.ts b/src/utils/permission.ts index 3ee31861..2bec39db 100644 --- a/src/utils/permission.ts +++ b/src/utils/permission.ts @@ -1,6 +1,5 @@ import { Maybe } from "$/utils/ts-utils"; -import { Permission } from "$/domain/entities/Permission"; -import { Permissions } from "$/domain/entities/DataSet"; +import { Permission, Permissions } from "$/domain/entities/Permission"; export const NO_ACCESS_NOTATION = "--------"; diff --git a/src/utils/string.ts b/src/utils/string.ts new file mode 100644 index 00000000..0469d84d --- /dev/null +++ b/src/utils/string.ts @@ -0,0 +1,26 @@ +/** + * Extracts the first two characters of a string and convert it to uppercase. + * If the input string is empty, returns an empty string. + * + * Example: + * Input: "us123" + * Output: "US" + * + */ + +export function extractFirstTwoLetters(value: string): string { + return (value.slice(0, 2) || "").toUpperCase(); +} + +/** + * Extracts the portion of a code string before the first underscore ("_") + * and converts it to uppercase. + * + * Example: + * Input: "us_region" + * Output: "US" + * + */ +export function extractPrefix(value: string): string { + return (value.split("_")[0] || "").toUpperCase(); +} diff --git a/src/webapp/components/dataset-wizard/DataSetWizard.tsx b/src/webapp/components/dataset-wizard/DataSetWizard.tsx index b0b5f28b..02490329 100644 --- a/src/webapp/components/dataset-wizard/DataSetWizard.tsx +++ b/src/webapp/components/dataset-wizard/DataSetWizard.tsx @@ -137,7 +137,7 @@ export function useValidateDataSetWizard(props: { const validationMap: ValidationStepType = { setup: () => dataSet.validateSetup(), indicators: () => dataSet.validateIndicatorsStep(), - share: () => dataSet.validateSharingStep(), + share: () => dataSet.validateRegionCodes(), }; const validate = validationMap[currentStep.key]; diff --git a/src/webapp/components/dataset-wizard/SummaryDataSet.tsx b/src/webapp/components/dataset-wizard/SummaryDataSet.tsx index dfe2c9b9..70846f0e 100644 --- a/src/webapp/components/dataset-wizard/SummaryDataSet.tsx +++ b/src/webapp/components/dataset-wizard/SummaryDataSet.tsx @@ -76,14 +76,7 @@ export const SummaryList = React.memo((props: { dataSet: DataSet; orgUnits: OrgU .map(indicator => indicator.coreCompetency?.name || "") .join(", "); - const regionsCodes = _(dataSet.access) - .filter(access => access.type === "groups") - .compactMap(access => { - return access.name.split("_")[0]; - }) - .uniq() - .value(); - + const regionsCodes = dataSet.getRegionCodesFromAccess(); const selectedRegions = config.regions.filter(region => regionsCodes.includes(region.code)); return (