Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
7 changes: 5 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: 2026-06-16T15:11:31.847Z\n"
"PO-Revision-Date: 2026-06-16T15:11:31.854Z\n"
"POT-Creation-Date: 2026-06-23T09:10:12.300Z\n"
"PO-Revision-Date: 2026-06-23T09:10:12.300Z\n"

msgid ""
"THIS NEW RELEASE INCLUDES SHARING SETTINGS PER INSTANCES. FOR THIS VERSION "
Expand Down Expand Up @@ -1374,6 +1374,9 @@ msgstr "Destination"
msgid "Set Credentials"
msgstr "Set Credentials"

msgid "Could not load metadata types"
msgstr "Could not load metadata types"

msgid "Every day"
msgstr "Every day"

Expand Down
5 changes: 4 additions & 1 deletion 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: 2026-06-16T15:11:31.847Z\n"
"POT-Creation-Date: 2026-06-16T15:55:22.319Z\n"
"PO-Revision-Date: 2020-07-10T06:53:30.625Z\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
Expand Down Expand Up @@ -1324,6 +1324,9 @@ msgstr ""
msgid "Set Credentials"
msgstr ""

msgid "Could not load metadata types"
msgstr "Could not load metadata types"

msgid "Every day"
msgstr ""

Expand Down
5 changes: 4 additions & 1 deletion i18n/fr.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: 2026-06-16T15:11:31.847Z\n"
"POT-Creation-Date: 2026-06-16T15:55:22.319Z\n"
"PO-Revision-Date: 2020-07-10T06:53:30.625Z\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
Expand Down Expand Up @@ -1324,6 +1324,9 @@ msgstr ""
msgid "Set Credentials"
msgstr ""

msgid "Could not load metadata types"
msgstr "Could not load metadata types"

msgid "Every day"
msgstr ""

Expand Down
5 changes: 4 additions & 1 deletion i18n/pt.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: 2026-06-16T15:11:31.847Z\n"
"POT-Creation-Date: 2026-06-16T15:55:22.319Z\n"
"PO-Revision-Date: 2020-07-10T06:53:30.625Z\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
Expand Down Expand Up @@ -1324,6 +1324,9 @@ msgstr ""
msgid "Set Credentials"
msgstr ""

msgid "Could not load metadata types"
msgstr "Could not load metadata types"

msgid "Every day"
msgstr ""

Expand Down
5 changes: 0 additions & 5 deletions src/data/aggregated/AggregatedDataExchangeApiExecutor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,8 +157,3 @@ type AdexImportSummary = {
value: string;
}>;
};

interface SourceDataResponse {
headers: { name: string }[];
rows: string[][];
}
1 change: 0 additions & 1 deletion src/data/rules/models/SynchronizationRuleModel.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { SynchronizationRuleData } from "../../../domain/rules/entities/SynchronizationRule";
import { Codec, Schema } from "../../../utils/codec";
import { NamedRefModel, RefModel } from "../../common/models/RefModel";
import { SharingSettingModel } from "../../common/models/SharingSettingModel";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,12 +59,6 @@ export class DownloadPayloadFromSyncRuleUseCase implements UseCase {

const sync: GenericSyncUseCase = this.compositionRoot.sync[rule.type](rule.toBuilder());

const aggregateDataExchanges = rule.aggregatedDataExchanges?.map(ade => ade.id) || [];

const payload: SynchronizationPayload = rule.useAggregatedDataExchange
? await this.aggregatedDataExchangeExecutor.getSourceData(aggregateDataExchanges)
: await this.buildPayload(rule.type, rule);

const date = moment().format("YYYYMMDDHHmm");

const mappedData =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -666,8 +666,29 @@ const MetadataTable: React.FC<MetadataTableProps> = ({
});
};

const filterRestrictsIds =
!filters.showOnlySelected &&
Boolean(
filters.search ||
filters.group ||
filters.level ||
filters.lastUpdated ||
filters.program ||
filters.optionSet ||
filters.category ||
filters.programType ||
filters.domainType
);

const idsAreComplete = model.getCollectionName() !== "organisationUnits" && !filterRestrictsIds;
const exclusion = excludedIds.map(id => ({ id }));
const { selection, crossTypeCount } = useSelection(model.getCollectionName(), ids, selectedIds, remoteInstance);
const { selection, crossTypeCount } = useSelection(
model.getCollectionName(),
ids,
selectedIds,
remoteInstance,
idsAreComplete
);

const crossTypeNotifications =
crossTypeCount > 0
Expand Down
Original file line number Diff line number Diff line change
@@ -1,32 +1,65 @@
import { useEffect, useState } from "react";
import { useEffect, useRef, useState } from "react";
import { DataSource } from "../../../../../domain/instance/entities/DataSource";
import { MetadataEntities } from "../../../../../domain/metadata/entities/MetadataEntities";
import { useAppContext } from "../../contexts/AppContext";
import _ from "lodash";

