diff --git a/i18n/en.pot b/i18n/en.pot index cb3498d8..57e52c6b 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: 2024-12-02T01:42:54.131Z\n" -"PO-Revision-Date: 2024-12-02T01:42:54.131Z\n" +"POT-Creation-Date: 2024-12-05T17:23:25.404Z\n" +"PO-Revision-Date: 2024-12-05T17:23:25.404Z\n" msgid "edit dataset" msgstr "" @@ -53,6 +53,9 @@ msgstr "" msgid "DataSet not found" msgstr "" +msgid "Data set name already exists: {{dataSetName}}" +msgstr "" + msgid "Add" msgstr "" @@ -161,6 +164,12 @@ msgstr "" msgid "{{action}} dataSet" msgstr "" +msgid "Data set name already exists" +msgstr "" + +msgid "Validation name in progress" +msgstr "" + msgid "Filters" msgstr "" diff --git a/i18n/es.po b/i18n/es.po index 0b7b2d4b..265dc222 100644 --- a/i18n/es.po +++ b/i18n/es.po @@ -1,7 +1,7 @@ msgid "" msgstr "" "Project-Id-Version: i18next-conv\n" -"POT-Creation-Date: 2024-12-02T01:42:54.131Z\n" +"POT-Creation-Date: 2024-12-05T17:23:25.404Z\n" "PO-Revision-Date: 2018-10-25T09:02:35.143Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -12,47 +12,49 @@ msgid "edit dataset" msgstr "editar dataset" msgid "create new dataset" -msgstr "" +msgstr "crear dataset" msgid "change sharing settings" -msgstr "" +msgstr "cambio de permisos" msgid "change organisation units" -msgstr "" +msgstr "cambio de unidades organizativas" msgid "clone dataset" -msgstr "" +msgstr "clonar dataset" msgid "No public access" -msgstr "" +msgstr "Sin acceso público" msgid "Public view/edit" -msgstr "" +msgstr "Vista/edición pública" msgid "Public view" -msgstr "" +msgstr "Acceso público" msgid "Project id is required" -msgstr "" +msgstr "El id del proyecto es obligatorio" msgid "Project name is required" -msgstr "" +msgstr "El nombre del proyecto es obligatorio" msgid "Cannot be blank: {{fieldName}}" -msgstr "" +msgstr "El campo no puede estar vacío: {{fieldName}}" msgid "{{fieldName}} must be a positive number" -msgstr "" +msgstr "{{fieldName}} debe ser un número positivo" msgid "At least one org. unit is required" -msgstr "" +msgstr "Al menos una unidad organizativa es obligatoria" msgid "At least one indicator is required" -msgstr "" +msgstr "Al menos un indicador es obligatorio" -#, fuzzy msgid "DataSet not found" -msgstr "Sección" +msgstr "DataSet no encontrado" + +msgid "Data set name already exists: {{dataSetName}}" +msgstr "Ya existe un dataset con el nombre: {{dataSetName}}" msgid "Add" msgstr "Añadir" @@ -61,28 +63,28 @@ msgid "List" msgstr "Listar" msgid "Are you sure you want to delete this/those dataset(s)?" -msgstr "" +msgstr "¿Estas seguro de que quieres eliminar este/estos dataset(s)?" msgid "This action cannot be undone." -msgstr "" +msgstr "Esta acción no se puede deshacer." msgid "Delete" -msgstr "" +msgstr "Eliminar" msgid "Cancel" -msgstr "" +msgstr "Cancelar" msgid "Loading logs..." -msgstr "" +msgstr "Cargando registros..." msgid "Logs" msgstr "Registros" msgid "Close" -msgstr "" +msgstr "Cerrar" msgid "Date" -msgstr "" +msgstr "Fecha" msgid "Action" msgstr "Acción" @@ -91,55 +93,55 @@ msgid "Status" msgstr "Estado" msgid "User" -msgstr "" +msgstr "Usuario" msgid "Datasets" msgstr "" msgid "Loading details..." -msgstr "" +msgstr "Cargando detalles..." msgid "Name" -msgstr "" +msgstr "Nombre" msgid "Short name" -msgstr "" +msgstr "Nombre corto" msgid "Created" -msgstr "" +msgstr "Creado" msgid "Last updated" -msgstr "" +msgstr "Ultima actualización" msgid "Id" msgstr "" msgid "Linked project" -msgstr "" +msgstr "Proyecto" msgid "No project linked" -msgstr "" +msgstr "Sin proyecto" msgid "Core competencies" msgstr "" msgid "Sharing" -msgstr "" +msgstr "Permisos" msgid "DataSets removed" -msgstr "" +msgstr "DataSets eliminados" msgid "Access" msgstr "" msgid "Edit" -msgstr "" +msgstr "Editar" msgid "Sharing Settings" msgstr "" msgid "Assign to Organisation Units" -msgstr "" +msgstr "Asignar a unidades organizativas" msgid "Set output/outcome period dates" msgstr "" @@ -148,22 +150,28 @@ msgid "Change output/outcome end date for year" msgstr "" msgid "Details" -msgstr "" +msgstr "Detalles" msgid "Clone" -msgstr "" +msgstr "Clonar" msgid "Search" -msgstr "" +msgstr "Buscar" msgid "Create" -msgstr "" +msgstr "Crear" msgid "{{action}} dataSet" msgstr "" +msgid "Data set name already exists" +msgstr "El nombre del dataset ya existe" + +msgid "Validation name in progress" +msgstr "Validación en progreso" + msgid "Filters" -msgstr "" +msgstr "Filtros" msgid "Scope" msgstr "" @@ -184,32 +192,31 @@ msgid "Active Filters" msgstr "" msgid "Search by name" -msgstr "" +msgstr "Buscar por nombre" msgid "Selected" -msgstr "" +msgstr "Seleccionado" msgid "No Selected" -msgstr "" +msgstr "No seleccionado" msgid "Show closed projects" -msgstr "" +msgstr "Mostrar proyectos cerrados" msgid "Filter projects" -msgstr "" +msgstr "Filtrar proyectos" msgid "" -msgstr "" +msgstr "Ninguno" msgid "Select Project" -msgstr "" +msgstr "Seleccionar proyecto" msgid "DataSet name" -msgstr "" +msgstr "Nombre de dataset" -#, fuzzy msgid "DataSet description" -msgstr "Sección" +msgstr "Descripción de dataset" msgid "Expiry Days" msgstr "" @@ -230,13 +237,13 @@ msgid "There is already a dataset with this name" msgstr "" msgid "Validating..." -msgstr "" +msgstr "Validando" msgid "Please select the regions you want to share this dataSet with" msgstr "" msgid "Saving..." -msgstr "" +msgstr "Guardando..." msgid "Data set saved successfully" msgstr "" @@ -245,38 +252,37 @@ msgid "The dataSet is finished. Press the button Save to save the data" msgstr "" msgid "Save" -msgstr "" +msgstr "Guardar" msgid "and {{number}} more." msgstr "" -#, fuzzy msgid "Description" -msgstr "Sección" +msgstr "Descripción" msgid "Linked Project" -msgstr "" +msgstr "Proyecto" msgid "Organisation Units" -msgstr "" +msgstr "Unidades organizativas" msgid "Setup" msgstr "" msgid "Indicators" -msgstr "" +msgstr "Indicadores" msgid "Share" -msgstr "" +msgstr "Permisos" msgid "Summary and Save" -msgstr "" +msgstr "Resumen y guardar" msgid "Replace" -msgstr "" +msgstr "Reemplazar" msgid "Merge" -msgstr "" +msgstr "Combinar" msgid "Bulk update strategy: {{actionLabel}}" msgstr "" @@ -291,7 +297,7 @@ msgid "DataSets" msgstr "" msgid "Projects" -msgstr "" +msgstr "Proyectos" msgid "Back" msgstr "Volver" @@ -306,7 +312,7 @@ msgid "Core Competencies" msgstr "" msgid "Loading projects..." -msgstr "" +msgstr "Cargando proyectos..." msgid "Public Access" msgstr "" @@ -315,7 +321,7 @@ msgid "User access" msgstr "" msgid "Groups" -msgstr "" +msgstr "Grupos" msgid "and {{number}} more..." msgstr "" @@ -324,7 +330,4 @@ msgid "Removing DataSets" msgstr "" msgid "Loading..." -msgstr "" - -#~ msgid "Hello {{name}}" -#~ msgstr "Hola {{name}}" +msgstr "Cargando..." diff --git a/src/CompositionRoot.ts b/src/CompositionRoot.ts index 4a976995..9b0d55f7 100644 --- a/src/CompositionRoot.ts +++ b/src/CompositionRoot.ts @@ -34,7 +34,7 @@ import { SaveDataSetUseCase } from "$/domain/usecases/SaveDataSetUseCase"; import { SaveOrgUnitDataSetUseCase } from "$/domain/usecases/SaveOrgUnitDataSetUseCase"; import { SaveSharingDataSetsUseCase } from "$/domain/usecases/SaveSharingDataSetsUseCase"; import { SearchSharingUseCase } from "$/domain/usecases/SearchSharingUseCase"; -import { ValidateNameUseCase } from "$/domain/usecases/ValidateNameUseCase"; +import { ValidateDataSetNameUseCase } from "$/domain/usecases/ValidateDataSetNameUseCase"; import { UserD2Repository } from "./data/repositories/UserD2Repository"; import { UserTestRepository } from "./data/repositories/UserTestRepository"; import { UserRepository } from "./domain/repositories/UserRepository"; @@ -67,7 +67,7 @@ function getCompositionRoot(repositories: Repositories) { repositories.dataSetsRepository, repositories.projectRepository ), - validateName: new ValidateNameUseCase(repositories.dataSetsRepository), + validateName: new ValidateDataSetNameUseCase(repositories.dataSetsRepository), save: new SaveDataSetUseCase( repositories.dataSetsRepository, repositories.dataElementRepository diff --git a/src/data/repositories/D2ApiCategoryOption.ts b/src/data/repositories/D2ApiCategoryOption.ts index 8f267c64..5f55e011 100644 --- a/src/data/repositories/D2ApiCategoryOption.ts +++ b/src/data/repositories/D2ApiCategoryOption.ts @@ -4,6 +4,7 @@ import { D2Api } from "$/types/d2-api"; import _ from "$/domain/entities/generic/Collection"; import { chunkRequest } from "$/data/utils"; import { FutureData } from "$/domain/entities/generic/Future"; +import { Maybe } from "$/utils/ts-utils"; export class D2ApiCategoryOption { constructor(private api: D2Api) {} @@ -22,3 +23,9 @@ export class D2ApiCategoryOption { } export type D2CategoryOptionType = { id: string; displayName: string; lastUpdated: ISODateString }; +export type D2CategoryOptionDates = { + startDate: Maybe; + endDate: Maybe; +}; + +export type D2CategoryOptionWithDates = D2CategoryOptionType & D2CategoryOptionDates; diff --git a/src/data/repositories/DataSetD2Api.ts b/src/data/repositories/DataSetD2Api.ts index 119bd76d..8787f93a 100644 --- a/src/data/repositories/DataSetD2Api.ts +++ b/src/data/repositories/DataSetD2Api.ts @@ -222,7 +222,6 @@ export class DataSetD2Api { data: this.buildPermission(d2DataSet.sharing.public, "data"), metadata: this.buildPermission(d2DataSet.sharing.public, "metadata"), }, - shortName: d2DataSet.displayShortName, access: this.buildAccessByType(d2DataSet.userAccesses, "users").concat( this.buildAccessByType(d2DataSet.userGroupAccesses, "groups") ), diff --git a/src/data/repositories/DataSetD2Repository.ts b/src/data/repositories/DataSetD2Repository.ts index 65899f73..ecbb9cc1 100644 --- a/src/data/repositories/DataSetD2Repository.ts +++ b/src/data/repositories/DataSetD2Repository.ts @@ -2,7 +2,7 @@ import { D2AttributeValue } from "@eyeseetea/d2-api/2.36"; import { D2Api, MetadataResponse } from "$/types/d2-api"; import { apiToFuture } from "$/data/api-futures"; -import { DataSet, DataSetList, DataSetToSave } from "$/domain/entities/DataSet"; +import { DataSet, DataSetList } from "$/domain/entities/DataSet"; import { Paginated } from "$/domain/entities/Paginated"; import { DataSetName, @@ -18,6 +18,7 @@ import { chunkRequest } from "$/data/utils"; import { D2Config } from "$/data/repositories/D2ApiConfig"; import { Indicator } from "$/domain/entities/Indicator"; import { Id, Ref } from "$/domain/entities/Ref"; +import { DataSetToSave } from "$/domain/entities/DataSetToSave"; export class DataSetD2Repository implements DataSetRepository { private d2DataSetApi: DataSetD2Api; @@ -280,10 +281,10 @@ export class DataSetD2Repository implements DataSetRepository { ) { return { id: dataSet.id || getUid(dataSet.name), + shortName: dataSet.shortName, name: dataSet.name, periodType: "Monthly", description: dataSet.description, - shortName: dataSet.shortName, publicAccess: this.d2DataSetApi.generateFullPermission(dataSet.permissions), dataSetElements: this.buildDataSetElements(dataSet), indicators: dataSet.indicators @@ -352,12 +353,11 @@ export class DataSetD2Repository implements DataSetRepository { attribute: { id: attributes.project.id }, value: dataSet.project?.id, }; - const createdByAttribute = { attribute: { id: attributes.createdByApp.id }, value: "true" }; - const attributesToSave = _([projectAttribute, createdByAttribute]) - .compactMap(attribute => (attribute.value ? attribute : undefined)) - .value(); + const attributesToSave = [projectAttribute, createdByAttribute].filter( + attribute => attribute.value + ); const filteredExisting = existingAttributes?.filter( diff --git a/src/data/repositories/OrgUnitD2Repository.ts b/src/data/repositories/OrgUnitD2Repository.ts index 22955efb..f94f6794 100644 --- a/src/data/repositories/OrgUnitD2Repository.ts +++ b/src/data/repositories/OrgUnitD2Repository.ts @@ -2,7 +2,7 @@ import { D2Api } from "$/types/d2-api"; import { apiToFuture } from "$/data/api-futures"; import { OrgUnit } from "$/domain/entities/DataSet"; import { Id } from "$/domain/entities/Ref"; -import { Future, FutureData } from "$/domain/entities/generic/Future"; +import { FutureData } from "$/domain/entities/generic/Future"; import { OrgUnitRepository } from "$/domain/repositories/OrgUnitRepository"; import { chunkRequest } from "$/data/utils"; @@ -10,9 +10,7 @@ export class OrgUnitD2Repository implements OrgUnitRepository { constructor(private api: D2Api) {} getByIds(ids: Id[]): FutureData { - if (ids.length === 0) return Future.success([]); - - const $requests = chunkRequest(ids, idsToFetch => { + const d2OrgsUnits$ = chunkRequest(ids, idsToFetch => { return apiToFuture( this.api.models.organisationUnits.get({ fields: { id: true, displayName: true, path: true }, @@ -22,8 +20,8 @@ export class OrgUnitD2Repository implements OrgUnitRepository { ).map(response => response.objects); }); - return $requests.map(response => { - return response.map(d2OrgUnit => { + return d2OrgsUnits$.map(d2OrgUnits => { + return d2OrgUnits.map(d2OrgUnit => { return { id: d2OrgUnit.id, name: d2OrgUnit.displayName, diff --git a/src/data/repositories/ProjectD2Repository.ts b/src/data/repositories/ProjectD2Repository.ts index c018ec93..0b92d3d2 100644 --- a/src/data/repositories/ProjectD2Repository.ts +++ b/src/data/repositories/ProjectD2Repository.ts @@ -8,9 +8,13 @@ import _ from "$/domain/entities/generic/Collection"; import { DataSetD2Api } from "$/data/repositories/DataSetD2Api"; import { ISODateString, Id } from "$/domain/entities/Ref"; import { DataSet } from "$/domain/entities/DataSet"; -import { D2CategoryOptionType } from "$/data/repositories/D2ApiCategoryOption"; +import { + D2CategoryOptionType, + D2CategoryOptionWithDates, +} from "$/data/repositories/D2ApiCategoryOption"; import { Future, FutureData } from "$/domain/entities/generic/Future"; import { D2ApiConfig, D2Config } from "$/data/repositories/D2ApiConfig"; +import { Maybe } from "$/utils/ts-utils"; export class ProjectD2Repository implements ProjectRepository { private d2DataSetApi: DataSetD2Api; @@ -23,32 +27,8 @@ export class ProjectD2Repository implements ProjectRepository { getList(): FutureData { return this.getCategories().flatMap(categories => { - return apiToFuture( - this.api.models.categoryOptions.get({ - fields: { - id: true, - displayName: true, - startDate: true, - endDate: true, - lastUpdated: true, - }, - filter: { "categories.code": { eq: categories.project.code } }, - order: "displayName:asc", - paging: false, - }) - ).map(d2Response => { - return d2Response.objects.map((d2CategoryOption): Project => { - return Project.build({ - dataSets: [], - id: d2CategoryOption.id, - name: d2CategoryOption.displayName, - lastUpdated: d2CategoryOption.lastUpdated, - isOpen: this.isProjectOpen( - d2CategoryOption.startDate, - d2CategoryOption.endDate - ), - }); - }); + return this.getCategoryOptionsByCode(categories.project.code).map(d2Response => { + return this.getProjectsWithDates(d2Response.objects); }); }); } @@ -57,7 +37,36 @@ export class ProjectD2Repository implements ProjectRepository { return this.getAllProjects(1, []); } - private isProjectOpen(date1: ISODateString, date2: ISODateString): boolean { + private getCategoryOptionsByCode(code: string) { + return apiToFuture( + this.api.models.categoryOptions.get({ + fields: { + id: true, + displayName: true, + startDate: true, + endDate: true, + lastUpdated: true, + }, + filter: { "categories.code": { eq: code } }, + order: "displayName:asc", + paging: false, + }) + ); + } + + private getProjectsWithDates(categoryOptions: D2CategoryOptionWithDates[]): Project[] { + return categoryOptions.map(d2CategoryOption => { + return Project.build({ + dataSets: [], + id: d2CategoryOption.id, + name: d2CategoryOption.displayName, + lastUpdated: d2CategoryOption.lastUpdated, + isOpen: this.isProjectOpen(d2CategoryOption.startDate, d2CategoryOption.endDate), + }); + }); + } + + private isProjectOpen(date1: Maybe, date2: Maybe): boolean { if (!date1 || !date2) return false; const today = new Date(); diff --git a/src/domain/entities/DataSet.ts b/src/domain/entities/DataSet.ts index 46653f52..4092788e 100644 --- a/src/domain/entities/DataSet.ts +++ b/src/domain/entities/DataSet.ts @@ -9,6 +9,7 @@ import { Either } from "$/domain/entities/generic/Either"; import { ValidationError } from "$/domain/entities/generic/Error"; import { validateOrgUnits, validateRequired } from "$/domain/entities/generic/Validation"; import { Indicator } from "$/domain/entities/Indicator"; +import { DataSetToSave } from "$/domain/entities/DataSetToSave"; export type DataSetAttrs = { created: ISODateString; @@ -18,7 +19,6 @@ export type DataSetAttrs = { lastUpdated: ISODateString; permissions: Permissions; project: Maybe; - shortName: string; coreCompetencies: CoreCompetency[]; access: AccessData[]; orgUnits: OrgUnit[]; @@ -28,9 +28,9 @@ export type DataSetAttrs = { indicators: Indicator[]; }; -export type DataSetToSave = Omit & { - orgUnits: Ref[]; -}; +// export type DataSetToSave = Omit & { +// orgUnits: Ref[]; +// }; export type OrgUnit = { id: Id; name: string; path: Id[] }; export type Permissions = { data: Permission; metadata: Permission }; @@ -46,6 +46,15 @@ export class DataSet extends Struct() { return allErrors.length === 0 ? Either.success(this) : Either.error(allErrors); } + get shortName(): string { + return this.truncateValue(this.name); + } + + private truncateValue(input: string): string { + const targetLength = 50; + return input.length > targetLength ? input.slice(0, targetLength) : input; + } + validateSetup(): Either[], DataSet> { const errors = this.buildSetupErrors(); return errors.length === 0 ? Either.success(this) : Either.error(errors); @@ -56,7 +65,7 @@ export class DataSet extends Struct() { return this._update({ project, name }); } - update(fieldName: keyof DataSet, value: string | number | boolean): DataSet { + update(fieldName: K, value: DataSet[K]): DataSet { return this._update({ [fieldName]: value }); } @@ -84,29 +93,6 @@ export class DataSet extends Struct() { ]); } - static createEmpty(id: Id): DataSet { - return DataSet.create({ - indicators: [], - access: [], - coreCompetencies: [], - created: "", - description: "", - id, - lastUpdated: "", - name: "", - orgUnits: [], - permissions: { - data: Permission.create({ read: false, write: false }), - metadata: Permission.create({ read: false, write: false }), - }, - project: undefined, - shortName: "", - expiryDays: 0, - openFuturePeriods: 0, - notifyUser: false, - }); - } - static buildOrgUnitsFromPaths(paths: string[]): OrgUnit[] { const orgUnits = paths.map(path => ({ id: _(path.split("/")).last() || "", @@ -159,4 +145,26 @@ export class DataSet extends Struct() { return ""; } } + + static initial(id: Id): DataSet { + return DataSet.create({ + indicators: [], + access: [], + coreCompetencies: [], + created: "", + description: "", + id, + lastUpdated: "", + name: "", + orgUnits: [], + permissions: { + data: Permission.create({ read: false, write: false }), + metadata: Permission.create({ read: false, write: false }), + }, + project: undefined, + expiryDays: 0, + openFuturePeriods: 0, + notifyUser: false, + }); + } } diff --git a/src/domain/entities/DataSetToSave.ts b/src/domain/entities/DataSetToSave.ts new file mode 100644 index 00000000..1dd67123 --- /dev/null +++ b/src/domain/entities/DataSetToSave.ts @@ -0,0 +1,8 @@ +import { DataSet, DataSetAttrs } from "$/domain/entities/DataSet"; +import { Ref } from "$/domain/entities/Ref"; + +export type DataSetToSaveAttrs = Omit & { + orgUnits: Ref[]; +}; + +export class DataSetToSave extends DataSet {} diff --git a/src/domain/entities/generic/Error.ts b/src/domain/entities/generic/Error.ts index 696a56c5..df0a0ab7 100644 --- a/src/domain/entities/generic/Error.ts +++ b/src/domain/entities/generic/Error.ts @@ -25,7 +25,7 @@ export function getErrorMessageFromErrors(errors: ValidationError[]): stri return errors .map(error => { return error.errors.map(err => - validationErrorMessages[err](error.property as string, error.value) + validationErrorMessages[err](error.property, error.value) ); }) .flat() @@ -33,17 +33,13 @@ export function getErrorMessageFromErrors(errors: ValidationError[]): stri } export function getErrors(errors: ValidationError[]): string[] { - return errors - .map(error => { - return error.errors.map(err => - validationErrorMessages[err](error.property as string, error.value) - ); - }) - .flat(); + return errors.flatMap(error => { + return error.errors.map(err => validationErrorMessages[err](error.property, error.value)); + }); } export type ValidationError = { - property: keyof T; + property: keyof T & string; value: unknown; errors: ValidationErrorKey[]; }; diff --git a/src/domain/repositories/DataSetRepository.ts b/src/domain/repositories/DataSetRepository.ts index c3be2054..b940faa2 100644 --- a/src/domain/repositories/DataSetRepository.ts +++ b/src/domain/repositories/DataSetRepository.ts @@ -1,7 +1,8 @@ import { FutureData } from "$/domain/entities/generic/Future"; -import { DataSet, DataSetList, DataSetToSave } from "$/domain/entities/DataSet"; +import { DataSet, DataSetList } from "$/domain/entities/DataSet"; import { Paginated } from "$/domain/entities/Paginated"; import { Id } from "$/domain/entities/Ref"; +import { DataSetToSave } from "$/domain/entities/DataSetToSave"; export interface DataSetRepository { getByIds(ids: Id[]): FutureData; diff --git a/src/domain/usecases/GetDataSetSettingsUseCase.ts b/src/domain/usecases/GetDataSetSettingsUseCase.ts index 9e39a182..6b4efc82 100644 --- a/src/domain/usecases/GetDataSetSettingsUseCase.ts +++ b/src/domain/usecases/GetDataSetSettingsUseCase.ts @@ -33,7 +33,7 @@ export class GetDataSetSettingsUseCase { private getOrCreateDataSet(dataSetId: Id): FutureData { if (!dataSetId) - return Future.success(DataSet.createEmpty(getUid(new Date().getTime().toString()))); + return Future.success(DataSet.initial(getUid(new Date().getTime().toString()))); return this.dataSetRepository.getByIds([dataSetId]).flatMap(dataSets => { const dataSet = dataSets[0]; return dataSet diff --git a/src/domain/usecases/SaveDataSetUseCase.ts b/src/domain/usecases/SaveDataSetUseCase.ts index 7f8e2609..1b82612a 100644 --- a/src/domain/usecases/SaveDataSetUseCase.ts +++ b/src/domain/usecases/SaveDataSetUseCase.ts @@ -2,62 +2,76 @@ import _ from "$/domain/entities/generic/Collection"; import { DataSet } from "$/domain/entities/DataSet"; import { Future, FutureData } from "$/domain/entities/generic/Future"; import { DataSetRepository } from "$/domain/repositories/DataSetRepository"; -import { Maybe } from "$/utils/ts-utils"; import { getErrors } from "$/domain/entities/generic/Error"; import { Id } from "$/domain/entities/Ref"; import { HashMap } from "$/domain/entities/generic/HashMap"; -import { DataElement } from "$/domain/entities/DataElement"; import { DataElementRepository } from "$/domain/repositories/DataElementRepository"; +import { Indicator } from "$/domain/entities/Indicator"; +import { DataSetUtils } from "$/domain/usecases/common/DataSetUtils"; +import i18n from "$/utils/i18n"; export class SaveDataSetUseCase { + private dataSetUtils: DataSetUtils; + constructor( private dataSetRepository: DataSetRepository, private dataElementRepository: DataElementRepository - ) {} + ) { + this.dataSetUtils = new DataSetUtils(this.dataSetRepository); + } execute(dataSet: DataSet): FutureData { - return this.getDataSetById(dataSet.id).flatMap(existingDataSet => { - const dataSetToSave = DataSet.create({ - ...(existingDataSet || {}), - ...dataSet, - shortName: this.truncateValue(dataSet.name), - }); + const result = dataSet.validate(); - const result = dataSetToSave.validate(); + if (result.isError()) { + const errors = getErrors(result.value.error); + return Future.error(new Error(errors.join("\n"))); + } - if (result.isError()) { - const errors = getErrors(result.value.error); - return Future.error(new Error(errors.join("\n"))); - } + return this.validateDataSetName(dataSet).flatMap(dataSetAlreadyExists => { + if (dataSetAlreadyExists) + return Future.error( + new Error( + i18n.t("Data set name already exists: {{dataSetName}}", { + nsSeparator: false, + dataSetName: dataSet.name, + }) + ) + ); - const relatedDataElementsFromIndicators = - this.getRelatedDataElementsFromIndicators(dataSetToSave); + return this.getDataElementsRelatedFromIndicators(dataSet).flatMap( + indicatorsWithDataElements => { + const dataSetWithIndicatorsRelated = dataSet.setIndicators( + indicatorsWithDataElements + ); + return this.dataSetRepository.save([dataSetWithIndicatorsRelated]); + } + ); + }); + } - const dataElementIdentifiables = relatedDataElementsFromIndicators - .values() - .flatMap(ids => ids); + private validateDataSetName(dataSet: DataSet): FutureData { + return this.dataSetUtils.dataSetNameExists({ name: dataSet.name, dataSetId: dataSet.id }); + } - return this.getDataElementsByIds(dataElementIdentifiables).flatMap(dataElements => { - const indicatorsWithDataElements = dataSetToSave.indicators.map(indicator => { - return indicator.setRelatedDataElements( - dataElements, - relatedDataElementsFromIndicators - ); - }); + private getDataElementsRelatedFromIndicators(dataSet: DataSet): FutureData { + const relatedDataElementsFromIndicators = + this.getRelatedDataElementsFromIndicators(dataSet); - const dataSetWithIndicatorsRelated = dataSetToSave.setIndicators( - indicatorsWithDataElements - ); + const dataElementIdentifiables = relatedDataElementsFromIndicators + .values() + .flatMap(ids => ids); - return this.dataSetRepository.save([dataSetWithIndicatorsRelated]); + return this.dataElementRepository.getBy(dataElementIdentifiables).map(dataElements => { + return dataSet.indicators.map(indicator => { + return indicator.setRelatedDataElements( + dataElements, + relatedDataElementsFromIndicators + ); }); }); } - private getDataElementsByIds(ids: Id[]): FutureData { - return this.dataElementRepository.getBy(ids); - } - private getRelatedDataElementsFromIndicators( dataSetToSave: DataSet ): HashMap { @@ -77,15 +91,4 @@ export class SaveDataSetUseCase { .compactMap(item => item) .value(); } - - private getDataSetById(id: string): FutureData> { - return this.dataSetRepository.getByIds([id]).map(dataSets => { - return _(dataSets).first(); - }); - } - - private truncateValue(input: string): string { - const targetLength = 50; - return input.length > targetLength ? input.slice(0, targetLength) : input; - } } diff --git a/src/domain/usecases/SaveOrgUnitDataSetUseCase.ts b/src/domain/usecases/SaveOrgUnitDataSetUseCase.ts index fdf1a2e5..22fe97be 100644 --- a/src/domain/usecases/SaveOrgUnitDataSetUseCase.ts +++ b/src/domain/usecases/SaveOrgUnitDataSetUseCase.ts @@ -1,8 +1,9 @@ import { FutureData } from "$/domain/entities/generic/Future"; -import { DataSet, DataSetToSave } from "$/domain/entities/DataSet"; +import { DataSet } from "$/domain/entities/DataSet"; import { Id, Ref } from "$/domain/entities/Ref"; import { DataSetRepository } from "$/domain/repositories/DataSetRepository"; import _ from "$/domain/entities/generic/Collection"; +import { DataSetToSave } from "$/domain/entities/DataSetToSave"; export class SaveOrgUnitDataSetUseCase { constructor(private dataSetRepository: DataSetRepository) {} diff --git a/src/domain/usecases/ValidateDataSetNameUseCase.ts b/src/domain/usecases/ValidateDataSetNameUseCase.ts new file mode 100644 index 00000000..9aa7786e --- /dev/null +++ b/src/domain/usecases/ValidateDataSetNameUseCase.ts @@ -0,0 +1,14 @@ +import { FutureData } from "$/domain/entities/generic/Future"; +import { DataSetRepository } from "$/domain/repositories/DataSetRepository"; +import { DataSetUtils, ValidateDataSetNameOptions } from "$/domain/usecases/common/DataSetUtils"; + +export class ValidateDataSetNameUseCase { + private dataSetUtils: DataSetUtils; + constructor(private dataSetRepository: DataSetRepository) { + this.dataSetUtils = new DataSetUtils(this.dataSetRepository); + } + + execute(options: ValidateDataSetNameOptions): FutureData { + return this.dataSetUtils.dataSetNameExists(options); + } +} diff --git a/src/domain/usecases/ValidateNameUseCase.ts b/src/domain/usecases/common/DataSetUtils.ts similarity index 75% rename from src/domain/usecases/ValidateNameUseCase.ts rename to src/domain/usecases/common/DataSetUtils.ts index d335a29b..e9fd58f6 100644 --- a/src/domain/usecases/ValidateNameUseCase.ts +++ b/src/domain/usecases/common/DataSetUtils.ts @@ -2,10 +2,10 @@ import { Id } from "$/domain/entities/Ref"; import { FutureData } from "$/domain/entities/generic/Future"; import { DataSetRepository } from "$/domain/repositories/DataSetRepository"; -export class ValidateNameUseCase { +export class DataSetUtils { constructor(private dataSetRepository: DataSetRepository) {} - execute(options: ValidateNameOptions): FutureData { + dataSetNameExists(options: ValidateDataSetNameOptions): FutureData { return this.dataSetRepository.getByName(options.name).map(dataSets => { return dataSets.some( dataSet => @@ -15,4 +15,5 @@ export class ValidateNameUseCase { }); } } -export type ValidateNameOptions = { name: string; dataSetId: Id }; + +export type ValidateDataSetNameOptions = { name: string; dataSetId: Id }; diff --git a/src/webapp/components/dataset-wizard/DataSetWizard.tsx b/src/webapp/components/dataset-wizard/DataSetWizard.tsx index 0fddbf3f..628d89b1 100644 --- a/src/webapp/components/dataset-wizard/DataSetWizard.tsx +++ b/src/webapp/components/dataset-wizard/DataSetWizard.tsx @@ -40,6 +40,8 @@ export const DataSetWizard = React.memo((props: DataSetWizardProps) => { const navigateTo = useNavigateTo(); const [validationStatus, setValidationStatus] = React.useState("idle"); + const { validateSteps } = useValidateDataSetWizard({ validationStatus, dataSet }); + const goBackToHome = React.useCallback(() => { navigateTo("createDataSets"); }, [navigateTo]); @@ -86,29 +88,6 @@ export const DataSetWizard = React.memo((props: DataSetWizardProps) => { updateDataSet, ]); - const validationInProgressOrError = - !validationStatus || validationStatus === "error" || validationStatus === "loading"; - - const validateSteps = React.useCallback( - (currentStep: WizardStep) => { - if (validationInProgressOrError) - return Promise.resolve(["Validation name in progress"]); - if (currentStep.key === "setup") { - const result = dataSet.validateSetup(); - if (result.isError()) { - return Promise.resolve(getErrors(result.value.error)); - } - } else if (currentStep.key === "indicators") { - const result = dataSet.validateIndicatorsStep(); - if (result.isError()) { - return Promise.resolve(getErrors(result.value.error)); - } - } - return Promise.resolve([]); - }, - [dataSet, validationInProgressOrError] - ); - return ( @@ -132,4 +111,44 @@ export const DataSetWizard = React.memo((props: DataSetWizardProps) => { ); }); +export function useValidateDataSetWizard(props: { + validationStatus: ValidationStatusType; + dataSet: DataSet; +}) { + const { validationStatus, dataSet } = props; + + const validationInProgressOrError = + !validationStatus || validationStatus === "error" || validationStatus === "loading"; + + const validateSteps = React.useCallback( + (currentStep: WizardStep) => { + if (validationInProgressOrError) { + const errorMessage = getErrorByValidationStatus(validationStatus); + return Promise.resolve([errorMessage]); + } else if (currentStep.key === "setup") { + const result = dataSet.validateSetup(); + return result.isError() + ? Promise.resolve(getErrors(result.value.error)) + : Promise.resolve([]); + } else { + return Promise.resolve([]); + } + }, + [dataSet, validationInProgressOrError] + ); + + return { validateSteps }; +} + +function getErrorByValidationStatus(status: ValidationStatusType): string { + switch (status) { + case "error": + return i18n.t("Data set name already exists"); + case "loading": + return i18n.t("Validation name in progress"); + default: + return ""; + } +} + DataSetWizard.displayName = "DataSetWizard"; diff --git a/src/webapp/components/dataset-wizard/SetupDataSet.tsx b/src/webapp/components/dataset-wizard/SetupDataSet.tsx index 27825026..d5d27cfb 100644 --- a/src/webapp/components/dataset-wizard/SetupDataSet.tsx +++ b/src/webapp/components/dataset-wizard/SetupDataSet.tsx @@ -18,6 +18,7 @@ import { Project } from "$/domain/entities/Project"; import { ProjectsSelectorModal } from "$/webapp/components/dataset-wizard/ProjectsSelectorModal"; import { Maybe } from "$/utils/ts-utils"; import { component } from "$/utils/react"; +import _ from "$/domain/entities/generic/Collection"; export type SetupDataSetProps = { dataSet: DataSet; @@ -28,7 +29,7 @@ export type SetupDataSetProps = { }; const SetupDataSet_ = React.memo((props: SetupDataSetProps) => { - const { api } = useAppContext(); + const { api, compositionRoot } = useAppContext(); const { dataSet, onChange, onValidate, projects, validationStatus } = props; const [projectModalOpen, setProjectModalOpen] = React.useState(false); @@ -64,11 +65,13 @@ const SetupDataSet_ = React.memo((props: SetupDataSetProps) => { const updateOrgUnits = React.useCallback( (paths: string[]) => { - const orgUnits = DataSet.buildOrgUnitsFromPaths(paths); - const updateData = DataSet.create({ ...dataSet, orgUnits }); - onChange(updateData); + const idsFromPaths = paths.map(path => _(path.split("/")).last() || ""); + compositionRoot.orgUnits.getByIds.execute(idsFromPaths).run(orgUnitsDetails => { + const updateData = DataSet.create({ ...dataSet, orgUnits: orgUnitsDetails }); + onChange(updateData); + }, console.error); }, - [onChange, dataSet] + [compositionRoot.orgUnits.getByIds, onChange, dataSet] ); const updateProject = React.useCallback( diff --git a/src/webapp/pages/register-dataset/RegisterDataSetPage.tsx b/src/webapp/pages/register-dataset/RegisterDataSetPage.tsx index c5b3537e..1577d683 100644 --- a/src/webapp/pages/register-dataset/RegisterDataSetPage.tsx +++ b/src/webapp/pages/register-dataset/RegisterDataSetPage.tsx @@ -45,9 +45,9 @@ export function useGetDataSetSettings(props: { id: Id }) { const { id } = props; const { compositionRoot } = useAppContext(); const snackbar = useSnackbar(); - const [status, setStatus] = React.useState("idle"); + const [status, setStatus] = React.useState("idle"); const [dataSet, updateDataSet] = React.useState( - DataSet.createEmpty(getUid(new Date().getTime().toString())) + DataSet.initial(getUid(new Date().getTime().toString())) ); const [dataSetSettings, setDataSetSettings] = React.useState(); @@ -84,6 +84,6 @@ function useGetProjects() { return { projects }; } -export type HttpStatus = "idle" | "loading" | "finished" | "error"; +export type LoadingStatus = "idle" | "loading" | "finished" | "error"; export const RegisterDataSetPage = component(RegisterDataSetPage_);