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

Add setup screen #470

Merged
merged 3 commits into from
Dec 9, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
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
6 changes: 2 additions & 4 deletions i18n/es.po
Original file line number Diff line number Diff line change
Expand Up @@ -197,9 +197,8 @@ msgstr ""
msgid "DataSet name"
msgstr ""

#, fuzzy
msgid "DataSet description"
msgstr "Sección"
msgstr ""

msgid "Expiry Days"
msgstr ""
Expand Down Expand Up @@ -240,9 +239,8 @@ msgstr ""
msgid "and {{number}} more."
msgstr ""

#, fuzzy
msgid "Description"
msgstr "Sección"
msgstr ""

msgid "Linked Project"
msgstr ""
Expand Down
4 changes: 2 additions & 2 deletions src/CompositionRoot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,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";
Expand Down Expand Up @@ -55,7 +55,7 @@ function getCompositionRoot(repositories: Repositories) {
repositories.dataSetsRepository,
repositories.projectRepository
),
validateName: new ValidateNameUseCase(repositories.dataSetsRepository),
validateName: new ValidateDataSetNameUseCase(repositories.dataSetsRepository),
save: new SaveDataSetUseCase(repositories.dataSetsRepository),
},
logs: {
Expand Down
7 changes: 7 additions & 0 deletions src/data/repositories/D2ApiCategoryOption.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {}
Expand All @@ -22,3 +23,9 @@ export class D2ApiCategoryOption {
}

export type D2CategoryOptionType = { id: string; displayName: string; lastUpdated: ISODateString };
export type D2CategoryOptionDates = {
startDate: Maybe<ISODateString>;
endDate: Maybe<ISODateString>;
};

export type D2CategoryOptionWithDates = D2CategoryOptionType & D2CategoryOptionDates;
1 change: 0 additions & 1 deletion src/data/repositories/DataSetD2Api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,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")
),
Expand Down
23 changes: 10 additions & 13 deletions src/data/repositories/DataSetD2Repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { D2AttributeValue } from "@eyeseetea/d2-api/2.36";
import { D2Api } 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,
Expand All @@ -16,6 +16,7 @@ import { DataSetD2Api, dataSetFieldsWithOrgUnits } from "$/data/repositories/Dat
import { Maybe } from "$/utils/ts-utils";
import { chunkRequest } from "$/data/utils";
import { D2Config } from "$/data/repositories/D2ApiConfig";
import { DataSetToSave } from "$/domain/entities/DataSetToSave";

export class DataSetD2Repository implements DataSetRepository {
private d2DataSetApi: DataSetD2Api;
Expand Down Expand Up @@ -166,10 +167,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),
userAccesses: dataSet.access
.filter(access => access.type === "users")
Expand Down Expand Up @@ -203,25 +204,21 @@ export class DataSetD2Repository implements DataSetRepository {
dataSet: DataSetToSave,
attributes: D2Config["attributes"]
) {
// if (!dataSet.project) return existingAttributes || [];
// const projectAttributeId = attributes.project.id;
// const projectAttribute = existingAttributes?.find(
// attribute => attribute.attribute.id === projectAttributeId
// );

const pa = { attribute: { id: attributes.project.id }, value: dataSet.project?.id };
const projectAttribute = {
attribute: { id: attributes.project.id },
value: dataSet.project?.id,
};
const createdByAttribute = { attribute: { id: attributes.createdByApp.id }, value: "true" };

const attributesToSave = _([pa, createdByAttribute])
.compactMap(attribute => (attribute.value ? attribute : undefined))
.value();
const attributesToSave = [projectAttribute, createdByAttribute].filter(
attribute => attribute.value
);

const filteredExisting =
existingAttributes?.filter(
attr => !attributesToSave.some(save => save.attribute.id === attr.attribute.id)
) || [];

// Combinar `filteredExisting` con `toSave`
return [...filteredExisting, ...attributesToSave];
}
}
11 changes: 4 additions & 7 deletions src/data/repositories/OrgUnitD2Repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,15 @@ 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";