export function useSelection(
collectionName: keyof MetadataEntities,
ids: string[],
selectedIds: string[],
remoteInstance: DataSource | undefined
remoteInstance: DataSource | undefined,
idsAreComplete: boolean
) {
const { compositionRoot } = useAppContext();
const [otherTypeCountFallback, setOtherTypeCountFallback] = useState(0);

// The current type's full id list (`ids`) is never loaded for organisationUnits,
// so we cannot tell cross-type selections apart by set difference. Resolve the
// selection's types via the API and count items in any collection other than
// the current one.
// When `ids` is not the full id list for the current type (filter active, or collection
// never populates ids like organisationUnits), set-difference miscounts same-type
// selections outside the filter as cross-type. Fall back to resolving types via the API.
const shouldFetchCrossTypeData = !idsAreComplete;
const idCollectionCache = useRef(new Map<string, string | null>());
useEffect(() => {
if (collectionName !== "organisationUnits" || selectedIds.length === 0) {
// Invalidate the cache when the resolution context changes (different instance or root).
idCollectionCache.current.clear();
}, [remoteInstance, compositionRoot]);

useEffect(() => {
if (!shouldFetchCrossTypeData || selectedIds.length === 0) {
setOtherTypeCountFallback(0);
return;
}

compositionRoot.metadata
.getByIds(selectedIds, remoteInstance, "id")
.then(pkg => setOtherTypeCountFallback(countPackageOtherType(pkg, collectionName)));
}, [collectionName, selectedIds, compositionRoot, remoteInstance]);
const selectedSet = new Set(selectedIds);
idCollectionCache.current = new Map([...idCollectionCache.current].filter(([id]) => selectedSet.has(id)));
const cache = idCollectionCache.current;
const uncachedIds = selectedIds.filter(id => !cache.has(id));
const countOtherType = () =>
selectedIds.filter(id => {
const col = cache.get(id);
return col !== null && col !== collectionName;
}).length;

if (uncachedIds.length === 0) {
setOtherTypeCountFallback(countOtherType());
return;
}

let cancelled = false;
compositionRoot.metadata.getByIds(uncachedIds, remoteInstance, "id").then(metadataByType => {
if (cancelled) return;
const resolved = new Map(
Object.entries(metadataByType).flatMap(([col, items]) =>
(items ?? []).map(item => [item.id, col] as const)
)
);
uncachedIds.forEach(id => cache.set(id, resolved.get(id) ?? null));
setOtherTypeCountFallback(countOtherType());
});

return () => {
cancelled = true;
};
}, [idsAreComplete, collectionName, selectedIds, compositionRoot, remoteInstance, shouldFetchCrossTypeData]);

const idSet = ids.length > 0 ? new Set(ids) : undefined;
const selection = (idSet ? selectedIds.filter(id => idSet.has(id)) : selectedIds).map(id => ({
Expand All @@ -35,13 +68,11 @@ export function useSelection(
indeterminate: false,
}));

const crossTypeCount = idSet ? selectedIds.filter(id => !idSet.has(id)).length : otherTypeCountFallback;
const crossTypeCount = shouldFetchCrossTypeData
? otherTypeCountFallback
: idSet
? selectedIds.filter(id => !idSet.has(id)).length
: 0;

return { selection, crossTypeCount };
}

function countPackageOtherType(pkg: Partial<Record<string, unknown[]>>, currentCollection: string): number {
return _.sumBy(Object.entries(pkg), ([collection, items]) =>
collection === currentCollection || !items ? 0 : items.length
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
UserGroupModel,
} from "../../../../../../models/dhis/metadata";
import MetadataTable from "../../metadata-table/MetadataTable";
import { useLatestRef } from "../../../hooks/useLatestRef";
import { ModuleWizardStepProps } from "../Steps";

const config = {
Expand All @@ -33,17 +34,19 @@ const config = {
export const MetadataSelectionStep = ({ module, onChange }: ModuleWizardStepProps<MetadataModule>) => {
const snackbar = useSnackbar();
const { models, childrenKeys } = config["module"][module.type];
const moduleRef = useLatestRef(module);

const changeSelection = useCallback(
(newMetadataIds: string[], newExcludedIds: string[]) => {
const additions = _.difference(newMetadataIds, module.metadataIds);
const previousMetadataIds = moduleRef.current.metadataIds;
const additions = _.difference(newMetadataIds, previousMetadataIds);
if (additions.length > 0) {
snackbar.info(i18n.t("Selected {{difference}} elements", { difference: additions.length }), {
autoHideDuration: 1000,
});
}

const removals = _.difference(module.metadataIds, newMetadataIds);
const removals = _.difference(previousMetadataIds, newMetadataIds);
if (removals.length > 0) {
snackbar.info(
i18n.t("Removed {{difference}} elements", {
Expand All @@ -53,9 +56,14 @@ export const MetadataSelectionStep = ({ module, onChange }: ModuleWizardStepProp
);
}

onChange(module.update({ metadataIds: newMetadataIds, excludedIds: newExcludedIds }));
const updatedModule = moduleRef.current.update({
metadataIds: newMetadataIds,
excludedIds: newExcludedIds,
});
moduleRef.current = updatedModule;
onChange(updatedModule);
},
[module, onChange, snackbar]
[moduleRef, onChange, snackbar]
);

return (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ConfirmationDialog, useSnackbar, Wizard, WizardStep } from "@eyeseetea/d2-ui-components";
import { Wizard, WizardStep } from "@eyeseetea/d2-ui-components";
import _ from "lodash";
import React, { useCallback, useEffect, useRef, useState } from "react";
import { useLocation } from "react-router-dom";
Expand Down Expand Up @@ -46,7 +46,6 @@ const SyncWizard: React.FC<SyncWizardProps> = ({
}
>(initialDialogState);
const [stepKey, setStepKey] = useState<string | undefined>(undefined);
const snackbar = useSnackbar();

const steps = config[syncRule.type]
.filter(({ showOnSyncDialog }) => !isDialog || showOnSyncDialog)
Expand Down
Loading
Loading