export class OrgUnitD2Repository implements OrgUnitRepository {
constructor(private api: D2Api) {}

getByIds(ids: Id[]): FutureData<OrgUnit[]> {
if (ids.length === 0) return Future.success([]);

const $requests = chunkRequest<D2OrgUnit>(ids, idsToFetch => {
const d2OrgsUnits$ = chunkRequest<D2OrgUnit>(ids, idsToFetch => {
return apiToFuture(
this.api.models.organisationUnits.get({
fields: { id: true, displayName: true, path: true },
Expand All @@ -22,9 +20,8 @@ export class OrgUnitD2Repository implements OrgUnitRepository {
).map(response => response.objects);
});

return $requests.map(response => {
// const allRecords = response.flatMap(r => r);
return response.map(d2OrgUnit => {
return d2OrgsUnits$.map(d2OrgUnits => {
return d2OrgUnits.map(d2OrgUnit => {
return {
id: d2OrgUnit.id,
name: d2OrgUnit.displayName,
Expand Down
65 changes: 37 additions & 28 deletions src/data/repositories/ProjectD2Repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -23,32 +27,8 @@ export class ProjectD2Repository implements ProjectRepository {

getList(): FutureData<Project[]> {
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 => {
Copy link
Collaborator

@tokland tokland Dec 6, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typically, we'd make getCategoryOptionsByCode return directly the categoryOptions, no need for the full response. It's a way of narrowing scopes.

return this.getProjectsWithDates(d2Response.objects);
});
});
}
tokland marked this conversation as resolved.
Show resolved Hide resolved
Expand All @@ -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<ISODateString>, date2: Maybe<ISODateString>): boolean {
if (!date1 || !date2) return false;
tokland marked this conversation as resolved.
Show resolved Hide resolved

const today = new Date();
Expand Down
49 changes: 35 additions & 14 deletions src/domain/entities/DataSet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import _ from "$/domain/entities/generic/Collection";
import { Either } from "$/domain/entities/generic/Either";
import { ValidationError } from "$/domain/entities/generic/Error";
import { validateOrgUnits, validateRequired } from "$/domain/entities/generic/Validation";
import { DataSetToSave } from "$/domain/entities/DataSetToSave";

export type DataSetAttrs = {
created: ISODateString;
Expand All @@ -17,7 +18,6 @@ export type DataSetAttrs = {
lastUpdated: ISODateString;
permissions: Permissions;
project: Maybe<Project>;
shortName: string;
coreCompetencies: CoreCompetency[];
access: AccessData[];
orgUnits: OrgUnit[];
Expand All @@ -26,9 +26,9 @@ export type DataSetAttrs = {
notifyUser: boolean;
};

export type DataSetToSave = Omit<DataSetAttrs, "orgUnits" | "created" | "lastUpdated"> & {
orgUnits: Ref[];
};
// export type DataSetToSave = Omit<DataSetAttrs, "orgUnits" | "created" | "lastUpdated"> & {
// orgUnits: Ref[];
// };

export type OrgUnit = { id: Id; name: string; path: Id[] };
export type Permissions = { data: Permission; metadata: Permission };
Expand All @@ -39,6 +39,15 @@ export type CoreCompetency = { id: Id; name: string; code: string };
export type DataSetList = Pick<DataSetAttrs, "id" | "name" | "lastUpdated" | "permissions">;

export class DataSet extends Struct<DataSetAttrs>() {
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<ValidationError<DataSet>[], DataSet> {
const errors: ValidationError<DataSet>[] = [
{
Expand All @@ -61,7 +70,7 @@ export class DataSet extends Struct<DataSetAttrs>() {
return this._update({ project, name });
}

update(fieldName: keyof DataSet, value: string | number | boolean): DataSet {
update<K extends keyof DataSet>(fieldName: K, value: DataSet[K]): DataSet {
return this._update({ [fieldName]: value });
}

Expand All @@ -73,15 +82,6 @@ export class DataSet extends Struct<DataSetAttrs>() {
return DataSet.create({ ...this, orgUnits });
}

static buildOrgUnitsFromPaths(paths: string[]): OrgUnit[] {
const orgUnits = paths.map(path => ({
id: _(path.split("/")).last() || "",
name: path,
path: path.split("/").slice(1),
}));
return orgUnits;
}

static buildAccess(permissions: Permissions): string {
const dataDescription = DataSet.buildAccessDescription(permissions.data);
const metadataDescription = DataSet.buildAccessDescription(permissions.metadata);
Expand All @@ -103,4 +103,25 @@ export class DataSet extends Struct<DataSetAttrs>() {
return "";
}
}

static initial(id: Id): DataSet {
return DataSet.create({
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,
});
}
}
8 changes: 8 additions & 0 deletions src/domain/entities/DataSetToSave.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { DataSet, DataSetAttrs } from "$/domain/entities/DataSet";
import { Ref } from "$/domain/entities/Ref";

export type DataSetToSaveAttrs = Omit<DataSetAttrs, "orgUnits" | "created" | "lastUpdated"> & {
orgUnits: Ref[];
};

export class DataSetToSave extends DataSet {}
14 changes: 5 additions & 9 deletions src/domain/entities/generic/Error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,25 +20,21 @@ export function getErrorMessageFromErrors<T>(errors: ValidationError<T>[]): 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()
.join("\n");
}

export function getErrors<T>(errors: ValidationError<T>[]): 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<T> = {
property: keyof T;
property: keyof T & string;
value: unknown;
errors: ValidationErrorKey[];
};
3 changes: 2 additions & 1 deletion src/domain/repositories/DataSetRepository.ts
Original file line number Diff line number Diff line change
@@ -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<DataSet[]>;
Expand Down
Loading
Loading